fix(subprocess): consumers with one mixed env map split DSH_* onto the managed channel
Codex review of the acp-only fix found lsp-local carries the identical defect: its server config's unrestricted env merges into the connection's ordinary spawn channel, so a configured DSH_* fact crashed the spawn with the reserved-namespace rejection. The partition now lives on the seam as splitEnvChannels() beside the scrub it complements; the ACP run and the LSP connection both use it, and each proves child delivery end-to-end (MOCK_ECHO_ENV / LSP_FAKE_ECHO_ENV fixture knobs). Seam + consumer README rows updated (en+zh, re-recorded). bash-local is already two-channel; mcp/pty/sdk bypass the seam and only share the scrub.
This commit is contained in:
@@ -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
|
||||
README.md: 462cb12ce96dbbb645c9a19126911d32d4ddd722
|
||||
README.zh.md: f5537a416c49106b128188efe4adb2d65304320a
|
||||
README.md: 3c70977b6ea783d5aa1766709d2a507e08b8ceca
|
||||
README.zh.md: d24100c5f850172845a24acadcbd2d0207b64859
|
||||
|
||||
@@ -23,7 +23,7 @@ The `servers` record key is the stable provider id reserved on `ctx.lsp`; each v
|
||||
|---|---|---|
|
||||
| `command` | (required) | Executable to spawn — absolute, or resolved on the child PATH at load. Launch uses no shell. |
|
||||
| `args` | `[]` | Arguments passed to the executable. |
|
||||
| `env` | `{}` | Extra env merged on top of the credential-scrubbed ambient env (vars matching `KEY`/`SECRET`/`TOKEN` are not forwarded). |
|
||||
| `env` | `{}` | Extra env merged on top of the credential-scrubbed ambient env (vars matching `KEY`/`SECRET`/`TOKEN` are not forwarded); `DSH_*` entries ride the subprocess seam's managed channel. |
|
||||
| `extensionToLanguage` | (required) | Lowercase leading-dot extension → LSP language id (e.g. `{ '.ts': 'typescript' }`). |
|
||||
| `initializationOptions` | `null` | Static `initialize` options forwarded to the server. |
|
||||
| `configuration` | `null` | Static answer to every `workspace/configuration` item. |
|
||||
|
||||
@@ -23,7 +23,7 @@ Namespace 插件(`name`/`inject`/`Config`/`apply`,无默认导出)
|
||||
|---|---|---|
|
||||
| `command` | (必填) | 要 spawn 的可执行文件:绝对路径,或在加载时从子进程 PATH 解析。不使用 shell 启动。 |
|
||||
| `args` | `[]` | 传给可执行文件的参数。 |
|
||||
| `env` | `{}` | 合并到已清理 credential 的环境之上的额外 env(匹配 `KEY`/`SECRET`/`TOKEN` 的变量不会转发)。 |
|
||||
| `env` | `{}` | 合并到已清理 credential 的环境之上的额外 env(匹配 `KEY`/`SECRET`/`TOKEN` 的变量不会转发);`DSH_*` 条目走 subprocess seam 的受管通道。 |
|
||||
| `extensionToLanguage` | (必填) | 小写、以点开头的扩展名 → LSP language id(例如 `{ '.ts': 'typescript' }`)。 |
|
||||
| `initializationOptions` | `null` | 转发给服务器的静态 `initialize` 选项。 |
|
||||
| `configuration` | `null` | 每个 `workspace/configuration` 配置项的静态答案。 |
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import type { Writable } from 'node:stream'
|
||||
import { splitEnvChannels } from '@deepseek-ai/dsh-subprocess'
|
||||
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import { encodeMessage, MessageDecoder } from './framing.ts'
|
||||
|
||||
@@ -98,7 +99,9 @@ export class LspConnection {
|
||||
stderr: { maxBytes: spec.maxStderrBytes },
|
||||
},
|
||||
graceMs: spec.pipeDrainGraceMs,
|
||||
env: spec.env,
|
||||
// spec.env mixes the scrubbed base with explicit config entries; a
|
||||
// configured DSH_* fact takes the managed channel the seam reserves.
|
||||
...splitEnvChannels(spec.env),
|
||||
})
|
||||
/* v8 ignore start -- 'pipe' dispositions expose both streams by the seam contract; defensive. */
|
||||
if (this.handle.stdin === undefined || this.handle.stdout === undefined) {
|
||||
|
||||
@@ -51,6 +51,15 @@ describe('LspConnection', () => {
|
||||
expect(conn.pid).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('routes explicit DSH_* env entries onto the managed channel', async () => {
|
||||
// A configured DSH_* fact must reach the child: the ordinary channel
|
||||
// rejects the reserved namespace, so the connection's spawn must split it
|
||||
// onto dshEnv. The fixture echoes the named variable back as hover text.
|
||||
const conn = connect({ LSP_FAKE_ECHO_ENV: 'DSH_LSP_TEST_FACT', DSH_LSP_TEST_FACT: 'managed' })
|
||||
await conn.request('initialize', { capabilities: {} })
|
||||
expect(await conn.request('textDocument/hover', {})).toEqual({ contents: 'managed' })
|
||||
})
|
||||
|
||||
it('rejects a request when the server replies with an error', async () => {
|
||||
const conn = connect({ LSP_FAKE_ERROR: '1' })
|
||||
await conn.request('initialize', { capabilities: {} })
|
||||
|
||||
@@ -58,7 +58,13 @@ function resultFor(method: string): unknown {
|
||||
case 'textDocument/definition': return envJson('LSP_FAKE_DEF', null)
|
||||
case 'textDocument/references': return envJson('LSP_FAKE_REFS', null)
|
||||
case 'textDocument/implementation': return envJson('LSP_FAKE_IMPL', null)
|
||||
case 'textDocument/hover': return envJson('LSP_FAKE_HOVER', null)
|
||||
case 'textDocument/hover': {
|
||||
// LSP_FAKE_ECHO_ENV names a variable whose VALUE becomes the hover
|
||||
// contents — a test can assert exactly what env reached this process.
|
||||
const echoName = process.env.LSP_FAKE_ECHO_ENV
|
||||
if (echoName !== undefined) return { contents: process.env[echoName] ?? `<${echoName} unset>` }
|
||||
return envJson('LSP_FAKE_HOVER', null)
|
||||
}
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user