paperclipai/paperclip · error · Error

Daytona syncOut requires a provider lease ID.

Error message

Daytona syncOut requires a provider lease ID.

What it means

Mirror of the syncIn guard for outbound transfer. onEnvironmentSyncOut throws at the top of the hook when params.lease.providerLeaseId is falsy, because downloading files from the sandbox requires resolving a specific live sandbox handle via its id.

Source

Thrown at packages/plugins/sandbox-providers/daytona/src/plugin.ts:2495

      const sandbox = await getSandbox(scope, { bypassTeardownGate: true });
      await ensureSandboxStarted(sandbox, timeoutSeconds);
      const result = await performSyncIn({
        sandbox,
        operations: params.operations,
        remoteDir,
        timeoutSeconds,
      });
      sandboxHandleCache.markFresh(scope);
      return result;
    });
  },

  // Opt-in native outbound transfer. See onEnvironmentSyncIn.
  async onEnvironmentSyncOut(
    params: PluginEnvironmentSyncOutParams,
  ): Promise<PluginEnvironmentSyncResult> {
    if (!params.lease.providerLeaseId) {
      throw new Error("Daytona syncOut requires a provider lease ID.");
    }
    const config = parseDriverConfig(params.config);
    const remoteDir = resolveSyncRemoteDir(params.lease);
    const timeoutSeconds = toTimeoutSeconds(config.timeoutMs);
    const scope = {
      driverKey: params.driverKey,
      companyId: params.companyId,
      environmentId: params.environmentId,
      providerLeaseId: params.lease.providerLeaseId,
      config,
    };
    return await withSandboxActivityGate(scope, async () => {
      const sandbox = await getSandbox(scope, { bypassTeardownGate: true });
      await ensureSandboxStarted(sandbox, timeoutSeconds);
      const result = await performSyncOut({
        sandbox,
        operations: params.operations,
        remoteDir,

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Ensure the lease passed to syncOut is the same one returned by acquire/resume and still carries providerLeaseId.
  2. Order teardown after syncOut completes; do not release/destroy the lease before artifact download.
  3. Add a caller-side guard that skips syncOut when providerLeaseId is empty rather than invoking the hook.

Example fix

// before
await plugin.onEnvironmentSyncOut({
  ...params,
  lease: { providerLeaseId: '' },
});

// after
if (!lease.providerLeaseId) return skipSyncOut();
await plugin.onEnvironmentSyncOut({ ...params, lease });
Defensive patterns

Strategy: validation

Validate before calling

function assertLeaseForSyncOut(lease: { providerLeaseId?: string | null }): string {
  if (!lease.providerLeaseId) {
    throw new Error('Cannot syncOut without a provider lease.');
  }
  return lease.providerLeaseId;
}

Type guard

function hasProviderLease(
  l: { providerLeaseId?: string | null },
): l is { providerLeaseId: string } {
  return typeof l.providerLeaseId === 'string' && l.providerLeaseId.length > 0;
}

Try / catch

try {
  await plugin.onEnvironmentSyncOut(params);
} catch (err) {
  if (err instanceof Error && /syncOut requires a provider lease ID/.test(err.message)) {
    // lease was released mid-run; re-acquire, then retry once
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling onEnvironmentSyncOut with a lease missing providerLeaseId, e.g. after the sandbox was torn down, or before acquire/resume returned an id, or with a lease object built client-side without the id.

Common situations: Job epilogues that try to fetch artifacts after the lease was already released; a lease-rotation bug handing an empty lease to the outbound-transfer step; concurrent teardown racing with syncOut.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/33d1e97ea24e872e. Report an issue: GitHub.