paperclipai/paperclip · error

Environment driver " " does not support duplex channels.

Error message

Environment driver "${driver.driver}" does not support duplex channels.

What it means

Thrown by the environment runtime's openDuplexChannel facade when the resolved driver for the lease does not implement openDuplexChannel (server/src/services/environment-runtime.ts:3474). Driver support is optional: only the plugin-backed sandbox driver provides duplex channels today; drivers like the local process driver expose syncOut/execute only. The check mirrors the native file sync guard above it ("does not support native file sync").

Solutions

  1. Feature-detect before calling: check typeof driver.openDuplexChannel === "function" or gate on the lease being a plugin sandbox lease.
  2. Use the driver's supported execution path (execute) when duplex is unavailable.
  3. Provision the environment with the plugin sandbox provider driver if duplex streaming is required.
  4. For custom drivers, implement openDuplexChannel (and effectiveSandboxCapabilities) to opt into the feature.

Example fix

// before
const ch = await envRuntime.openDuplexChannel({ lease, environment, command }); // throws for local driver

// after
const duplexSupported = typeof getDriver(getLeaseDriverKey(lease, environment))?.openDuplexChannel === "function";
if (!duplexSupported) {
  await envRuntime.execute({ lease, environment, command });
} else {
  const ch = await envRuntime.openDuplexChannel({ lease, environment, command });
}
Defensive patterns

Strategy: type-guard

Validate before calling

const driverKey = getLeaseDriverKey(lease, environment);
const driver = requireDriverKey(driverKey);
if (typeof driver.openDuplexChannel !== "function") { return await runtime.execute({ lease, environment, command }); }

Type guard

function driverSupportsDuplex(driver: { openDuplexChannel?: unknown }): boolean {
  return typeof driver.openDuplexChannel === "function";
}

Try / catch

try { return await runtime.openDuplexChannel(input); } catch (err) { if (/does not support duplex channels/.test((err as Error).message)) { return await runtime.execute(input); /* degrade to one-shot */ } throw err; }

Prevention

When it happens

Trigger: Calling openDuplexChannel on the runtime facade for a lease whose driver key resolves to a driver without an openDuplexChannel method — e.g. a local/docker environment lease, or a plugin sandbox driver instantiated without a pluginWorkerManager.

Common situations: Code written against plugin sandboxes run against local dev environments; a custom driver registered without duplex support; attempting interactive streaming against a driver that only supports one-shot execute.

Related errors


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

Appendix: source

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

      const noCapabilities = () =>
        classifyEnvironmentCapabilities({
          verifiedMethods: [],
          declared: null,
          narrowing: null,
          supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.plugin.supportedCapabilities,
        });

      if (!pluginId || !driverKey) {
        // The lease carries no plugin pin, so there is no exact plugin to read
        // the declaration or the live worker methods from. Fail closed: resolve
        // every capability false.
        return noCapabilities();
      }

      // Read the declaration from the exact plugin that acquired the lease, not
      // the plugin the environment's current config names. A plugin uninstall,
      // a manifest change, or a config edit between the acquire and this call
      // must not let a stale declaration or a different plugin's worker grant a
      // capability this lease never verified.
      const plugin = await pluginRegistry.getById(pluginId);
      const declaredDriver =
        plugin && plugin.status === "ready"
          ? plugin.manifestJson.environmentDrivers?.find((candidate) => candidate.driverKey === driverKey)
          : undefined;
      if (!plugin || plugin.status !== "ready" || !declaredDriver) {
        return noCapabilities();
      }

      // Read the live worker method list fresh on every call. A worker restart
      // can drop or add a method between the acquire and this call, so the
      // runtime never trusts a cached method list for a capability grant.
      const verifiedMethods = workerManager.getWorker(plugin.id)?.supportedMethods ?? [];
      const declared = resolveDeclaredSandboxCapabilities(declaredDriver);
      const narrowing = buildSandboxCapabilityNarrowing({
        leasePolicy: input.lease.leasePolicy,
        leaseMetadata: metadata,

View on GitHub (pinned to 3f1d897a7c)