{"record":{"id":"2cc91d4b712d79f6","repo":"paperclipai/paperclip","slug":"sandbox-duplex-channel-needs-a-plugin-id-a-provid","errorCode":null,"errorMessage":"Sandbox duplex channel needs a plugin id, a provider key, and a provider lease id on the lease.","messagePattern":"Sandbox duplex channel needs a plugin id, a provider key, and a provider lease id on the lease\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/environment-runtime.ts","lineNumber":2487,"sourceCode":"    },\n\n    async syncOut(input) {\n      return await callPluginEnvironmentSync(\"environmentSyncOut\", input);\n    },\n\n    async openDuplexChannel(input) {\n      // Plugin-backed sandbox providers only: open the host-owned duplex route on\n      // the plugin worker. The lease scope mirrors the sandbox execute path — the\n      // provider driver key, the company, the environment, and the provider lease\n      // id — so the route binds to the same worker session the runner streams.\n      if (!input.lease.metadata?.sandboxProviderPlugin || !pluginWorkerManager) {\n        throw new Error(\"Sandbox driver does not support duplex channels for this lease.\");\n      }\n      const pluginId = readString(input.lease.metadata?.pluginId);\n      const providerKey = readString(input.lease.metadata?.provider);\n      const providerLeaseId = readString(input.lease.providerLeaseId);\n      if (!pluginId || !providerKey || !providerLeaseId) {\n        throw new Error(\n          \"Sandbox duplex channel needs a plugin id, a provider key, and a provider lease id on the lease.\",\n        );\n      }\n      const worker = pluginWorkerManager.getWorker(pluginId);\n      if (!worker) {\n        throw new Error(`Plugin worker \"${pluginId}\" is not running for the duplex channel.`);\n      }\n      const managerInput: WorkerManagerDuplexChannelOpenInput = {\n        driverKey: providerKey,\n        companyId: input.lease.companyId,\n        environmentId: input.environment.id,\n        providerLeaseId,\n        command: input.command,\n      };\n      const session = await worker.openDuplexChannel(managerInput);\n      return adaptDuplexChannelHostSession(session);\n    },\n","sourceCodeStart":2469,"sourceCodeEnd":2505,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/services/environment-runtime.ts#L2469-L2505","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait driver.openDuplexChannel({ lease, environment, command }); // throws: needs plugin id, provider key, provider lease id\n\n// after\nconst ok = typeof lease.metadata?.pluginId === \"string\"\n  && typeof lease.metadata?.provider === \"string\"\n  && typeof lease.providerLeaseId === \"string\";\nif (!ok) lease = await acquireFreshPluginLease(environment);\nawait driver.openDuplexChannel({ lease, environment, command });","handlingStrategy":"validation","validationCode":"const { pluginId, provider } = (lease.metadata ?? {}) as { pluginId?: unknown; provider?: unknown };\nif (typeof pluginId !== \"string\" || typeof provider !== \"string\" || typeof lease.providerLeaseId !== \"string\") { lease = await acquireFreshPluginSandboxLease(environment); }","typeGuard":"function hasDuplexLeaseIdentity(lease: { metadata?: Record<string, unknown> | null; providerLeaseId?: string | null }): boolean {\n  return typeof lease.metadata?.pluginId === \"string\"\n    && typeof lease.metadata?.provider === \"string\"\n    && typeof lease.providerLeaseId === \"string\";\n}","tryCatchPattern":"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; }","preventionTips":["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."],"tags":["sandbox","duplex-channel","lease-metadata","plugin","validation"],"backgroundTag":"missing-metadata-field","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}