paperclipai/paperclip · error

CreateOS lease does not belong to this environment.

Error message

CreateOS lease does not belong to this environment.

What it means

release() verifies that the lease being released (or destroyed) actually belongs to the calling environment by checking leaseMetadata against params (provider 'createos', same companyId, environmentId, and apiUrl from current config). This error is thrown when releasing a lease whose metadata does not match the environment issuing the release, preventing cross-environment destruction of sandboxes.

Solutions

  1. Pass the original lease.metadata unchanged as leaseMetadata when releasing — do not reconstruct or drop fields.
  2. If config changed, release using the config values that were active when the lease was acquired, or destroy the sandbox directly in the CreateOS dashboard.
  3. Verify companyId/environmentId in the release params match those captured in the lease metadata.
  4. Re-acquire a fresh lease under current config and discard the stale one if metadata reconciliation is impossible.

Example fix

// before: metadata dropped when persisting the lease
await driver.releaseLease({ companyId, environmentId, providerLeaseId: id, leaseMetadata: undefined });
// after: persist and replay the full metadata
await driver.releaseLease({
  companyId, environmentId,
  providerLeaseId: lease.providerLeaseId,
  leaseMetadata: lease.metadata, // unchanged from acquire
});
Defensive patterns

Strategy: validation

Validate before calling

function canRelease(params) {
  const m = params.leaseMetadata;
  return Boolean(m) && m.provider === "createos" &&
    m.companyId === params.companyId && m.environmentId === params.environmentId &&
    m.apiUrl === currentConfig.apiUrl;
}
if (!canRelease(releaseParams)) console.warn("lease metadata mismatch; release will be rejected");

Type guard

function hasMatchingLeaseMetadata(p): p is typeof p & { leaseMetadata: Record<string, unknown> } {
  const m = (p as any)?.leaseMetadata;
  return Boolean(m) && m.provider === "createos" && m.companyId === p.companyId && m.environmentId === p.environmentId;
}

Try / catch

try {
  await driver.destroyLease(releaseParams);
} catch (e) {
  if (e.message.includes("does not belong to this environment")) {
    logger.warn(`stale lease ${releaseParams.providerLeaseId}; destroy manually in CreateOS dashboard`);
  } else throw e;
}

Prevention

When it happens

Trigger: onEnvironmentReleaseLease or onEnvironmentDestroyLease is called with params.providerLeaseId set but params.leaseMetadata missing, not provider 'createos', or with companyId/environmentId/apiUrl differing from params; commonly after config (apiUrl) changed since the lease was created.

Common situations: Releasing a persisted lease after the CreateOS connection's apiUrl was edited; host cleanup job releasing leases from environment A under environment B's credentials; passing null/undefined leaseMetadata because the lease record was restored from an old schema version; manually constructing release params without copying the original metadata.

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/b57261444f80ea47. Report an issue: GitHub.

Appendix: source

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

    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);
      if (calls.size === 0) active.delete(scope);
      finish();
    }
  }

  async function release(params: PluginEnvironmentReleaseLeaseParams, destroy: boolean) {
    const id = params.providerLeaseId;
    if (!id) return;
    if (!metadataMatches(params, params.leaseMetadata)) throw new Error("CreateOS lease does not belong to this environment.");
    const scope = key(params, id);
    if (closing.has(scope)) throw new Error("CreateOS lease cleanup is already in progress.");
    closing.add(scope);
    try {
      await stopActive(scope);
      const config = parseConfig(params.config);
      const client = new CreateosClient(config);
      if (destroy || !config.reuseLease) {
        await client.destroySandbox(id);
        unconfirmedCleanup.delete(scope);
      } else {
        if (unconfirmedCleanup.has(scope)) throw new Error("CreateOS process cleanup is unconfirmed; destroy this lease before reusing it.");
        try { await client.transition(id, "paused", AbortSignal.timeout(config.timeoutMs)); }
        catch (error) { if (!(error instanceof CreateosApiError && error.status === 404)) throw error; }
      }
    } finally {
      closing.delete(scope);
    }

View on GitHub (pinned to 3f1d897a7c)