Workload Identity
Workload Identity lets a Limrun instance authenticate to the Limrun API with its own credential. When you create the instance, you choose the permissions it receives. Code running inside the workload can then access those resources without your organization's API key.
The same model applies to iOS simulators, Android emulators, Xcode sandboxes, and Gradle sandboxes. Assets are the first supported resource type. For example, you can let a build upload to one named asset while denying access to other assets in your organization.
Identity, permissions, and access tokens
A workload identity identifies the running instance and its organization. Its role contains the permissions currently delegated to it. An access token is the short-lived credential the workload uses to make authorized API requests.
The request flow is:
- Your application creates an instance using its own credential and includes
spec.role.permissions. - Limrun checks that your credential is allowed to delegate every requested permission.
- The workload exchanges its identity for an access token carrying its assigned permissions.
- Each API request using that token must satisfy the permission required for its resource and operation.
The creator's credential stays with your application. A successful instance creation does not grant the workload all of the creator's permissions.
An instance token such as status.token has a different purpose: it lets a client control that instance. Workload Identity gives the workload access to other resources. See credentials for client and device credentials.
See Permissions and access tokens for the complete resource, action, and organization-role tables.
Delegated permissions
All four instance create APIs accept the same inline role:
{
"spec": {
"role": {
"permissions": [
"asset:name/input.zip:read"
]
}
}
}This fragment grants permission to read and download input.zip. A permission has the form resource:target:verb. Each resource type defines its targets and verbs. The asset permission reference lists the supported asset operations, IDs, names, and wildcard patterns.
The role is a limit on the workload's own credential. It does not change the capabilities needed to operate the instance itself. It also cannot restrict a separate API key, access token, or signed URL that you supply to a process or build.
You can replace a running instance's permissions with a conditional PATCH. Requesting an access token inherits the current role and cannot expand it. Existing tokens retain their grants until expiry.
Delegation limits
You can delegate only permissions covered by your own credential. A valid permission outside your grants causes creation or a role update to return 403 Forbidden. Malformed permissions return 400 Bad Request.
Asset name patterns have an additional rule. Delegating any pattern containing * or ? requires the general asset:* target for that verb, or an equivalent broader permission. Holding a name pattern lets you delegate matching concrete names, but not another pattern, even an identical one. This avoids relying on pattern-overlap comparisons. See asset name patterns for examples.
For example, this role permits uploads to assets named release-1.0.zip,
release-2.0.zip, and other names matching release-*.zip:
spec: {
role: {
permissions: ['asset:name/release-*.zip:upload'],
},
}Here, name/release-*.zip is the permission target. Delegating it requires
asset:*:upload, asset:*:all, or an equivalent broader grant. Use a concrete
name such as name/release-1.0.zip as the upload destination. Add matching
create permission if the workload must create an asset that does not exist yet.
Explicit and omitted roles
role: { permissions: [] } grants no access to other resources through the workload credential. It leaves the instance's normal operating capabilities available.
Omitting role uses the organization's compatibility default. For organizations with legacy upload access enabled, that default permits uploads to every asset in the organization. An explicit list makes the intended access independent of this default.
A role must contain a non-null permissions array with at most 100 entries. role: {} is invalid. Named role references are not supported. See instance role validation and reuse for API behavior.
Updating a running workload
GET returns the current role and an ETag for live instances. Send that ETag in
If-Match when replacing spec.role.permissions. The same operation is available
on /v1/ios_instances/{id}, /v1/android_instances/{id},
/v1/xcode_instances/{id}, and /v1/gradle_instances/{id}.
const { data, response } = await lim.xcodeInstances.get(id).withResponse();
const ifMatch = response.headers.get('etag');
if (!ifMatch || !data.spec.role) throw new Error('Instance is no longer live');
await lim.xcodeInstances.update(id, {
ifMatch,
spec: {
role: {
permissions: [...new Set([
...data.spec.role.permissions,
'asset:name/releases/app.ipa:create',
'asset:name/releases/app.ipa:upload',
'asset:name/releases/app.ipa:update',
])],
},
},
});# Read the instance and copy the quoted ETag response header.
curl -i "https://api.limrun.com/v1/xcode_instances/$INSTANCE_ID" \
-H "Authorization: Bearer $LIM_API_KEY"
# Replace the entire permission list using that ETag.
curl -X PATCH "https://api.limrun.com/v1/xcode_instances/$INSTANCE_ID" \
-H "Authorization: Bearer $LIM_API_KEY" \
-H 'Content-Type: application/json' \
-H 'If-Match: "ETAG_FROM_GET"' \
-d '{"spec":{"role":{"permissions":["asset:name/releases/app.ipa:all"]}}}'A successful update returns 204 No Content. The array replaces the previous
list, while other instance fields stay unchanged. Use [] to remove all delegated
grants. A concurrent change returns 412 Precondition Failed; read the instance
again and recompute the list before retrying. Malformed bodies and missing
If-Match return 400. Terminating instances reject updates with 409.
Historical records have no editable ETag. List results may lag behind live GETs.
Removing grants does not revoke previously issued tokens or signed URLs. Workload tokens can remain usable for up to ten minutes; signed URLs retain their own expiry. Build daemons exchange identity again for each permission-based upload.
The same PATCH endpoint can replace customer labels through metadata.labels,
independently of permissions. Omit spec for a labels-only change, or supply both
fields to update them atomically. Use an empty labels object to clear customer labels.
See updating labels for examples.
Updating requires an API key with control of the instance, such as
xcode:<instance-id>:all or xcode:*:all. Use the corresponding ios, android,
or gradle resource for other workloads. If you supply permissions, your key must
cover every entry in the replacement list, including entries you preserve.
A labels-only update does not reauthorize the unchanged role. Signed access tokens,
instance tokens, and workload identities cannot call this management endpoint.
To update labels and permissions together, include metadata alongside spec
in the same request. Both updates succeed together or neither is applied:
const { data: current, response: snapshot } = await lim.xcodeInstances.get(id).withResponse();
const etag = snapshot.headers.get('etag');
if (!etag || !current.spec.role) throw new Error('Instance is no longer live');
await lim.xcodeInstances.update(id, {
ifMatch: etag,
metadata: { labels: { ...current.metadata.labels, branch: 'release' } },
spec: { role: { permissions: current.spec.role.permissions } },
});Authentication inside the workload
Limrun provides an identity token file and the environment variables LIMRUN_WORKLOAD_IDENTITY_TOKEN_FILE and DIRECTOR_URL to the workload environment. The TypeScript SDK discovers them when no API key is configured:
import Limrun from '@limrun/api';
// Run inside the workload environment, with LIM_API_KEY unset.
const lim = new Limrun();On the first API call, the SDK exchanges the identity for an access token. It caches the token and refreshes it before expiry by rereading the identity file. Your code does not need to manage these exchanges.
An API key supplied to the constructor or through LIM_API_KEY takes precedence. Set workloadTokenFile: null to disable identity discovery. These environment variables belong to the workload environment; they are not credentials you need to embed in a mobile app running on a simulator or emulator.
Other clients can use the same REST token exchange. An exchange with no requested permission list inherits the assigned role. A request for a smaller list must stay within that role and obey the delegation rules above. A workload with an empty permission list cannot obtain an access token through this exchange.
Credential lifetime
Workload access tokens expire after ten minutes by default. REST callers can request a shorter lifetime. Refresh requires the current workload identity; an access token cannot refresh itself.
Terminating the instance prevents new exchanges. An issued access token remains valid until it expires or is revoked. A signed upload or download URL also carries its own access until its expiry. Terminating an instance does not invalidate URLs it has already received.
Example: upload a build artifact
This TypeScript example runs on your machine or backend, where LIM_API_KEY authenticates instance creation. It uses the TypeScript SDK and a local Xcode project at ./MyProject with scheme MyApp. The build runs inside the new workload and uploads using that workload's identity.
import Limrun from '@limrun/api';
const lim = new Limrun();
const existingPermissions = ['asset:name/input.zip:read'];
const instance = await lim.xcodeInstances.create({
wait: true,
spec: {
role: {
permissions: [
...existingPermissions,
'asset:name/preview/app.ipa:create',
'asset:name/preview/app.ipa:upload',
'asset:name/preview/app.ipa:update',
],
},
},
});
try {
const xcode = await lim.xcodeInstances.createClient({ instance });
await xcode.sync('./MyProject', { watch: false });
await xcode.xcodebuild(
{ scheme: 'MyApp' },
{ upload: { target: 'name/preview/app.ipa', ttl: '24h' } },
);
} finally {
await lim.xcodeInstances.delete(instance.metadata.id);
}The permissions authorize the upload. The separate upload.target tells the build where to send its artifact. Assigning a permission alone does not trigger an upload. The SDK sends your permission array unchanged; it does not add grants from upload.target.
For a name target, create lets the workload create the asset if it is absent. upload allows writing the file, and update allows recording app metadata and expiry. Build uploads default to a 14-day expiry, so they need update even when you omit ttl. An existing asset ID needs no create grant. Downloading the result requires a separate read grant.
Gradle builds and uploadLatestRbeBuild use the same target shape. The build REST APIs call the field assetUpload. Targets identify one concrete asset; a permission pattern does not choose a destination. See build with Xcode and build with Gradle for build options.
An explicit upload: { target, token } uses that supplied access token. If the token is rejected or expired, the build does not fall back to workload credentials. upload: { signedUploadUrl } remains supported and needs no workload permission grants.
CLI permissions
When the CLI creates an instance for a named upload, it adds the exact-name create, upload, and update permissions automatically. You do not need --permission for the upload itself:
lim xcode build ./MyProject --scheme MyApp --upload preview/app.ipa --upload-ttl 24h
lim gradle build ./my-app --upload result.apk --permission asset:name/input.zip:readRepeatable --permission values add other grants to the creation request. --upload takes a plain asset name; the CLI adds the name/ prefix. Automatic upload names cannot contain * or ?. RBE --auto-upload <name> follows the same rules.
All four create commands, run, Xcode build/test/sync/RBE, and Gradle build/sync accept --permission. Every CLI creation sends an explicit list, empty when no grants are needed. Auxiliary simulators receive an empty list; ios create --xcode gives both newly created workloads the requested grants.
A named upload keeps the selected instance. The CLI reads its live role and adds create, upload, and update permissions for the exact asset name. It preserves existing grants and retries concurrent changes. Your credential must control the instance and cover the resulting permission list. RBE auto-upload adds the grants at tunnel startup; manual RBE uploads add them before uploading. The daemon obtains fresh workload credentials for each permission-based upload. --reuse-if-exists still matches labels and leaves the stored role unchanged; --permission applies only when creating a new instance.
--signed-upload-url and LIM_SIGNED_UPLOAD_URL keep their existing behavior and add no workload grants.
Give Limrun workloads permissions to access resources without copying your API key into them.
Was this guide helpful?