Share a sandbox service with a signed URL
Create, list, and revoke time-limited public URLs for sandbox services with the Fleet SDKs.
Use a signed service URL when a person or external system needs temporary access to a service running inside a Fleet sandbox. The URL is public, time-limited, and revocable. Requests still route only to the named service on the bound sandbox.
A signed service URL is a bearer credential. Anyone who has it can access the service until the URL expires or is revoked. Do not put signed URLs in source control, logs, analytics events, or public chat channels.
Prerequisites#
- A Fleet pool whose template exposes the service you want to share.
- A bound sandbox claim. The signed URL does not create or keep a claim alive.
- OAuth client credentials with permission to manage the claim.
- The published
@trycua/fleet0.1.2 TypeScript package and a Node.js environment withfetchandAbortSignal.timeout.
The published Python cua-sandbox 0.7.0 package also exposes
sandbox.services.create_signed_url(), list_signed_urls(), and
revoke_signed_url(). This guide uses the TypeScript API below. See the
service reference for the Python
signatures.
If you do not have a pool yet, follow Create Fleet capacity. The service name passed to the signed URL API must match a service declared on that pool's template.
Signed URLs accept expiration times from 60 seconds through 24 hours. The optional label is useful for recording why a link was created and is limited to 120 UTF-8 bytes.
Authenticate#
Obtain credentials through
Fleet authentication.
This example uses OAuth client credentials and the https://run.cua.ai Fleet
endpoint. Set the credentials and resource names before running it:
export CUA_CLIENT_ID="<your-client-id>"
export CUA_CLIENT_SECRET="<your-client-secret>"
export CUA_POOL_NAME="my-team-pool"
export CUA_CLAIM_NAME="signed-url-demo"Set CUA_FLEET_BASE_URL or CUA_TOKEN_URL only when you use non-default Fleet
or OAuth endpoints.
Create and revoke a signed URL#
The example claims a sandbox from an existing pool, creates a one-hour URL for
the mcp service, waits while you use the URL, and then revokes it before
releasing the claim.
Install the Fleet SDK and a TypeScript runner:
npm install @trycua/fleet@0.1.2
npm install --save-dev tsx typescript
npm pkg set type=moduleSave the following script as share-service.ts:
import { createInterface } from 'node:readline/promises';
import {
CreateClaimRequestBuilder,
CyclopsClient,
CyclopsCredentials,
FetchHttpClient,
type Claim,
type CyclopsConfiguration,
type SignedServiceUrl,
uniffiInitAsync,
} from '@trycua/fleet/node';
const BASE_URL = process.env.CUA_FLEET_BASE_URL ?? 'https://run.cua.ai';
const TOKEN_URL =
process.env.CUA_TOKEN_URL ??
'https://auth.cua.ai/realms/cyclops-cs/protocol/openid-connect/token';
const CLIENT_ID = requiredEnv('CUA_CLIENT_ID');
const CLIENT_SECRET = requiredEnv('CUA_CLIENT_SECRET');
const POOL_NAME = requiredEnv('CUA_POOL_NAME');
const CLAIM_NAME = process.env.CUA_CLAIM_NAME ?? 'signed-url-demo';
const SERVICE_NAME = 'mcp';
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing environment variable: ${name}`);
return value;
}
await uniffiInitAsync();
const configuration: CyclopsConfiguration = {
baseUrl: BASE_URL,
tokenUrl: TOKEN_URL,
credentials: new CyclopsCredentials(CLIENT_ID, CLIENT_SECRET),
poolPollIntervalMs: 5_000n,
poolPollLimit: 120,
claimPollIntervalMs: 5_000n,
claimPollLimit: 120,
};
const client = CyclopsClient.connect(configuration, new FetchHttpClient()) as CyclopsClient;
let claim: Claim | undefined;
let signedUrl: SignedServiceUrl | undefined;
try {
const pool = await client.getPool(POOL_NAME);
claim = await client.createClaim(
new CreateClaimRequestBuilder().pool(pool).name(CLAIM_NAME).build()
);
const sandbox = await client.waitClaim(claim);
signedUrl = await client.createSignedServiceUrl({
sandbox,
service: SERVICE_NAME,
label: 'Customer demo',
expiresInSeconds: 3600,
});
console.log(`Share this URL: ${signedUrl.url}`);
const urls = await client.listSignedServiceUrls(sandbox);
for (const item of urls) {
const state = item.revokedAt ? 'revoked' : 'available';
console.log(`${item.label ?? item.id}: ${state}, expires ${item.expiresAt}`);
}
const readline = createInterface({ input: process.stdin, output: process.stdout });
await readline.question('Press Enter to revoke the URL... ');
readline.close();
} finally {
try {
if (signedUrl) await client.revokeSignedServiceUrl(signedUrl);
} finally {
try {
if (claim) await client.deleteClaim(claim);
} finally {
client.uniffiDestroy();
}
}
}Run it:
npx tsx share-service.tsThe Fleet client manages signed URLs directly. Keep the returned
SignedServiceUrl record because revokeSignedServiceUrl() uses it to identify
the URL and namespace. The nested cleanup blocks revoke the URL, release the
claim, and destroy the native client in that order.
Use the URL safely#
- Keep the claim bound while clients use the URL. Releasing or deleting the claim can remove the backing service before the URL expires.
- Choose the shortest practical expiration time. Create a new URL instead of extending access through a long-lived link.
- Use labels that identify the recipient or purpose without including secrets or personal data.
- Revoke the URL immediately when sharing is complete. Expiration is a fallback, not a substitute for revocation.
- Treat a
503response from URL management operations as signed URLs being unavailable in the current Fleet environment.
Troubleshoot#
The SDK reports an unknown service#
The service name must be present in the bound sandbox's service list. Reconcile the pool template with the service and target port, then create a new claim.
The URL stops working before its expiration time#
Confirm that the claim is still bound and that the backing service still exists. A signed URL grants access to a service; it does not extend the claim's lifecycle.
The SDK says signed service URLs are unavailable#
Confirm that the Fleet environment has signed service URLs configured. The
TypeScript @trycua/fleet 0.1.2 and Python cua-sandbox 0.7.0 include the
create, list, and revoke operations, but package availability does not enable
the feature on the server.