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
- Inspect lease.metadata for pluginId and provider keys and lease.providerLeaseId — confirm all three are present strings.
- Re-acquire the sandbox lease from the plugin provider so the full metadata shape is written by the current driver version.
- Update the sandbox plugin and server together so metadata keys stay in sync.
- 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
- Treat pluginId/provider/providerLeaseId as a contract between server and plugin — keep versions aligned when either changes.
- Never persist leases across plugin upgrades without validating the metadata shape on restore.
- Unit-test the metadata shape at lease acquisition time.
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
- Sandbox driver does not support duplex channels for this…
- A sandbox command is required.
- Daytona duplex channel: no cached sandbox resolves the…
- DUPLEX_CHANNEL_CAPABILITY_DENIED
- Plugin worker " " is not running for the duplex channel.
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 tearView on GitHub (pinned to 3f1d897a7c)