Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/master' into feat/python-sdk-macos-x64

fz 1 nedēļu atpakaļ
vecāks
revīzija
61e4829ed7
100 mainītis faili ar 635 papildinājumiem un 129 dzēšanām
  1. 2 2
      .agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md
  4. 6 0
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml
  5. 99 0
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md
  6. 99 0
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md
  7. 3 2
      apps/cli/package.json
  8. 2 2
      apps/cli/reference/README.i18n.yaml
  9. 1 1
      apps/cli/reference/README.md
  10. 1 1
      apps/cli/reference/README.zh.md
  11. 14 1
      apps/cli/src/profile-boot.ts
  12. 1 1
      apps/web/package.json
  13. 2 2
      docs/config-catalog.i18n.yaml
  14. 2 1
      docs/config-catalog.md
  15. 2 1
      docs/config-catalog.zh.md
  16. 2 2
      docs/module-graph.i18n.yaml
  17. 19 12
      docs/module-graph.md
  18. 19 12
      docs/module-graph.zh.md
  19. 6 0
      docs/user/guide/network-proxy.i18n.yaml
  20. 85 0
      docs/user/guide/network-proxy.md
  21. 85 0
      docs/user/guide/network-proxy.zh.md
  22. 2 1
      package.json
  23. 1 1
      packages/acp/acp/package.json
  24. 1 1
      packages/api/gateway/package.json
  25. 1 1
      packages/api/remotes/package.json
  26. 1 1
      packages/api/session-controller/package.json
  27. 1 1
      packages/api/settings-controller/package.json
  28. 1 1
      packages/api/workspace-controller/package.json
  29. 1 1
      packages/attachment/attachment-local/package.json
  30. 1 1
      packages/attachment/attachment/package.json
  31. 2 2
      packages/boot/app-boot/README.i18n.yaml
  32. 1 1
      packages/boot/app-boot/README.md
  33. 1 1
      packages/boot/app-boot/README.zh.md
  34. 1 1
      packages/boot/app-boot/package.json
  35. 25 7
      packages/boot/app-boot/src/index.ts
  36. 77 0
      packages/boot/app-boot/tests/app-boot.spec.ts
  37. 1 1
      packages/boot/cmdline/package.json
  38. 1 1
      packages/bundle/acp-app/package.json
  39. 1 1
      packages/bundle/base/package.json
  40. 1 1
      packages/bundle/headless/package.json
  41. 1 1
      packages/bundle/sdk-app/package.json
  42. 1 1
      packages/bundle/sdk-minimal/package.json
  43. 1 1
      packages/bundle/web-app/package.json
  44. 1 1
      packages/client/connection/package.json
  45. 1 1
      packages/client/hmr/package.json
  46. 1 1
      packages/client/locale/package.json
  47. 1 1
      packages/client/modules/package.json
  48. 1 1
      packages/client/store/package.json
  49. 1 1
      packages/client/ui-agent-preset/package.json
  50. 1 1
      packages/client/ui-approval/package.json
  51. 1 1
      packages/client/ui-attachment/package.json
  52. 1 1
      packages/client/ui-brand-official/package.json
  53. 1 1
      packages/client/ui-chat/package.json
  54. 1 1
      packages/client/ui-commands/package.json
  55. 1 1
      packages/client/ui-conversation/package.json
  56. 1 1
      packages/client/ui-deliverables/package.json
  57. 1 1
      packages/client/ui-directory-picker-browse/package.json
  58. 1 1
      packages/client/ui-directory-picker-native/package.json
  59. 1 1
      packages/client/ui-goal/package.json
  60. 1 1
      packages/client/ui-input-trigger/package.json
  61. 1 1
      packages/client/ui-jobs/package.json
  62. 1 1
      packages/client/ui-layout/package.json
  63. 1 1
      packages/client/ui-message-feedback/package.json
  64. 1 1
      packages/client/ui-model-selection/package.json
  65. 1 1
      packages/client/ui-permission-presets/package.json
  66. 1 1
      packages/client/ui-plan/package.json
  67. 1 1
      packages/client/ui-primitives/package.json
  68. 1 1
      packages/client/ui-reference/package.json
  69. 1 1
      packages/client/ui-renderer/package.json
  70. 1 1
      packages/client/ui-schedule/package.json
  71. 1 1
      packages/client/ui-session/package.json
  72. 1 1
      packages/client/ui-settings-general/package.json
  73. 1 1
      packages/client/ui-settings-models/package.json
  74. 1 1
      packages/client/ui-settings-plugin-inventory/package.json
  75. 1 1
      packages/client/ui-settings-plugins/package.json
  76. 1 1
      packages/client/ui-settings/package.json
  77. 1 1
      packages/client/ui-sidebar/package.json
  78. 1 1
      packages/client/ui-skill/package.json
  79. 1 1
      packages/client/ui-slots/package.json
  80. 1 1
      packages/client/ui-subagent/package.json
  81. 1 1
      packages/client/ui-theme/package.json
  82. 1 1
      packages/client/ui-tool/package.json
  83. 1 1
      packages/client/ui-trajectory/package.json
  84. 1 1
      packages/client/ui-user-questions/package.json
  85. 1 1
      packages/client/ui-workflow-run/package.json
  86. 1 1
      packages/client/ui-workspace/package.json
  87. 1 1
      packages/client/web/package.json
  88. 1 1
      packages/code-runtime/code-runtime-worker-thread/package.json
  89. 1 1
      packages/code-runtime/code-runtime/package.json
  90. 1 1
      packages/compaction/command-compact/package.json
  91. 1 1
      packages/compaction/compaction-basic/package.json
  92. 1 1
      packages/compaction/compaction-tool-result-pruner/package.json
  93. 1 1
      packages/compaction/compaction/package.json
  94. 1 1
      packages/context/agent-instructions/package.json
  95. 1 1
      packages/context/file-reference-local/package.json
  96. 1 1
      packages/context/file-reference/package.json
  97. 1 1
      packages/context/session-reference/package.json
  98. 1 1
      packages/context/time-context/package.json
  99. 1 1
      packages/context/tmux-context/package.json
  100. 1 1
      packages/core/agent-default-model/package.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml

@@ -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 .agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md
-2026-08-04-configuration-source-ownership.md: 1fe5908ab77632732996bd1d5c1eed9c8ab048e6
-2026-08-04-configuration-source-ownership.zh.md: 197cb936cdff303e23425d008c7a2cb738500ae0
+2026-08-04-configuration-source-ownership.md: 29dd5fd623d38532502af8c4e4afd972236fc139
+2026-08-04-configuration-source-ownership.zh.md: e5cbf70826b9314daccb7a62e5da4603e38158de

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md

@@ -42,7 +42,7 @@ The launching environment wins because `DEEPSEEK_API_KEY=… dsh`, a CI secret,
 
 **The project the harness is launched in is trusted, by default and without a prompt.** A checkout may carry its own endpoint, its own ordinary variables, and its own key; the key ranks below the managed store, so a key stored through the Models page is never displaced by one a checkout happens to contain. `LaunchEnvironmentSnapshot.getFrom(name, sources)` still searches only the layers a caller names, and omitting one is a refusal rather than a demotion — the mechanism exists for decisions where a layer must be unreachable; this decision includes the project layer.
 
-**Trust does not extend to changing the harness itself.** `loadLayeredEnv` rejects, at load and before anything is materialized, any `.env` that sets a variable governing how a process launches (`PATH`, `SHELL`, `NODE_OPTIONS`, `LD_PRELOAD`), which ambient program handles an operation (`EDITOR`, `PAGER`, `BROWSER`), what code a runtime executes before the program it was asked to run (`BASH_ENV`, `PERL5OPT`, `PYTHONSTARTUP`, `RUBYOPT`, `JAVA_TOOL_OPTIONS`, the Git hook commands), where model-visible instructions load from (the whole `DSH_*` namespace, `HOME`, `XDG_*`), or how the network is reached and trusted (proxy and CA variables). Matching is case-insensitive, so `https_proxy` is not a bypass.
+**Trust does not extend to changing the harness itself.** `loadLayeredEnv` rejects, at load and before anything is materialized, any `.env` that sets a variable governing how a process launches (`PATH`, `SHELL`, `NODE_OPTIONS`, `LD_PRELOAD`), which ambient program handles an operation (`EDITOR`, `PAGER`, `BROWSER`), what code a runtime executes before the program it was asked to run (`BASH_ENV`, `PERL5OPT`, `PYTHONSTARTUP`, `RUBYOPT`, `JAVA_TOOL_OPTIONS`, the Git hook commands), where model-visible instructions load from (the whole `DSH_*` namespace, `HOME`, `XDG_*`), or how the network is reached and trusted (proxy and CA variables). Matching is case-insensitive, so `https_proxy` is not a bypass. One exemption, recorded in [the proxy policy note](2026-08-27-outbound-proxy-policy.md): the four proxy names are accepted from `$DSH_HOME/.env`, which no `.env` can relocate, and still refused from the invoking directory's file.
 
 The line is that these take effect with no user action, before any turn, outside the permission policy and the sandbox. `DSH_PERMISSION_MODE` would switch off the approvals that make trusting a project meaningful at all, and `BASH_ENV` runs a file of the project's choosing on every single `bash -c` the bash tool issues — the project's code running under the agent's policy is the deal; the project rewriting that policy is not. Enumerating these is a losing game one variable at a time, which is why the whole `DSH_*` namespace is denied rather than an audited subset, and why the list is organised by what a variable *does* rather than by which runtime owns it. There is no opt-out: an escape hatch would have to be readable from somewhere, and anything a discovered file could set is the hole itself.
 
@@ -53,7 +53,7 @@ The line is that these take effect with no user action, before any turn, outside
 ## Consequences
 
 - The web credential form now takes effect against an older key in the user's `.env`; only a key exported in the launching shell still makes it read-only, and the diagnostic says so.
-- A `.env` holding `DSH_*`, `PATH`, `BROWSER`, or a proxy variable fails the launch instead of being applied. Developers keeping switches in a repository `.env` move them to their shell — a deliberate, loud break.
+- A `.env` holding `DSH_*`, `PATH`, `BROWSER`, or — in the invoking directory — a proxy variable fails the launch instead of being applied. Developers keeping switches in a repository `.env` move them to their shell — a deliberate, loud break.
 - Composition is no longer overridable by a stale shell endpoint. It is still overridable by a user's stored `settings.yaml`, which is the settings seam's layering and not something this note changes; the product CLI offers no flag above it, so a deployment that must win against stored settings owns its own bin or loader tree.
 - Not solved: the layers are still materialized into `process.env`, so ordinary project variables continue to reach child processes under the subprocess scrub. Bootstrap variables cannot come from a file at all; the environment package records the remaining subprocess reach as a limitation.
 - Exa and Perplexity still capture their key at load time rather than through the credential seam. They no longer read raw `process.env` — they resolve through the trusted layers — but converting them to per-request credential resolution is separate work.

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md

@@ -43,7 +43,7 @@ inherited process environment      (read-only, wins)
 
 **harness 被启动于其中的项目默认可信,且不做询问。** 一个 checkout 可以携带自己的 endpoint、自己的普通变量和自己的密钥;密钥排在受管存储之下,因此通过 Models 页存下的密钥绝不会被 checkout 中恰好带有的那一个顶掉。`LaunchEnvironmentSnapshot.getFrom(name, sources)` 仍然只搜索调用方点名的层,省略某层仍是拒绝而不是降级——该机制供要求某一层不可达的决策使用;本决策包含项目层。
 
-**信任不延伸到改变 harness 本身。** `loadLayeredEnv` 会在加载时、且在物化任何内容之前,拒绝任何设置了下列变量的 `.env`:决定进程如何启动的(`PATH`、`SHELL`、`NODE_OPTIONS`、`LD_PRELOAD`)、决定由哪个环境程序处理一项操作的(`EDITOR`、`PAGER`、`BROWSER`)、决定运行时在执行被要求运行的程序之前先执行哪些代码的(`BASH_ENV`、`PERL5OPT`、`PYTHONSTARTUP`、`RUBYOPT`、`JAVA_TOOL_OPTIONS`、Git 的钩子命令)、决定模型可见指令从哪里加载的(整个 `DSH_*` 命名空间、`HOME`、`XDG_*`),以及决定网络如何访问以及如何建立信任的(proxy 与 CA 变量)。匹配不区分大小写,因此 `https_proxy` 不是绕过手段。
+**信任不延伸到改变 harness 本身。** `loadLayeredEnv` 会在加载时、且在物化任何内容之前,拒绝任何设置了下列变量的 `.env`:决定进程如何启动的(`PATH`、`SHELL`、`NODE_OPTIONS`、`LD_PRELOAD`)、决定由哪个环境程序处理一项操作的(`EDITOR`、`PAGER`、`BROWSER`)、决定运行时在执行被要求运行的程序之前先执行哪些代码的(`BASH_ENV`、`PERL5OPT`、`PYTHONSTARTUP`、`RUBYOPT`、`JAVA_TOOL_OPTIONS`、Git 的钩子命令)、决定模型可见指令从哪里加载的(整个 `DSH_*` 命名空间、`HOME`、`XDG_*`),以及决定网络如何访问以及如何建立信任的(proxy 与 CA 变量)。匹配不区分大小写,因此 `https_proxy` 不是绕过手段。唯一的豁免记录在[代理策略笔记](2026-08-27-outbound-proxy-policy.zh.md)中:四个代理名可从 `$DSH_HOME/.env` 接受——没有任何 `.env` 能挪动该文件——但仍拒绝来自调用目录文件的同名变量。
 
 这条界线在于:它们无需任何用户动作、在任何轮次开始之前、且在权限策略与沙箱之外就生效。`DSH_PERMISSION_MODE` 会关掉让「信任项目」根本成立的那道审批,而 `BASH_ENV` 会在 bash 工具每次发出 `bash -c` 时执行项目指定的文件——项目的代码在 agent(智能体)的策略下运行是约定,项目改写那份策略不是。一个变量一个变量地枚举是必输的游戏,所以整个 `DSH_*` 命名空间被拒绝而不是只拒绝一份经审查的子集,也所以这份清单是按变量*做什么*而不是按哪个运行时拥有它来组织的。不设逃生门:逃生门本身总得从某处读取,而任何被发现的文件能设置的东西,就是那个漏洞本身。
 
@@ -54,7 +54,7 @@ inherited process environment      (read-only, wins)
 ## Consequences
 
 - Web 凭据表单现在能压过用户 `.env` 里更旧的密钥;只有在启动 shell 里 export 的密钥才会让它变成只读,诊断信息也会这么说。
-- 含 `DSH_*`、`PATH`、`BROWSER` 或 proxy 变量的 `.env` 会导致启动失败而不是被应用。把开关放在仓库 `.env` 里的开发者需要改放到 shell——这是一次刻意且响亮的破坏。
+- 含 `DSH_*`、`PATH`、`BROWSER` 或(在调用目录中)proxy 变量的 `.env` 会导致启动失败而不是被应用。把开关放在仓库 `.env` 里的开发者需要改放到 shell——这是一次刻意且响亮的破坏。
 - composition 不再会被陈旧的 shell endpoint 覆盖。但它仍然会被用户已存的 `settings.yaml` 覆盖,这是 settings seam 的分层方式,本 Note 不改变它;产品 CLI 没有高于它的标志,因此需要压过已存 settings 的部署方要自带 bin 或 loader 配置树。
 - 未解决的:各层仍然会被物化进 `process.env`,因此普通项目变量继续按子进程清洗规则抵达子进程。bootstrap 变量完全不能来自文件;环境包将其余变量仍可抵达子进程这一点记录为一项限制。
 - Exa 与 Perplexity 仍在加载时捕获密钥,而不是经凭据 seam。它们不再读裸 `process.env`——改为经受信层解析——但把它们改造成按请求解析凭据是另一件事。

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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 .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md
+2026-08-27-outbound-proxy-policy.md: 67927a9d1404e0b14c6e420cc5bea462be5c87ad
+2026-08-27-outbound-proxy-policy.zh.md: cf0b203a061522450a2a1b0c337a060b0c387684

+ 99 - 0
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md

@@ -0,0 +1,99 @@
+# Agent Note: One outbound proxy policy, installed before anything can request
+
+Status: implemented
+
+English | [中文](2026-08-27-outbound-proxy-policy.zh.md)
+
+## Problem
+
+Node's built-in `fetch` ignores `HTTP_PROXY` and `HTTPS_PROXY`. Every other tool a developer runs — curl, git, npm, pip — honours them, so a user behind a proxy exports the variables once and expects everything to follow. The harness did not: `setGlobalDispatcher`, `ProxyAgent`, and `EnvHttpProxyAgent` appeared zero times across `packages/` and `apps/`, so the model request, every web search, `web_fetch`, MCP over HTTP, the OTLP exporter, and the E2B SDK all connected directly, silently, with no diagnostic anywhere.
+
+The repository had briefly had an answer and lost it without noticing. PR #971 set `NODE_USE_ENV_PROXY=1` in `bin/dsh`; eleven days later `bbb1b1cc38 cleanup: remove managed source installer` deleted that launcher wholesale, taking the flag with it. What survived was one sentence in `apps/cli/reference/README.md` telling the reader to set a variable that nothing consumed any more.
+
+That sentence could not have worked anyway, for three measured reasons. `NODE_USE_ENV_PROXY` samples the environment at process start, while `loadLayeredEnv()` merges the `.env` layers afterwards, so a proxy declared in `$DSH_HOME/.env` is invisible to it. It reaches Node 24.0+ and, on the 22 line, only 22.21+ — while `engines` admits `^22.19.0`, where the variable does not exist and setting it warns about nothing. And it does not reach `web-fetch-http` at all: that provider passes its own `dispatcher` to `fetch`, and an explicit dispatcher overrides the global one whatever the flag says.
+
+## Decision
+
+**One policy, resolved once from the launch environment, installed as the global dispatcher.** `packages/util/http-proxy` resolves a `ProxyPolicy` and installs it in `runProfile` immediately after the environment snapshot is provided and before any entry mounts. Node's `fetch` resolves undici's global dispatcher, so every plain `fetch()` and every SDK that reaches `globalThis.fetch` is covered without touching its code — nine call sites at the time of writing, and every future one for free. `loadLayeredEnv` has exactly one caller and `apps/web` ships no bin, so this single site covers every profile including `sdk-minimal`, which does not layer over `base`.
+
+Resolution reads the launcher's snapshot rather than `process.env`, which is what makes a proxy in `$DSH_HOME/.env` work — the capability the environment-variable approach cannot have. Only that file: `loadLayeredEnv` refuses a proxy name in the project `.env` exactly as it refuses `PATH` or `NODE_OPTIONS` there, because that file arrives with a clone and must not choose the harness's route. The home file is exempt for the four proxy names alone, and `DSH_HOME` is itself bootstrap-only, so no `.env` can point the exemption at a directory a repository controls.
+
+**A library in `util/`, not a plugin.** Transport policy has one answer per process: nothing to swap, and no scope narrower than the process to give one. The package exports functions and mounts nothing — `boot`, `web`, `subprocess`, and `workflow` all consume it, and `util/` is the group every other group may depend on.
+
+An earlier revision put it in a new `net/` package group, reasoning that depending on `undici` disqualified it from a "zero-dependency" group. That reading was wrong: [dependencies over hand-rolling](../process/2026-07-26-dependencies-over-hand-rolling.md) records that the charter governs *harness* dependencies — util stays free of them so any group can depend on util — and does not ban external packages. What did need removing was the dependency on `dsh-launch-environment`: resolution needs one method from it, so it names a structural `EnvLookup` instead and the launcher passes its snapshot unchanged.
+
+The plugin that revision shipped is gone with it. It let a composition declare the policy in `cordis.yml`, but no shipped bundle mounted it, so the launcher's path was the only reachable one — and its `Config` was the sole supplier of a configuration branch nothing else could reach.
+
+**Four functions, because the call sites converged rather than the package growing an export each.** An earlier revision exported six: a dispatcher factory, a `node:http` agent factory, a proxy-URL lookup, a policy accessor, an installer, and a child-environment builder. Each existed for one SDK's transport, which is how a transport-policy package turns into a catalogue of other packages' constraints. Review asked whether the call sites could converge instead; they could, and each removal took a whole shape with it. Telemetry stopped being routed at all, retiring the `node:http` factory. `web-fetch-http` builds its own pinning agent under an annotated exemption, retiring the dispatcher factory. E2B reads `route.proxy`, retiring the proxy-URL lookup.
+
+What remains is `installProxyFromEnvironment`, `proxyRouteFor`, `proxyEnvironmentForChild`, and `clearedProxyEnv` — one per way a caller can need the policy, none per SDK. Installation absorbed resolution and diagnostic reporting, which no caller needed apart: a resolved policy that is not installed routes nothing.
+
+`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; an unmount between the two returned a direct, unpinned agent for a URL the first read had cleared as proxied. A route carries both, so the branch and the request cannot disagree. Its dispatcher is the process-wide one, closed rather than destroyed on disposal, so a request already in flight when a policy is unmounted still finishes.
+
+**The installed dispatcher routes by the policy, not by an environment it re-parses.** Installation builds an `Agent` whose per-origin `factory` asks `proxyForUrl` where that origin goes, and returns a `ProxyAgent` or undici's own default client for it. undici's `EnvHttpProxyAgent` was the first choice and is wrong for this policy: when no `HTTPS_PROXY` is present it sets its HTTPS agent to the HTTP one, so a scheme this package keeps direct after refusing a SOCKS or malformed URL would still be tunnelled while the diagnostic said otherwise. Routing through the one predicate removes that class of divergence by construction rather than by test. Publishing the policy into the environment remains, but now serves one reader only: a spawned child, which has no policy object to consult.
+
+This keeps `proxyForUrl()` and the dispatcher answering from one set of values. They must agree: if they disagreed about a URL, `web-fetch-http` would pin a connection the dispatcher meant to tunnel.
+
+**Resolution supplies what neither Node nor undici does.** `ALL_PROXY` backs both schemes; a blank value counts as unset, because undici's `??` chain lets an empty lowercase name shadow a populated uppercase one; loopback is always bypassed, since the Web UI, the Connection transport, and every local test server would otherwise route through the proxy and loop. The bypass list carries `::1` *and* `[::1]`: undici's own matcher reads a bare `::1` as host `:` port `1` and never exempts it.
+
+**Rejection is quiet, and never reroutes the refused scheme.** A slot the user filled and this package refused keeps that scheme direct rather than falling through to `ALL_PROXY` or the HTTP proxy, so the diagnostic and the route agree. A SOCKS URL, an unparseable string, or an unsupported scheme is reported on stderr and skipped — the variable may have been exported for other tools, and a typo in it must not stop the agent from starting. The environment is the only source, so no configuration surface exists where `AGENTS.md`'s fail-loud rule would apply instead.
+
+**Through a proxy, `web_fetch` stops resolving and pinning.** The provider validates a public address set and pins the connection to it. Through a proxy there is nothing to pin — the proxy performs the origin's DNS — and a pinned direct connection would bypass the proxy entirely. So a proxied hop skips resolution, and configuring a proxy is a statement that the proxy is trusted with destination selection. A hop the policy bypasses, which includes every loopback and every `NO_PROXY` entry, takes the resolved-and-pinned path unchanged. Kimi Code and Claude Code reached this same conclusion independently.
+
+The URL-level policy is untouched: `http(s)` only, no embedded credentials, the length cap, and the cross-origin redirect refusal all still apply on every hop.
+
+**A spawned child gets the policy through its environment; a model-executing worker gets nothing.** `proxyEnvironmentForChild()` merges into `scrubbedParentEnv()`, the one function every spawner already shares. The workflow worker does NOT receive it: it executes the model-authored script body, and a proxy URL may carry `user:password`. That is the same containment the code runtime keeps and `docs/defensive-patterns.md` requires, so a workflow's own requests go direct.
+
+The child keeps the user's own values, and that is what once broke it. Node parses `HTTP_PROXY` and `HTTPS_PROXY` under `NODE_USE_ENV_PROXY` before running the program and exits on any scheme other than `http:` or `https:`; a `socks4://` kept for `curl` therefore ended every Node child — MCP servers, subagent CLIs, `npm` — before its first line, while this process had reported only that the scheme stayed direct. Measured on Node 24.17: `socks4://`, `ftp://`, and a malformed value all exit 1; `socks5://` happens to be accepted there. The flag is now withheld whenever a value the child receives is one this package refused, so such a child connects directly and `curl` still reads the value it was kept for. Handing the child the resolved value instead would have kept Node proxied at the price of silently rewriting what the user set for another tool.
+
+This accepts a documented seam. Such a context matches bypass entries by Node's rules, which differ from this package's in separators and IPv4-range support, and the flag exists only on Node 22.21+ and 24+.
+
+**Two SDKs do not reach `globalThis.fetch`, and reading their code said otherwise.** The audit first classified the OTLP exporter and the E2B SDK as covered, on a grep that found `globalThis.fetch` in `@opentelemetry/otlp-exporter-base`. That match is the *browser* transport; on Node the delegate selects `http-exporter-transport`, which posts through `node:http` — where a global dispatcher does not reach. E2B is a second shape again: it builds its own undici `Agent`/`ProxyAgent` and takes a `proxy` URL that it never reads from the environment. Both were measured direct. E2B is handed `route.proxy` from `proxyRouteFor`, the same call `web-fetch-http` makes. Telemetry is deliberately left direct, and that exclusion is the more interesting half.
+
+**Telemetry stays direct on purpose.** Routing it needs one of two things, and both cost more than the channel is worth. An `http.Agent` reads the environment through `proxyEnv`, which arrived in Node 22.21 and 24.5 — inside the engines range, so 22.19, 22.20, and 24.0–24.4 would stay direct regardless, and the proxy package would have to keep a `createNodeHttpAgent` export for a path that works on some runtimes. Replacing the transport with the SDK's `fetch` delegate covers every runtime, but that delegate has no compression, and the shipped `base` bundle enables gzip: a realistic OTLP batch measures 6.4x smaller with it. An attempt that refused `exporter.compression` instead broke every test that boots the shipped bundle, and one that gzipped at the serializer worked but put transport code in a telemetry plugin to keep it working.
+
+Weighed against that, telemetry is the one outbound channel whose loss costs the user nothing: no tool, no model request, and no session depends on it, and an export that cannot connect is already dropped silently. A user behind a mandatory proxy is left exactly where they were before this change rather than regressed. `egress.spec.ts` now asserts the exclusion — an SDK upgrade that moved the exporter onto `fetch` would start routing telemetry through a proxy silently, and that case is what makes it visible.
+
+**Every call site carries an egress test, because reading the code was not enough.** `egress.spec.ts` in each owning package drives that site's real code path at an unresolvable `.invalid` host through a fake proxy and asserts the proxy saw the request. Nine of them cover the search backends, pi-ai discovery, MCP over HTTP, E2B, a spawned child Node, a worker thread, and telemetry's exclusion. The gate below cannot see inside a dependency; these can, and they are what turns "an SDK changed its transport" from a silent regression into a failing test.
+
+**A gate keeps the defect from returning.** `verify-no-bare-dispatcher` parses the TypeScript AST — `scripts/AGENTS.md` requires syntax-aware discovery, and a line-wise regex missed both the `{ dispatcher }` shorthand this repository already uses and a `new Alias(...)` behind a renamed import. It rejects an undici agent construction and an explicit `dispatcher` option outside the owning package. `proxyRouteFor(url)` is the sanctioned replacement, and the one call site that genuinely owns its transport — `web-fetch-http`, pinning a request to addresses it validated — says so with a `proxy-exempt:` comment. The rule exists because `web-fetch-http`'s original `new Agent` was entirely reasonable when it was written — proxying simply did not exist yet, and nothing would have caught it.
+
+## Alternatives considered
+
+**Document `NODE_USE_ENV_PROXY=1` and stop.** Rejected on three measurements, above: invisible to `$DSH_HOME/.env`, absent on the lowest supported Node, and bypassed by `web-fetch-http` regardless. It is also what the repository already claimed to do.
+
+**Thread a policy value to every call site.** DeepSeek-Reasonix does this across 98 sites, buying a per-provider opt-out. Rejected: that opt-out exists for a need this harness does not have, and nine sites changed by hand means the tenth is forgotten — Pi's changelog records OAuth and Bedrock as two separate after-the-fact fixes of exactly that kind. The isolation argument for it is real, and is answered instead by handling worker threads explicitly and by proving disposal restores the previous dispatcher.
+
+**`http.setGlobalProxyFromEnv()`.** Node's own programmatic switch covers `fetch` and `node:http` together and returns a restore function — the shape `ctx.effect()` wants. Unusable: `added: v24.14.0`, with nothing on the 22 line. Worth revisiting if `engines` ever rises past it.
+
+**Patch `globalThis.fetch` via `undici.install()`.** Pi does, to keep fetch and the dispatcher on one undici when a newer Node's bundled fetch mishandles compressed responses through a userland dispatcher. Rejected as speculative here: this repository's `engines` ceiling has not reached that runtime.
+
+**Make this a capability seam.** Rejected. Service Definition / Provider / Consumer is for swappable backends; this has one implementation and one answer per process. If operating-system proxy or PAC support ever lands, `resolveProxyPolicy` is the extension point.
+
+**Read the operating system's proxy settings.** Rejected for this change. Only Codex and Reasonix among six surveyed products do it, and Codex keeps it behind a default-off flag. Measured on the author's machine, it would have found nothing: the proxy application had written the setting to the Wi-Fi service while the primary interface was a USB ethernet adapter with no proxy, so `scutil --proxy` reported none while the exported variables worked. It also needs its own bypass matcher, because an operating system list carries CIDR entries that neither undici nor Node matches.
+
+**Give the `code-runtime` worker the proxy too.** Rejected. Model-authored programs run there with no ambient environment at all — a stronger containment than the scrubbed environment spawned commands get — and a proxy URL may carry credentials. Handing model code a credentialed URL to reach the network is the wrong trade; the exclusion is recorded in that package's limitations.
+
+## Consequences
+
+A user who exports `HTTPS_PROXY`, or writes it into `$DSH_HOME/.env`, is proxied everywhere the harness makes a request, with no flag and no configuration. The launcher installs it exactly once, before the first plugin mounts.
+
+Because the operating system's settings are not read, the user-facing documentation is now load-bearing rather than supplementary: a user who only toggled "system proxy" in a proxy application gets nothing and no diagnostic. `docs/user/guide/network-proxy.md` therefore states which variables to export and why a browser is proxied when a terminal is not — the three-mechanism confusion is the single most common report, and it is not specific to this harness.
+
+`web_fetch`'s safety story now has two shapes, and its README says so: direct hops keep address validation and pinning, proxied hops delegate destination selection to a proxy the operator configured. This is the one outward-facing security promise the change alters.
+
+Reaching Node's built-in `fetch` from a userland undici depends on both writing the legacy `Symbol.for('undici.globalDispatcher.1')` slot. That is an implicit cross-version coupling rather than a contract — corepack#834 records it breaking — so `tests/install.spec.ts` drives a real request through a loopback proxy. A version bump that breaks the coupling fails there instead of in the field.
+
+The suite is hermetic against the developer's own environment: every Vitest configuration runs `scripts/test-proxy-environment.ts`, which clears all eight proxy names in both casings before any test, and `install.spec.ts` restores the machine's values around each case that sets its own. It has to. An exported lowercase `all_proxy` decided a test's outcome during development, because resolution reads lowercase first.
+
+## Testing
+
+`packages/util/http-proxy` holds 84 tests at 100% per-file coverage. Resolution covers precedence, the `ALL_PROXY` fallback, blank-shadowing, the SOCKS and malformed diagnostics, and the HTTPS-only environment that leaves `http:` direct; routing covers the whole loopback range structurally, and bypass matching covers suffixes, ports, both IPv6 spellings, and the CIDR entry that deliberately does not match. Installation drives a real loopback proxy and asserts the absolute-form request arrives, that a bypassed target does not, and that disposal restores the dispatcher, the policy, and the environment. Every case installs through `installProxyFromEnvironment`, so no test can assert a policy object a real launch could not produce.
+
+`packages/web/web-fetch-http/tests/proxy.spec.ts` asserts the decision that matters most: under a proxy the public-address resolver is never called, while a bypassed hop still calls it exactly once, and the cross-origin redirect refusal survives on the proxied path.
+
+`verify-no-bare-dispatcher.spec.ts` proves the gate rejects the exact shape this package was introduced to fix, accepts `proxyRouteFor`, accepts an annotated exemption, and passes on the current tree.
+
+The egress suite carries telemetry's case in the negative: the shipped backend exports under an installed policy and the fake proxy sees nothing, so the deliberate exclusion is asserted rather than merely documented. A parity suite checks `proxyForUrl` against where a real `fetch` actually went for every form in the documented `NO_PROXY` vocabulary; since the dispatcher routes by that same predicate, what it now catches is a form `bypassesProxy` reads differently from how the vocabulary documents it, and any future dispatcher that reintroduces a second matcher.
+
+No recorded-session snapshot changes: nothing here alters a model-visible input or product-user-visible transcript output.

+ 99 - 0
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md

@@ -0,0 +1,99 @@
+# Agent Note: 一份出站代理策略,在任何请求发生之前装好
+
+Status: implemented
+
+[English](2026-08-27-outbound-proxy-policy.md) | 中文
+
+## Problem
+
+Node 内置的 `fetch` 会忽略 `HTTP_PROXY` 与 `HTTPS_PROXY`。开发者运行的其他工具——curl、git、npm、pip——都遵循它们,所以代理后面的用户导出一次变量就期待一切随之生效。Harness 并没有:`setGlobalDispatcher`、`ProxyAgent` 与 `EnvHttpProxyAgent` 在 `packages/` 与 `apps/` 中出现次数为零,因此模型请求、每次 web 搜索、`web_fetch`、走 HTTP 的 MCP、OTLP 导出器与 E2B SDK 全部直连,且是静默的,任何地方都没有诊断。
+
+仓库曾短暂拥有过答案,又在无人察觉时弄丢了。PR #971 在 `bin/dsh` 里设置了 `NODE_USE_ENV_PROXY=1`;十一天后 `bbb1b1cc38 cleanup: remove managed source installer` 整体删除了那个启动器,把该标志一并带走。留下的只有 `apps/cli/reference/README.md` 里的一句话,让读者去设置一个已经无人消费的变量。
+
+即便照做,那句话也不可能生效,原因有三条且都经过实测。`NODE_USE_ENV_PROXY` 在进程启动时对环境取快照,而 `loadLayeredEnv()` 是在之后才合并 `.env` 层,因此写在 `$DSH_HOME/.env` 中的代理对它不可见。它只覆盖 Node 24.0+,在 22 线上只覆盖 22.21+——而 `engines` 允许 `^22.19.0`,那里根本没有这个变量,设置了也不会有任何警告。它也完全触及不到 `web-fetch-http`:该提供方向 `fetch` 传入自己的 `dispatcher`,而显式 dispatcher 无论标志如何都会覆盖全局的那个。
+
+## Decision
+
+**一份策略,从启动环境解析一次,装为全局 dispatcher。** `packages/util/http-proxy` 解析出 `ProxyPolicy`,并在 `runProfile` 中于环境快照提供之后、任何 entry 挂载之前完成安装。Node 的 `fetch` 解析的正是 undici 的全局 dispatcher,因此每一处普通 `fetch()` 以及每一个最终落到 `globalThis.fetch` 的 SDK 都无需改动即被覆盖——撰写时是九个调用点,未来新增的也自动覆盖。`loadLayeredEnv` 只有一个调用方,且 `apps/web` 不提供 bin,因此这一处即覆盖全部 profile,包括不叠加 `base` 的 `sdk-minimal`。
+
+解析读取的是启动器的快照而非 `process.env`,这正是让 `$DSH_HOME/.env` 中的代理生效的原因——也是环境变量方案不可能具备的能力。仅限该文件:`loadLayeredEnv` 拒绝项目 `.env` 里的代理名,正如它在那里拒绝 `PATH` 或 `NODE_OPTIONS`,因为那个文件随 clone 一起到来,不得替 Harness 选择路由。home 文件仅对这四个代理名豁免,而 `DSH_HOME` 本身是 bootstrap-only,因此没有任何 `.env` 能把这份豁免指向仓库控制的目录。
+
+**放在 `util/` 的库,而非插件。** 传输策略每个进程只有一个答案:没有可替换的实现,也没有比进程更窄的作用域可赋予。因此本包只导出函数、不挂载任何东西——`boot`、`web`、`subprocess` 与 `workflow` 都消费它,而 `util/` 正是其他所有组都可以依赖的那一组。
+
+早先的修订把它放进新建的 `net/` 包组,理由是依赖 `undici` 使它不符合“零依赖”组。那个理解是错的:[优先使用依赖而非手写](../process/2026-07-26-dependencies-over-hand-rolling.zh.md) 记录了该章程约束的是 *harness* 依赖——util 不依赖它们,任何组才都能依赖 util——并不禁止外部包。真正需要去掉的是对 `dsh-launch-environment` 的依赖:解析只用到它的一个方法,于是改为声明结构化的 `EnvLookup`,启动器原样传入自己的快照即可。
+
+那次修订一并引入的插件也随之删除。它让某个组合可以把策略写进 `cordis.yml`,但没有任何随附 bundle 挂载它,因此启动器那条路径是唯一可达的——而它的 `Config` 是那条配置分支唯一的供给方,别处无从到达。
+
+**四个函数——收敛的是调用方,而不是让本包为每个 SDK 各加一个导出。** 早先一版导出六个:dispatcher 工厂、`node:http` agent 工厂、代理 URL 查询、策略访问器、安装器与子进程环境构造器。每一个都为某个 SDK 的传输而存在,而这正是一个传输策略包退化成「别的包的约束目录」的过程。Review 问能不能反过来让调用方收敛;能,而且每删掉一个导出都带走了一整种写法。遥测不再被路由,`node:http` agent 工厂随之退场。`web-fetch-http` 在带注释的豁免下自建 pin agent,dispatcher 工厂随之退场。E2B 读 `route.proxy`,代理 URL 查询随之退场。
+
+剩下的是 `installProxyFromEnvironment`、`proxyRouteFor`、`proxyEnvironmentForChild` 与 `clearedProxyEnv`——按「调用方需要策略的方式」各一个,而不是按 SDK 各一个。安装吸收了解析与诊断上报,因为没有调用方需要把它们分开:解析出来却不安装的策略什么也路由不了。
+
+`proxyRouteFor` 还堵掉了旧访问器让人写得出来的一个缺陷。`web-fetch-http` 先读策略决定是否 pin,再读一次去构造传输;两次读取之间发生卸载,就会为第一次读取已判定走代理的 URL 返回一个直连且未 pin 的 agent。路由把两者一起交出,分支与请求便无从分歧。它携带的是进程级 dispatcher,dispose 时是 close 而非 destroy,因此策略被卸载时已经发出的请求仍会跑完。
+
+**已安装的 dispatcher 按策略路由,而不是重新解析一遍环境。** 安装过程构造一个 `Agent`,其按 origin 调用的 `factory` 会询问 `proxyForUrl` 该 origin 的去向,并据此返回 `ProxyAgent` 或 undici 自带的默认客户端。undici 的 `EnvHttpProxyAgent` 曾是首选,但对这套策略是错的:没有 `HTTPS_PROXY` 时它会把 HTTPS agent 设为 HTTP agent,于是本包在拒绝某个 SOCKS 或畸形 URL 后本应保持直连的 scheme 仍会被隧道转发,而诊断却声称直连。让路由走同一个谓词,从构造上而非靠测试消除了这一类分歧。把策略发布到环境中的做法保留下来,但如今只服务一类读者:派生的子进程——它没有策略对象可查。
+
+这样 `proxyForUrl()` 与 dispatcher 就从同一组值给出答案。两者必须一致:一旦对某个 URL 产生分歧,`web-fetch-http` 就会把 dispatcher 本打算隧道转发的连接固定到某个地址上。
+
+**解析补上 Node 与 undici 都不提供的部分。** `ALL_PROXY` 为两种协议兜底;空值视为未设置,因为 undici 的 `??` 链会让空的小写名遮住有值的大写名;loopback 始终绕过,否则 Web UI、Connection 传输以及每一个本地测试服务器都会经由代理并形成回环。绕过列表同时携带 `::1` **与** `[::1]`:undici 自带的匹配器会把裸写的 `::1` 读成主机 `:` 端口 `1`,从而永不豁免它。
+
+**拒绝是静默的,且绝不为被拒协议改道。** 用户填写而被本包拒绝的槽位,会让该协议保持直连,而不是继续回退到 `ALL_PROXY` 或 HTTP 代理,从而让诊断与实际路由一致。SOCKS URL、无法解析的字符串或不受支持的协议,会在 stderr 上报告并跳过——该变量可能是为其他工具导出的,它的笔误不应阻止 agent 启动。环境是唯一来源,因此不存在一个本应适用 `AGENTS.md` 「配置错误必须响」规则的配置面。
+
+**经由代理时,`web_fetch` 不再解析与固定地址。** 该提供方会校验一组公网地址并把连接固定到其上。经由代理时没有可固定的对象——origin 的 DNS 由代理执行——而固定后的直连会彻底绕开代理。因此代理转发的一跳跳过解析,配置代理即表示信任该代理进行目的地选择。被策略绕过的一跳,包括每一个 loopback 与每一条 `NO_PROXY` 条目,仍走原有的解析并固定路径。Kimi Code 与 Claude Code 各自独立得出了同一结论。
+
+URL 层策略未受影响:仅 `http(s)`、禁止内嵌凭据、长度上限与跨域重定向拒绝在每一跳上依然生效。
+
+**派生的子进程通过环境获得策略;执行模型代码的 worker 什么也不获得。** `proxyEnvironmentForChild()` 并入 `scrubbedParentEnv()`——每个 spawner 本就共享的那一个函数。workflow worker **不**接收它:它执行的是模型编写的脚本体,而代理 URL 可能携带 `user:password`。这与 code runtime 保持的隔离相同,也是 `docs/defensive-patterns.md` 的要求,因此 workflow 自身的请求直连。
+
+子进程拿到的是用户自己的值,而这恰恰曾把它弄坏。Node 在 `NODE_USE_ENV_PROXY` 下会在运行程序之前先解析 `HTTP_PROXY` 与 `HTTPS_PROXY`,遇到 `http:`/`https:` 之外的协议直接退出;于是一个为 `curl` 保留的 `socks4://` 会让每个 Node 子进程——MCP server、subagent CLI、`npm`——在第一行之前就终结,而本进程此前只报告过该协议保持直连。在 Node 24.17 上实测:`socks4://`、`ftp://` 与畸形值均以 1 退出;`socks5://` 恰好在该版本被接受。现在只要子进程收到的某个值是本包拒绝过的,就扣下该标志,这样的子进程直连,`curl` 仍读到为它保留的值。若改为把解析后的值交给子进程,Node 固然能继续走代理,代价却是悄悄改写用户为另一工具设置的值。
+
+这接受了一处已记录的接缝。此类上下文按 Node 自己的规则匹配绕过条目,其分隔符与 IPv4 区间支持与本包不同,且该标志仅存在于 Node 22.21+ 与 24+。
+
+**有两个 SDK 并不落到 `globalThis.fetch`,而读代码给出的答案是相反的。** 审计最初把 OTLP 导出器与 E2B SDK 判为已覆盖,依据是在 `@opentelemetry/otlp-exporter-base` 里 grep 到了 `globalThis.fetch`。那处命中属于**浏览器**传输;在 Node 上 delegate 选择的是 `http-exporter-transport`,它通过 `node:http` 投递——那里全局 dispatcher 触及不到。E2B 又是另一种形态:它自建 undici `Agent`/`ProxyAgent`,并接受一个自己从不从环境读取的 `proxy` URL。两者都实测为直连。E2B 接收 `proxyRouteFor` 给出的 `route.proxy`,与 `web-fetch-http` 调的是同一个函数。遥测则被有意保留为直连,而这个排除项才是更值得说的一半。
+
+**遥测的直连是有意为之。** 要让它走代理只有两条路,代价都超过这条通道本身的价值。`http.Agent` 通过 `proxyEnv` 读取环境,而该选项自 Node 22.21 与 24.5 才有——落在 engines 范围之内,因此 22.19、22.20 与 24.0–24.4 无论如何仍是直连,而代理包还得为一条只在部分运行时生效的路径保留 `createNodeHttpAgent` 导出。改用 SDK 的 `fetch` delegate 替换传输可以覆盖所有运行时,但该 delegate 没有压缩能力,而随附的 `base` bundle 启用了 gzip:实测一批真实规模的 OTLP 数据启用后体积只有 1/6.4。曾有一版转而在加载期拒绝 `exporter.compression`,结果凡是启动随附 bundle 的测试全部失败;另一版在 serializer 处 gzip 确实能跑通,但代价是把传输层代码塞进了遥测插件。
+
+与之相比,遥测是唯一一条丢失了对用户毫无代价的出网通道:没有任何工具、模型请求或会话依赖它,而连不上的导出本就被静默丢弃。处在强制代理后的用户,只是停留在本次改动之前的状态,而不是被弄坏。`egress.spec.ts` 现在断言这一排除——若某次 SDK 升级把导出器挪到 `fetch` 上,遥测就会开始静默走代理,而该用例正是让这件事暴露出来的东西。
+
+**每个出网点都配一份出网测试,因为读代码不够。** 各所属包中的 `egress.spec.ts` 驱动该点的真实代码路径,目标是无法解析的 `.invalid` 主机,穿过一个假代理,并断言代理确实收到了请求。九份测试覆盖搜索后端、pi-ai 发现、走 HTTP 的 MCP、E2B、派生的子 Node、worker 线程,以及遥测的排除。下面那条门禁看不进依赖内部;这些能,它们把「某个 SDK 换了传输」从静默回归变成失败的测试。
+
+**用门禁防止该缺陷复现。** `verify-no-bare-dispatcher` 解析 TypeScript AST——`scripts/AGENTS.md` 要求 source-ownership 门禁使用语法感知发现,而逐行正则漏掉了本仓库已在使用的 `{ dispatcher }` 简写,以及重命名导入后的 `new Alias(...)`。它在所属包之外拒绝 undici agent 构造与显式 `dispatcher` 选项。`proxyRouteFor(url)` 是受支持的替代;唯一一处确实自有传输的调用点——`web-fetch-http`,它把请求钉在已校验的地址上——用 `proxy-exempt:` 注释说明。这条规则之所以存在,是因为 `web-fetch-http` 里原本那行 `new Agent` 在写下时完全合理——那时根本还没有代理这回事,也没有任何机制会拦下它。
+
+## Alternatives considered
+
+**只写文档,让用户设 `NODE_USE_ENV_PROXY=1`。** 基于上文三条实测被否决:对 `$DSH_HOME/.env` 不可见、在最低支持的 Node 上不存在、且无论如何被 `web-fetch-http` 绕过。而这恰恰是仓库此前声称的做法。
+
+**把策略值传递到每一个调用点。** DeepSeek-Reasonix 在 98 处这样做,换来每提供方的 opt-out。被否决:该能力服务于本 Harness 并不具备的需求,而手工改九处意味着第十处会被遗忘——Pi 的变更日志正记录了 OAuth 与 Bedrock 两次事后补漏。它关于隔离性的论点确实成立,本方案改为显式处理 worker 线程、并以「dispose 后还原前一个 dispatcher」的断言来回应。
+
+**`http.setGlobalProxyFromEnv()`。** Node 自带的程序化开关同时覆盖 `fetch` 与 `node:http`,并返回还原函数——正是 `ctx.effect()` 想要的形态。不可用:`added: v24.14.0`,22 线上完全没有。若 `engines` 日后升过该版本,值得回头替换。
+
+**用 `undici.install()` patch `globalThis.fetch`。** Pi 这样做,是为了让 fetch 与 dispatcher 处于同一个 undici——较新 Node 的内置 fetch 经 userland dispatcher 处理压缩响应时会出错。此处被否决为投机性复杂度:本仓库 `engines` 的上限尚未触及该运行时。
+
+**做成能力接缝。** 被否决。Service Definition/Provider/Consumer 用于可替换后端;这里每个进程只有一种实现、一个答案。若日后要支持操作系统代理或 PAC,`resolveProxyPolicy` 就是扩展点。
+
+**读取操作系统的代理设置。** 本次变更中被否决。所调研的六个产品中只有 Codex 与 Reasonix 这样做,且 Codex 把它放在默认关闭的开关之后。在作者机器上实测,它什么也读不到:代理软件把设置写在了 Wi-Fi 服务上,而主接口是一块没有代理的 USB 以太网卡,因此 `scutil --proxy` 报告无代理,而导出的环境变量却工作正常。它还需要自带的绕过匹配器,因为操作系统的列表含有 undici 与 Node 都不匹配的 CIDR 条目。
+
+**也把代理给 `code-runtime` worker。** 被否决。模型编写的程序在那里运行时完全没有环境变量——这比派生命令得到的 scrubbed 环境更严——而代理 URL 可能携带凭据。把带凭据的 URL 交给模型代码去访问网络是错误的取舍;该排除已记入那个包的限制清单。
+
+## Consequences
+
+导出了 `HTTPS_PROXY`、或把它写进 `$DSH_HOME/.env` 的用户,在 Harness 发起请求的每一处都会走代理,无需任何标志与配置。启动器在第一个插件挂载之前恰好安装一次。
+
+由于不读取操作系统设置,面向用户的文档从补充材料变成了承重件:仅在代理软件里拨了「系统代理」开关的用户什么也得不到,且没有诊断。因此 `docs/user/guide/network-proxy.md` 说明了要导出哪些变量,以及为什么浏览器走代理而终端不走——这个「三套机制」的困惑是最常见的报障,且并非本 Harness 特有。
+
+`web_fetch` 的安全叙述现在有两种形态,其 README 已如实说明:直连的一跳保留地址校验与固定,代理转发的一跳把目的地选择交给运维方配置的代理。这是本次变更唯一改动的对外安全承诺。
+
+userland undici 能触及 Node 内置的 `fetch`,依赖于两者都会写入 legacy 的 `Symbol.for('undici.globalDispatcher.1')` 槽位。那是跨版本的隐式耦合而非约定——corepack#834 记录了它失效的实例——因此 `tests/install.spec.ts` 会驱动一次真实请求穿过 loopback 代理。破坏该耦合的版本升级会在那里失败,而不是流到线上。
+
+测试套件对开发者自身的环境免疫:每份 Vitest 配置都会先运行 `scripts/test-proxy-environment.ts`,在任何测试之前清除全部八个代理变量名的两种大小写形式;`install.spec.ts` 则在每个自行设值的用例前后还原本机的值。这是必需的。开发过程中,一个已导出的小写 `all_proxy` 曾决定了某个测试的结果,因为解析优先读取小写。
+
+## Testing
+
+`packages/util/http-proxy` 有 84 个测试,per-file 覆盖率 100%。解析覆盖优先级、`ALL_PROXY` 兜底、空值遮蔽、SOCKS 与畸形值诊断,以及只设 https 变量时 `http:` 保持直连;路由以结构化方式覆盖整个 loopback 网段,绕过匹配覆盖后缀、端口、两种 IPv6 写法,以及刻意不匹配的 CIDR 条目。安装驱动一个真实的 loopback 代理,断言绝对形式的请求确实抵达、被绕过的目标不抵达,且 dispose 会还原 dispatcher、策略与环境。所有用例一律经 `installProxyFromEnvironment` 安装,因此没有测试能断言一次真实启动无法产生的策略对象。
+
+`packages/web/web-fetch-http/tests/proxy.spec.ts` 断言了最关键的那个决定:经由代理时公网地址解析器完全不被调用,而被绕过的一跳仍恰好调用一次,且跨域重定向拒绝在代理路径上依然成立。
+
+`verify-no-bare-dispatcher.spec.ts` 证明该门禁能拒掉本包所要修复的那种写法、接受 `proxyRouteFor`、接受带注释的豁免,并在当前代码树上通过。
+
+出网测试以负向形式承载遥测这一项:随附后端在已安装策略下执行导出,而假代理什么也没收到——这个有意的排除因此是被断言的,而不只是被记录的。另有一组一致性测试,对文档所述 `NO_PROXY` 词汇中的每种形态,把 `proxyForUrl` 的判断与真实 `fetch` 的实际去向相互核对;由于 dispatcher 正是按同一谓词路由,它现在能抓住的是 `bypassesProxy` 对某种形态的读法与词汇文档不一致,以及未来任何重新引入第二个匹配器的 dispatcher。
+
+无录制会话快照变更:本次改动不影响任何模型可见输入或产品用户可见的 transcript 输出。

+ 3 - 2
apps/cli/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh",
   "description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },
@@ -97,7 +97,8 @@
     "@deepseek-ai/schemastery": "workspace:^",
     "commander": "^15.0.0",
     "js-yaml": "^4.2.0",
-    "node-addon-require-builtin": "^0.1.4"
+    "node-addon-require-builtin": "^0.1.4",
+    "@deepseek-ai/dsh-http-proxy": "workspace:^"
   },
   "devDependencies": {
     "@agentclientprotocol/sdk": "1.4.0",

+ 2 - 2
apps/cli/reference/README.i18n.yaml

@@ -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 apps/cli/reference/README.md
-README.md: b43c147036ad230ed85cebafa3df89d67a802f6b
-README.zh.md: 9191139e28b4bb449593514cb61750de42dba24e
+README.md: 78328be4d614ebf647d6f00a6f0d66978919da3e
+README.zh.md: c140c3ba528509611ae17712851c6d406c190972

+ 1 - 1
apps/cli/reference/README.md

@@ -98,4 +98,4 @@ Install external plugin bundles through `dsh plugin --profile <name> add <packag
 <a id="source-execution"></a>
 ## Source execution
 
-From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
+From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment, and `runProfile` resolves the outbound proxy from that snapshot before any entry mounts, so `HTTP_PROXY`/`HTTPS_PROXY` (and a proxy declared in a `.env` layer) apply without any further flag. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.

+ 1 - 1
apps/cli/reference/README.zh.md

@@ -98,4 +98,4 @@ dsh web --help
 <a id="source-execution"></a>
 ## 源码执行
 
-请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>`。`package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
+请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>`。`package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境,且 `runProfile` 会在任何 entry 挂载之前从该快照解析出站代理,因此 `HTTP_PROXY`/`HTTPS_PROXY`(以及写在 `.env` 层中的代理)无需任何额外开关即可生效。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。

+ 14 - 1
apps/cli/src/profile-boot.ts

@@ -30,6 +30,7 @@ import {
   type Profile,
 } from '@deepseek-ai/dsh-app-boot'
 import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
+import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
 import { DSH_LAUNCH_ENVIRONMENT_KEY, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
 import { provideCmdline, type AppReady } from '@deepseek-ai/dsh-cmdline'
 import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
@@ -207,10 +208,22 @@ function suppressShutdownError(ctx: Context, signal: AbortSignal, error: unknown
  * @returns the settled root context and the shutdown controller.
  */
 export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> {
+  // Before the first plugin mounts and before anything can issue a request: Node's fetch ignores the
+  // proxy environment on its own, so every profile would otherwise connect directly. Resolving from
+  // the launcher's snapshot — not `process.env` — is what lets a proxy declared in a `.env` layer
+  // work, which the NODE_USE_ENV_PROXY flag cannot do because Node samples the environment at start.
+  const disposeProxy = await installProxyFromEnvironment(
+    options.environment,
+    (message) => { process.stderr.write(`${NAME}: ${message}\n`) },
+  )
+
   const composed = await composeProfile(options.profile, options.patchFiles)
   const app: { current?: Context } = {}
   const appReady = createAppReady()
-  const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() })
+  const shutdown = createProcessShutdown(async () => {
+    await app.current?.fiber.dispose()
+    await disposeProxy()
+  })
   const signalShutdown = new AbortController()
   const interrupt = (code: number): void => {
     signalShutdown.abort()

+ 1 - 1
apps/web/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-web-frontend",
   "description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -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 docs/config-catalog.md
-config-catalog.md: 6a0f7f23885cd85ccb1ff20e9322f133767aed4a
-config-catalog.zh.md: cae9fa49a6986130c8b7261cf41e04b4d6b4e0e1
+config-catalog.md: 14aedbe0c0de0d0bc6779d3993c8a03135dc93e3
+config-catalog.zh.md: 4388de1391a273e1e088f1590d0ea1da98258927

+ 2 - 1
docs/config-catalog.md

@@ -513,7 +513,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
+Source: [`packages/e2b/e2b/src/index.ts:45`](../packages/e2b/e2b/src/index.ts)
 
 <a id="deepseek-aidsh-experimental-agent-team"></a>
 
@@ -3454,6 +3454,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
 - `@deepseek-ai/dsh-experimental-webworker-runtime` ([`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts))
 - `@deepseek-ai/dsh-home-paths` ([`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts))
 - `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
+- `@deepseek-ai/dsh-http-proxy` ([`packages/util/http-proxy/src/index.ts`](../packages/util/http-proxy/src/index.ts))
 - `@deepseek-ai/dsh-launch-environment` ([`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts))
 - `@deepseek-ai/dsh-llm-mock-server` ([`packages/test-support/llm-mock-server/src/index.ts`](../packages/test-support/llm-mock-server/src/index.ts))
 - `@deepseek-ai/dsh-loader-smoke` ([`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts))

+ 2 - 1
docs/config-catalog.zh.md

@@ -515,7 +515,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
+来源:[`packages/e2b/e2b/src/index.ts:45`](../packages/e2b/e2b/src/index.ts)
 
 <a id="deepseek-aidsh-experimental-agent-team"></a>
 
@@ -3455,6 +3455,7 @@ export interface Config {
 - `@deepseek-ai/dsh-experimental-webworker-runtime`([`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts))
 - `@deepseek-ai/dsh-home-paths`([`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts))
 - `@deepseek-ai/dsh-hook-protocol`([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
+- `@deepseek-ai/dsh-http-proxy`([`packages/util/http-proxy/src/index.ts`](../packages/util/http-proxy/src/index.ts))
 - `@deepseek-ai/dsh-launch-environment`([`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts))
 - `@deepseek-ai/dsh-llm-mock-server`([`packages/test-support/llm-mock-server/src/index.ts`](../packages/test-support/llm-mock-server/src/index.ts))
 - `@deepseek-ai/dsh-loader-smoke`([`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts))

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -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 docs/module-graph.md
-module-graph.md: 514d09c5883ec8196e0706fbd7a3ccf45b8aafb5
-module-graph.zh.md: 444100e17ce6794efa39ef2e0a10872a6e1c3484
+module-graph.md: e6ca3e11a28b70310ecbd3118e1c736227626abf
+module-graph.zh.md: 9025dc6cce64d89cceaded2e92e3e5b465f79c7a

+ 19 - 12
docs/module-graph.md

@@ -12,6 +12,7 @@ flowchart TD
     pkg_brand["brand"]
     pkg_deque["deque"]
     pkg_home_paths["home-paths"]
+    pkg_http_proxy["http-proxy"]
     pkg_launch_environment["launch-environment"]
     pkg_native_command["native-command"]
     pkg_output_retention["output-retention"]
@@ -360,9 +361,7 @@ flowchart TD
   pkg_web --> pkg_llm
   pkg_attachment --> pkg_brand
   pkg_credentials --> pkg_invariants
-  pkg_subprocess_e2b --> pkg_e2b
-  pkg_subprocess_e2b --> pkg_subprocess
-  pkg_subprocess_e2b --> pkg_timeout
+  pkg_e2b --> pkg_http_proxy
   pkg_experimental_code_runtime_python --> pkg_code_runtime
   pkg_experimental_code_runtime_python --> pkg_timeout
   pkg_experimental_code_runtime_python --> pkg_util_values
@@ -386,8 +385,7 @@ flowchart TD
   pkg_storage_domain --> pkg_storage
   pkg_storage_json --> pkg_storage
   pkg_storage_sqlite --> pkg_storage
-  pkg_subprocess_local --> pkg_subprocess
-  pkg_subprocess_local --> pkg_timeout
+  pkg_subprocess --> pkg_http_proxy
   pkg_typert_loader --> pkg_typert_registry
   pkg_session --> pkg_scope
   pkg_system_prompt --> pkg_invariants
@@ -395,6 +393,7 @@ flowchart TD
   pkg_system_prompt --> pkg_scope
   pkg_skill --> pkg_llm
   pkg_skill --> pkg_scope
+  pkg_web_fetch_http --> pkg_http_proxy
   pkg_web_fetch_http --> pkg_timeout
   pkg_web_fetch_http --> pkg_web
   pkg_web_search_exa --> pkg_launch_environment
@@ -411,6 +410,11 @@ flowchart TD
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_launch_environment
+  pkg_subprocess_e2b --> pkg_e2b
+  pkg_subprocess_e2b --> pkg_subprocess
+  pkg_subprocess_e2b --> pkg_timeout
+  pkg_subprocess_local --> pkg_subprocess
+  pkg_subprocess_local --> pkg_timeout
   pkg_skill_badge --> pkg_skill
   pkg_spill --> pkg_brand
   pkg_spill --> pkg_llm
@@ -434,6 +438,7 @@ flowchart TD
   pkg_settings --> pkg_brand
   pkg_settings --> pkg_invariants
   pkg_settings --> pkg_session
+  pkg_session_snapshot --> pkg_http_proxy
   pkg_session_snapshot --> pkg_session
   pkg_agent --> pkg_invariants
   pkg_agent --> pkg_llm
@@ -603,6 +608,7 @@ flowchart TD
   pkg_terminal --> pkg_agent
   pkg_terminal --> pkg_brand
   pkg_loader_smoke --> pkg_agent
+  pkg_loader_smoke --> pkg_http_proxy
   pkg_loader_smoke --> pkg_llm
   pkg_loader_smoke --> pkg_session
   pkg_workflow --> pkg_agent
@@ -1142,6 +1148,7 @@ flowchart TD
 | [`brand`](../packages/util/brand) | `util` | — |
 | [`deque`](../packages/util/deque) | `util` | — |
 | [`home-paths`](../packages/util/home-paths) | `util` | — |
+| [`http-proxy`](../packages/util/http-proxy) | `util` | — |
 | [`launch-environment`](../packages/util/launch-environment) | `util` | — |
 | [`native-command`](../packages/util/native-command) | `util` | — |
 | [`output-retention`](../packages/util/output-retention) | `util` | — |
@@ -1203,7 +1210,6 @@ flowchart TD
 | [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | — |
 | [`client-web`](../packages/client/web) | `client` | — |
 | [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — |
-| [`e2b`](../packages/e2b/e2b) | `e2b` | — |
 | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — |
 | [`experimental-agent-team-web-profile`](../packages/experimental/agent-team-web-profile) | `experimental` | — |
 | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — |
@@ -1216,7 +1222,6 @@ flowchart TD
 | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — |
 | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | — |
 | [`storage`](../packages/storage/storage) | `storage` | — |
-| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | — |
 | [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | — |
 | [`llm-mock-server`](../packages/test-support/llm-mock-server) | `test-support` | — |
 | [`typert-generator`](../packages/typert/generator) | `typert` | — |
@@ -1226,7 +1231,7 @@ flowchart TD
 | [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
 | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
 | [`credentials`](../packages/credentials/credentials) | `credentials` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`e2b`](../packages/e2b/e2b) | `e2b` | [`http-proxy`](../packages/util/http-proxy) |
 | [`experimental-code-runtime-python`](../packages/experimental/code-runtime-python) | `experimental` | [`code-runtime`](../packages/code-runtime/code-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
 | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
 | [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
@@ -1237,18 +1242,20 @@ flowchart TD
 | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
 | [`storage-json`](../packages/storage/storage-json) | `storage` | [`storage`](../packages/storage/storage) |
 | [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`storage`](../packages/storage/storage) |
-| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
 | [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
 | [`session`](../packages/core/session) | `core` | [`scope`](../packages/core/scope) |
 | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
 | [`skill`](../packages/skill/skill) | `skill` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
-| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
+| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`http-proxy`](../packages/util/http-proxy), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
 | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
 | [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
 | [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) |
 | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
 | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
+| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
 | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`system-prompt`](../packages/core/system-prompt) |
@@ -1259,7 +1266,7 @@ flowchart TD
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`session`](../packages/core/session) |
 | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
-| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`session`](../packages/core/session) |
+| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) |
 | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
@@ -1299,7 +1306,7 @@ flowchart TD
 | [`bash-local`](../packages/shell/bash-local) | `shell` | [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
-| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
 | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |

+ 19 - 12
docs/module-graph.zh.md

@@ -14,6 +14,7 @@ flowchart TD
     pkg_brand["brand"]
     pkg_deque["deque"]
     pkg_home_paths["home-paths"]
+    pkg_http_proxy["http-proxy"]
     pkg_launch_environment["launch-environment"]
     pkg_native_command["native-command"]
     pkg_output_retention["output-retention"]
@@ -362,9 +363,7 @@ flowchart TD
   pkg_web --> pkg_llm
   pkg_attachment --> pkg_brand
   pkg_credentials --> pkg_invariants
-  pkg_subprocess_e2b --> pkg_e2b
-  pkg_subprocess_e2b --> pkg_subprocess
-  pkg_subprocess_e2b --> pkg_timeout
+  pkg_e2b --> pkg_http_proxy
   pkg_experimental_code_runtime_python --> pkg_code_runtime
   pkg_experimental_code_runtime_python --> pkg_timeout
   pkg_experimental_code_runtime_python --> pkg_util_values
@@ -388,8 +387,7 @@ flowchart TD
   pkg_storage_domain --> pkg_storage
   pkg_storage_json --> pkg_storage
   pkg_storage_sqlite --> pkg_storage
-  pkg_subprocess_local --> pkg_subprocess
-  pkg_subprocess_local --> pkg_timeout
+  pkg_subprocess --> pkg_http_proxy
   pkg_typert_loader --> pkg_typert_registry
   pkg_session --> pkg_scope
   pkg_system_prompt --> pkg_invariants
@@ -397,6 +395,7 @@ flowchart TD
   pkg_system_prompt --> pkg_scope
   pkg_skill --> pkg_llm
   pkg_skill --> pkg_scope
+  pkg_web_fetch_http --> pkg_http_proxy
   pkg_web_fetch_http --> pkg_timeout
   pkg_web_fetch_http --> pkg_web
   pkg_web_search_exa --> pkg_launch_environment
@@ -413,6 +412,11 @@ flowchart TD
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_launch_environment
+  pkg_subprocess_e2b --> pkg_e2b
+  pkg_subprocess_e2b --> pkg_subprocess
+  pkg_subprocess_e2b --> pkg_timeout
+  pkg_subprocess_local --> pkg_subprocess
+  pkg_subprocess_local --> pkg_timeout
   pkg_skill_badge --> pkg_skill
   pkg_spill --> pkg_brand
   pkg_spill --> pkg_llm
@@ -436,6 +440,7 @@ flowchart TD
   pkg_settings --> pkg_brand
   pkg_settings --> pkg_invariants
   pkg_settings --> pkg_session
+  pkg_session_snapshot --> pkg_http_proxy
   pkg_session_snapshot --> pkg_session
   pkg_agent --> pkg_invariants
   pkg_agent --> pkg_llm
@@ -605,6 +610,7 @@ flowchart TD
   pkg_terminal --> pkg_agent
   pkg_terminal --> pkg_brand
   pkg_loader_smoke --> pkg_agent
+  pkg_loader_smoke --> pkg_http_proxy
   pkg_loader_smoke --> pkg_llm
   pkg_loader_smoke --> pkg_session
   pkg_workflow --> pkg_agent
@@ -1144,6 +1150,7 @@ flowchart TD
 | [`brand`](../packages/util/brand) | `util` | — |
 | [`deque`](../packages/util/deque) | `util` | — |
 | [`home-paths`](../packages/util/home-paths) | `util` | — |
+| [`http-proxy`](../packages/util/http-proxy) | `util` | — |
 | [`launch-environment`](../packages/util/launch-environment) | `util` | — |
 | [`native-command`](../packages/util/native-command) | `util` | — |
 | [`output-retention`](../packages/util/output-retention) | `util` | — |
@@ -1205,7 +1212,6 @@ flowchart TD
 | [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | — |
 | [`client-web`](../packages/client/web) | `client` | — |
 | [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — |
-| [`e2b`](../packages/e2b/e2b) | `e2b` | — |
 | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — |
 | [`experimental-agent-team-web-profile`](../packages/experimental/agent-team-web-profile) | `experimental` | — |
 | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — |
@@ -1218,7 +1224,6 @@ flowchart TD
 | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — |
 | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | — |
 | [`storage`](../packages/storage/storage) | `storage` | — |
-| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | — |
 | [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | — |
 | [`llm-mock-server`](../packages/test-support/llm-mock-server) | `test-support` | — |
 | [`typert-generator`](../packages/typert/generator) | `typert` | — |
@@ -1228,7 +1233,7 @@ flowchart TD
 | [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
 | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
 | [`credentials`](../packages/credentials/credentials) | `credentials` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`e2b`](../packages/e2b/e2b) | `e2b` | [`http-proxy`](../packages/util/http-proxy) |
 | [`experimental-code-runtime-python`](../packages/experimental/code-runtime-python) | `experimental` | [`code-runtime`](../packages/code-runtime/code-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
 | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
 | [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
@@ -1239,18 +1244,20 @@ flowchart TD
 | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
 | [`storage-json`](../packages/storage/storage-json) | `storage` | [`storage`](../packages/storage/storage) |
 | [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`storage`](../packages/storage/storage) |
-| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
 | [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
 | [`session`](../packages/core/session) | `core` | [`scope`](../packages/core/scope) |
 | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
 | [`skill`](../packages/skill/skill) | `skill` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
-| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
+| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`http-proxy`](../packages/util/http-proxy), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
 | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
 | [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
 | [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) |
 | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
 | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
+| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
 | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`system-prompt`](../packages/core/system-prompt) |
@@ -1261,7 +1268,7 @@ flowchart TD
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`session`](../packages/core/session) |
 | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
-| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`session`](../packages/core/session) |
+| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) |
 | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
@@ -1301,7 +1308,7 @@ flowchart TD
 | [`bash-local`](../packages/shell/bash-local) | `shell` | [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
-| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
 | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |

+ 6 - 0
docs/user/guide/network-proxy.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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 docs/user/guide/network-proxy.md
+network-proxy.md: d53d48688490c741f0ed7f950c5ff39db02dc1e9
+network-proxy.zh.md: 1eee1e67abb700e15f3b2cea1b22bf036c694302

+ 85 - 0
docs/user/guide/network-proxy.md

@@ -0,0 +1,85 @@
+# Run DSH behind a network proxy
+
+English | [中文](network-proxy.zh.md)
+
+DSH routes its outbound requests — model calls, web search, page fetches, and MCP servers over HTTP — through the proxy named by the standard proxy environment variables. It reads them at launch; nothing else needs configuring. A few paths stay direct by design or by runtime limit, listed under "What stays direct" below.
+
+## Export the variables
+
+```sh
+export HTTPS_PROXY=http://127.0.0.1:7890
+export HTTP_PROXY=http://127.0.0.1:7890
+```
+
+Put both lines in your shell profile so every `dsh` invocation inherits them, or in `$DSH_HOME/.env` (`~/.dsh/.env` by default) next to your API key; an exported variable always wins over that file. A project's own `.env` cannot set them: it arrives with `git clone`, and DSH refuses to start rather than let a repository decide where your traffic goes.
+
+A proxy that needs credentials takes them in the URL: `http://user:password@proxy.example:8080`. DSH never prints the URL back: a diagnostic names the variable it rejected, so neither the username nor the password appears anywhere.
+
+## Why your browser is proxied but your terminal is not
+
+This is the most common surprise, and it is not specific to DSH. There is no single "system proxy" that all software obeys — there are three unrelated mechanisms:
+
+| Mechanism | Who follows it |
+|---|---|
+| The operating system's proxy settings | Safari, most native macOS apps, Chrome and Edge |
+| The `HTTP_PROXY` / `HTTPS_PROXY` environment variables | `curl`, `git`, `npm`, `pip`, and DSH |
+| TUN mode (a virtual network interface) | Everything, transparently |
+
+The "system proxy" switch in a proxy application such as Clash writes only the first one. Browsers pick it up; command-line tools never see it. That is why exporting the variables is a separate step, and why turning on TUN mode makes both work without any variables at all.
+
+DSH does not read the operating system's proxy settings. Export the variables, or use TUN mode.
+
+## Choose what stays direct
+
+`NO_PROXY` lists hosts to reach directly:
+
+```sh
+export NO_PROXY=internal.example.com,.corp.example.com,registry.local
+```
+
+An entry names a host and matches it together with every subdomain under it: `NO_PROXY=example.com` also sends `api.example.com` direct. A leading `.` or `*.` is accepted and means the same thing. An entry may carry a `:port`, and `*` bypasses everything.
+
+**CIDR ranges do not work.** An operating system bypass list often contains entries like `10.0.0.0/8` or `192.168.0.0/16`; copying those into `NO_PROXY` has no effect. Use host names or domain suffixes instead.
+
+You do not need to list `localhost` or `127.0.0.1`. DSH always bypasses loopback, because its own Web UI and local servers would otherwise route through the proxy and loop.
+
+## Limits worth knowing
+
+**SOCKS proxies are not supported.** A `socks5://` value is reported at startup and skipped, and DSH connects directly for the scheme that named it — setting `HTTPS_PROXY=socks5://…` alongside a usable `HTTP_PROXY` leaves `https:` direct rather than borrowing the HTTP proxy. Point the variables at your proxy application's HTTP port instead — most expose both, and the HTTP one is usually a neighbouring port number.
+
+**`ALL_PROXY` alone is enough.** DSH falls back to it for both schemes, even though Node and curl differ on this. Setting `HTTPS_PROXY` explicitly is still clearer.
+
+**A TLS-intercepting corporate proxy needs its certificate.** If requests fail with a certificate error once the proxy is reachable, point Node at your organisation's CA bundle before launching:
+
+```sh
+export NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.pem
+```
+
+Node reads that variable only at process start, so export it before running `dsh`.
+
+**Tools DSH runs for you follow the same proxy.** Commands in the bash tool, `git`, `gh`, and MCP servers started as child processes all inherit these variables. A child that is itself a Node program honors them only on Node 22.21 or later; an older Node connects directly. If one of your proxy variables holds a value DSH rejected — a SOCKS URL, say — Node-based tools also connect directly rather than fail to start, while `curl` and `git` still read that value.
+
+**A password in the proxy URL reaches those tools too.** `HTTPS_PROXY=http://alice:s3cret@proxy.example:8080` is a normal environment variable, so every command DSH runs — including the ones the model writes — can read it, and a command that prints its environment puts the password in output that is kept. This is how the variable already behaves for everything else in your shell. If that matters, give the proxy a credential-free entry point, or authenticate it some other way than in the URL.
+
+## What stays direct
+
+Not every request DSH makes goes through the proxy:
+
+- **Anything on this machine.** Loopback is always direct: `localhost`, the whole `127.0.0.0/8` range, `::1`, and `0.0.0.0`. A proxy cannot usefully reach a service that only listens locally.
+- **Code the model writes.** The workflow and code-runtime workers never receive the proxy settings, so a script the model authors cannot read a proxy URL that may carry a password. Such a script reaches the network only if it configures that itself.
+- **Usage telemetry.** The OTLP exporter uses Node's own HTTP client rather than the one a proxy configures, so telemetry connects directly and simply fails where direct egress is blocked. Nothing you do in DSH depends on it. Set `DSH_TELEMETRY_MODE=DISABLED` to turn it off entirely.
+- **`web_fetch` to a literal private address.** A URL naming an address like `http://10.0.0.5/` is refused rather than handed to the proxy, the same refusal it gets with no proxy configured.
+
+## Check that it worked
+
+Ask the agent to fetch a page and watch your proxy application's connection log:
+
+```sh
+dsh --profile headless "fetch https://example.com and tell me the page title"
+```
+
+If the request does not appear there, confirm the variables survive into DSH's own environment:
+
+```sh
+env | grep -i proxy
+```

+ 85 - 0
docs/user/guide/network-proxy.zh.md

@@ -0,0 +1,85 @@
+# 在网络代理后面运行 DSH
+
+[English](network-proxy.md) | 中文
+
+DSH 会把自身的出站请求——模型调用、web 搜索、页面抓取、走 HTTP 的 MCP 服务器——都经由标准代理环境变量所指定的代理发出。它在启动时读取这些变量,不需要其他配置。有几条路径出于设计或运行时限制保持直连,下文"哪些保持直连"一节列出了它们。
+
+## 导出环境变量
+
+```sh
+export HTTPS_PROXY=http://127.0.0.1:7890
+export HTTP_PROXY=http://127.0.0.1:7890
+```
+
+把这两行写进 shell 配置,这样每次调用 `dsh` 都会继承它们;也可以写进 `$DSH_HOME/.env`(默认 `~/.dsh/.env`),和 API key 放在一起;导出的环境变量始终优先于该文件。项目自己的 `.env` 不能设置它们:它随 `git clone` 一起到来,DSH 宁可拒绝启动,也不让一个仓库决定你的流量去向。
+
+需要凭据的代理把凭据写在 URL 里:`http://user:password@proxy.example:8080`。DSH 绝不会回显这个 URL:诊断只点名被拒绝的变量,因此用户名和密码都不会出现在任何地方。
+
+## 为什么浏览器走代理、终端却不走
+
+这是最常见的意外,而且并非 DSH 特有。**根本不存在一个所有软件都遵循的"系统代理"**——实际上有三套互不相干的机制:
+
+| 机制 | 谁会遵循 |
+|---|---|
+| 操作系统的代理设置 | Safari、绝大多数 macOS 原生应用、Chrome 与 Edge |
+| `HTTP_PROXY` / `HTTPS_PROXY` 环境变量 | `curl`、`git`、`npm`、`pip` 以及 DSH |
+| TUN 模式(虚拟网卡) | 所有程序,且对应用透明 |
+
+Clash 这类代理软件里的"系统代理"开关只写第一套。浏览器会读到它,命令行工具则永远看不到。这就是为什么导出环境变量是一个独立步骤,也是为什么打开 TUN 模式后两者都能工作、且完全不需要变量。
+
+DSH 不读取操作系统的代理设置。请导出环境变量,或使用 TUN 模式。
+
+## 指定哪些目标保持直连
+
+`NO_PROXY` 列出需要直连的主机:
+
+```sh
+export NO_PROXY=internal.example.com,.corp.example.com,registry.local
+```
+
+一个条目写的是主机名,它连同其下所有子域名一起匹配:`NO_PROXY=example.com` 也会让 `api.example.com` 直连。前缀 `.` 或 `*.` 可以写,含义相同。条目可带 `:port`,`*` 则放行全部。
+
+**CIDR 网段不生效。** 操作系统的绕过列表常含 `10.0.0.0/8` 或 `192.168.0.0/16` 这类条目;把它们复制进 `NO_PROXY` 不会有任何效果。请改用主机名或域名后缀。
+
+不需要列出 `localhost` 或 `127.0.0.1`。DSH 始终绕过 loopback,否则它自己的 Web UI 与本地服务器都会经由代理并形成回环。
+
+## 值得知道的限制
+
+**不支持 SOCKS 代理。** `socks5://` 形式的值会在启动时被报告并跳过,指定它的那个 scheme 转为直连——把 `HTTPS_PROXY=socks5://…` 与一个可用的 `HTTP_PROXY` 一起设置时,`https:` 会保持直连,而不会去借用 HTTP 代理。请把变量指向代理软件的 HTTP 端口——多数软件两者都提供,且 HTTP 端口通常就在相邻的端口号上。
+
+**只设 `ALL_PROXY` 也够用。** DSH 会用它为两种协议兜底,尽管 Node 与 curl 在这一点上并不一致。显式设置 `HTTPS_PROXY` 仍然更清楚。
+
+**做 TLS 拦截的企业代理需要它的证书。** 如果代理已经可达但请求仍报证书错误,请在启动前把 Node 指向你所在组织的 CA 包:
+
+```sh
+export NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.pem
+```
+
+Node 只在进程启动时读取该变量,所以要在运行 `dsh` 之前导出。
+
+**DSH 替你运行的工具遵循同一个代理。** bash 工具里的命令、`git`、`gh`,以及作为子进程启动的 MCP 服务器都会继承这些变量。子进程若本身是 Node 程序,则需 Node 22.21 或更高版本才会遵循;更旧的 Node 会直连。如果你的某个代理变量是 DSH 拒绝的值——比如 SOCKS URL——基于 Node 的工具同样直连而不是起不来,`curl` 与 `git` 则仍会读取那个值。
+
+**代理 URL 里的密码同样会到达这些工具。** `HTTPS_PROXY=http://alice:s3cret@proxy.example:8080` 就是一个普通环境变量,因此 DSH 运行的每一条命令——包括模型编写的那些——都能读到它,而打印环境的命令会把密码写进被保留的输出。这与该变量在你 shell 里对其他一切程序的行为一致。若这一点重要,请为代理提供一个无需凭据的入口,或改用 URL 之外的方式认证。
+
+## 哪些保持直连
+
+并非 DSH 发出的每个请求都会走代理:
+
+- **本机上的一切。** loopback 始终直连:`localhost`、整个 `127.0.0.0/8` 段、`::1` 与 `0.0.0.0`。代理无法有意义地访问一个只在本地监听的服务。
+- **模型编写的代码。** workflow 与 code-runtime worker 从不接收代理配置,因此模型编写的脚本读不到可能携带密码的代理 URL。这类脚本只有自行配置才能联网。
+- **使用情况遥测。** OTLP 导出器用的是 Node 自带的 HTTP 客户端,而不是代理所配置的那个,因此遥测直连;在禁止直连出网的环境里它只会失败。DSH 的任何功能都不依赖它。设 `DSH_TELEMETRY_MODE=DISABLED` 可完全关闭。
+- **`web_fetch` 访问字面量私网地址。** 形如 `http://10.0.0.5/` 的 URL 会被拒绝而非交给代理,与未配置代理时得到的拒绝相同。
+
+## 验证是否生效
+
+让 agent 抓取一个页面,同时观察代理软件的连接日志:
+
+```sh
+dsh --profile headless "fetch https://example.com and tell me the page title"
+```
+
+如果请求没有出现在那里,确认变量确实进入了 DSH 自己的环境:
+
+```sh
+env | grep -i proxy
+```

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-root",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "license": "MIT",
   "private": true,
   "type": "module",
@@ -110,6 +110,7 @@
     "verify-npm-install-layout": "tsx scripts/verify-npm-install-layout.ts",
     "verify-client-packages": "tsx scripts/verify-client-packages.ts",
     "verify-client-ui-i18n": "tsx scripts/verify-client-ui-i18n.ts",
+    "verify-no-bare-dispatcher": "tsx scripts/verify-no-bare-dispatcher.ts",
     "verify-vendored-links": "tsx scripts/verify-vendored-links.ts",
     "verify-cordis-config": "tsx scripts/verify-cordis-config.ts",
     "rescope-vendor": "tsx scripts/rescope-vendor.ts",

+ 1 - 1
packages/acp/acp/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-acp",
   "description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/api/gateway/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-api-gateway",
   "description": "Typert Remote Host dispatcher and Client API endpoint",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/api/remotes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-api-remotes",
   "description": "Remote BFF assembly for application-selected Host capabilities",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/api/session-controller/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-api-session-controller",
   "description": "Session Remote commands, cold reads, and live control transport",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/api/settings-controller/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-api-settings-controller",
   "description": "Remote owner for the configuration surfaces over the settings-domain seams",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/api/workspace-controller/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-api-workspace-controller",
   "description": "Workspace Remote commands and reconnect-safe state transport",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/attachment/attachment-local/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-attachment-local",
   "description": "Private content-addressed DSH_HOME attachment storage",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/attachment/attachment/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-attachment",
   "description": "Durable immutable attachment storage seam for the DeepSeek Harness",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 2 - 2
packages/boot/app-boot/README.i18n.yaml

@@ -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 packages/boot/app-boot/README.md
-README.md: a9ed535c2662237229e0702dcf41eae4f93af5df
-README.zh.md: 4f7688f7db5060bb1bf00ca5d091ca4ad16466ea
+README.md: ddbcc900fcbb38e83b35f517f9d38cf0e04927f3
+README.zh.md: 184e1a086d09bf6807fd8fb2d17f7157ba8deaa6

+ 1 - 1
packages/boot/app-boot/README.md

@@ -49,7 +49,7 @@ A profile is how one dsh installation ships different app surfaces: `web`, `head
 
 Your machine-local preferences also live in the Harness home:
 
-- **`.env`** — your ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. Variables that decide how the process starts (`PATH`, proxies, `DSH_*`, `XDG_*` and similar) are rejected from files: export them instead. For a non-product bin that just wants one directory's `.env`, a missing file is fine and an unloadable one prints one labelled warning line.
+- **`.env`** — your ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. Variables that decide how the process starts (`PATH`, `DSH_*`, `XDG_*` and similar) are rejected from files: export them instead. The four proxy names (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`) are accepted from the Harness-home file only, never from the invoking directory's, which arrives with a clone. For a non-product bin that just wants one directory's `.env`, a missing file is fine and an unloadable one prints one labelled warning line.
 - **`cordis.patch.yml`** — your tweak layer, applied after every bundle layer (per-profile first, then the home-level file, which therefore outranks it): replace one entry's whole config (restating the fields you keep), insert new entries, or interpolate `!!js` expressions at boot. A patch naming an entry that does not exist prints a stderr warning; an empty or comments-only file fails boot — disable the layer with `[]` instead.
 
 Profiles with `patchReload: live` watch both user patch files: a valid edit recomposes without restart, while a rejected edit leaves the last good app running. A `startup` profile installs neither those watchers nor the launcher's watch-only HMR fallback.

+ 1 - 1
packages/boot/app-boot/README.zh.md

@@ -49,7 +49,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 
 你的机器本地偏好同样位于 harness home 中:
 
-- **`.env`**——你的普通环境层:调用目录的文件优先于 harness home 的文件,两者都低于继承环境。决定进程如何启动的变量(`PATH`、代理、`DSH_*`、`XDG_*` 等)会被文件拒绝:请改为导出。对于只想加载某个目录 `.env` 的非产品 bin,文件缺失不影响启动,文件无法加载时输出一行带标签的警告。
+- **`.env`**——你的普通环境层:调用目录的文件优先于 harness home 的文件,两者都低于继承环境。决定进程如何启动的变量(`PATH`、`DSH_*`、`XDG_*` 等)会被文件拒绝:请改为导出。四个代理名(`HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`、`NO_PROXY`)只从 harness home 的文件接受,绝不从调用目录的文件接受——后者随 clone 一起到来。对于只想加载某个目录 `.env` 的非产品 bin,文件缺失不影响启动,文件无法加载时输出一行带标签的警告。
 - **`cordis.patch.yml`**——你的 tweak 层,应用在所有组合包层之后(先应用逐 profile 的文件,再应用 home 级文件,因此后者优先级更高):替换某个条目的整个 config(重述你要保留的字段)、插入新条目,或在启动时插值 `!!js` 表达式。patch 指定的条目不存在时输出 stderr 警告;空文件或仅含注释的文件会导致启动失败——如需禁用该层,请改用 `[]`。
 
 带 `patchReload: live` 的 profile 会监视两份用户 patch 文件:有效编辑无需重启即可重新组合,被拒绝的编辑则让最后一个可用应用继续运行。`startup` profile 既不安装这些监视器,也不安装 launcher 的仅监视 HMR 回退。

+ 1 - 1
packages/boot/app-boot/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-app-boot",
   "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 25 - 7
packages/boot/app-boot/src/index.ts

@@ -119,9 +119,19 @@ const BOOTSTRAP_NAMES = new Set([
 /** Name prefixes no discovered file may set. */
 const BOOTSTRAP_PREFIXES = ['DSH_', 'XDG_', 'DYLD_', 'BASH_FUNC_']
 
+/**
+ * The bootstrap names the Harness-home `.env` alone may set. A proxy chooses the route every
+ * request takes, so the invoking directory's file — which arrives with a clone — keeps refusing
+ * them; the home file is the user's own, and `DSH_HOME` is itself bootstrap-only, so no `.env` can
+ * relocate this exemption. The CA and TLS names in the same group stay refused everywhere: they
+ * change what is trusted, not where traffic goes.
+ */
+const HOME_LAYER_PROXY_NAMES = new Set(['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY'])
+
 /**
  * Whether a variable may come only from the inherited process environment
- * because it changes process, runtime, VCS, or network bootstrap.
+ * because it changes process, runtime, VCS, or network bootstrap. The Harness-home
+ * file is additionally allowed {@link HOME_LAYER_PROXY_NAMES}.
  * @param name - the variable name.
  * @returns true when only the inherited environment may supply it.
  */
@@ -136,13 +146,15 @@ function isBootstrapOnly(name: string): boolean {
  * @param binName - the diagnostic prefix on the thrown error.
  * @param dir - the directory whose `.env` to read.
  * @param warn - sink for the one-line unreadable-file diagnostic.
+ * @param home - the resolved Harness home; when `dir` is it, {@link HOME_LAYER_PROXY_NAMES} are accepted.
  * @returns the parsed entries, or `undefined` when the file is absent or unreadable.
- * @throws when the file declares a name {@link isBootstrapOnly} rejects.
+ * @throws when the file declares a name {@link isBootstrapOnly} rejects and this layer may not set.
  */
 function readEnvLayer(
-  binName: string, dir: string, warn: (line: string) => void,
+  binName: string, dir: string, warn: (line: string) => void, home: string,
 ): { path: string; values: Record<string, string> } | undefined {
   const path = resolve(dir, '.env')
+  const isHome = resolve(dir) === home
   let content: string
   try {
     content = readFileSync(path, 'utf8')
@@ -157,10 +169,16 @@ function readEnvLayer(
   const values = parseEnv(content) as Record<string, string>
   for (const name of Object.keys(values)) {
     if (!isBootstrapOnly(name)) continue
+    const proxyName = HOME_LAYER_PROXY_NAMES.has(name.toUpperCase())
+    if (isHome && proxyName) continue
+    // A proxy name has a second way out that the other bootstrap names do not, so its message says so.
+    const remedy = proxyName
+      ? `export ${name}, or put it in ${resolve(home, '.env')}, which does not travel with a repository`
+      : `export ${name} instead of putting it in a .env file`
     throw new Error(
       `${binName}: ${path} sets "${name}", which only the launching environment may set`
       + ' (it decides how this process starts, where its code and instructions load from, or how it'
-      + ` reaches the network); export ${name} instead of putting it in a .env file`,
+      + ` reaches the network); ${remedy}`,
     )
   }
   return { path, values }
@@ -175,7 +193,7 @@ function readEnvLayer(
  * @param cwd - the invoking directory whose `.env` is the project layer.
  * @param warn - sink for the one-line misconfiguration diagnostics.
  * @returns this run's frozen environment snapshot.
- * @throws when either file declares a bootstrap-only variable.
+ * @throws when either file declares a bootstrap-only variable, except {@link HOME_LAYER_PROXY_NAMES} in the Harness-home file.
  */
 export function loadLayeredEnv(
   binName: string, cwd: string = process.cwd(),
@@ -184,8 +202,8 @@ export function loadLayeredEnv(
   const home = resolveDshHome()
   const inherited = { ...process.env } as Record<string, string>
   // Parse both layers first: a rejection must not leave one file applied.
-  const project = readEnvLayer(binName, cwd, warn)
-  const user = home === resolve(cwd) ? undefined : readEnvLayer(binName, home, warn)
+  const project = readEnvLayer(binName, cwd, warn, home)
+  const user = home === resolve(cwd) ? undefined : readEnvLayer(binName, home, warn, home)
   // Apply the checked values without replacing a higher-ranked name.
   for (const layer of [project, user]) {
     if (layer === undefined) continue

+ 77 - 0
packages/boot/app-boot/tests/app-boot.spec.ts

@@ -149,6 +149,83 @@ describe('loadLayeredEnv', () => {
     }
   })
 
+  const PROXY = ['HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy', 'NO_PROXY', 'no_proxy'] as const
+  function clearProxy(): void {
+    for (const name of PROXY) Reflect.deleteProperty(process.env, name)
+  }
+
+  it('accepts the proxy names from the Harness-home .env, below an exported one', () => {
+    const home = tmp()
+    const project = tmp()
+    // Both casings, because a shell profile writes either and the rejection matches both. Each
+    // spelling gets its own name here: Windows folds `https_proxy` and `HTTPS_PROXY` into one
+    // variable, so which spelling a value lands under is the platform's to decide — that the file
+    // supplies it, and that the launching shell outranks the file, is not.
+    writeFileSync(join(home, '.env'), 'HTTP_PROXY=http://from-home:8080\nno_proxy=example.com\nHTTPS_PROXY=http://from-home:8443\n')
+    clear(); clearProxy()
+    vi.stubEnv('DSH_HOME', home)
+    vi.stubEnv('HTTPS_PROXY', 'http://exported:8080')
+    try {
+      const snapshot = loadLayeredEnv(NAME, project, vi.fn())
+      expect(snapshot.get('HTTP_PROXY')).toEqual({ value: 'http://from-home:8080', source: 'user-env', path: join(home, '.env') })
+      expect(snapshot.get('no_proxy')).toEqual({ value: 'example.com', source: 'user-env', path: join(home, '.env') })
+      // The launching shell still outranks the file for the same variable.
+      expect(snapshot.get('HTTPS_PROXY')).toEqual({ value: 'http://exported:8080', source: 'process' })
+      expect(process.env.HTTP_PROXY).toBe('http://from-home:8080')
+      expect(process.env.HTTPS_PROXY).toBe('http://exported:8080')
+    } finally {
+      clear(); clearProxy()
+      vi.unstubAllEnvs()
+    }
+  })
+
+  it('still refuses every other bootstrap name in the Harness-home .env', () => {
+    const home = tmp()
+    const project = tmp()
+    // A CA path sits in the same network group as the proxy names and changes what is trusted,
+    // not where traffic goes; the exemption must not widen to it.
+    writeFileSync(join(home, '.env'), 'SSL_CERT_FILE=/tmp/ca.pem\n')
+    clear()
+    vi.stubEnv('DSH_HOME', home)
+    try {
+      expect(() => loadLayeredEnv(NAME, project, vi.fn())).toThrow(/only the launching environment may set/)
+    } finally {
+      clear()
+      vi.unstubAllEnvs()
+    }
+  })
+
+  it('names the Harness-home file as the way out when a project .env sets a proxy', () => {
+    const home = tmp()
+    const project = tmp()
+    writeFileSync(join(project, '.env'), 'HTTP_PROXY=http://attacker.example\n')
+    clear(); clearProxy()
+    vi.stubEnv('DSH_HOME', home)
+    try {
+      expect(() => loadLayeredEnv(NAME, project, vi.fn()))
+        .toThrow(`export HTTP_PROXY, or put it in ${join(home, '.env')}, which does not travel with a repository`)
+      expect(process.env.HTTP_PROXY).toBeUndefined()
+    } finally {
+      clear(); clearProxy()
+      vi.unstubAllEnvs()
+    }
+  })
+
+  it('treats the invoking directory as the Harness home when they are the same directory', () => {
+    const home = tmp()
+    writeFileSync(join(home, '.env'), 'HTTP_PROXY=http://from-home:8080\n')
+    clear(); clearProxy()
+    vi.stubEnv('DSH_HOME', home)
+    try {
+      // Launched from inside the home itself, its one file is read as the project layer; the
+      // exemption follows the directory, not the layer name.
+      expect(loadLayeredEnv(NAME, home, vi.fn()).get('HTTP_PROXY')?.value).toBe('http://from-home:8080')
+    } finally {
+      clear(); clearProxy()
+      vi.unstubAllEnvs()
+    }
+  })
+
   it('reports each file value with its absolute path', () => {
     const home = tmp()
     const project = tmp()

+ 1 - 1
packages/boot/cmdline/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-cmdline",
   "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/bundle/acp-app/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-acp-app",
   "description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/bundle/base/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-base",
   "description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/bundle/headless/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-headless",
   "description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/bundle/sdk-app/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-sdk-app",
   "description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/bundle/sdk-minimal/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-sdk-minimal",
   "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, editor, and JSONL sessions",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/bundle/web-app/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-web-app",
   "description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/connection/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-connection",
   "description": "Authenticated RPC transport, generation lifecycle, and browser fixture",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/hmr/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-hmr",
   "description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/locale/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-locale",
   "description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/modules/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-modules",
   "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/store/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-store",
   "description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-agent-preset/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-agent-preset",
   "description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-approval/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-approval",
   "description": "Approval composer takeover over the scoped Remote Event waterfall",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-attachment/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-attachment",
   "description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-brand-official/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-brand-official",
   "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-chat/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-chat",
   "description": "Chat Conversation target, node definitions, renderers, and details surface",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-commands/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-commands",
   "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-conversation/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-conversation",
   "description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-deliverables/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-deliverables",
   "description": "Produced-files turn tail and clickable final-response file references for Web",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-directory-picker-browse/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-directory-picker-browse",
   "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-directory-picker-native/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-directory-picker-native",
   "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the host's OS chooser",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-goal/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-goal",
   "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-input-trigger/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-input-trigger",
   "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-jobs/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-jobs",
   "description": "Session-header background-job list: live registry state mirrored from session/jobs frames",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "type": "module",
   "main": "lib/index.js",
   "types": "lib/types/index.d.ts",

+ 1 - 1
packages/client/ui-layout/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-layout",
   "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-message-feedback/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-message-feedback",
   "description": "Per-message feedback controls contributed to the assistant-message action strip, backed by the messageFeedback Host Remote",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-model-selection/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-model-selection",
   "description": "Model selection over the shared model catalog, Session projection, and session.selectModel",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-permission-presets/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-permission-presets",
   "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-plan/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-plan",
   "description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-primitives/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-primitives",
   "description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-reference/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-reference",
   "description": "Unified Web @file and @session reference source",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-renderer/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-renderer",
   "description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-schedule/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-schedule",
   "description": "Read-only active Schedule catalog in the Web Session header",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "type": "module",
   "main": "lib/index.js",
   "types": "lib/types/index.d.ts",

+ 1 - 1
packages/client/ui-session/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-session",
   "description": "Session Controller adapter for React and session-scoped slots",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-settings-general/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-settings-general",
   "description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-settings-models/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-settings-models",
   "description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-settings-plugin-inventory/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-settings-plugin-inventory",
   "description": "Read-only Cordis Loader inventory tab in Web Plugins settings",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-settings-plugins/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-settings-plugins",
   "description": "Plugins settings section with feature-owned tabs and configurable host-plane plugin cards",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-settings/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-settings",
   "description": "Settings domain base plugin: the settings-namespace scope service and the canonical settings slot-type contract",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-sidebar/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-sidebar",
   "description": "Sidebar plugin: session multi-level tree, search, grouping, state dots",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-skill/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-skill",
   "description": "Web skill references and the dedicated skill tool row",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-slots/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-slots",
   "description": "Slot registry pure core: SlotMap declaration merging, single register composition API, four-share props types, store-seat types, renderer install seam",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-subagent/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-subagent",
   "description": "Subagent conversation catalog, continuation routing UI, and '@' reference source",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-theme/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-theme",
   "description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-tool/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-tool",
   "description": "Client Tool call-tree renderer and keyed per-tool presentation slot",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-trajectory/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-trajectory",
   "description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-user-questions/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-user-questions",
   "description": "Web ask_user_question composer takeover and plan-review presentation UI",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-workflow-run/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-workflow-run",
   "description": "Durable workflow-run Conversation Node and nested member disclosure for dsh web",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/ui-workspace/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-workspace",
   "description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/client/web/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-client-web",
   "description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/code-runtime/code-runtime-worker-thread/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-code-runtime-worker-thread",
   "description": "Worker-thread implementation of the DeepSeek Harness code-execution seam",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/code-runtime/code-runtime/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-code-runtime",
   "description": "Abstract code-execution seam (ctx.codeRuntime) for the DeepSeek Harness",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/compaction/command-compact/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-command-compact",
   "description": "Human-facing slash command for explicit session compaction",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/compaction/compaction-basic/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-compaction-basic",
   "description": "Token-meter-driven compaction policy and LLM summarization backend for the DeepSeek Harness",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/compaction/compaction-tool-result-pruner/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-compaction-tool-result-pruner",
   "description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/compaction/compaction/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-compaction",
   "description": "Abstract compaction service seam (ctx.compaction) for the DeepSeek Harness",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/context/agent-instructions/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-agent-instructions",
   "description": "Workspace context loader for AGENTS.md/CLAUDE.md instruction files",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/context/file-reference-local/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-file-reference-local",
   "description": "Local-filesystem ctx.fileReferences provider with bounded fuzzy indexes",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/context/file-reference/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-file-reference",
   "description": "File-reference discovery contract and shared @file grammar",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/context/session-reference/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-session-reference",
   "description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/context/time-context/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-time-context",
   "description": "Opt-in durable per-step context with the current time and elapsed time",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/context/tmux-context/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-tmux-context",
   "description": "Opt-in durable per-step context with this agent's tmux pane and window location",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/core/agent-default-model/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-agent-default-model",
   "description": "Default model selection shared by Agent entry points",
-  "version": "0.1.2-alpha.5",
+  "version": "0.1.2-rc.1",
   "publishConfig": {
     "access": "public"
   },

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels