paperclipai/paperclip · error

Sandbox driver does not support duplex channels for this…

Error message

Sandbox driver does not support duplex channels for this lease.

What it means

Thrown by the plugin-backed sandbox environment driver's openDuplexChannel when the lease was not created by a plugin sandbox provider or the server has no plugin worker manager. Duplex channels (bidirectional command streaming into a sandbox) are only implemented as a host-owned route on a plugin worker; the guard at server/src/services/environment-runtime.ts:2481 requires lease.metadata.sandboxProviderPlugin to be set and a pluginWorkerManager to exist, otherwise the operation is unsupported for that lease.

Solutions

  1. Verify the lease is a plugin-provider sandbox lease: its metadata must include sandboxProviderPlugin (set at provisioning time by the plugin sandbox driver).
  2. Ensure the server runtime was initialized with a pluginWorkerManager and the sandbox plugin is enabled in server config.
  3. Provision the environment through the plugin sandbox provider so leases carry the required metadata before requesting duplex channels.
  4. If you only need one-shot command execution, use the regular sandbox execute path which does not require a duplex channel.

Example fix

// before
const channel = await runtime.openDuplexChannel({ lease, environment, command }); // throws: driver does not support duplex channels

// after
if (!lease.metadata?.sandboxProviderPlugin) {
  const result = await runtime.execute({ lease, environment, command }); // one-shot path
} else {
  const channel = await runtime.openDuplexChannel({ lease, environment, command });
}
Defensive patterns

Strategy: type-guard

Validate before calling

const isPluginLease = Boolean(lease.metadata?.sandboxProviderPlugin);
const pluginWorkersEnabled = Boolean(serverFeatures.pluginWorkerManager);
if (!isPluginLease || !pluginWorkersEnabled) { await runtime.execute({ lease, environment, command }); } // one-shot fallback path

Type guard

function supportsPluginDuplexChannel(lease: { metadata?: Record<string, unknown> | null }): boolean {
  return lease.metadata?.sandboxProviderPlugin === true || typeof lease.metadata?.sandboxProviderPlugin === "string";
}

Try / catch

try { return await driver.openDuplexChannel(input); } catch (err) { if (/does not support duplex channels/.test(String((err as Error).message))) { return await runtime.execute(input); } throw err; }

Prevention

When it happens

Trigger: Calling openDuplexChannel on the environment runtime with a lease whose metadata lacks sandboxProviderPlugin (a local/docker/inline lease routed to the plugin adapter), or when the server was constructed without a pluginWorkerManager (plugin subsystem disabled) even though the lease metadata claims a plugin provider.

Common situations: Agent tooling attempts to stream commands into a sandbox that was provisioned by a non-plugin driver; the feature flag/config enabling plugin workers is off in that deployment; the lease was created before the plugin sandbox provider existed (old metadata shape).

Related errors


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

Appendix: source

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

          },
          resolvePluginSandboxRpcTimeoutMs(workerConfig),
        );
      }

      // Built-in provider path. Resolve the recorded config secrets through the
      // 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

View on GitHub (pinned to 3f1d897a7c)