Files
deepseek-harness/packages/lsp/lsp-local/README.md
Tianyi Cui 5717726835 subprocess: one explicit env channel on the spawn spec
Drop SubprocessSpawnSpec.dshEnv and splitEnvChannels(); childEnv() is now
scrubbed-base + explicit entries with no namespace validation. The invariant
dropped is the reserved-namespace check on explicit entries (DSH_* rejected
from env, non-DSH_* rejected from dshEnv). Explicit-entry trust already
covers it: an explicit credential-shaped entry has always merged after the
scrub as a deliberate caller opt-in, and an explicit DSH_* entry is the same
deliberate act — the staleness invariant lives entirely in scrubbedParentEnv
dropping AMBIENT credential-shaped and DSH_* names, which stays. The
validation's only observed effect was rejecting legitimate explicit entries:
both recent CI breakages (DSH_GATE_CONCURRENCY exported into every job
crashing lsp specs, DSH_PERMISSION_MODE in acp config.env crashing the
child spawn) were this check firing on values a caller meant to pass, each
fixed by routing around the bureaucracy the seam itself imposed.

The bash seam keeps its own request/spec dshEnv field: that is bash-owned
trusted-plugin vocabulary (the ctx.bashEnv collected overlay) whose merge-last
position guarantees a caller env entry cannot displace a managed fact;
bash-local now flattens ENV_OVERRIDES -> spec.env -> spec.dshEnv into the
seam's one env map. subagent-acp and lsp-local pass their single config env
map straight through. DshEnvironment/DshEnvironmentKey/DSH_ENV_PREFIX stay on
the subprocess seam as the namespace vocabulary (bash re-exports them;
scrubbedParentEnv filters on the prefix).

Tests: the two channel-rejection specs and the splitEnvChannels partition
spec are deleted; one spawn spec now proves an explicit DSH_* env entry
reaches the child while an ambient one is scrubbed; the acp/lsp forwarding
specs keep their MOCK_ECHO_ENV / LSP_FAKE_ECHO_ENV assertions with the split
comments rewritten to merge-after-scrub. Docs (en+zh, re-recorded) and the
owning Agent Notes updated; cordis api/services catalogs regenerated.
2026-07-27 04:14:51 +08:00

59 lines
6.8 KiB
Markdown

# @deepseek-ai/dsh-lsp-local
English | [中文](README.zh.md)
A **generic local stdio language-server backend** for `ctx.lsp`. One plugin instance accepts a named server table and registers one isolated provider per entry. This is a generic host, not a language-server catalog or installer — deployments configure commands and mappings explicitly; presets belong in `cordis.yml` overlays.
Namespace plugin (`name` / `inject` / `Config` / `apply`, no default export).
## What it does
- Resolves every server-local setting before registration; an invalid mapping or registration conflict rolls back earlier entries, so a failed load leaves no provider routes.
- Lazily single-flights one server process per `(server id, canonical workspace realpath)`. A live server error is not replayed; if the selected pooled transport fails before or during a read-only query, the provider awaits its disposal and retries that query once on a fresh process.
- Uses a compatibility-first **transient-open** sequence per query: canonicalize and read the source with Node APIs, `textDocument/didOpen` (version 1, full text), the requested request, then `textDocument/didClose` in `finally`. A failed or canceled `didOpen` write terminates the instance before the pool can reuse it. Documents close after each call, so the first version needs no `didChange`, content cache, or document LRU.
- Serializes each source-read/open/query/close lifecycle through one abortable per-workspace queue so queued calls read current source only when their turn starts; distinct workspaces run in parallel.
- After protocol shutdown fails, terminates the server's descendant tree through the subprocess seam (POSIX process-group signaling; Windows `taskkill /T /F`). Tree-kill delivery is contained like every group signal — it races server exit — and quiescence is confirmed by the handle's tree-liveness wait rather than by the kill's own outcome.
- Reads sources through Node filesystem APIs in the subprocess's host namespace — NOT `ctx.fs`, and emits no `fs/observed`: only the LSP result is model-visible, so a query does not satisfy read-before-write policy.
## Configuration
The `servers` record key is the stable provider id reserved on `ctx.lsp`; each value has this shape:
| Server key | Default | Meaning |
|---|---|---|
| `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); an explicit `DSH_*` entry merges after the seam's scrub of ambient ones. |
| `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. |
| `maxMessageBytes` | `16000000` | Largest single framed message accepted from the server. |
| `maxStderrBytes` | `1000000` | Largest stderr tail retained for diagnostics. |
| `maxDocumentBytes` | `4000000` | Largest source file this host will open. |
| `shutdownTimeoutMs` | `5000` | Graceful `shutdown`/`exit` budget before escalation. |
| `killGraceMs` | `2000` | Grace for request cancellation and for SIGTERM→SIGKILL escalation. |
`servers` must contain at least one entry, and every id must be non-empty. Timer budgets must be positive integers no greater than Node's `2_147_483_647` ms timer limit. All executables resolve at load after credential scrubbing; a bad later entry prevents every provider from registering. Processes launch lazily on the first matching query.
## Protocol behavior
Initialization advertises `general.positionEncodings: ['utf-16']`, `workspace: { workspaceFolders: true, configuration: true }`, `textDocument.hover.contentFormat: ['markdown', 'plaintext']`, and `linkSupport: true` for definition and implementation, with no dynamic registration. The server's returned capabilities are authoritative: an unsupported operation, or synchronization without transient open/close, fails the query. An omitted server `positionEncoding` defaults to `utf-16`; any other value is a protocol error. The client answers `workspace/configuration` from static config, accepts lifecycle bookkeeping requests, and rejects `workspace/applyEdit` — it never applies edits or runs commands. Navigation maps `Location` directly and `LocationLink` from `targetUri` + `targetSelectionRange`; hover normalization takes valid `MarkupContent.value`, preserves string `MarkedString`s, renders language-tagged values as fenced code, and joins arrays with one blank line. Missing results, malformed ranges or positions, and malformed hover encodings fail as structured `LSP_MALFORMED_RESPONSE` errors.
## Security boundary
The provider trusts its configured server and claims no sandbox confinement. It canonicalizes and reads source through Node APIs, rejecting a source that is missing, non-regular, non-UTF-8, oversized, or whose canonical path resolves outside the canonical workspace (symlink aliases share one instance). Result locations may be external, but an external path cannot become a query source. The first implementation therefore requires trusted host-local deployment; restricted, remote, or virtual workspaces require another provider.
## Model Experience
Indirectly, through `dsh-tool-lsp`, which surfaces this provider's normalized results; this host contributes no prompt or schema itself.
#### KV Cache effect
No direct invalidation; `dsh-tool-lsp` owns request-prefix changes.
## Known Limitations and Deferred Work
- **Trusted host-local only** — no sandbox confinement, no private cache/temp write contract; supporting untrusted binaries or restricted/remote/virtual workspaces requires a later process/filesystem contract and a different provider ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md)). Containment resolves `realpath`, then opens the source through one handle with `O_NOFOLLOW | O_NONBLOCK` (final-component symlink guard plus nonblocking rejection of FIFOs) and a bounded read; a concurrent mutator that swaps an *ancestor* directory for a symlink between the resolve and the open is an accepted residual TOCTOU under this trusted-deployment model, not closed with non-portable `openat` segment walks.
- **Transient-open compatibility floor** — servers whose synchronization omits open/close (or advertise `None`) are unsupported even if closed-document queries would work; the pinned TypeScript e2e establishes one compatibility floor, not a cross-language claim.
- **Per-server/workspace serialization latency** — parallel agents sharing one server and workspace queue behind one process; long-lived workspace processes consume memory until disposal.