Add E2B PTY, LSP, and code runtime providers

This commit is contained in:
Tianyi Cui
2026-07-28 13:59:47 +08:00
parent e7b682f1f6
commit 6667102890
82 changed files with 5462 additions and 644 deletions

View File

@@ -1,13 +1,38 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { boot } from '@deepseek-ai/dsh-app-boot'
import { AgentMessageId } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-code-runtime-e2b'
import type {} from '@deepseek-ai/dsh-e2b'
import type {} from '@deepseek-ai/dsh-fs-e2b'
import type {} from '@deepseek-ai/dsh-bash-local'
import type {} from '@deepseek-ai/dsh-lsp-e2b'
import type {} from '@deepseek-ai/dsh-pty-e2b'
const configPath = process.argv[2]
if (configPath === undefined) throw new Error('usage: bin.ts <cordis.yml>')
const ctx = await boot('e2b-composition', resolve(configPath))
const ownerFiber = ctx.plugin(() => {})
const ownerId = SessionId('e2b-live-owner')
const owner: Agent = {
id: ownerId,
options: {},
session: new Session(ownerId),
status: 'idle',
acceptsNextStep: false,
ctx: ownerFiber.ctx,
followup: () => AgentMessageId('unused'),
steer: () => AgentMessageId('unused'),
inject: () => AgentMessageId('unused'),
send: () => AgentMessageId('unused'),
cancel() {},
whenIdle: () => Promise.resolve(),
}
const unregisterOwner = ctx.agents.register(owner)
let terminalId: Awaited<ReturnType<typeof ctx.pty.spawn>>['sessionId'] | undefined
try {
const fromFs = await ctx.fs.resolve('from-fs.txt')
await ctx.fs.writeText(fromFs, 'written-by-fs\n', { kind: 'createIfAbsent' })
@@ -22,11 +47,129 @@ try {
}
const fromBash = await ctx.fs.resolve('from-bash.txt')
const fsRead = await ctx.fs.readText(fromBash)
const lspFixture = await readFile(new URL('./fixture-lsp.mjs', import.meta.url), 'utf8')
const remoteLspFixture = await ctx.fs.resolve('fixture-lsp.mjs')
await ctx.fs.writeText(remoteLspFixture, lspFixture, { kind: 'createIfAbsent' })
const remoteSource = await ctx.fs.resolve('multibyte.ts')
await ctx.fs.writeText(remoteSource, 'const café = "你好"\nconsole.log(café)\n', { kind: 'createIfAbsent' })
const hover = await ctx.lsp.query({
operation: 'hover',
filePath: 'multibyte.ts',
position: { line: 0, character: 7 },
workspaceRoot: process.cwd(),
})
const definition = await ctx.lsp.query({
operation: 'goToDefinition',
filePath: 'multibyte.ts',
position: { line: 0, character: 7 },
workspaceRoot: process.cwd(),
})
const terminal = await ctx.pty.spawn(owner, { type: 'shell' })
terminalId = terminal.sessionId
const terminalEcho = await ctx.pty.startSend(owner, terminal.sessionId, {
text: "printf 'PTY-你好\\n'",
submit: true,
}).done
const sleeping = ctx.pty.startSend(owner, terminal.sessionId, { text: 'sleep 30', submit: true })
await new Promise(resolveDelay => setTimeout(resolveDelay, 150))
const terminalSignal = await ctx.pty.signal(owner, terminal.sessionId, 'SIGINT')
const interrupted = await sleeping.done
const terminalScrollback = ctx.pty.read(owner, terminal.sessionId, { count: 50 })
await ctx.pty.kill(owner, terminal.sessionId, 'live E2B composition complete')
terminalId = undefined
const code = await ctx.codeRuntime.run({
program: `
console.log('remote-log 你好', 42)
const arrayPrototype = Array.prototype
const objectPrototype = Object.prototype
const setPrototype = Set.prototype
const stringPrototype = String.prototype
Array.isArray = () => false
Object.defineProperty = Object.getPrototypeOf = Object.keys = () => { throw new Error('mutated object method') }
Object.hasOwn = () => false
Object.is = () => true
objectPrototype.propertyIsEnumerable = () => false
Number.isFinite = Number.isSafeInteger = () => false
Reflect.apply = Reflect.ownKeys = () => { throw new Error('mutated reflect method') }
setPrototype.add = setPrototype.delete = setPrototype.has = () => { throw new Error('mutated set method') }
stringPrototype.charCodeAt = stringPrototype.codePointAt = stringPrototype.slice = () => { throw new Error('mutated string method') }
Buffer.byteLength = () => 0
Function.prototype.toString = () => 'mutated'
objectPrototype.constructor = arrayPrototype.constructor = null
globalThis.Array = globalThis.Buffer = globalThis.Function = globalThis.Number = globalThis.Object = globalThis.Promise = globalThis.Reflect = globalThis.Set = globalThis.String = undefined
process.stdout.write('post-mutation', () => {})
const doubled: number = await bridge.double({ value: 21 })
let typed = false
try {
await bridge.fail({ reason: 'expected' })
} catch (error) {
typed = error instanceof BridgeError && (error as { member: string }).member === 'fail'
}
return { doubled, typed }
`,
bindings: [{
global: 'bridge',
errorClass: { name: 'BridgeError', memberNameProperty: 'member' },
functions: {
double: async (args) => {
const value = (args as { value: number }).value
return value * 2
},
fail: async () => { throw new Error('binding rejected') },
},
}],
})
const hostileOutput = await ctx.codeRuntime.run({
program: `
const payload = '🙂'.repeat(4096)
String.prototype[Symbol.iterator] = () => { throw new Error('mutated string iterator') }
console.log(payload)
return true
`,
bindings: [],
})
const timedOut = await ctx.codeRuntime.run({
program: 'await new Promise(() => {})',
bindings: [],
})
const abortController = new AbortController()
const aborting = ctx.codeRuntime.run({
program: 'await new Promise(() => {})',
bindings: [],
signal: abortController.signal,
})
setTimeout(() => { abortController.abort('live abort') }, 50)
const aborted = await aborting
const remoteProcesses = await (await ctx.e2b.getSandbox()).commands.list()
const lingeringCodeRunners = remoteProcesses.filter(processInfo =>
JSON.stringify([processInfo.cmd, processInfo.args]).includes('code-runtime-runner.mjs'),
)
process.stdout.write(`${JSON.stringify({
sandboxId: await ctx.e2b.sandboxId,
bashRead: bashRead.stdout.text,
fsRead,
hover,
definition,
terminal: {
motd: terminal.motd,
echo: terminalEcho,
signal: terminalSignal,
interrupted,
scrollback: terminalScrollback.text,
},
code,
hostileOutput,
timedOut,
aborted,
lingeringCodeRunners: lingeringCodeRunners.length,
})}\n`)
} finally {
if (terminalId !== undefined) await ctx.pty.kill(owner, terminalId, 'fixture cleanup').catch(() => false)
unregisterOwner()
await ownerFiber.dispose()
await ctx.fiber.dispose()
}

View File

@@ -17,3 +17,43 @@
- id: fs-e2b
name: '@deepseek-ai/dsh-fs-e2b'
- id: agents
name: '@deepseek-ai/dsh-agent'
- id: pty
name: '@deepseek-ai/dsh-pty'
- id: pty-e2b
name: '@deepseek-ai/dsh-pty-e2b'
config:
pollIntervalMs: 25
idleSilenceMs: 2000
timeoutMs: 5000
disposeGraceMs: 1000
- id: lsp
name: '@deepseek-ai/dsh-lsp'
- id: lsp-e2b
name: '@deepseek-ai/dsh-lsp-e2b'
config:
servers:
fixture:
command: node
args:
- !!js process.cwd() + '/fixture-lsp.mjs'
extensionToLanguage:
.ts: typescript
shutdownTimeoutMs: 1000
killGraceMs: 500
- id: code-runtime-e2b
name: '@deepseek-ai/dsh-code-runtime-e2b'
config:
computeMs: 500
maxWallMs: 5000
maxOutputBytes: 4096
maxOldGenerationSizeMb: 128
maxFrameBytes: 4194304
killGraceMs: 500

View File

@@ -0,0 +1,85 @@
import { Buffer } from 'node:buffer'
let pending = Buffer.alloc(0)
let source = ''
let sourceUri = ''
function send(message) {
const body = Buffer.from(JSON.stringify(message))
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`)
process.stdout.write(body)
}
function respond(id, result) {
send({ jsonrpc: '2.0', id, result })
}
function dispatch(message) {
switch (message.method) {
case 'initialize':
respond(message.id, {
capabilities: {
positionEncoding: 'utf-16',
textDocumentSync: { openClose: true, change: 1 },
definitionProvider: true,
referencesProvider: true,
implementationProvider: true,
hoverProvider: true,
},
})
return
case 'textDocument/didOpen':
source = message.params.textDocument.text
sourceUri = message.params.textDocument.uri
return
case 'textDocument/didClose':
source = ''
sourceUri = ''
return
case 'textDocument/hover':
if (!source.includes('const café = "你好"')) {
send({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: 'multibyte source was corrupted' } })
return
}
respond(message.id, {
contents: { kind: 'markdown', value: '**remote hover** 你好 café' },
range: { start: { line: 0, character: 6 }, end: { line: 0, character: 10 } },
})
return
case 'textDocument/definition':
case 'textDocument/references':
case 'textDocument/implementation':
respond(message.id, [{
uri: sourceUri,
range: { start: { line: 0, character: 6 }, end: { line: 0, character: 10 } },
}])
return
case 'shutdown':
respond(message.id, null)
return
case 'exit':
process.exit(0)
return
}
}
function drain() {
for (;;) {
const headerEnd = pending.indexOf('\r\n\r\n')
if (headerEnd < 0) return
const header = pending.subarray(0, headerEnd).toString('ascii')
const match = /(?:^|\r\n)Content-Length: ([0-9]+)(?:\r\n|$)/i.exec(header)
if (!match) throw new Error('missing Content-Length')
const length = Number(match[1])
const bodyStart = headerEnd + 4
if (pending.length < bodyStart + length) return
const body = pending.subarray(bodyStart, bodyStart + length)
pending = pending.subarray(bodyStart + length)
dispatch(JSON.parse(body.toString('utf8')))
}
}
process.stdin.on('data', chunk => {
pending = Buffer.concat([pending, chunk])
drain()
})