From e9b607473837534c7b36ee41dce428f4f82feec6 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 15 Oct 2026 03:49:32 -0700 Subject: [PATCH 1/2] fix(resources): point tenant isolation lease errors to leases.allocate The thrown message is now "tenant isolation requires lease id from leases.allocate.", and the admission tests match that wording. The Limrun page adds a provider that calls leases.allocate in acquire, returns lease.leaseId with sessionIsolation set to tenant, and calls leases.release with the same id. The note states that the daemon is started with the instance URL and token, and that neither call creates or deletes the instance. Fixes #3350 --- .../src/resources/lease-registry-scope.ts | 4 ++- .../__tests__/request-admission.test.ts | 8 ++--- website/docs/docs/limrun.md | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/daemon/src/resources/lease-registry-scope.ts b/packages/daemon/src/resources/lease-registry-scope.ts index aa643b9a39..4b65255f7b 100644 --- a/packages/daemon/src/resources/lease-registry-scope.ts +++ b/packages/daemon/src/resources/lease-registry-scope.ts @@ -256,7 +256,9 @@ export function normalizeLeaseAdmissionRequest( const runId = normalizeRunId(request.runId); if (!runId) throw new AppError('INVALID_ARGS', 'tenant isolation requires run id.'); const leaseId = normalizeLeaseId(request.leaseId); - if (!leaseId) throw new AppError('INVALID_ARGS', 'tenant isolation requires lease id.'); + if (!leaseId) { + throw new AppError('INVALID_ARGS', 'tenant isolation requires lease id from leases.allocate.'); + } return { ...request, tenantId, runId, leaseId, leaseBackend }; } function normalizeOptionalLeaseScope( diff --git a/packages/daemon/src/sessions/__tests__/request-admission.test.ts b/packages/daemon/src/sessions/__tests__/request-admission.test.ts index 0b92d7116b..9d471f096d 100644 --- a/packages/daemon/src/sessions/__tests__/request-admission.test.ts +++ b/packages/daemon/src/sessions/__tests__/request-admission.test.ts @@ -61,7 +61,7 @@ test('close on a lease-less but stored tenant-isolated session still requires a registry, session, ), - /tenant isolation requires lease id/, + /tenant isolation requires lease id from leases\.allocate/, ); }); @@ -84,7 +84,7 @@ test('close with an app target still requires a lease id even with no lease anyw registry, undefined, ), - /tenant isolation requires lease id/, + /tenant isolation requires lease id from leases\.allocate/, ); }); @@ -123,7 +123,7 @@ test('non-close commands on a tenant-isolated session still require a lease id', registry, undefined, ), - /tenant isolation requires lease id/, + /tenant isolation requires lease id from leases\.allocate/, ); }); @@ -143,7 +143,7 @@ test.each(['bogus', 'proxy', 'browserstack'])( registry, undefined, ), - /tenant isolation requires lease id/, + /tenant isolation requires lease id from leases\.allocate/, ); }, ); diff --git a/website/docs/docs/limrun.md b/website/docs/docs/limrun.md index b4cf15c391..66d2e27e98 100644 --- a/website/docs/docs/limrun.md +++ b/website/docs/docs/limrun.md @@ -97,6 +97,35 @@ const runtime = new LimrunRuntime({ To drive existing instances, pass `instances: { ios: { apiUrl, token } }` (Android also takes `adbUrl`), with or without `apiKey`. The runtime never creates or deletes instances for a platform listed there. +A provider that reaches a harness-owned instance through the Node.js client allocates a Limrun lease in `acquire` and releases that same lease in `release`. Put the returned `lease.leaseId` on the client scope, and pass that same id to `leases.release`. Start the daemon with the instance URL and token (`LIM_IOS_INSTANCE_URL` and `LIM_IOS_INSTANCE_TOKEN`). `leases.allocate` attaches to that instance. `leases.release` returns `{ released, provider }`. Neither call creates or deletes the instance. + +```ts +import { createAgentDeviceClient } from 'agent-device'; + +const scope = { + tenant: 'limrun', + runId: process.env.GITHUB_RUN_ID ?? 'local-run', + leaseBackend: 'ios-instance', + leaseProvider: 'limrun', +} as const; + +const provider = { + acquire: async (req: { slot: string }) => { + const lease = await createAgentDeviceClient({ session: `lease-${req.slot}` }).leases.allocate({ + ...scope, + platform: 'ios', + }); + return { + id: lease.leaseId, + client: { ...scope, leaseId: lease.leaseId, sessionIsolation: 'tenant' as const }, + }; + }, + release: async (lease: { id: string }) => { + await createAgentDeviceClient({}).leases.release({ ...scope, leaseId: lease.id }); + }, +}; +``` + After allocating a lease, call `runtime.getDeviceSession(device)` to get the allocated device's capabilities: app inventory, foreground state where Limrun exposes it, key input, bounded log reads, recording, remote asset installation, and the interactor. On Android it also exposes agent-device's `AndroidAdbProvider` for helpers and reversible port forwarding. On iOS it exposes a typed `simctl` execution handle so your bridge can own runner lifecycle and launch policy. The runtime does not expose raw Limrun clients. ## Get artifacts and troubleshoot From d42f9b553bc5d23da1726b67fc412a2cb80e5c36 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:38:31 -0700 Subject: [PATCH 2/2] docs(limrun): allocate the provider lease with retainOnClose The provider example releases its lease itself, so allocate it with retainOnClose: true. Without it, a session close before release ends the lease and leases.release returns released: false. Also note that a retained lease still expires after its ttlMs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KFTE4BHbGYzgf8Uhzcppdh --- website/docs/docs/limrun.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/docs/docs/limrun.md b/website/docs/docs/limrun.md index 66d2e27e98..83d66d117d 100644 --- a/website/docs/docs/limrun.md +++ b/website/docs/docs/limrun.md @@ -97,7 +97,7 @@ const runtime = new LimrunRuntime({ To drive existing instances, pass `instances: { ios: { apiUrl, token } }` (Android also takes `adbUrl`), with or without `apiKey`. The runtime never creates or deletes instances for a platform listed there. -A provider that reaches a harness-owned instance through the Node.js client allocates a Limrun lease in `acquire` and releases that same lease in `release`. Put the returned `lease.leaseId` on the client scope, and pass that same id to `leases.release`. Start the daemon with the instance URL and token (`LIM_IOS_INSTANCE_URL` and `LIM_IOS_INSTANCE_TOKEN`). `leases.allocate` attaches to that instance. `leases.release` returns `{ released, provider }`. Neither call creates or deletes the instance. +A provider that reaches a harness-owned instance through the Node.js client allocates a Limrun lease in `acquire` and releases that same lease in `release`. Put the returned `lease.leaseId` on the client scope, and pass that same id to `leases.release`. Start the daemon with the instance URL and token (`LIM_IOS_INSTANCE_URL` and `LIM_IOS_INSTANCE_TOKEN`). `leases.allocate` attaches to that instance. `leases.release` returns `{ released, provider }`. Neither call creates or deletes the instance. Allocate with `retainOnClose: true` because the provider releases the lease itself: without it, a session `close` before `release` ends the lease and `leases.release` returns `released: false`. A retained lease still expires after its `ttlMs`, so keep heartbeating it between sessions. ```ts import { createAgentDeviceClient } from 'agent-device'; @@ -114,6 +114,7 @@ const provider = { const lease = await createAgentDeviceClient({ session: `lease-${req.slot}` }).leases.allocate({ ...scope, platform: 'ios', + retainOnClose: true, }); return { id: lease.leaseId,