Merge branch 'mergefwd-1-seam' into mergefwd-3-wire

This commit is contained in:
Yichen Jiang
2026-08-09 00:27:40 +08:00
199 changed files with 12103 additions and 1413 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write examples/headless-agent/README.md
README.md: e00f3d2d4fd21a860239f3d3a3e5eb2d7520f14a
README.zh.md: 6cd845783b1c112ba73676474b176ec28a4d0b78
README.md: 6e80e56dec70c2be341ae5dfbad70e13a5109715
README.zh.md: ea8c41b9ae75f0cee3edd78e59408f37c19182d3

View File

@@ -17,6 +17,16 @@ The product command is [`dsh run`](../../apps/cli/README.md): it accepts one non
Snapshot suites run this directory's configuration through [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts), an unexported test-only process that emits canonical session events as JSONL before its result record. That stream is test infrastructure, not a supported CLI output format. Child sessions surface only through parent tool events and results.
## E2B POC overlay
[`e2b.cordis.yml`](e2b.cordis.yml) replaces the local filesystem and subprocess providers with one shared E2B sandbox while retaining `dsh-bash-local` and the same model-facing tools. Put `E2B_API_KEY` beside `DEEPSEEK_API_KEY` in the gitignored root `.env`, then run the credential-gated live composition, which drives FS, Bash, PTY, and LSP in one sandbox and proves final deletion:
```sh
pnpm exec vitest run --config vitest.e2e.config.ts packages/e2b/e2b/tests/composition.e2e.ts
```
The overlay creates the same absolute cwd inside the sandbox, but it does not upload or mount the host workspace. File and Bash mutations exist only in E2B; Cordis, model calls, agent/session state, session logs, skills, and SDK buffers remain on the host. The composition kills its sandbox on timeout and disposal. It is a provider-composition POC, not a whole-harness migration or a workspace-sync feature.
## Advanced configuration
[`advanced.cordis.yml`](advanced.cordis.yml) adds Code Mode and the Cordis tools to the test composition.

View File

@@ -17,6 +17,16 @@ pnpm run dsh run "fix the failing test in this workspace"
快照套件通过 [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts) 运行本目录的配置。这个未导出且仅供测试使用的进程会在结果记录之前,以 JSONL 发出规范会话事件。该事件流属于测试基础设施,不是受支持的 CLI命令行界面输出格式。子会话只通过父会话的工具事件和结果对外显示。
## E2B POC overlay
[`e2b.cordis.yml`](e2b.cordis.yml) 使用一个共享 E2B 沙箱替换本地文件系统与进程管理提供方,同时保留 `dsh-bash-local` 和相同的面向模型工具。请在 git 忽略的根目录 `.env` 中,将 `E2B_API_KEY``DEEPSEEK_API_KEY` 放在一起,然后运行凭据门控的实机组合测试;它在同一个沙箱中驱动 FS、Bash、PTY 和 LSP并证明沙箱最终被删除
```sh
pnpm exec vitest run --config vitest.e2e.config.ts packages/e2b/e2b/tests/composition.e2e.ts
```
该 overlay 会在沙箱中创建拼写相同的绝对 cwd但不会上传或挂载宿主工作区。文件与 Bash 变更只存在于 E2BCordis、模型调用、agent会话状态、会话日志、skill技能和 SDK 缓冲仍在宿主上。该组合会在超时和资源释放时终止其沙箱。它是提供方组合 POC而不是完整 harness 迁移或工作区同步功能。
## 高级配置
[`advanced.cordis.yml`](advanced.cordis.yml) 在测试组装中添加 Code Mode 和 Cordis 工具。

View File

@@ -0,0 +1,57 @@
# POC overlay: keep the advanced headless agent and model-facing tools, but
# place its filesystem and process substrate in one short-lived E2B sandbox;
# the generic Bash, PTY, and LSP consumers compose above them.
#
# One-world invariant: e2b.cwd, sandbox-policy.workspaceRoot, and bash-local's
# default workdir (implicit host process.cwd()) must all name the same remote
# directory. Only e2b.cwd is created at sandbox open; dropping its !!js line
# falls back to /home/user/workspace while Bash and PTY keep targeting the
# host path, so every tool call fails with a remote spawn error.
- id: base
name: '@cordisjs/plugin-include'
config:
path: ./advanced.cordis.yml
patches:
- id: subprocess
name: '@deepseek-ai/dsh-subprocess-local'
disabled: true
- id: fs-local
name: '@deepseek-ai/dsh-fs-local'
disabled: true
- insert:
- id: e2b
name: '@deepseek-ai/dsh-e2b'
config:
cwd: !!js process.cwd()
timeoutMs: 300000
- id: subprocess-e2b
name: '@deepseek-ai/dsh-subprocess-e2b'
- id: fs-e2b
name: '@deepseek-ai/dsh-fs-e2b'
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: danger-full-access
workspaceRoot: !!js process.cwd()
- id: pty
name: '@deepseek-ai/dsh-pty'
- id: pty-local
name: '@deepseek-ai/dsh-pty-local'
- id: tool-pty
name: '@deepseek-ai/dsh-tool-pty'
- id: lsp
name: '@deepseek-ai/dsh-lsp'
- id: lsp-local
name: '@deepseek-ai/dsh-lsp-local'
config:
servers:
typescript:
command: npx
args: [--yes, typescript-language-server@5.0.0, --stdio]
extensionToLanguage:
.ts: typescript
.tsx: typescriptreact
.js: javascript
.jsx: javascriptreact
- id: tool-lsp
name: '@deepseek-ai/dsh-tool-lsp'

View File

@@ -0,0 +1,202 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { boot } from '@deepseek-ai/dsh-app-boot'
import { Inbox } 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-fs-e2b'
import type {} from '@deepseek-ai/dsh-bash-local'
import type {} from '@deepseek-ai/dsh-lsp-local'
import type {} from '@deepseek-ai/dsh-pty-local'
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 session = Session.create(ownerId)
const owner: Agent = {
id: ownerId,
options: {},
session,
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
status: 'idle',
ctx: ownerFiber.ctx,
send() {},
followup() {},
steer() {},
inject() {},
cancel() {},
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
const unregisterOwner = ctx.agents.register(owner)
let terminalId: Awaited<ReturnType<typeof ctx.pty.spawn>>['sessionId'] | undefined
try {
const sandbox = await ctx.e2b.getSandbox()
const fromFs = await ctx.fs.resolve('from-fs.txt')
const written = await ctx.fs.writeText(fromFs, 'written-by-fs\n', { kind: 'createIfAbsent' })
const observed = await ctx.fs.stat(fromFs)
if (observed?.version !== written.version) {
throw new Error(`E2B rename did not preserve version metadata: ${JSON.stringify({ written, observed })}`)
}
await ctx.fs.editText(
fromFs,
{ oldString: 'written-by-fs', newString: 'versioned-by-fs', replaceAll: false },
{ version: observed.version },
)
const bashRead = await ctx.bash.run(ctx.bash.resolve({ command: 'cat from-fs.txt' }))
if (bashRead.exitCode !== 0 || bashRead.stdout.text !== 'versioned-by-fs\n') {
throw new Error(`E2B Bash could not read the FS write: ${JSON.stringify(bashRead)}`)
}
const bashWrite = await ctx.bash.run(ctx.bash.resolve({ command: "printf 'written-by-bash\\n' > from-bash.txt" }))
if (bashWrite.exitCode !== 0) {
throw new Error(`E2B Bash could not write the shared filesystem: ${JSON.stringify(bashWrite)}`)
}
const fromBash = await ctx.fs.resolve('from-bash.txt')
const fsRead = await ctx.fs.readText(fromBash)
const environmentHandle = ctx.subprocess.spawn({
argv: ['env'],
cwd: process.cwd(),
stdio: { stdin: 'ignore', stdout: { maxBytes: 65_536 }, stderr: { maxBytes: 4_096 } },
graceMs: 500,
env: {
'FOO-BAR': 'hyphen-value',
DSH_EXPLICIT: 'managed-value',
TOKEN_EXPLICIT: 'credential-value',
},
})
const environmentOutcome = await environmentHandle.done
const environmentText = environmentHandle.collected.stdout?.readFrom(0).text
if (environmentOutcome.exitCode !== 0 || environmentText === undefined) {
throw new Error(`E2B subprocess environment probe failed: ${JSON.stringify(environmentOutcome)}`)
}
const environmentLines = new Set(environmentText.trimEnd().split('\n'))
const explicitEnvironment = [
'FOO-BAR=hyphen-value',
'DSH_EXPLICIT=managed-value',
'TOKEN_EXPLICIT=credential-value',
].every(entry => environmentLines.has(entry))
if (!explicitEnvironment) throw new Error(`E2B subprocess dropped an explicit environment entry: ${environmentText}`)
const splitUtf8Handle = ctx.subprocess.spawn({
argv: ['bash', '-c', "printf '\\344'; sleep 0.05; printf '\\275'; sleep 0.05; printf '\\240'; sleep 0.05; printf '\\345'; sleep 0.05; printf '\\245'; sleep 0.05; printf '\\275'"],
cwd: process.cwd(),
stdio: { stdin: 'ignore', stdout: { maxBytes: 32 }, stderr: { maxBytes: 4_096 } },
graceMs: 500,
env: {},
})
const splitUtf8Outcome = await splitUtf8Handle.done
const splitUtf8Output = splitUtf8Handle.collected.stdout?.readFrom(0).text
if (splitUtf8Outcome.exitCode !== 0 || splitUtf8Output !== '你好') {
throw new Error(`E2B subprocess corrupted split UTF-8 output: ${JSON.stringify({ splitUtf8Outcome, splitUtf8Output })}`)
}
const outputDrainStarted = Date.now()
const outputDrainHandle = ctx.subprocess.spawn({
argv: ['bash', '-c', "bash -c 'exec -a dsh-output-drain-descendant sleep 30' & printf 'leader-done\\n'"],
cwd: process.cwd(),
stdio: { stdin: 'ignore', stdout: { maxBytes: 64 }, stderr: { maxBytes: 4_096 } },
graceMs: 250,
env: {},
})
const outputDrainOutcome = await outputDrainHandle.done
const outputDrainText = outputDrainHandle.collected.stdout?.readFrom(0).text
const outputDrainElapsedMs = Date.now() - outputDrainStarted
outputDrainHandle.terminate()
const outputDrainExited = await outputDrainHandle.waitForExit(AbortSignal.timeout(5_000))
const outputDrainProcesses = await sandbox.commands.list()
const outputDrainClean = !outputDrainProcesses.some(processInfo =>
JSON.stringify([processInfo.cmd, processInfo.args]).includes('dsh-output-drain-descendant'),
)
if (outputDrainOutcome.exitCode !== 0 || outputDrainText !== 'leader-done\n'
|| outputDrainElapsedMs >= 10_000 || !outputDrainExited || !outputDrainClean) {
throw new Error(`E2B subprocess did not bound descendant-held output: ${JSON.stringify({
outputDrainOutcome, outputDrainText, outputDrainElapsedMs, outputDrainExited, outputDrainClean,
})}`)
}
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 # file.ts')
await ctx.fs.writeText(remoteSource, 'const café = "你好"\nconsole.log(café)\n', { kind: 'createIfAbsent' })
const hover = await ctx.lsp.query({
operation: 'hover',
filePath: 'multibyte # file.ts',
position: { line: 0, character: 7 },
workspaceRoot: process.cwd(),
})
const definition = await ctx.lsp.query({
operation: 'goToDefinition',
filePath: 'multibyte # file.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: "printf 'DSH_SLEEP_%s\\n' READY; sleep 30",
submit: true,
})
let sleepReadyOutput = ''
const sleepReadyDeadline = Date.now() + 5_000
while (!sleepReadyOutput.includes('DSH_SLEEP_READY\n')) {
sleepReadyOutput += sleeping.readOutput().delta
if (sleepReadyOutput.includes('DSH_SLEEP_READY\n')) break
const settled = await Promise.race([
sleeping.done.then(result => ({ result })),
new Promise<undefined>(resolveDelay => setTimeout(() => { resolveDelay(undefined) }, 25)),
])
if (settled !== undefined) {
throw new Error(`E2B PTY successor settled before executing: ${JSON.stringify(settled.result)}`)
}
if (Date.now() >= sleepReadyDeadline) throw new Error(`E2B PTY successor did not execute: ${sleepReadyOutput}`)
}
const terminalSignal = await ctx.pty.signal(owner, terminal.sessionId, 'SIGINT')
const interrupted = await sleeping.done
const stubborn = await ctx.pty.startSend(owner, terminal.sessionId, {
text: "bash -c 'trap \"\" TERM; exec sleep 30' & printf 'DSH_STUBBORN_PID=%s\\n' \"$!\"",
submit: true,
}).done
const stubbornMatch = /DSH_STUBBORN_PID=([1-9][0-9]*)/.exec(stubborn.viewport)
if (stubbornMatch?.[1] === undefined) throw new Error(`E2B PTY did not report its stubborn child: ${stubborn.viewport}`)
const stubbornPid = Number(stubbornMatch[1])
const terminalScrollback = ctx.pty.read(owner, terminal.sessionId, { count: 50 })
await ctx.pty.kill(owner, terminal.sessionId, 'live E2B composition complete')
terminalId = undefined
const stubbornProbe = await sandbox.commands.run(`if kill -0 ${stubbornPid} 2>/dev/null; then printf alive; else printf gone; fi`)
const terminalTreeCleanup = stubbornProbe.stdout === 'gone'
if (!terminalTreeCleanup) throw new Error(`E2B PTY left process ${stubbornPid} alive after close`)
process.stdout.write(`${JSON.stringify({
sandboxId: (await ctx.e2b.getSandbox()).sandboxId,
bashRead: bashRead.stdout.text,
fsRead,
explicitEnvironment,
splitUtf8Output,
hover,
definition,
terminal: {
motd: terminal.motd,
echo: terminalEcho,
signal: terminalSignal,
interrupted,
treeCleanup: terminalTreeCleanup,
scrollback: terminalScrollback.text,
},
})}\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

@@ -0,0 +1,57 @@
# One-world invariant (same pairing as examples/headless-agent/e2b.cordis.yml):
# e2b.cwd and sandbox-policy.workspaceRoot must name the same remote directory,
# which is also bash-local's implicit default workdir.
- id: e2b
name: '@deepseek-ai/dsh-e2b'
config:
cwd: !!js process.cwd()
timeoutMs: 180000
- id: subprocess-e2b
name: '@deepseek-ai/dsh-subprocess-e2b'
- id: bash
name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 30000
- id: fs-e2b
name: '@deepseek-ai/dsh-fs-e2b'
- id: agents
name: '@deepseek-ai/dsh-agent'
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: danger-full-access
workspaceRoot: !!js process.cwd()
- id: pty
name: '@deepseek-ai/dsh-pty'
- id: pty-local
name: '@deepseek-ai/dsh-pty-local'
config:
pollIntervalMs: 25
exactProbeAfterMs: 150
idleSilenceMs: 2000
handoffGraceMs: 500
timeoutMs: 5000
disposeGraceMs: 1000
- id: lsp
name: '@deepseek-ai/dsh-lsp'
- id: lsp-local
name: '@deepseek-ai/dsh-lsp-local'
config:
servers:
fixture:
command: node
args:
- !!js process.cwd() + '/fixture-lsp.mjs'
extensionToLanguage:
.ts: typescript
shutdownTimeoutMs: 1000
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()
})

View File

@@ -18,6 +18,9 @@
mode: danger-full-access
workspaceRoot: !!js process.env.DSH_CWD ?? process.cwd()
- id: subprocess
name: '@deepseek-ai/dsh-subprocess-local'
- id: pty
name: '@deepseek-ai/dsh-pty'

View File

@@ -26,7 +26,9 @@
"@deepseek-ai/dsh-compact-basic": "workspace:*",
"@deepseek-ai/dsh-compact-tool-result-prune": "workspace:*",
"@deepseek-ai/dsh-credentials-local": "workspace:*",
"@deepseek-ai/dsh-e2b": "workspace:*",
"@deepseek-ai/dsh-fs-local": "workspace:*",
"@deepseek-ai/dsh-fs-e2b": "workspace:*",
"@deepseek-ai/dsh-fs-policy": "workspace:*",
"@deepseek-ai/dsh-fs-sandbox": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:*",
@@ -76,6 +78,7 @@
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
"@deepseek-ai/dsh-subagent-spawn": "workspace:*",
"@deepseek-ai/dsh-subprocess-local": "workspace:*",
"@deepseek-ai/dsh-subprocess-e2b": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tasks-local": "workspace:*",
"@deepseek-ai/dsh-time-context": "workspace:*",