paperclipai/paperclip · error

CreateOS execution requires a lease from this environment.

Error message

CreateOS execution requires a lease from this environment.

What it means

track() guards every sandbox operation (execute, sync in/out) with a lease-ownership check: the lease must have a providerLeaseId and its metadata must match the calling environment (provider 'createos', same companyId, environmentId, and apiUrl derived from the current config). This error is thrown when the lease passed to an operation was created by a different environment, company, or API URL, or has no providerLeaseId at all.

Solutions

  1. Release and re-acquire the lease under the current environment/config so its metadata is regenerated to match.
  2. Confirm the lease's metadata fields (provider, companyId, environmentId, apiUrl) match the params used for the execute call.
  3. If you changed apiUrl or connection config, drop stale persisted leases and re-probe the environment.
  4. Check that the host is not swapping or truncating lease.metadata when deserializing persisted run state.

Example fix

// before
const result = await driver.execute({ ...params, lease: leaseFromOtherEnv });
// after: verify ownership before executing
if (lease.metadata?.companyId !== params.companyId || lease.metadata?.environmentId !== params.environmentId) {
  lease = await acquireLease(params); // re-acquire for this environment
}
const result = await driver.execute({ ...params, lease });
Defensive patterns

Strategy: validation

Validate before calling

function canUseLease(lease, params, apiUrl) {
  const m = lease?.metadata;
  return Boolean(lease?.providerLeaseId) && m?.provider === "createos" &&
    m?.companyId === params.companyId && m?.environmentId === params.environmentId &&
    m?.apiUrl === apiUrl;
}

Type guard

function isUsableCreateosLease(l): l is PluginEnvironmentLease & { providerLeaseId: string } {
  return typeof (l as any)?.providerLeaseId === "string" && (l as any).providerLeaseId.length > 0 &&
    (l as any)?.metadata?.provider === "createos";
}

Try / catch

try {
  return await driver.execute({ ...params, lease });
} catch (e) {
  if (e.message.includes("requires a lease from this environment")) {
    lease = await driver.acquireLease(params); // re-acquire matching lease
    return await driver.execute({ ...params, lease });
  }
  throw e;
}

Prevention

When it happens

Trigger: onEnvironmentExecute (or sync) is called with params.lease whose metadata is undefined, whose metadata.provider !== 'createos', whose companyId/environmentId differ from params, or whose metadata.apiUrl differs from the apiUrl parsed from the current config; or params.lease.providerLeaseId is null/empty.

Common situations: Changing the CreateOS apiUrl (or shape/rootfs/region settings that feed apiUrl resolution) after a lease was acquired, so old lease metadata no longer matches config; passing a lease from environment A into an execute call for environment B; a host bug persisting/stripping lease metadata across process restarts; accidentally passing a non-CreateOS lease object.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/94969a1a094d5f69. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugins/sandbox-providers/createos/src/plugin.ts:80

  function key(params: PluginEnvironmentDriverBaseParams, id: string): string {
    const config = parseConfig(params.config);
    const account = createHash("sha256").update(resolveApiKey(config)).digest("hex");
    return JSON.stringify([params.companyId, params.environmentId, config.apiUrl, account, id]);
  }

  async function stopActive(scope: string) {
    const calls = [...(active.get(scope) ?? [])];
    for (const call of calls) call.controller.abort();
    await Promise.all(calls.map((call) => call.done));
  }

  async function track<T>(
    params: PluginEnvironmentDriverBaseParams & { lease: PluginEnvironmentLease },
    work: (client: CreateosClient, signal: AbortSignal) => Promise<T>,
    timeoutOverride?: number,
  ): Promise<T> {
    if (!params.lease.providerLeaseId || !metadataMatches(params, params.lease.metadata)) throw new Error("CreateOS execution requires a lease from this environment.");
    const scope = key(params, params.lease.providerLeaseId);
    if (shuttingDown || closing.has(scope) || unconfirmedCleanup.has(scope)) throw new Error("CreateOS lease is closing or requires cleanup.");
    const config = parseConfig(params.config);
    const timeoutMs = timeoutOverride ?? config.timeoutMs;
    if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86_400_000) throw new Error("Invalid CreateOS command timeout.");
    const controller = new AbortController();
    let finish!: () => void;
    const entry: Active = { controller, done: new Promise<void>((resolve) => { finish = resolve; }) };
    const calls = active.get(scope) ?? new Set<Active>();
    calls.add(entry);
    active.set(scope, calls);
    try {
      return await work(new CreateosClient(config), AbortSignal.any([controller.signal, AbortSignal.timeout(timeoutMs)]));
    } catch (error) {
      if (error instanceof CreateosCleanupError) unconfirmedCleanup.add(scope);
      throw error;
    } finally {
      calls.delete(entry);

View on GitHub (pinned to 3f1d897a7c)