paperclipai/paperclip · error
DUPLEX_CHANNEL_CAPABILITY_DENIED
DUPLEX_CHANNEL_CAPABILITY_DENIED
Error message
Sandbox lease does not grant the duplex command stream capability.
What it means
Authorization refusal for duplex channels, centralized in the runtime facade: before delegating to the driver it resolves the lease's effective sandbox capability snapshot via driver.effectiveSandboxCapabilities and requires the opt-in duplexCommandStream capability to be exactly true (server/src/services/environment-runtime.ts:3485, constant DUPLEX_CHANNEL_CAPABILITY_DENIED). A missing snapshot, a snapshot without the capability, or a driver that cannot resolve capabilities all fail closed — unauthorized leases never reach the worker.
Solutions
- Grant the duplexCommandStream capability in the environment/sandbox configuration the lease is derived from, then acquire a new lease.
- Verify with the driver's effectiveSandboxCapabilities (or equivalent API) that the resolved snapshot shows duplexCommandStream === true before opening the channel.
- If the snapshot unexpectedly resolves to null, fix the capability resolution path on the driver — absence is treated as denial by design.
- Callers that do not need interactive streaming should use execute instead of requesting the capability.
Example fix
// before
await envRuntime.openDuplexChannel({ lease, environment, command }); // DUPLEX_CHANNEL_CAPABILITY_DENIED
// after (opt in at environment config / lease acquisition)
const lease = await acquireSandboxLease(environment, {
capabilities: { duplexCommandStream: true },
});
await envRuntime.openDuplexChannel({ lease, environment, command }); Defensive patterns
Strategy: validation
Validate before calling
const effective = (await driver.effectiveSandboxCapabilities?.({ lease, environment, command })) ?? null;
if (effective?.duplexCommandStream !== true) throw new Error("Lease lacks duplexCommandStream; opt the environment/lease in before opening a channel."); Type guard
function leaseGrantsDuplex(snapshot: { duplexCommandStream?: unknown } | null | undefined): boolean {
return snapshot?.duplexCommandStream === true;
} Try / catch
try { return await runtime.openDuplexChannel(input); } catch (err) { if ((err as Error).message === "Sandbox lease does not grant the duplex command stream capability.") { throw new PermissionError("Opt the lease into duplexCommandStream and re-acquire it."); } throw err; } Prevention
- Opt leases into duplexCommandStream explicitly at acquisition; absence is denial by design.
- Do not request the capability broadly — grant it only to leases that need interactive streaming.
- Re-check the effective snapshot after capability config changes; leases do not gain capabilities retroactively.
When it happens
Trigger: openDuplexChannel on a lease whose effective capability snapshot does not include duplexCommandStream: true — capability never opted in at lease/environment creation, revoked since acquisition, or the driver cannot resolve a snapshot (returns undefined → null → denied). Reached only after the driver-support check passed.
Common situations: New deployments enabling duplex-based tooling without opting leases into the capability; capability gates rolled out server-side before configurations were updated; tests using bare leases without capability metadata.
Related errors
- Daytona duplex channel: no cached sandbox resolves the…
- Plugin worker " " is not running for the duplex channel.
- Sandbox driver does not support duplex channels for this…
- Sandbox duplex channel needs a plugin id, a provider key…
- A sandbox command is required.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-08-21).
Data as JSON: /api/errors/ec727a874bad4398.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/environment-runtime.ts:3546
// 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,
});
return classifyEnvironmentCapabilities({
verifiedMethods,
declared,
narrowing,
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.plugin.supportedCapabilities,
});
},
};
}View on GitHub (pinned to 3f1d897a7c)