paperclipai/paperclip · error

Sandbox duplex channel needs a plugin id, a provider key…

Error message

Sandbox duplex channel needs a plugin id, a provider key, and a provider lease id on the lease.

What it means

Thrown by the plugin sandbox driver's openDuplexChannel when the lease metadata is incomplete: it needs readString(lease.metadata.pluginId), readString(lease.metadata.provider), and lease.providerLeaseId to address the correct plugin worker and provider session. Any of these missing or non-string produces this fixed error before any worker call is made (server/src/services/environment-runtime.ts:2487).

Solutions

  1. Inspect lease.metadata for pluginId and provider keys and lease.providerLeaseId — confirm all three are present strings.
  2. Re-acquire the sandbox lease from the plugin provider so the full metadata shape is written by the current driver version.
  3. Update the sandbox plugin and server together so metadata keys stay in sync.
  4. If providerLeaseId is the missing one, ensure the provider-side lease was granted before the channel open is attempted.

Example fix

// before
await driver.openDuplexChannel({ lease, environment, command }); // throws: needs plugin id, provider key, provider lease id

// after
const ok = typeof lease.metadata?.pluginId === "string"
  && typeof lease.metadata?.provider === "string"
  && typeof lease.providerLeaseId === "string";
if (!ok) lease = await acquireFreshPluginLease(environment);
await driver.openDuplexChannel({ lease, environment, command });
Defensive patterns

Strategy: validation

Validate before calling

const { pluginId, provider } = (lease.metadata ?? {}) as { pluginId?: unknown; provider?: unknown };
if (typeof pluginId !== "string" || typeof provider !== "string" || typeof lease.providerLeaseId !== "string") { lease = await acquireFreshPluginSandboxLease(environment); }

Type guard

function hasDuplexLeaseIdentity(lease: { metadata?: Record<string, unknown> | null; providerLeaseId?: string | null }): boolean {
  return typeof lease.metadata?.pluginId === "string"
    && typeof lease.metadata?.provider === "string"
    && typeof lease.providerLeaseId === "string";
}

Try / catch

try { await openDuplex(input); } catch (err) { if (/needs a plugin id, a provider key/.test((err as Error).message)) { input.lease = await acquireFreshPluginSandboxLease(env); await openDuplex(input); } else throw err; }

Prevention

When it happens

Trigger: openDuplexChannel on a lease that has sandboxProviderPlugin set but whose metadata lacks pluginId or provider, or whose providerLeaseId is unset — e.g. the provider driver stored metadata under different keys, the lease was deserialized from an older shape, or the provider lease handshake never completed.

Common situations: Plugin sandbox provider version mismatch (metadata keys renamed between plugin versions); a lease restored/persisted from an older release losing providerLeaseId; partially initialized lease where the provider grant step failed silently.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-08-21). Data as JSON: /api/errors/2cc91d4b712d79f6. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/environment-runtime.ts:2537

      // durable orphan record, not the environment binding, for the same reason
      // as the plugin path above. The teardown targets the recorded provider,
      // never the current environment provider.
      const cleanupConfig = await resolveSandboxCleanupConfigSecrets(
        db,
        input.lease.companyId,
        metadataConfig,
        { issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId },
      );
      await destroySandboxProviderLease({
        config: cleanupConfig,
        providerLeaseId: input.lease.providerLeaseId,
      });
    },

    async isPendingCleanupWorkerReady(input) {
      // Resolve the recorded provider the same way `retryPendingSandboxTeardown`
      // does, so the probe reads the same target the teardown uses.
      const recordedProvider =
        input.lease.provider ??
        (typeof input.lease.metadata?.provider === "string" ? input.lease.metadata.provider : null);
      // A missing provider is a permanent misconfiguration. Report ready, so the
      // teardown runs, throws, and counts toward the cap.
      if (!recordedProvider) return true;
      // A built-in provider has no plugin worker, so it is always ready.
      if (isBuiltinSandboxProvider(recordedProvider)) return true;
      // No worker manager is a permanent condition here. Report ready, so the
      // teardown runs, throws its own "no worker manager" error, and counts
      // toward the cap.
      if (!pluginWorkerManager) return true;
      // Resolve the installed plugin without a wait. A plugin reload or a plugin
      // reinstall can remove the plugin row for a short window, so a missing
      // plugin is a transient condition, not a permanent one. Report not ready,
      // so the sweep skips the lease without a claim, and a later sweep retries
      // after the plugin returns. A teardown while the plugin is missing only
      // throws and burns a finite attempt, so a long reload could exhaust the
      // retries and strand the sandbox. A permanent uninstall also cannot tear

View on GitHub (pinned to 3f1d897a7c)