From 68bda5105e1573eebfca9530973b5056993518f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 28 Jul 2026 18:07:41 +0200 Subject: [PATCH 1/2] feat: expose Limrun runtime --- package.json | 4 ++++ src/__tests__/limrun-public.test.ts | 13 +++++++++++++ src/__tests__/package-exports.test.ts | 1 + src/sdk/limrun.ts | 1 + tsdown.config.ts | 1 + website/docs/docs/client-api.md | 4 +++- website/docs/docs/device-clouds.md | 12 +++++++++++- 7 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/limrun-public.test.ts create mode 100644 src/sdk/limrun.ts diff --git a/package.json b/package.json index 1d85e43354..3d1fc84a9d 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,10 @@ "import": "./dist/src/android-adb.js", "types": "./dist/src/android-adb.d.ts" }, + "./limrun": { + "import": "./dist/src/limrun.js", + "types": "./dist/src/limrun.d.ts" + }, "./contracts": { "import": "./dist/src/contracts.js", "types": "./dist/src/contracts.d.ts" diff --git a/src/__tests__/limrun-public.test.ts b/src/__tests__/limrun-public.test.ts new file mode 100644 index 0000000000..e4d47628b7 --- /dev/null +++ b/src/__tests__/limrun-public.test.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { createLimrunRuntimeFromEnv } from '../sdk/limrun.ts'; + +test('public limrun entrypoint creates the provider runtime from environment configuration', async () => { + assert.equal(createLimrunRuntimeFromEnv({}), undefined); + + const runtime = createLimrunRuntimeFromEnv({ LIMRUN_API_KEY: 'lim_test_key' }); + assert.ok(runtime); + assert.equal(runtime.provider, 'limrun'); + + await runtime.shutdown(); +}); diff --git a/src/__tests__/package-exports.test.ts b/src/__tests__/package-exports.test.ts index eec37eaab0..529728af44 100644 --- a/src/__tests__/package-exports.test.ts +++ b/src/__tests__/package-exports.test.ts @@ -29,6 +29,7 @@ const supportedSubpaths = [ './remote-config', './install-source', './android-adb', + './limrun', './contracts', './selectors', './finders', diff --git a/src/sdk/limrun.ts b/src/sdk/limrun.ts new file mode 100644 index 0000000000..be0526b54c --- /dev/null +++ b/src/sdk/limrun.ts @@ -0,0 +1 @@ +export { createLimrunRuntimeFromEnv } from '../providers/limrun/runtime.ts'; diff --git a/tsdown.config.ts b/tsdown.config.ts index faee43c0e2..c3ddd9b899 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -51,6 +51,7 @@ export default defineConfig({ 'remote-config': 'src/sdk/remote-config.ts', 'install-source': 'src/sdk/install-source.ts', 'android-adb': 'src/sdk/android-adb.ts', + limrun: 'src/sdk/limrun.ts', contracts: 'src/sdk/contracts.ts', selectors: 'src/sdk/selectors.ts', finders: 'src/sdk/finders.ts', diff --git a/website/docs/docs/client-api.md b/website/docs/docs/client-api.md index dadb61a08d..703fb9864e 100644 --- a/website/docs/docs/client-api.md +++ b/website/docs/docs/client-api.md @@ -61,8 +61,10 @@ Public subpath API exposed for Node consumers: - `listAndroidAppsWithAdb(executor)` - `getAndroidAppStateWithAdb(executor)` - types: `AndroidAdbExecutor`, `AndroidAdbExecutorOptions`, `AndroidPortReverseEndpoint` +- `agent-device/limrun` + - `createLimrunRuntimeFromEnv(env)` -The `contracts`, `selectors`, `finders`, `install-source`, `android-adb`, `artifacts`, `batch`, `metro`, `remote-config`, and `io` subpaths are the supported Node entry points. The former compatibility subpaths `agent-device/android-apps` and `agent-device/daemon`, plus hosted-runtime subpaths `agent-device/cloud-webdriver`, `agent-device/commands`, `agent-device/backend`, `agent-device/testing/conformance`, and `agent-device/observability`, are not published. +The `contracts`, `selectors`, `finders`, `install-source`, `android-adb`, `limrun`, `artifacts`, `batch`, `metro`, `remote-config`, and `io` subpaths are the supported Node entry points. The former compatibility subpaths `agent-device/android-apps` and `agent-device/daemon`, plus hosted-runtime subpaths `agent-device/cloud-webdriver`, `agent-device/commands`, `agent-device/backend`, `agent-device/testing/conformance`, and `agent-device/observability`, are not published. ## Basic usage diff --git a/website/docs/docs/device-clouds.md b/website/docs/docs/device-clouds.md index db324dcff5..49d0b1cf04 100644 --- a/website/docs/docs/device-clouds.md +++ b/website/docs/docs/device-clouds.md @@ -251,7 +251,17 @@ const client = createAgentDeviceClient({ }); ``` -The JavaScript client does not publish provider SDK subpaths. Use the normal typed client methods; provider implementation details stay internal. Limrun uses the same client shape with `leaseProvider: 'limrun'`, `platform: 'android'` or `platform: 'ios'`, and `LIMRUN_API_KEY` in the daemon environment. +Use the normal typed client methods when agent-device owns the daemon. Limrun uses the same client +shape with `leaseProvider: 'limrun'`, `platform: 'android'` or `platform: 'ios'`, and +`LIMRUN_API_KEY` in the daemon environment. + +Bridges that host the provider runtime themselves can reuse agent-device's Limrun implementation: + +```ts +import { createLimrunRuntimeFromEnv } from 'agent-device/limrun'; + +const runtime = createLimrunRuntimeFromEnv(process.env); +``` ## MCP Experience From 2449efc70453de12d11e3f28f3ca706870624985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 28 Jul 2026 18:35:07 +0200 Subject: [PATCH 2/2] fix: address Limrun export feedback --- src/__tests__/limrun-public.test.ts | 13 --------- src/__tests__/limrun-runtime.test.ts | 28 +++++++------------ src/provider-device-runtimes.ts | 14 +++++++--- src/providers/limrun/runtime.ts | 15 ++-------- src/sdk/limrun.ts | 2 +- .../installed-package-metro.test.ts | 6 ++++ website/docs/docs/client-api.md | 3 +- website/docs/docs/device-clouds.md | 13 +++++++-- 8 files changed, 41 insertions(+), 53 deletions(-) delete mode 100644 src/__tests__/limrun-public.test.ts diff --git a/src/__tests__/limrun-public.test.ts b/src/__tests__/limrun-public.test.ts deleted file mode 100644 index e4d47628b7..0000000000 --- a/src/__tests__/limrun-public.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import assert from 'node:assert/strict'; -import { test } from 'vitest'; -import { createLimrunRuntimeFromEnv } from '../sdk/limrun.ts'; - -test('public limrun entrypoint creates the provider runtime from environment configuration', async () => { - assert.equal(createLimrunRuntimeFromEnv({}), undefined); - - const runtime = createLimrunRuntimeFromEnv({ LIMRUN_API_KEY: 'lim_test_key' }); - assert.ok(runtime); - assert.equal(runtime.provider, 'limrun'); - - await runtime.shutdown(); -}); diff --git a/src/__tests__/limrun-runtime.test.ts b/src/__tests__/limrun-runtime.test.ts index a5116e2db0..6a55bc14d5 100644 --- a/src/__tests__/limrun-runtime.test.ts +++ b/src/__tests__/limrun-runtime.test.ts @@ -3,11 +3,12 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; -import { LimrunRuntime } from '../providers/limrun/runtime.ts'; +import { LimrunRuntime } from '../sdk/limrun.ts'; import { createExpiredProviderLeaseReleaser } from '../daemon/provider-lease-expiry.ts'; import type { SimulatorLease } from '../daemon/lease-registry.ts'; import type { DeviceInfo } from '../kernel/device.ts'; import { runCmd } from '../utils/exec.ts'; +import { readVersion } from '../utils/version.ts'; type LimrunInstancePage = { getPaginatedItems: () => Array<{ metadata: { id: string } }>; @@ -118,7 +119,6 @@ afterEach(() => { test('Limrun runtime identifies direct CLI usage to the Limrun API', async () => { const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); const lease: SimulatorLease = { @@ -139,7 +139,7 @@ test('Limrun runtime identifies direct CLI usage to the Limrun API', async () => assert.deepEqual(limrunMockState.constructorOptions[0]?.defaultHeaders, { 'x-agent-device-client': 'agent-device-cli', - 'x-agent-device-version': '9.9.9-test', + 'x-agent-device-version': readVersion(), }); const iosCreateCalls = limrunMockState.iosCreate.mock.calls as unknown as Array< [{ metadata?: { labels?: Record } }] @@ -157,7 +157,7 @@ test('Limrun runtime identifies direct CLI usage to the Limrun API', async () => }); test('Limrun iOS uses shared deep-link classification', async () => { - const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' }); + const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' }); try { const device = await allocateLimrunDevice(runtime, { @@ -179,7 +179,7 @@ test('Limrun iOS uses shared deep-link classification', async () => { }); test('Limrun reclaims a labeled iOS instance without an in-memory session', async () => { - const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' }); + const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' }); const lease: SimulatorLease = { leaseId: 'lease-recovered-ios', tenantId: 'team-a', @@ -220,7 +220,7 @@ test('Limrun recovers a failed expired lease release after a daemon restart', as heartbeatAt: 1, expiresAt: 60_001, }; - const firstRuntime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' }); + const firstRuntime = new LimrunRuntime({ apiKey: 'lim_test_key' }); const firstReleaser = createExpiredProviderLeaseReleaser({ recoverExpiredLease: firstRuntime.recoverExpiredLease, recoverableProviderIds: ['limrun'], @@ -239,7 +239,7 @@ test('Limrun recovers a failed expired lease release after a daemon restart', as assert.deepEqual(limrunMockState.iosDelete.mock.calls[0], ['ios-instance-1']); firstReleaser.shutdown(); - recoveredRuntime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' }); + recoveredRuntime = new LimrunRuntime({ apiKey: 'lim_test_key' }); recoveredReleaser = createExpiredProviderLeaseReleaser({ recoverExpiredLease: recoveredRuntime.recoverExpiredLease, recoverableProviderIds: ['limrun'], @@ -266,7 +266,6 @@ test('Limrun recovers a failed expired lease release after a daemon restart', as test('Limrun Android reverses localhost URL ports through the persistent ADB tunnel', async () => { const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); try { @@ -358,7 +357,6 @@ function assertAndroidTunnelLifecycle(openUrl: string): void { test('Limrun Android installs direct local artifacts through Limrun assets', async () => { const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); const lease: SimulatorLease = { leaseId: 'lease-android', @@ -410,7 +408,7 @@ test('Limrun Android installs direct local artifacts through Limrun assets', asy }); test('Limrun Android shares an in-flight ADB tunnel across concurrent port reverse requests', async () => { - const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' }); + const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' }); try { await allocateLimrunDevice(runtime, androidLease()); @@ -441,7 +439,6 @@ test('Limrun iOS installs direct local artifacts through Limrun assets', async ( fs.writeFileSync(ipaPath, 'demo'); const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); const lease: SimulatorLease = { leaseId: 'lease-ios', @@ -505,7 +502,7 @@ test('Limrun iOS reads the bundle display name before uploading an app bundle', '', ].join(''), ); - const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' }); + const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' }); try { const device = await allocateLimrunDevice(runtime, { @@ -529,7 +526,6 @@ test('Limrun iOS reads the bundle display name before uploading an app bundle', test('Limrun iOS maps supported orientation and rejects unsupported upside-down orientation', async () => { const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); try { @@ -556,7 +552,6 @@ test('Limrun iOS maps supported orientation and rejects unsupported upside-down test('Limrun Android configures an explicit port reverse', async () => { const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); const lease: SimulatorLease = { leaseId: 'lease-android', @@ -601,7 +596,7 @@ test('Limrun Android configures an explicit port reverse', async () => { }); test('Limrun Android preserves canonical ADB failure classification', async () => { - const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' }); + const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' }); try { await allocateLimrunDevice(runtime, androidLease()); @@ -638,7 +633,6 @@ test('Limrun deletes iOS instance when post-create validation fails', async () = } as never); const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); const lease: SimulatorLease = { leaseId: 'lease-ios', @@ -671,7 +665,6 @@ test('Limrun deletes Android instance when post-create validation fails', async } as never); const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); const lease: SimulatorLease = { leaseId: 'lease-android', @@ -697,7 +690,6 @@ test('Limrun deletes Android instance when post-create validation fails', async test('Limrun keeps session tracked when release fails so release can be retried', async () => { const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', - version: '9.9.9-test', }); const lease: SimulatorLease = { leaseId: 'lease-android', diff --git a/src/provider-device-runtimes.ts b/src/provider-device-runtimes.ts index 2f102e12fe..4835e7c41f 100644 --- a/src/provider-device-runtimes.ts +++ b/src/provider-device-runtimes.ts @@ -16,9 +16,15 @@ export async function createDefaultProviderDeviceRuntimes( env: DefaultProviderDeviceRuntimeEnv = process.env, ): Promise { const runtimes = createDefaultCloudWebDriverProviderRuntimes(env); - if (!env.LIMRUN_API_KEY?.trim()) return runtimes; + const apiKey = env.LIMRUN_API_KEY?.trim(); + if (!apiKey) return runtimes; - const { createLimrunRuntimeFromEnv } = await import('./providers/limrun/runtime.ts'); - const limrunRuntime = createLimrunRuntimeFromEnv(env); - return limrunRuntime ? [...runtimes, limrunRuntime] : runtimes; + const { LimrunRuntime } = await import('./providers/limrun/runtime.ts'); + return [ + ...runtimes, + new LimrunRuntime({ + apiKey, + region: env.LIMRUN_REGION?.trim() || undefined, + }), + ]; } diff --git a/src/providers/limrun/runtime.ts b/src/providers/limrun/runtime.ts index 57f2e6b814..634007e160 100644 --- a/src/providers/limrun/runtime.ts +++ b/src/providers/limrun/runtime.ts @@ -47,24 +47,13 @@ type LimrunInstance = { type LimrunRuntimeSession = LimrunIosSession | LimrunAndroidSession; -type LimrunRuntimeOptions = { +export type LimrunRuntimeOptions = { apiKey: string; region?: string; - version?: string; }; const LIMRUN_CLIENT_HEADER = 'agent-device-cli'; -export function createLimrunRuntimeFromEnv(env: NodeJS.ProcessEnv): LimrunRuntime | undefined { - const apiKey = env.LIMRUN_API_KEY?.trim(); - if (!apiKey) return undefined; - return new LimrunRuntime({ - apiKey, - region: env.LIMRUN_REGION?.trim() || undefined, - version: readVersion(), - }); -} - export class LimrunRuntime implements ProviderDeviceRuntime { private readonly limrun: Limrun; private readonly sessions = new Map(); @@ -101,7 +90,7 @@ export class LimrunRuntime implements ProviderDeviceRuntime { apiKey: options.apiKey, defaultHeaders: { 'x-agent-device-client': LIMRUN_CLIENT_HEADER, - 'x-agent-device-version': options.version ?? readVersion(), + 'x-agent-device-version': readVersion(), }, }); } diff --git a/src/sdk/limrun.ts b/src/sdk/limrun.ts index be0526b54c..9162a45410 100644 --- a/src/sdk/limrun.ts +++ b/src/sdk/limrun.ts @@ -1 +1 @@ -export { createLimrunRuntimeFromEnv } from '../providers/limrun/runtime.ts'; +export { LimrunRuntime, type LimrunRuntimeOptions } from '../providers/limrun/runtime.ts'; diff --git a/test/integration/installed-package-metro.test.ts b/test/integration/installed-package-metro.test.ts index 423e9524d1..20e905600f 100644 --- a/test/integration/installed-package-metro.test.ts +++ b/test/integration/installed-package-metro.test.ts @@ -316,6 +316,11 @@ test('installed package exposes Node APIs and packaged companion tunnel entrypoi './finders': (mod) => mod.findBestMatchesByLocator([], 'text', 'anything').matches.length, './install-source': (mod) => typeof mod.isTrustedInstallSourceUrl('https://example.test/app.apk'), './io': (mod) => typeof mod.createLocalArtifactAdapter({ cwd: process.cwd() }).reserveOutput, + './limrun': async (mod) => { + const runtime = new mod.LimrunRuntime({ apiKey: 'lim_test_key' }); + await runtime.shutdown(); + return runtime.provider; + }, './metro': (mod) => mod.buildBundleUrl('https://public.example.test', 'ios'), './remote-config': (mod) => typeof mod, './selectors': (mod) => mod.isSelectorToken('||') && typeof mod.parseSelectorChain === 'function', @@ -388,6 +393,7 @@ test('installed package exposes Node APIs and packaged companion tunnel entrypoi './finders': 0, './install-source': 'boolean', './io': 'function', + './limrun': 'limrun', // Type-only subpath: resolving the module from the packed exports map is // the entire runtime check. './remote-config': 'object', diff --git a/website/docs/docs/client-api.md b/website/docs/docs/client-api.md index 703fb9864e..cc857ed49d 100644 --- a/website/docs/docs/client-api.md +++ b/website/docs/docs/client-api.md @@ -62,7 +62,8 @@ Public subpath API exposed for Node consumers: - `getAndroidAppStateWithAdb(executor)` - types: `AndroidAdbExecutor`, `AndroidAdbExecutorOptions`, `AndroidPortReverseEndpoint` - `agent-device/limrun` - - `createLimrunRuntimeFromEnv(env)` + - `new LimrunRuntime(options)` + - types: `LimrunRuntimeOptions` The `contracts`, `selectors`, `finders`, `install-source`, `android-adb`, `limrun`, `artifacts`, `batch`, `metro`, `remote-config`, and `io` subpaths are the supported Node entry points. The former compatibility subpaths `agent-device/android-apps` and `agent-device/daemon`, plus hosted-runtime subpaths `agent-device/cloud-webdriver`, `agent-device/commands`, `agent-device/backend`, `agent-device/testing/conformance`, and `agent-device/observability`, are not published. diff --git a/website/docs/docs/device-clouds.md b/website/docs/docs/device-clouds.md index 49d0b1cf04..958e327452 100644 --- a/website/docs/docs/device-clouds.md +++ b/website/docs/docs/device-clouds.md @@ -255,12 +255,19 @@ Use the normal typed client methods when agent-device owns the daemon. Limrun us shape with `leaseProvider: 'limrun'`, `platform: 'android'` or `platform: 'ios'`, and `LIMRUN_API_KEY` in the daemon environment. -Bridges that host the provider runtime themselves can reuse agent-device's Limrun implementation: +The first-party agent-device-cloud bridge hosts the provider runtime itself and can reuse +agent-device's Limrun implementation: ```ts -import { createLimrunRuntimeFromEnv } from 'agent-device/limrun'; +import { LimrunRuntime } from 'agent-device/limrun'; -const runtime = createLimrunRuntimeFromEnv(process.env); +const apiKey = process.env.LIMRUN_API_KEY; +if (!apiKey) throw new Error('LIMRUN_API_KEY is required'); + +const runtime = new LimrunRuntime({ + apiKey, + region: process.env.LIMRUN_REGION, +}); ``` ## MCP Experience