The package exported six functions, four of them shaped by one SDK's
transport each: a dispatcher factory, a `node:http` agent factory, a
proxy-URL lookup, and a policy accessor. Review asked whether the call
sites could converge instead of the package growing an export per SDK.
They could, and each removal took a whole shape with it:
- The OTLP exporter moves to the SDK's `fetch` delegate, retiring
`createNodeHttpAgent`. Its Node-version floor goes too: `proxyEnv` on
an `http.Agent` needs 22.21 or 24.5, inside the engines range, so
telemetry was direct on 22.19, 22.20, and 24.0-24.4. The cost is
`compression`, a Node-transport option; the plugin now refuses it,
`keepAlive`, and `httpAgentOptions` at load instead of ignoring them.
- `web-fetch-http` builds its own address-pinning agent under an
annotated `proxy-exempt:` exemption, retiring `createDispatcher`.
Pinning is per-request state a process-wide dispatcher cannot hold.
- E2B reads `route.proxy`, retiring `proxyUrlFor`.
What remains is `installProxyFromEnvironment`, `proxyRouteFor`,
`proxyEnvironmentForChild`, and `clearedProxyEnv` — one per way a caller
can need the policy. Installation absorbs resolution and diagnostic
reporting, which no caller needed apart.
`proxyRouteFor` also closes a defect the old accessor made expressible:
`web-fetch-http` read the policy to decide whether to pin, then read it
again to build a transport, so an unmount between the two returned a
direct, unpinned agent for a URL the first read had cleared as proxied.
A route carries the answer and the transport that answer assumed.
Every egress spec now installs through `installProxyFromEnvironment`, so
no test asserts a policy object a real launch could not produce.
Second review pass on the outbound proxy work.
The installed dispatcher was undici's EnvHttpProxyAgent, which reuses the HTTP
proxy for `https:` whenever no HTTPS proxy is present. That is exactly the state
this package resolves after refusing a SOCKS or malformed URL the user named for
`https:`, so the scheme the diagnostic reported as direct was tunnelled anyway.
The dispatcher is now an Agent whose per-origin factory calls `proxyForUrl`, so
routing and `proxyForUrl` cannot disagree by parsing the same list twice.
`childProxyEnv` returned only the names the user exported, which left a child
Node direct whenever the proxy came from `ALL_PROXY` or from cordis.yml — Node's
`NODE_USE_ENV_PROXY` reads neither — and stripped the merged loopback bypass so
the child sent its own localhost traffic to the proxy. A scheme the user named
in either casing still reaches the child exactly as written; one they named in
neither now carries the resolved value, and the bypass list is always the merged
one.
A nested install (the plugin mounted over the launcher's policy) recorded the
outer policy's published values as the user's, then cleared the record on
disposal, so every later child inherited the normalization instead. The record
now belongs to the outermost install and is restored, not dropped.
`web_fetch` read the active policy twice — once to skip address pinning, again
inside the transport — so a disposal landing between the two reads produced an
unpinned direct connection to a host nothing validated. One snapshot now decides
both.
Also: the node:http proxy test asserted a route the engines range does not always
have, and the gate could not see undici bound through `await import('undici')`,
the form this repository actually uses.
The workflow worker no longer receives proxy configuration: it executes the
model-authored script body, and a proxy URL may carry credentials. A child
process now inherits the values the user exported rather than this process's
normalization, so a SOCKS proxy set for curl survives and no HTTPS_PROXY is
invented. `mode: 'off'` installs a direct dispatcher instead of recording a
policy the global dispatcher ignores, and the environment snapshot is taken
before any write so Windows restores the user's values.
E2B picks its proxy from the control-plane URL the SDK will really call, the
OTLP agent honors `exporter.keepAlive`, and a scheme whose own value was
refused stays direct instead of borrowing another scheme's proxy.
verify-no-bare-dispatcher parses the TypeScript AST as scripts/AGENTS.md
requires; it immediately found the `{ dispatcher }` shorthand the regex missed.
NODE_USE_ENV_PROXY reaches Node 24.0+ and 22.21+, while engines admits 22.19.
Assert the direct connection on an older runtime instead of only the proxied
one, so the seam is executable rather than prose.
Node's built-in fetch ignores HTTP_PROXY, so every harness request connected
directly regardless of what the user exported. Resolve one policy from the
launch environment and install it as undici's global dispatcher, then wire the
four surfaces a global dispatcher cannot reach: web_fetch's pinned transport,
the OTLP exporter's node:http agent, the E2B SDK's own proxy option, and the
environment a child process or worker thread is given.
Each outbound call site carries an egress test that drives its real code path
through a fake proxy; that measurement is what found the OTLP and E2B gaps.
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.
Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.
The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.
Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
Remove the one-consumer bounded-read primitive and shared terminal lifecycle controller, make terminal cleanup one awaited provider operation, and reuse one Code Runtime contract suite. Keep only reproduced cancellation and policy fixes; defer unproven replacement, prompt-attribution, and streaming-frame concerns to scoped markers.
- scrubbedParentEnv folds case before the DSH_ prefix check (Windows env
names are case-insensitive; a parent dsh_* entry read back as \*
in the child) and the service spec pins the lowercase probe.
- The acp.snapshot.ts pwsh probe follows resolvePwshPath() like the package
suites, so a Windows host with only an install-location pwsh still runs
the scenario.
- pwsh-tool-turn is re-recorded around [Console]::Out.Write('PWSH_OK'):
the fixture carries no platform newline, so one recording replays on
Windows and POSIX alike (record + refresh; replay-verified keyless).
- The pwsh-local Known Limitations bullet drops the self-defeating no-op
advice: & { } is scoped to param(...), using/#requires scripts run from a
file (both languages, pairing re-recorded).
- The capability-seams graph moves ctx.bashEnv ownership to bash-env and
lists pwsh-local/tool-pwsh on the ctx.bash seam (source updated,
docs regenerated).
- The tool-bash presenter fixture retires the stale 'command aborted'
literal for the shipped 'tool call aborted' message.
Delete kill(signal?) from SubprocessHandle: consumers stop a process only
through terminate()'s tree-scoped SIGTERM→graceMs→SIGKILL escalation
(idempotent, also driven by the spec's abort signal, a no-op once the tree
is gone). The single-signal verb had exactly one consumer family —
lsp-local — and what it bought there was a private re-implementation of
the same escalation. The internal kill closure stays in spawn.ts as the
dispose ladder's tier primitive; terminate() now routes through it too.
lsp-local collapses onto the seam's escalation:
- LspConnection replaces its terminate()/kill() pair with one terminate()
that delegates to handle.terminate(). Behavior change: the
framing-failure path terminates instead of instant SIGKILL, so a
misbehaving server now gets SIGTERM plus the killGraceMs window to
flush before SIGKILL.
- ConnectionSpec.pipeDrainGraceMs becomes killGraceMs: one grace, the
spawn spec's graceMs, drives both the escalation window and post-exit
pipe draining (the provider already passed killGraceMs for it).
- LspInstance.forceTerminate() drops its hand-rolled bounded first wait
(LSP_KILL_GRACE) and escalateProcessTree (deleted with its export and
unit test): the seam's escalation already commits to SIGKILL after
killGraceMs, so only the unbounded quiescence awaits stay load-bearing.
Tests: kill()-shaped spawn specs become terminate()-shaped or fold into
the terminate() suites (group-wide delivery; the settled no-op case was
already pinned by 'terminate() after the tree died'); tree-survivor
coverage is intact. A stderr-'inherit' disposition test completes the
stdout/stderr symmetry so the scoped subprocess+lsp coverage gate stands
alone instead of leaning on subagent-acp's cross-package runs.
Docs: SubprocessHandle type-equiv block, seam/impl/group READMEs, and the
consumer-migration Agent Note lose the kill(signal?) vocabulary (zh pairs
re-recorded); cordis api/services catalogs regenerated.
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.
SubprocessHandle loses dispose(graces) and SubprocessDisposeGraces: the
stdin-EOF→SIGTERM→SIGKILL sequence is teardown POLICY encoding one
consumer's cooperation shape, not process vocabulary — the seam keeps
kill/terminate/waitForExit, and waitForExit(signal?) is the quiescence
probe a consumer ladder needs. dsh-subagent-acp owns disposeAcpChild()
over those public verbs (tier tests move into its suite; a never-exits
stub pins the fail-loud path); dsh-subprocess-local sheds the ladder,
its deadline import, and the dsh-timeout dependency. Every future
backend now owes four verbs and no teardown policy. New bilingual
ladder-ownership Agent Note records the decision; catalogs regenerated.
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.
Review direction (tianyicui, PR #660): make the interface closer to Node's
API so the other process-running places can adopt it. The spec gains
per-stream stdio dispositions — 'pipe' (raw Readable/Writable for protocol
streams), 'inherit' (diagnostics to the parent), and collect mode ({maxBytes,
spill?} — the old bounded tail-keep shape, now with spill optional for
diagnostic tails). SubprocessOutcome carries exit facts only; collected
output stays readable through handle.collected after settlement (spill fds
are sealed at the settle boundary). The handle grows Node-style kill(signal)
(single signal, tree-scoped, no-op after settlement), terminate() (the
SIGTERM→grace→SIGKILL escalation, also driven by the spec signal),
waitForExit() (tree liveness, not just the direct child), and dispose()
(the cooperative stdin-EOF→SIGTERM→SIGKILL ladder from subagent-subprocess,
graces caller-supplied). Tree semantics are platform-correct: POSIX detached
groups with direct-child fallback; Windows taskkill /T with an injectable
runner. scrubbedParentEnv/SENSITIVE_ENV_PATTERN move to the seam as the one
shared scrub definition.
bash-local maps its config onto collect modes and batch stdin and reads
results through the collected readers; its kill() maps to terminate() so
task_kill keeps escalation semantics.
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.
ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
drift-checked type-equiv blocks on the new subprocess.md page, restoring
their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).