/** E2B filesystem and process backend for the harness LSP capability seam. */ import { Buffer } from 'node:buffer' import { posix } from 'node:path' import type { Context } from 'cordis' import z from 'schemastery' import { FileType, quoteE2BShellArg, resolveE2BExecutable, } from '@deepseek-ai/dsh-e2b' import type { Sandbox } from '@deepseek-ai/dsh-e2b' import { LspError, LspProviderId } from '@deepseek-ai/dsh-lsp' import type { LspProvider, LspProviderQuery, LspQueryResult } from '@deepseek-ai/dsh-lsp' import { LspInstance } from '@deepseek-ai/dsh-lsp-local' import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess' import E2BSubprocessService from '@deepseek-ai/dsh-subprocess-e2b' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import { LSP_PROXY_SOURCE } from './proxy-source.ts' import { E2BLspTransport } from './transport.ts' export { E2BLspTransport } from './transport.ts' /** Cordis plugin name. */ export const name = 'lsp-e2b' /** Services required by the remote provider. */ export const inject = ['e2b', 'lsp', 'subprocess'] const DEFAULT_MAX_MESSAGE_BYTES = 16_000_000 const DEFAULT_MAX_STDERR_BYTES = 1_000_000 const DEFAULT_MAX_DOCUMENT_BYTES = 4_000_000 const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000 const DEFAULT_KILL_GRACE_MS = 2_000 /* jscpd:ignore-start -- Loader requires each backend to expose its own statically walkable server schema. */ /** One configured language server inside the shared E2B sandbox. */ export interface LspE2BServerConfig { /** Remote executable, absolute or resolved on the sandbox PATH. */ command: string /** Lowercase leading-dot extension to LSP language id. */ extensionToLanguage: Record /** Remote executable arguments. */ args?: string[] /** Explicit remote environment overrides. */ env?: Record /** Static `initialize` options. */ initializationOptions?: unknown /** Static answer to every `workspace/configuration` item. */ configuration?: unknown /** Largest LSP message accepted from the server. */ maxMessageBytes?: number /** Largest remote stderr tail retained for diagnostics. */ maxStderrBytes?: number /** Largest remote source opened for one query. */ maxDocumentBytes?: number /** Graceful LSP shutdown budget. */ shutdownTimeoutMs?: number /** Request-cancel and TERM-to-KILL grace. */ killGraceMs?: number } /** Plugin configuration. */ export interface Config { /** Non-empty provider-id to remote-server table. */ servers: Record } type ResolvedServerConfig = Required const ServerConfig: z = z.object({ command: z.string().required(), args: z.array(String).default([]), env: z.dict(String).default({}), extensionToLanguage: z.dict(String).required(), initializationOptions: z.any().default(null), configuration: z.any().default(null), maxMessageBytes: z.number().default(DEFAULT_MAX_MESSAGE_BYTES), maxStderrBytes: z.number().default(DEFAULT_MAX_STDERR_BYTES), maxDocumentBytes: z.number().default(DEFAULT_MAX_DOCUMENT_BYTES), shutdownTimeoutMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_SHUTDOWN_TIMEOUT_MS), killGraceMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_KILL_GRACE_MS), }) /** Schemastery plugin configuration. */ export const Config: z = z.object({ servers: z.dict(ServerConfig).required(), }) /* jscpd:ignore-end */ interface RemoteSource { canonicalPath: string text: string } function abortReason(signal: AbortSignal): unknown { try { signal.throwIfAborted() } catch (error: unknown) { return error } return new DOMException('The operation was aborted', 'AbortError') } function abortable(promise: Promise, signal?: AbortSignal): Promise { if (signal === undefined) return promise // AbortSignal permits opaque reasons, and callers observe the exact reason. // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors if (signal.aborted) return Promise.reject(abortReason(signal)) return new Promise((resolve, reject) => { const onAbort = (): void => { cleanup() // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- Preserve the signal's exact opaque reason. reject(abortReason(signal)) } const cleanup = (): void => { signal.removeEventListener('abort', onAbort) } signal.addEventListener('abort', onAbort, { once: true }) void promise.then( (value) => { cleanup(); resolve(value) }, (error: unknown) => { cleanup(); reject(error instanceof Error ? error : new Error(String(error))) }, ) }) } function validateServerConfig(providerId: string, config: ResolvedServerConfig): void { if (config.command.length === 0) throw new Error(`lsp-e2b: servers.${providerId}.command must be non-empty`) for (const name of ['maxMessageBytes', 'maxStderrBytes', 'maxDocumentBytes', 'shutdownTimeoutMs', 'killGraceMs'] as const) { const value = config[name] if (!Number.isSafeInteger(value) || value <= 0 || (name.endsWith('Ms') && value > MAX_TIMER_DELAY_MS)) { throw new Error(`lsp-e2b: servers.${providerId}.${name} must be a positive safe integer${name.endsWith('Ms') ? ` no greater than ${MAX_TIMER_DELAY_MS}` : ''}`) } } } async function canonicalRemotePath(sandbox: Sandbox, path: string, signal?: AbortSignal): Promise { signal?.throwIfAborted() const result = await sandbox.commands.run(`realpath -e -- ${quoteE2BShellArg(path)}`, signal === undefined ? {} : { signal }) signal?.throwIfAborted() const canonical = result.stdout.trim() if (!posix.isAbsolute(canonical) || canonical.includes('\n')) throw new Error(`remote path ${JSON.stringify(path)} did not resolve canonically`) return canonical } /** * Canonicalize and validate one workspace inside E2B. * @param sandbox - Shared sandbox that owns the workspace. * @param workspaceRoot - Remote workspace path supplied by the query. * @param signal - Optional query cancellation signal. * @returns The canonical remote directory path. */ export async function canonicalizeE2BWorkspace( sandbox: Sandbox, workspaceRoot: string, signal?: AbortSignal, ): Promise { const canonical = await canonicalRemotePath(sandbox, workspaceRoot, signal) const info = await sandbox.files.getInfo(canonical, signal === undefined ? {} : { signal }) signal?.throwIfAborted() if (info.type !== FileType.DIR) throw new Error(`workspace root ${JSON.stringify(workspaceRoot)} is not a directory`) return canonical } /** * Resolve, contain, and read one UTF-8 query source inside E2B. * @param sandbox - Shared sandbox that owns the source. * @param filePath - Absolute path or path relative to the canonical workspace. * @param workspace - Canonical remote workspace directory. * @param maxDocumentBytes - Maximum source size before and after reading. * @param signal - Optional query cancellation signal. * @returns The canonical source path and decoded text. */ export async function readE2BSource( sandbox: Sandbox, filePath: string, workspace: string, maxDocumentBytes: number, signal?: AbortSignal, ): Promise { const requested = posix.isAbsolute(filePath) ? filePath : posix.resolve(workspace, filePath) const canonicalPath = await canonicalRemotePath(sandbox, requested, signal) const relative = posix.relative(workspace, canonicalPath) if (relative === '..' || relative.startsWith('../') || posix.isAbsolute(relative)) { throw new Error(`source ${JSON.stringify(filePath)} resolves outside the workspace`) } const info = await sandbox.files.getInfo(canonicalPath, signal === undefined ? {} : { signal }) if (info.type !== FileType.FILE) throw new Error(`source ${JSON.stringify(filePath)} is not a regular file`) if (info.size > maxDocumentBytes) { throw new Error(`source ${JSON.stringify(filePath)} is ${info.size} bytes, over the ${maxDocumentBytes}-byte limit`) } const bytes = await sandbox.files.read(canonicalPath, { format: 'bytes', ...signal === undefined ? {} : { signal } }) signal?.throwIfAborted() if (bytes.length > maxDocumentBytes) { throw new Error(`source ${JSON.stringify(filePath)} grew past the ${maxDocumentBytes}-byte limit while reading`) } let text: string try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes) } catch (error: unknown) { throw new Error(`source ${JSON.stringify(filePath)} is not valid UTF-8 text`, { cause: error }) } return { canonicalPath, text } } /* jscpd:ignore-start -- Provider identity mirrors the seam while remote source and process ownership stay local. */ /** One pooled remote provider with an isolated server per canonical workspace. */ export class E2BLspProvider implements LspProvider { readonly id: LspProviderId readonly extensionToLanguage: Readonly> private readonly instances = new Map() private readonly queues = new Map>() private disposed = false constructor( providerId: string, private readonly sandbox: Sandbox, private readonly subprocess: E2BSubprocessService, private readonly config: ResolvedServerConfig, private readonly executable: string, private readonly nodeExecutable: string, private readonly proxyPath: string, ) { this.id = LspProviderId(providerId) this.extensionToLanguage = config.extensionToLanguage } /* jscpd:ignore-end */ async query(request: LspProviderQuery, signal?: AbortSignal): Promise { this.assertActive(signal) const workspace = await canonicalizeE2BWorkspace(this.sandbox, request.workspaceRoot, signal) this.assertActive(signal) return this.enqueue(workspace, signal, async () => { this.assertActive(signal) const source = await readE2BSource(this.sandbox, request.filePath, workspace, this.config.maxDocumentBytes, signal) this.assertActive(signal) let instance = this.instanceFor(workspace) try { return await instance.query(request, source, signal) } catch (error: unknown) { if (!instance.isTransportFailure(error)) throw error await instance.dispose() this.evict(workspace, instance) this.assertActive(signal) instance = this.instanceFor(workspace) return await instance.query(request, source, signal) } finally { if (instance.dead) { await instance.dispose() this.evict(workspace, instance) } } }) } /* jscpd:ignore-start -- Queue and pooling semantics are shared; transport failure and disposal identities differ. */ /** Stop accepting work and await every remote server and queued query. */ async disposeAll(): Promise { this.disposed = true const instances = [...this.instances.values()] const queues = [...this.queues.values()] this.instances.clear() await Promise.all([...instances.map(instance => instance.dispose()), ...queues]) this.queues.clear() } private assertActive(signal?: AbortSignal): void { if (this.disposed) throw new LspError('lsp-e2b provider is disposed', 'LSP_DISPOSED') signal?.throwIfAborted() } private enqueue(workspace: string, signal: AbortSignal | undefined, run: () => Promise): Promise { const previous = this.queues.get(workspace) ?? Promise.resolve() const result = abortable(previous, signal).then(run) const tail = previous.then(() => result).then(() => undefined, () => undefined) this.queues.set(workspace, tail) void tail.then(() => { if (this.queues.get(workspace) === tail) this.queues.delete(workspace) }) return result } private instanceFor(workspace: string): LspInstance { this.assertActive() const existing = this.instances.get(workspace) if (existing !== undefined) return existing const created = this.createInstance(workspace) this.instances.set(workspace, created) return created } /* jscpd:ignore-end */ private createInstance(workspace: string): LspInstance { return new LspInstance({ command: this.executable, args: this.config.args, cwd: workspace, env: this.config.env, configuration: this.config.configuration, initializationOptions: this.config.initializationOptions, maxMessageBytes: this.config.maxMessageBytes, maxStderrBytes: this.config.maxStderrBytes, shutdownTimeoutMs: this.config.shutdownTimeoutMs, killGraceMs: this.config.killGraceMs, clientProcessId: null, }, (spec: SubprocessSpawnSpec) => { const originalArgv = Buffer.from(JSON.stringify(spec.argv)).toString('base64') const inner = this.subprocess.spawn({ ...spec, argv: [this.nodeExecutable, this.proxyPath, originalArgv], stdio: { stdin: 'pipe', stdout: 'pipe', stderr: { maxBytes: this.config.maxStderrBytes }, }, }) const rawBound = Math.max(this.config.maxMessageBytes, this.config.maxStderrBytes) return new E2BLspTransport(inner, rawBound * 2 + 1024, this.config.maxStderrBytes) }) } private evict(workspace: string, instance: LspInstance): void { if (this.instances.get(workspace) === instance) this.instances.delete(workspace) } } /** Install the proxy, resolve remote commands, and atomically register providers. */ export async function apply(ctx: Context, config: Config): Promise { if (!(ctx.subprocess instanceof E2BSubprocessService)) { throw new Error('lsp-e2b requires @deepseek-ai/dsh-subprocess-e2b as ctx.subprocess') } const subprocess = ctx.subprocess const entries = Object.entries(config.servers) if (entries.length === 0) throw new Error('lsp-e2b: servers must contain at least one server') const sandbox = await ctx.e2b.getSandbox() const proxyPath = posix.join(ctx.e2b.runtimeRoot, 'lsp-stdio-proxy.mjs') await sandbox.files.write([{ path: proxyPath, data: LSP_PROXY_SOURCE }]) await sandbox.commands.run(`chmod 600 -- ${quoteE2BShellArg(proxyPath)}`) const nodeExecutable = await resolveE2BExecutable(sandbox, 'node') const providers = await Promise.all(entries.map(async ([providerId, raw]) => { if (providerId.trim() === '') throw new Error('lsp-e2b: server ids must be non-empty strings') const resolved = raw as ResolvedServerConfig validateServerConfig(providerId, resolved) const executable = await resolveE2BExecutable(sandbox, resolved.command) return new E2BLspProvider(providerId, sandbox, subprocess, resolved, executable, nodeExecutable, proxyPath) })) /* jscpd:ignore-start -- Every provider table publishes atomically through the same registry contract. */ ctx.effect(() => { const disposers: Array<() => void> = [] try { for (const provider of providers) disposers.push(ctx.lsp.registerProvider(provider)) } catch (error: unknown) { for (const dispose of disposers.reverse()) dispose() throw error } return async () => { for (const dispose of disposers.reverse()) dispose() await Promise.all(providers.map(provider => provider.disposeAll())) } }, 'lsp-e2b.registerProviders') /* jscpd:ignore-end */ }