{"record":{"id":"b57261444f80ea47","repo":"paperclipai/paperclip","slug":"createos-lease-does-not-belong-to-this-environment","errorCode":null,"errorMessage":"CreateOS lease does not belong to this environment.","messagePattern":"CreateOS lease does not belong to this environment\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/plugin.ts","lineNumber":107,"sourceCode":"    const calls = active.get(scope) ?? new Set<Active>();\n    calls.add(entry);\n    active.set(scope, calls);\n    try {\n      return await work(new CreateosClient(config), AbortSignal.any([controller.signal, AbortSignal.timeout(timeoutMs)]));\n    } catch (error) {\n      if (error instanceof CreateosCleanupError) unconfirmedCleanup.add(scope);\n      throw error;\n    } finally {\n      calls.delete(entry);\n      if (calls.size === 0) active.delete(scope);\n      finish();\n    }\n  }\n\n  async function release(params: PluginEnvironmentReleaseLeaseParams, destroy: boolean) {\n    const id = params.providerLeaseId;\n    if (!id) return;\n    if (!metadataMatches(params, params.leaseMetadata)) throw new Error(\"CreateOS lease does not belong to this environment.\");\n    const scope = key(params, id);\n    if (closing.has(scope)) throw new Error(\"CreateOS lease cleanup is already in progress.\");\n    closing.add(scope);\n    try {\n      await stopActive(scope);\n      const config = parseConfig(params.config);\n      const client = new CreateosClient(config);\n      if (destroy || !config.reuseLease) {\n        await client.destroySandbox(id);\n        unconfirmedCleanup.delete(scope);\n      } else {\n        if (unconfirmedCleanup.has(scope)) throw new Error(\"CreateOS process cleanup is unconfirmed; destroy this lease before reusing it.\");\n        try { await client.transition(id, \"paused\", AbortSignal.timeout(config.timeoutMs)); }\n        catch (error) { if (!(error instanceof CreateosApiError && error.status === 404)) throw error; }\n      }\n    } finally {\n      closing.delete(scope);\n    }","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/plugin.ts#L89-L125","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass the original lease.metadata unchanged as leaseMetadata when releasing — do not reconstruct or drop fields.","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.","Verify companyId/environmentId in the release params match those captured in the lease metadata.","Re-acquire a fresh lease under current config and discard the stale one if metadata reconciliation is impossible."],"exampleFix":"// before: metadata dropped when persisting the lease\nawait driver.releaseLease({ companyId, environmentId, providerLeaseId: id, leaseMetadata: undefined });\n// after: persist and replay the full metadata\nawait driver.releaseLease({\n  companyId, environmentId,\n  providerLeaseId: lease.providerLeaseId,\n  leaseMetadata: lease.metadata, // unchanged from acquire\n});","handlingStrategy":"validation","validationCode":"function canRelease(params) {\n  const m = params.leaseMetadata;\n  return Boolean(m) && m.provider === \"createos\" &&\n    m.companyId === params.companyId && m.environmentId === params.environmentId &&\n    m.apiUrl === currentConfig.apiUrl;\n}\nif (!canRelease(releaseParams)) console.warn(\"lease metadata mismatch; release will be rejected\");","typeGuard":"function hasMatchingLeaseMetadata(p): p is typeof p & { leaseMetadata: Record<string, unknown> } {\n  const m = (p as any)?.leaseMetadata;\n  return Boolean(m) && m.provider === \"createos\" && m.companyId === p.companyId && m.environmentId === p.environmentId;\n}","tryCatchPattern":"try {\n  await driver.destroyLease(releaseParams);\n} catch (e) {\n  if (e.message.includes(\"does not belong to this environment\")) {\n    logger.warn(`stale lease ${releaseParams.providerLeaseId}; destroy manually in CreateOS dashboard`);\n  } else throw e;\n}","preventionTips":["Persist lease.metadata alongside providerLeaseId and replay it exactly on release.","Snapshot the apiUrl used at acquire time with the lease so stale leases can be released with their original config.","Never hand-build release params from partial data; always carry through what acquire returned.","On metadata mismatch, prefer dashboard/API-side destruction over blind re-acquisition to avoid orphaned sandboxes."],"tags":["lease","ownership","metadata-mismatch","cleanup"],"backgroundTag":"permission-denied","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}