{"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":2537,"sourceCode":"      // durable orphan record, not the environment binding, for the same reason\n      // as the plugin path above. The teardown targets the recorded provider,\n      // never the current environment provider.\n      const cleanupConfig = await resolveSandboxCleanupConfigSecrets(\n        db,\n        input.lease.companyId,\n        metadataConfig,\n        { issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId },\n      );\n      await destroySandboxProviderLease({\n        config: cleanupConfig,\n        providerLeaseId: input.lease.providerLeaseId,\n      });\n    },\n\n    async isPendingCleanupWorkerReady(input) {\n      // Resolve the recorded provider the same way `retryPendingSandboxTeardown`\n      // does, so the probe reads the same target the teardown uses.\n      const recordedProvider =\n        input.lease.provider ??\n        (typeof input.lease.metadata?.provider === \"string\" ? input.lease.metadata.provider : null);\n      // A missing provider is a permanent misconfiguration. Report ready, so the\n      // teardown runs, throws, and counts toward the cap.\n      if (!recordedProvider) return true;\n      // A built-in provider has no plugin worker, so it is always ready.\n      if (isBuiltinSandboxProvider(recordedProvider)) return true;\n      // No worker manager is a permanent condition here. Report ready, so the\n      // teardown runs, throws its own \"no worker manager\" error, and counts\n      // toward the cap.\n      if (!pluginWorkerManager) return true;\n      // Resolve the installed plugin without a wait. A plugin reload or a plugin\n      // reinstall can remove the plugin row for a short window, so a missing\n      // plugin is a transient condition, not a permanent one. Report not ready,\n      // so the sweep skips the lease without a claim, and a later sweep retries\n      // after the plugin returns. A teardown while the plugin is missing only\n      // throws and burns a finite attempt, so a long reload could exhaust the\n      // retries and strand the sandbox. A permanent uninstall also cannot tear","sourceCodeStart":2519,"sourceCodeEnd":2555,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/environment-runtime.ts#L2519-L2555","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":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-08-21T17:58:32.592Z","contentChangedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}