Llim.run

Sandboxed agents

LIM_API_KEY is an organization-wide credential: it can create, list, and delete every instance you have. An agent in an untrusted or shared sandbox should not hold it. Create the instance outside the sandbox and hand the agent only that instance's own URL and token. The lim CLI drives a pinned instance without any API key.

Create the instance and capture its credentials

Run the create where the API key lives: your machine, the orchestrator, or CI. Every instance returns its own status.apiUrl and status.token:

lim ios create --json > instance.json
jq -r .status.apiUrl instance.json    # the instance's API URL
jq -r .status.token  instance.json    # the instance's token

If the orchestrator is a backend service, the TypeScript SDK makes the same call:

import Limrun from '@limrun/api';

const lim = new Limrun({ apiKey: process.env.LIM_API_KEY });
const instance = await lim.iosInstances.create({ wait: true });
// Hand instance.status.apiUrl and instance.status.token to the sandbox.

Create each resource as the agent needs it:

ResourceCLITypeScript SDK
iOS simulatorlim ios create --jsonlim.iosInstances.create({ wait: true })
Android emulatorlim android create --no-connect --jsonlim.androidInstances.create({ wait: true })
Xcode sandboxlim xcode create --jsonlim.xcodeInstances.create({ wait: true })
Gradle sandboxlim gradle create --jsonlim.gradleInstances.create({ wait: true })

--no-connect skips the local ADB tunnel, whose output would otherwise mix into the JSON. wait: true returns once the instance is ready.

Point the sandboxed CLI at the instance

Export the pair inside the sandbox. Every lim ios command picks them up directly, and nothing is persisted:

export LIM_IOS_INSTANCE_URL=https://.../v1/ios_.../api
export LIM_IOS_INSTANCE_TOKEN=<instance-token>

lim ios element-tree    # works with no LIM_API_KEY

To pin the instance once per workspace instead, run set-instance, which stores it in ~/.lim/last-instances.json:

lim ios set-instance --api-url https://.../v1/ios_.../api --token <instance-token>

Both forms exist for every instance type:

Instance typeEnvironment variablesPin command
iOS simulatorLIM_IOS_INSTANCE_URL, LIM_IOS_INSTANCE_TOKENlim ios set-instance
Android emulatorLIM_ANDROID_INSTANCE_URL, LIM_ANDROID_INSTANCE_TOKENlim android set-instance
Xcode sandboxLIM_XCODE_INSTANCE_URL, LIM_XCODE_INSTANCE_TOKENlim xcode set-instance
Gradle sandboxLIM_GRADLE_INSTANCE_URL, LIM_GRADLE_INSTANCE_TOKENlim gradle set-instance

If the agent should run lim android connect, also pass the ADB WebSocket URL (status.adbWebSocketUrl) as LIM_ANDROID_INSTANCE_ADB_URL or with the --adb-websocket-url flag of lim android set-instance.

When more than one is present, an explicit --id wins over the environment pair, which wins over the pinned instance.

Build without the API key

lim xcode build targets an Xcode sandbox, which is its own resource with its own credentials. Create it where the key lives:

lim xcode create --json > xcode.json

Then, inside the sandbox, export its pair and build:

export LIM_XCODE_INSTANCE_URL=$(jq -r .status.apiUrl xcode.json)
export LIM_XCODE_INSTANCE_TOKEN=$(jq -r .status.token xcode.json)

lim xcode build .       # remote build, still no API key

lim gradle build follows the same pattern with lim gradle create --json and the LIM_GRADLE_INSTANCE_URL and LIM_GRADLE_INSTANCE_TOKEN pair.

Share files through signed asset URLs

Asset Storage lives on the management API, so the sandbox cannot mint asset URLs itself. Create the asset outside and hand the sandbox its signed URLs, which carry their own authorization.

To let the agent ship a build artifact, create the asset where the key lives and pass its upload URL in:

: > agent-build.zip                     # empty placeholder; the agent's upload replaces it
lim asset push ./agent-build.zip
lim asset list --name agent-build.zip --upload-url --json | jq -r '.[0].signedUploadUrl'

From the TypeScript SDK, one call returns both URLs without uploading anything:

const asset = await lim.assets.getOrCreate({ name: 'agent-build.zip' });
// Hand asset.signedUploadUrl to the sandbox. The same call also returns
// asset.signedDownloadUrl for later.

In the sandbox, the build uploads straight to that URL without touching the management API:

lim xcode build . --signed-upload-url "$UPLOAD_URL"

To hand the agent a file, such as a prebuilt app or a fixture, push it outside with lim asset push ./MyApp.app.zip and read its download URL with lim asset list --name MyApp.app.zip --download-url --json. The agent installs it without a key, and the instance downloads the URL server-side:

lim ios install-app "$DOWNLOAD_URL"

What still needs the key

Anything on the management API fails with an authentication error inside the sandbox: creating, listing, and deleting instances, and minting asset URLs the way lim xcode build --upload does. That is the point. The agent drives the one instance it was given, and lifecycle stays with whoever holds the key. Delete the instance from the orchestrator when the work is done.

Next steps

plug

Per-instance MCP server

Give the same instance token to an agent that prefers MCP tool calls.

book-open

Concepts

How API keys, instance tokens, and signed stream URLs relate.

terminal

CLI reference

Shared flags and every environment variable the CLI reads.