From b694c33d18141304c94833db5fbc0f51fb178c08 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Fri, 31 Jul 2026 20:39:53 +0800 Subject: [PATCH] Default shipped UI sessions to workspace-write --- ...31-even-out-shipped-tool-rosters.i18n.yaml | 4 +- ...026-07-31-even-out-shipped-tool-rosters.md | 6 +-- ...-07-31-even-out-shipped-tool-rosters.zh.md | 6 +-- ...mission-default-for-new-sessions.i18n.yaml | 4 +- ...-31-permission-default-for-new-sessions.md | 2 +- ...-permission-default-for-new-sessions.zh.md | 2 +- .../2026-07-31-web-default-search.i18n.yaml | 4 +- .../feature/2026-07-31-web-default-search.md | 2 +- .../2026-07-31-web-default-search.zh.md | 2 +- ...-workspace-write-surface-default.i18n.yaml | 6 +++ ...6-07-31-workspace-write-surface-default.md | 35 +++++++++++++++ ...7-31-workspace-write-surface-default.zh.md | 35 +++++++++++++++ apps/cli/README.i18n.yaml | 4 +- apps/cli/README.md | 2 + apps/cli/README.zh.md | 2 + apps/cli/composition.md | 24 +++++++--- apps/cli/config/base.cordis.yml | 43 +++++++++++++++--- apps/cli/config/tui.cordis.yml | 2 +- apps/cli/config/web.cordis.yml | 44 ------------------- apps/cli/tests/shipped-composition.e2e.ts | 44 ++++++++++++------- apps/web/tests/access-confirmation.e2e.ts | 9 +--- apps/web/tests/seeded-history.e2e.ts | 12 ++--- apps/web/tests/settings-chrome.e2e.ts | 8 ++-- apps/web/tests/shipped-composition.e2e.ts | 10 ++--- .../snapshots/code-mode-round/ui.expected.md | 2 +- .../cordis-tool-round/ui.expected.md | 2 +- .../snapshots/fresh-round-trip/ui.expected.md | 2 +- .../lifecycle-chrome/hero.expected.md | 2 +- .../lifecycle-chrome/plan-active.expected.md | 2 +- .../lifecycle-chrome/reloaded.expected.md | 2 +- .../live-interactions/cancel.expected.md | 2 +- .../live-interactions/error-auth.expected.md | 2 +- .../live-interactions/loading.expected.md | 2 +- .../live-interactions/retry.expected.md | 2 +- .../snapshots/message-actions/ui.expected.md | 2 +- .../plan-review/approved.expected.md | 2 +- .../question-composer/answered.expected.md | 2 +- .../queue-actions/collapsed.expected.md | 2 +- .../queue-actions/editing.expected.md | 2 +- .../snapshots/queue-actions/ui.expected.md | 2 +- .../seeded-history/command-row.expected.md | 4 +- .../snapshots/seeded-history/ui.expected.md | 2 +- .../settings-chrome/dialog.expected.md | 4 +- .../snapshots/steering/settled.expected.md | 2 +- .../snapshots/web-search-round/ui.expected.md | 2 +- .../credentials-local/README.i18n.yaml | 4 +- .../credentials/credentials-local/README.md | 4 +- .../credentials-local/README.zh.md | 4 +- 48 files changed, 227 insertions(+), 143 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md create mode 100644 .agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.zh.md diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml index 83e965b391..91d5d6c5ee 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.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/feature/2026-07-31-even-out-shipped-tool-rosters.md -2026-07-31-even-out-shipped-tool-rosters.md: 316e5045e559e2da162c53d64989ccecfd18b857 -2026-07-31-even-out-shipped-tool-rosters.zh.md: ed39212dc4877f4df1dc1c6e84142b61a866c548 +2026-07-31-even-out-shipped-tool-rosters.md: 5aaf4798c1297fc273cd715838feb6441ffc0d61 +2026-07-31-even-out-shipped-tool-rosters.zh.md: 79d8dbb8e0aa3cdf462f930ea63a5621dc2d9243 diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md index 316e5045e5..5aaf4798c1 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md @@ -16,7 +16,7 @@ The rows that are not surface-specific move into [`base.cordis.yml`](../../../.. Two rows stay surface-specific. `tmux-context` is TUI-only because a browser surface has no terminal multiplexer to describe. `session-reference` is TUI-only because it drives the shared session-query index from the launcher's process-local path, and the browser sidebar reconciles that index on its own first search. -**This change adds only.** No row is removed from either surface and no existing row's configuration is edited: the executors, the sandbox composition, the access defaults, `tools.mode`, and the workflow tool are exactly what they were. A reader comparing the two catalogs before and after should find additions and nothing else. +**This roster decision adds only.** No tool row is removed from either surface, and a catalog comparison finds additions and nothing else. The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md). ### What stays unmounted, and why @@ -42,7 +42,7 @@ The layer that would make MCP a default is the one this repository does not have That tail also inserts [`composition-settled.ts`](../../../../apps/cli/tests/fixtures/composition-settled.ts), which announces settled Loader activation on the terminal stream. The TUI renders as soon as its own fiber starts, so a prompt typed at the banner can reach the loop while tool rows and persistence are still activating and assemble a partial catalog; gating the smoke's first prompt on that marker is what makes the assertion deterministic. -The same smoke pins the TUI's unchanged execution posture from the same artifact: `tool-bash` emits its `sandbox_permissions` escalation pair only when the mounted executor has wider modes to escalate to, so asserting its **absence** fails if a later change quietly sandboxes this surface. +The same smoke also pins the TUI execution posture from the same artifact. Those sandbox-schema and initial-permission assertions belong to the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md), independently of this roster. [`apps/web/tests/shipped-composition.e2e.ts`](../../../../apps/web/tests/shipped-composition.e2e.ts) covers the Web surface in the built lane, asserting its catalog, that its access default is untouched, and that `workspace-write`'s writable roots include the temp directories — a trap that makes sandbox tests lie when the workspace sits under `/tmp` ([`roots.ts`](../../../../packages/sandbox/sandbox/src/roots.ts)). @@ -66,4 +66,4 @@ The same model gets the same tools on both surfaces, and the difference that exi `apps/cli` gains five workspace dependencies: four the shipped tree now mounts, plus `dsh-mcp-client`, which it does not mount and which exists so an installed `dsh` can. -Nothing about execution changed. The TUI still runs the model's commands through unrestricted executors with no approval seam, and the Web surface still defaults to `danger-full-access`. Both are pinned by assertions in this change, which makes them visible rather than fixed — the sandbox decision is still open. +Execution policy stays independent of the roster. The [shared workspace-write decision](2026-07-31-workspace-write-surface-default.md) owns both surfaces' sandboxed executors and default permission; changing that policy does not add or remove a tool. diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md index ed39212dc4..79d8dbb8e0 100644 --- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md @@ -16,7 +16,7 @@ Status: implemented 有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。 -**本次改动只做加法。** 两个 surface 都没有任何一行被移除,也没有任何既有行的配置被编辑:执行器、沙箱组合、访问默认值、`tools.mode` 以及 workflow 工具,全都保持原样。对比改动前后的两份目录,读者应当只看到新增,别无其他。 +**本次工具清单决策只做加法。** 两个 surface 均未移除任何工具行,目录对比只会发现新增,别无其他。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.md)。 ### 什么保持不挂,以及为什么 @@ -42,7 +42,7 @@ Status: implemented 该尾部还插入了 [`composition-settled.ts`](../../../../apps/cli/tests/fixtures/composition-settled.ts),它在终端字节流上宣告 Loader 激活已 settle。TUI 在自己的 fiber 一启动就渲染,因此在 banner 处敲下的提示词可能在工具行与持久化仍在激活时就抵达循环,从而组装出不完整的目录;把冒烟的首个提示词 gate 在该标记上,正是断言得以确定的原因。 -同一份冒烟还从同一份产物上钉住 TUI 未改变的执行姿态:`tool-bash` 只在挂载的执行器确实有更宽模式可升级时才发出 `sandbox_permissions` 升级参数对,因此断言它的**缺席**会在日后有人悄悄给这个 surface 加上沙箱时失败。 +同一份冒烟还根据同一份产物固定 TUI 的执行姿态。那些沙箱 schema 与初始权限断言归[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.md)所有,独立于本工具清单决策。 [`apps/web/tests/shipped-composition.e2e.ts`](../../../../apps/web/tests/shipped-composition.e2e.ts) 在构建产物 lane 中覆盖 Web surface,断言它的工具目录、它的访问默认值未被触碰,以及 `workspace-write` 的可写根包含临时目录——一个会让沙箱测试说谎的陷阱,当工作区落在 `/tmp` 下时([`roots.ts`](../../../../packages/sandbox/sandbox/src/roots.ts))。 @@ -66,4 +66,4 @@ Status: implemented `apps/cli` 增加五个 workspace 依赖:四个是交付树现在挂载的,外加 `dsh-mcp-client`——它并不被挂载,存在的意义是让已安装的 `dsh` 能挂。 -执行相关的一切都没有变。TUI 仍以不受限执行器运行模型的命令且没有批准接缝,Web surface 仍默认 `danger-full-access`。两者都由本次改动中的断言钉住,这让它们变得可见而非被修复——沙箱那个决定仍然悬着。 +执行策略独立于工具清单。[共享 workspace-write 决策](2026-07-31-workspace-write-surface-default.md)拥有两个 surface 的沙箱执行器与默认权限;更改该策略不会增加或移除工具。 diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml index 8d4467858b..3227fac451 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.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/feature/2026-07-31-permission-default-for-new-sessions.md -2026-07-31-permission-default-for-new-sessions.md: 35812b53d0c1448afd95b9a063eda6658fb1bef3 -2026-07-31-permission-default-for-new-sessions.zh.md: a75deaec323b57f2bc88e84dd4d5c7d7d98cd177 +2026-07-31-permission-default-for-new-sessions.md: ffa4c8a07bdd08ca52edbc14fe10372ad76e8cf8 +2026-07-31-permission-default-for-new-sessions.zh.md: 5fc42724754acc1653ed93b48644794a38f52ba7 diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md index 35812b53d0..ffa4c8a07b 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md @@ -22,7 +22,7 @@ ApiProxy explicitly adds `permission` to its Web settings allowlist beside the c Changing Permission in Settings updates `settings.yaml` and the selector immediately, but does not alter the open session. Every later session is reconstructable from its three pinned permission facts, including after the user changes the default again or the process restarts. Deployments whose composed sandbox and approval defaults match no preset must configure `defaultPreset` explicitly. -The assembled Web snapshot now contains a functional Permission selector. Its keyless browser scenario writes `read-only`, verifies an existing `danger-full-access` session is unchanged, and verifies a subsequently created session starts with the read-only event triplet. +The assembled Web snapshot contains a functional Permission selector. Its keyless browser scenario writes `read-only`, verifies an existing `workspace-write` session is unchanged, and verifies a subsequently created session starts with the read-only event triplet. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md index a75deaec32..5fc4272475 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md @@ -22,7 +22,7 @@ ApiProxy 在可配置提供方 namespace 之外,将 `permission` 显式加入 在 Settings 中更改「权限」会立即更新 `settings.yaml` 和选择器,但不会改变已打开的会话。之后的每个会话都可以从三个已固定的权限事实中重建,即使用户再次更改默认值或进程重启也不受影响。如果部署中组合后的沙箱和审批默认值与任何 preset 都不匹配,则必须显式配置 `defaultPreset`。 -组装后的 Web 快照现在包含功能完整的「权限」选择器。其无密钥浏览器场景会写入 `read-only`,验证现有的 `danger-full-access` 会话保持不变,并验证随后创建的会话以 read-only 事件三元组启动。 +组装后的 Web 快照包含功能完整的「权限」选择器。其无密钥浏览器场景会写入 `read-only`,验证现有的 `workspace-write` 会话保持不变,并验证随后创建的会话以 read-only 事件三元组启动。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml index 6b244f3d12..2c8f3eea48 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-web-default-search.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/feature/2026-07-31-web-default-search.md -2026-07-31-web-default-search.md: d9616c27410bb5be9b385a9aaa56c22f6054eeb1 -2026-07-31-web-default-search.zh.md: 27cd330427669a78c03b939c737b37f79fd7965a +2026-07-31-web-default-search.md: 121a1dff5fffd4223eefcc7475fff874276658aa +2026-07-31-web-default-search.zh.md: ac98f4806413cb6050a08354a553942437866fe1 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-default-search.md b/.agents/notes/implemented/feature/2026-07-31-web-default-search.md index d9616c2741..121a1dff5f 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-default-search.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-default-search.md @@ -16,7 +16,7 @@ DeepSeek search uses the same `DEEPSEEK_API_KEY` credential reference as the off Search keeps its endpoint distinct from chat completions: `DEEPSEEK_SEARCH_BASE_URL` overrides the Anthropic-compatible base, while `DEEPSEEK_BASE_URL` continues to configure conversation requests. Each `web_search` performs an auxiliary DeepSeek Messages call with the native search server tool. Immediately before dispatch, the provider appends a log-only `web/deepseek-search-llm-request` event to the initiating Agent session with the resolved endpoint, API version, and exact secret-free JSON body. Credential preflight remains provider-local and races caller cancellation; neither concern expands the generic Web or credentials seams. -The default mount does not create a Web-specific permission policy. `web_search` executes outside the bash/filesystem sandbox and approval presets, following `dsh-tool-web`'s existing contract. It does not mount `web_fetch` or a local fetch provider, so the default does not grant model-selected arbitrary URL retrieval. The shipped deployment already defaults to `danger-full-access`; a future restricted-network product stance must add a `tools/pre-execute` policy or capability-specific network confinement rather than implying that filesystem access mode governs Web calls. +The default mount does not create a Web-specific permission policy. `web_search` executes outside the bash/filesystem sandbox and approval presets, following `dsh-tool-web`'s existing contract. It does not mount `web_fetch` or a local fetch provider, so the default does not grant model-selected arbitrary URL retrieval. The shipped `workspace-write` default governs file mutations only; a restricted-network product stance requires a `tools/pre-execute` policy or capability-specific network confinement rather than implying that filesystem access mode governs Web calls. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md index 27cd330427..ac98f48064 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md @@ -16,7 +16,7 @@ DeepSeek 搜索使用与官方会话适配器相同的 `DEEPSEEK_API_KEY` 凭据 搜索端点与 chat completions 保持独立:`DEEPSEEK_SEARCH_BASE_URL` 覆盖 Anthropic 兼容基址,`DEEPSEEK_BASE_URL` 则继续配置会话请求。每次 `web_search` 都会发起一次辅助 DeepSeek Messages 调用,并携带原生搜索服务器工具。发出请求前一刻,提供方会向发起请求的 agent(智能体)会话追加仅用于日志的 LLM(大语言模型)请求事件 `web/deepseek-search-llm-request`,其中包含已解析端点、API 版本,以及不含密钥的精确 JSON 请求体。凭据预检仍留在提供方内部,并与调用方取消存在竞态;这两项关注点都不会扩展通用 Web seam 或凭据 seam。 -默认挂载不会创建 Web 专用权限策略。`web_search` 在 bash/文件系统沙箱及审批预设之外执行,并遵循 `dsh-tool-web` 的现有契约。组合不挂载 `web_fetch` 或本地抓取提供方,因此默认配置不会允许模型自行选择任意 URL 进行抓取。已交付部署的默认值本就是 `danger-full-access`;未来如果产品采取受限网络策略,必须添加 `tools/pre-execute` 策略或按能力限制网络访问,而不能暗示文件系统访问模式会管辖 Web 调用。 +默认挂载不会创建 Web 专用权限策略。`web_search` 在 bash/文件系统沙箱及审批预设之外执行,并遵循 `dsh-tool-web` 的现有契约。组合不挂载 `web_fetch` 或本地抓取提供方,因此默认配置不会允许模型自行选择任意 URL 进行抓取。已交付的 `workspace-write` 默认值只管辖文件修改;若产品采取受限网络策略,就需要添加 `tools/pre-execute` 策略或按能力限制网络访问,而不能暗示文件系统访问模式会管辖 Web 调用。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.i18n.yaml new file mode 100644 index 0000000000..14facd28d9 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.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/feature/2026-07-31-workspace-write-surface-default.md +2026-07-31-workspace-write-surface-default.md: a0b216122e301b5332ed761155d743dc78fa3bab +2026-07-31-workspace-write-surface-default.zh.md: 4391daa32b142ea976e3b04833163936913c17bc diff --git a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md new file mode 100644 index 0000000000..a0b216122e --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.md @@ -0,0 +1,35 @@ +# Agent Note: Workspace-write defaults for shipped surfaces + +Status: implemented + +English | [中文](2026-07-31-workspace-write-surface-default.zh.md) + +## Problem + +The shipped terminal and browser surfaces exposed the same coding tools under different unconfined compositions. Web mounted the sandbox and permission services but selected `danger-full-access`; the TUI mounted the unrestricted local bash and filesystem providers directly. A fresh coding session could therefore mutate any path its same-UID process could reach before the user deliberately chose that authority. + +## Decision + +[`base.cordis.yml`](../../../../apps/cli/config/base.cordis.yml) owns one sandbox and permission stack for every shipped TUI, Web, and browser-backed headless session: `dsh-sandbox-local`, `dsh-sandbox-policy`, `dsh-bash-sandbox`, `dsh-fs-sandbox`, `dsh-user-approval`, and `dsh-permission`. The composition fallback is the `workspace-write` preset, which bundles `workspace-write` file effects with the `ask` approval policy. `DSH_PERMISSION_MODE` remains an explicit process override; a stored `permission.defaultPreset` remains the user preference for later sessions and outranks the fallback through the Settings seam. + +A genuinely fresh session pins `permission/preset: workspace-write`, `sandbox/mode: workspace-write`, and `approval/policy: ask` before execution. Existing and resumed sessions retain their logged permission, and changing the General-settings default affects only sessions created afterward. The browser keeps its Access picker, answerable approval cards, and risk confirmation for Full access. The TUI gains the existing `/permission` command because the shared Permission service activates its command child there. + +The mode governs file effects only. Sandboxed bash and filesystem mutations admit the session workspace and platform temporary roots; reads, network access, and process visibility remain outside this policy. If no platform runner can enforce a confined bash call, execution fails closed instead of falling through to an unrestricted command. + +## Testing + +The keyless shipped-TUI pseudo-terminal smoke boots the real Loader tree, reads the persisted first request, and asserts both the `sandbox_permissions`/`justification` bash schema and the initial workspace-write event triplet. The shipped-Web composition smoke asserts the same policy, approval, and Permission defaults. The assembled browser Settings snapshot opens on Workspace Write, preserves an existing workspace-write session while changing the future default, and still proves the confirmed Full-access path. + +## Alternatives considered + +**Keep the sandbox stack in `web.cordis.yml` and duplicate it into `tui.cordis.yml`.** Rejected because the plugin identities, presets, fallback, and executor swap are identical. Two copies would make a security default depend on keeping surface overlays synchronized; the shared base is their one owner. + +**Leave the TUI unrestricted and change only the browser fallback.** Rejected because it preserves the unexplained surface difference and leaves a fresh terminal session with the authority this decision removes. + +**Add a terminal approval dialog in the same change.** Rejected as a separate interaction and lifecycle decision. The TUI has no `approval/request` answerer, so a one-shot automatic escalation currently settles unavailable and fails closed; a user who needs wider authority can deliberately select another preset through `/permission`. + +## Consequences + +Fresh sessions can modify the active workspace and temporary roots without extra prompts, while an attempted mutation elsewhere is denied before it reaches the target. Full access remains available by explicit selection, and browser selection retains its acknowledgement dialog. Stored user defaults and logged session permissions are not rewritten. + +The browser-backed headless entry inherits the Web composition and therefore the same default. The TUI's missing approval answerer is a deliberate limitation of this change: automatic wider retries fail closed there instead of displaying a permission question. diff --git a/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.zh.md b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.zh.md new file mode 100644 index 0000000000..4391daa32b --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-31-workspace-write-surface-default.zh.md @@ -0,0 +1,35 @@ +# Agent Note: 已交付界面的 workspace-write 默认值 + +Status: implemented + +[English](2026-07-31-workspace-write-surface-default.md) | 中文 + +## 问题 + +已交付的终端和浏览器界面在两套不同的无约束组合下暴露相同的编码工具。Web 挂载了沙箱与权限服务,却选择 `danger-full-access`;TUI 则直接挂载不受限的本地 bash 与文件系统提供方。因此,在用户主动选择这类权限之前,全新的编码会话就能修改其同 UID 进程可达的任意路径。 + +## 决策 + +[`base.cordis.yml`](../../../../apps/cli/config/base.cordis.yml) 为所有已交付的 TUI、Web 以及由浏览器支撑的无头会话统一持有一套沙箱与权限栈:`dsh-sandbox-local`、`dsh-sandbox-policy`、`dsh-bash-sandbox`、`dsh-fs-sandbox`、`dsh-user-approval` 和 `dsh-permission`。组合回退值为 `workspace-write` preset,其中包含 `workspace-write` 文件效果模式与 `ask` 审批策略。`DSH_PERMISSION_MODE` 仍是显式的进程级覆盖;已存储的 `permission.defaultPreset` 仍是面向后续会话的用户偏好,并通过 Settings seam 优先于该回退值。 + +真正的新会话会在执行前固定 `permission/preset: workspace-write`、`sandbox/mode: workspace-write` 和 `approval/policy: ask`。现有会话和恢复的会话保留日志中记录的权限,更改「通用」设置中的默认值只影响之后创建的会话。浏览器保留 Access 选择器、可应答的审批卡片,以及选择 Full access 时的风险确认。共享 Permission 服务在 TUI 中激活其命令子件,因此 TUI 会获得现有的 `/permission` 命令。 + +该模式只管辖文件效果。受沙箱约束的 bash 与文件系统修改只允许写入会话工作区和平台临时根目录;读取、网络访问与进程可见性仍不受该策略约束。若没有平台 runner 能强制执行受限的 bash 调用,执行会以拒绝方式关闭,不会退回不受限命令。 + +## 测试 + +已交付 TUI 的无密钥伪终端冒烟测试会启动真实 Loader 树,读取已持久化的首个请求,并断言 bash schema 中的 `sandbox_permissions`/`justification`,以及初始的 workspace-write 事件三元组。已交付 Web 组合的冒烟测试断言相同的策略、审批与 Permission 默认值。组装后的浏览器 Settings 快照打开时选中 Workspace Write,在更改后续会话默认值时保持现有 `workspace-write` 会话不变,并仍然验证经确认后选择 Full access 的路径。 + +## 曾考虑的替代方案 + +**将沙箱栈留在 `web.cordis.yml`,并在 `tui.cordis.yml` 中复制一份。** 不予采纳,因为插件标识、preset、回退值与执行器替换完全相同。两份副本会让安全默认值依赖两个界面覆盖层持续同步;共享 base 才是它们的唯一归属。 + +**保留不受限的 TUI,只更改浏览器回退值。** 不予采纳,因为这会保留无法解释的界面差异,并让全新的终端会话继续拥有本决策要移除的权限。 + +**在同一次变更中添加终端审批对话框。** 不予采纳,因为这是另一个交互与生命周期决策。TUI 没有 `approval/request` 应答者,因此一次性自动升权当前会落定为不可用并以拒绝方式关闭;需要更宽权限的用户可以通过 `/permission` 主动选择其他 preset。 + +## 后果 + +全新的会话无需额外提示即可修改当前工作区与临时根目录,尝试修改其他位置则会在触及目标前被拒绝。Full access 仍可通过显式选择获得,浏览器选择时也仍会显示确认对话框。系统不会重写已存储的用户默认值和会话日志中记录的权限。 + +由浏览器支撑的无头入口继承 Web 组合,因此默认值相同。TUI 缺少审批应答者是本次变更的明确限制:自动请求更宽权限的重试会在那里以拒绝方式关闭,而不会显示权限询问。 diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index 9d93c662d1..e113087417 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/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/README.md -README.md: e2567bebea80b154c01d1cad8094008818e5fd13 -README.zh.md: a3b7f433040ce0ff02848e3741f407dffe8f8cb8 +README.md: c43658e066b42c58252b1665c2cd3883d5cb5d4b +README.zh.md: 0690231e637135f6f2481880bee520958ad5b5c0 diff --git a/apps/cli/README.md b/apps/cli/README.md index e2567bebea..c43658e066 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -23,6 +23,8 @@ The TUI surface: The Web and headless surfaces boot `base.cordis.yml` plus `web.cordis.yml`, then apply `$DSH_HOME/config.yaml`; an explicit `--config ` replaces that personal overlay. Both surfaces otherwise share the same composition: both treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root ` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, opt into first-message model titles, use the same bounded transient model-request retry policy as the TUI, and mount a disposable in-memory SQLite content-index service. That service is ACTIVE at boot, while its `node:sqlite` module and database handle open only on the first content search. This keeps Node 22 startup output free of SQLite's experimental warning before search is used; the first actual search may still emit the runtime warning. Each service instance owns its database, so parallel invocations neither share unsupported SQLite state nor leave derived index files behind, and the first search lazily reconciles live and persisted logs. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`). +The shared composition defaults new TUI, Web, and headless sessions to the `workspace-write` permission preset (`workspace-write` file mode plus `ask` approval policy). Sandbox-enforced bash and filesystem mutations may write only under the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. The browser answers one-shot approval requests and exposes the Access picker; the TUI exposes `/permission`, but has no approval-request answerer, so an automatic wider retry there fails closed until the user deliberately changes the session preset. `DSH_PERMISSION_MODE` changes the process fallback, while a stored General-settings Permission value applies to later sessions without changing an open one. + The shipped TUI and Web compositions register the native DeepSeek adapter plus pi-ai OpenAI and Anthropic profiles. Credentials and endpoint overrides come from the provider-standard `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`, `OPENAI_API_KEY` / `OPENAI_BASE_URL`, and `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL` pairs in the boot's layered environment. Every surface also registers `web_search` and only `web_search`. Search uses DeepSeek's Anthropic-compatible Messages endpoint, resolves the same `DEEPSEEK_API_KEY` reference for every call, and accepts the separate `DEEPSEEK_SEARCH_BASE_URL` endpoint override; each search is an auxiliary model request with its own latency and token cost. `web_fetch` remains disabled and the composition mounts no default fetch provider, so deployments that need arbitrary page retrieval must opt in through an overlay. The deployment decision and its security boundary live in the [default Web search Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-default-search.md). diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index a3b7f43304..0690231e63 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -23,6 +23,8 @@ TUI 界面: Web 和无头界面启动 `base.cordis.yml` 与 `web.cordis.yml`,随后应用 `$DSH_HOME/config.yaml`;显式的 `--config ` 会替代该个人覆盖。除此之外,两者共享同一套组合:两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root ` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,选用首条消息模型标题,采用与 TUI 相同的有界暂时性模型请求重试策略,并挂载一个可丢弃的内存 SQLite 内容索引服务。该服务在启动时处于 ACTIVE 状态,但其 `node:sqlite` 模块与数据库句柄分别要到首次内容搜索才会导入和打开。这样可使 Node 22 在尚未使用搜索时的启动输出不出现 SQLite 实验性警告;首次实际搜索仍可能发出运行时警告。每个服务实例独占自己的数据库,因此并行调用既不会共享不受支持的 SQLite 状态,也不会留下派生索引文件,首次搜索还会惰性对账实时日志与持久化日志。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。 +共享组合把新建 TUI、Web 和无头会话的权限默认设为 `workspace-write` preset(`workspace-write` 文件模式加 `ask` 审批策略)。由沙箱强制约束的 bash 与文件系统修改只能写入会话工作区和平台临时根目录;读取、网络访问和进程可见性不受该策略约束。浏览器可以应答一次性审批请求,并提供 Access 选择器;TUI 提供 `/permission`,但没有审批请求应答者,因此自动请求更宽权限的重试会以拒绝方式关闭,直到用户主动更改会话 preset。`DSH_PERMISSION_MODE` 会更改进程回退值,而「通用」设置中已存储的「权限」值只适用于之后的会话,不会更改已打开的会话。 + 已交付的 TUI 和 Web 组合会注册原生 DeepSeek 适配器,以及 pi-ai 的 OpenAI 和 Anthropic 提供方配置。凭据和端点覆盖来自启动分层环境中的提供方标准变量对:`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`、`OPENAI_API_KEY` / `OPENAI_BASE_URL` 和 `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`。 每个界面也都只注册 `web_search` 这一个 Web 工具。搜索使用 DeepSeek 的 Anthropic 兼容 Messages 端点,每次调用都会解析同一个 `DEEPSEEK_API_KEY` 凭据引用,并接受独立的 `DEEPSEEK_SEARCH_BASE_URL` 端点覆盖;每次搜索都是一次辅助模型请求,会产生独立的延迟与 token 成本。`web_fetch` 仍处于禁用状态,组合也未挂载默认抓取提供方;需要任意页面抓取能力的部署必须通过覆盖层选择启用。部署决策及其安全边界见[默认 Web 搜索 Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-default-search.md)。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index b552570a5c..cb6b98a3b0 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -42,8 +42,16 @@ flowchart LR cfg --> plugin_tui_telemetry_otel plugin_tui_subprocess["subprocess
@deepseek-ai/dsh-subprocess-local"] cfg --> plugin_tui_subprocess - plugin_tui_bash_local["bash-local
@deepseek-ai/dsh-bash-local"] - cfg --> plugin_tui_bash_local + plugin_tui_sandbox["sandbox
@deepseek-ai/dsh-sandbox-local"] + cfg --> plugin_tui_sandbox + plugin_tui_sandbox_policy["sandbox-policy
@deepseek-ai/dsh-sandbox-policy"] + cfg --> plugin_tui_sandbox_policy + plugin_tui_bash_sandbox["bash-sandbox
@deepseek-ai/dsh-bash-sandbox"] + cfg --> plugin_tui_bash_sandbox + plugin_tui_approval["approval
@deepseek-ai/dsh-user-approval"] + cfg --> plugin_tui_approval + plugin_tui_permission["permission
@deepseek-ai/dsh-permission"] + cfg --> plugin_tui_permission plugin_tui_tool_bash["tool-bash
@deepseek-ai/dsh-tool-bash"] cfg --> plugin_tui_tool_bash plugin_tui_tool_tasks["tool-tasks
@deepseek-ai/dsh-tool-tasks"] @@ -126,8 +134,8 @@ flowchart LR cfg --> plugin_tui_system_prompt plugin_tui_agent_loop["agent-loop
@deepseek-ai/dsh-agent-loop"] cfg --> plugin_tui_agent_loop - plugin_tui_fs_local["fs-local
@deepseek-ai/dsh-fs-local"] - cfg --> plugin_tui_fs_local + plugin_tui_fs_sandbox["fs-sandbox
@deepseek-ai/dsh-fs-sandbox"] + cfg --> plugin_tui_fs_sandbox plugin_tui_llm_deepseek["llm-deepseek
@deepseek-ai/dsh-llm-deepseek"] cfg --> plugin_tui_llm_deepseek ``` @@ -151,7 +159,11 @@ flowchart LR | `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` | | `telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` | | `subprocess` | `@deepseek-ai/dsh-subprocess-local` | -| `bash-local` | `@deepseek-ai/dsh-bash-local` | +| `sandbox` | `@deepseek-ai/dsh-sandbox-local` | +| `sandbox-policy` | `@deepseek-ai/dsh-sandbox-policy` | +| `bash-sandbox` | `@deepseek-ai/dsh-bash-sandbox` | +| `approval` | `@deepseek-ai/dsh-user-approval` | +| `permission` | `@deepseek-ai/dsh-permission` | | `tool-bash` | `@deepseek-ai/dsh-tool-bash` | | `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` | | `fs-policy` | `@deepseek-ai/dsh-fs-policy` | @@ -193,7 +205,7 @@ flowchart LR | `tools` | `@deepseek-ai/dsh-tools` | | `system-prompt` | `@deepseek-ai/dsh-system-prompt` | | `agent-loop` | `@deepseek-ai/dsh-agent-loop` | -| `fs-local` | `@deepseek-ai/dsh-fs-local` | +| `fs-sandbox` | `@deepseek-ai/dsh-fs-sandbox` | | `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` | Source config: [`apps/cli/config/base.cordis.yml`](config/base.cordis.yml). diff --git a/apps/cli/config/base.cordis.yml b/apps/cli/config/base.cordis.yml index 7e648eedc8..e88cbdb39d 100644 --- a/apps/cli/config/base.cordis.yml +++ b/apps/cli/config/base.cordis.yml @@ -131,11 +131,42 @@ - id: subprocess name: '@deepseek-ai/dsh-subprocess-local' -- id: bash-local - name: '@deepseek-ai/dsh-bash-local' +# Every shipped product surface starts with the same file-effect boundary. +# The environment remains an explicit deployment override; otherwise fresh +# sessions pin workspace-write + ask through the permission service below. +- id: sandbox + name: '@deepseek-ai/dsh-sandbox-local' + +- id: sandbox-policy + name: '@deepseek-ai/dsh-sandbox-policy' + config: + mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write' + workspaceRoot: !!js process.cwd() + +- id: bash-sandbox + name: '@deepseek-ai/dsh-bash-sandbox' config: timeoutMs: 60000 +- id: approval + name: '@deepseek-ai/dsh-user-approval' + config: + policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'workspace-write') === 'danger-full-access' ? 'never' : 'ask'" + +- id: permission + name: '@deepseek-ai/dsh-permission' + config: + presets: + read-only: + sandbox: read-only + approval: ask + workspace-write: + sandbox: workspace-write + approval: ask + danger-full-access: + sandbox: danger-full-access + approval: never + - id: tool-bash name: '@deepseek-ai/dsh-tool-bash' @@ -339,10 +370,10 @@ config: agents: [] -# The filesystem provider. `cwd` defaults to the package's `process.cwd()`; the -# TUI states it explicitly because that value is also the session workspace. -- id: fs-local - name: '@deepseek-ai/dsh-fs-local' +# The sandboxed filesystem provider. `cwd` defaults to `process.cwd()`; the TUI +# states it explicitly because that value is also the session workspace. +- id: fs-sandbox + name: '@deepseek-ai/dsh-fs-sandbox' # The native DeepSeek adapter. No key or endpoint is inlined: both resolve per # request from the `llm-deepseek:` settings section over this entry, with the diff --git a/apps/cli/config/tui.cordis.yml b/apps/cli/config/tui.cordis.yml index d9ce7de680..02d8649447 100644 --- a/apps/cli/config/tui.cordis.yml +++ b/apps/cli/config/tui.cordis.yml @@ -46,7 +46,7 @@ reasoningEffort: max # This single-session app resolves relative paths from the process cwd. -- id: fs-local +- id: fs-sandbox config: cwd: !!js process.cwd() diff --git a/apps/cli/config/web.cordis.yml b/apps/cli/config/web.cordis.yml index f0c2e5ea28..38bcd387f6 100644 --- a/apps/cli/config/web.cordis.yml +++ b/apps/cli/config/web.cordis.yml @@ -36,50 +36,6 @@ apiKey: !!js process.env.DEEPSEEK_API_KEY baseURL: !!js process.env.DEEPSEEK_BASE_URL -# The web surface replaces the unrestricted local executors with the shared -# sandbox policy. Its default preserves the previous unrestricted behavior; -# DSH_PERMISSION_MODE and the browser permission picker can confine a session. -- insert: - - id: sandbox - name: '@deepseek-ai/dsh-sandbox-local' - - - id: sandbox-policy - name: '@deepseek-ai/dsh-sandbox-policy' - config: - mode: !!js process.env.DSH_PERMISSION_MODE ?? 'danger-full-access' - workspaceRoot: !!js process.cwd() - - - id: bash-sandbox - name: '@deepseek-ai/dsh-bash-sandbox' - - - id: approval - name: '@deepseek-ai/dsh-user-approval' - config: - policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'danger-full-access') === 'danger-full-access' ? 'never' : 'ask'" - - - id: permission - name: '@deepseek-ai/dsh-permission' - config: - presets: - read-only: - sandbox: read-only - approval: ask - workspace-write: - sandbox: workspace-write - approval: ask - danger-full-access: - sandbox: danger-full-access - approval: never - - - id: fs-sandbox - name: '@deepseek-ai/dsh-fs-sandbox' - -- id: bash-local - disabled: true - -- id: fs-local - disabled: true - # ── web-only host rows, the transport layer, and the browser roster ───────── # `dshClient` rows are the browser roster the modules node half scans into diff --git a/apps/cli/tests/shipped-composition.e2e.ts b/apps/cli/tests/shipped-composition.e2e.ts index ac1e9d5456..aa1a803c85 100644 --- a/apps/cli/tests/shipped-composition.e2e.ts +++ b/apps/cli/tests/shipped-composition.e2e.ts @@ -11,6 +11,7 @@ import { acknowledgeTuiFirstRunWelcome } from '../src/tui-onboarding/tui-first-r const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) +const PERMISSION_SUMMARY = 'current preset workspace-write (available: read-only, workspace-write, danger-full-access)' // An overlay over the shipped tree, so the catalog under test is the one // `base.cordis.yml` + `tui.cordis.yml` assemble; the tail only swaps the model // and redirects session artifacts. @@ -67,6 +68,8 @@ interface LoggedHeader { names: string[] /** `bash`'s assembled parameter properties; the escalation pair is present only under a confining executor. */ bashArguments: Record + /** Initial permission facts pinned by the shipped composition. */ + permissionEvents: Array<[string, unknown]> } /** @@ -82,18 +85,22 @@ async function loggedHeader(cwd: string): Promise { // A single keyless run writes one session log. const logRelPath = entries.find(name => name.endsWith('.jsonl')) if (logRelPath === undefined) throw new Error(`no session log written under ${sessionsDir}`) - const lines = (await readFile(join(sessionsDir, logRelPath), 'utf8')).split('\n').filter(Boolean) - for (const line of lines) { - const event = JSON.parse(line) as SessionEvent - if (event.type !== 'request/header') continue - const tools = event.data.header.tools ?? [] - const bash = tools.find(schema => schema.name === 'bash') - return { - names: tools.map(schema => schema.name).sort(), - bashArguments: (bash?.parameters as { properties?: Record } | undefined)?.properties ?? {}, - } + const events = (await readFile(join(sessionsDir, logRelPath), 'utf8')).split('\n').filter(Boolean) + .map(line => JSON.parse(line) as SessionEvent) + const header = events.find(event => event.type === 'request/header') + if (header === undefined || header.type !== 'request/header') { + throw new Error(`session log ${logRelPath} has no request/header event`) + } + const tools = header.data.header.tools ?? [] + const bash = tools.find(schema => schema.name === 'bash') + return { + names: tools.map(schema => schema.name).sort(), + bashArguments: (bash?.parameters as { properties?: Record } | undefined)?.properties ?? {}, + permissionEvents: events.flatMap(event => + event.type === 'permission/preset' || event.type === 'sandbox/mode' || event.type === 'approval/policy' + ? [[event.type, event.data] as [string, unknown]] + : []), } - throw new Error(`session log ${logRelPath} has no request/header event`) } describe('shipped dsh composition (real Loader tree in a PTY)', () => { @@ -110,17 +117,22 @@ describe('shipped dsh composition (real Loader tree in a PTY)', () => { // Artifact CI builds and smokes concurrently on a contended runner. ...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}), actions: [ - { waitFor: COMPOSITION_SETTLED_MARKER, send: 'Describe the shipped composition.\r' }, + { waitFor: COMPOSITION_SETTLED_MARKER, send: '/permission\r' }, + { waitFor: PERMISSION_SUMMARY, send: 'Describe the shipped composition.\r' }, { waitFor: COMPOSITION_REPLY_TEXT, send: '/exit\r' }, ], inspect: async (cwd) => { observed = await loggedHeader(cwd) }, }) expect(output).toContain(COMPOSITION_REPLY_TEXT) + expect(output).toContain(PERMISSION_SUMMARY) expect(observed?.names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TUI_TOOLS) expect([[], RIPGREP_TOOLS]).toContainEqual(observed?.names.filter(name => RIPGREP_TOOLS.includes(name))) - // The TUI mounts the unrestricted local executors, so `tool-bash` emits no - // escalation pair. Pinning its absence keeps a later sandbox change from - // arriving here unannounced. - expect(Object.keys(observed?.bashArguments ?? {})).not.toContain('sandbox_permissions') + expect(observed?.bashArguments).toHaveProperty('sandbox_permissions') + expect(observed?.bashArguments).toHaveProperty('justification') + expect(observed?.permissionEvents).toEqual([ + ['permission/preset', { preset: 'workspace-write' }], + ['sandbox/mode', { mode: 'workspace-write' }], + ['approval/policy', { policy: 'ask' }], + ]) }, LOADER_SMOKE_TEST_TIMEOUT_MS) }) diff --git a/apps/web/tests/access-confirmation.e2e.ts b/apps/web/tests/access-confirmation.e2e.ts index fae329362b..c6c6e627bd 100644 --- a/apps/web/tests/access-confirmation.e2e.ts +++ b/apps/web/tests/access-confirmation.e2e.ts @@ -70,14 +70,7 @@ describe('web e2e: Full access confirmation', () => { const access = page.locator('button[aria-label^="访问模式"]').first() await access.waitFor({ timeout: 10_000 }) - // Normalize the starting preset through the real command path. The - // shipped web config may already start at Full access. - if ((await access.getAttribute('aria-label'))?.endsWith('Full access') === true) { - await access.click() - await page.getByRole('menuitem', { name: 'Workspace Write' }).click() - await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 }) - .toBe('访问模式,当前:Workspace Write') - } + expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:Workspace Write') await access.click() await page.getByRole('menuitem', { name: 'Full access' }).click() diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index 9acb0fa136..fb3bbeb186 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -339,19 +339,19 @@ describe('web e2e: seeded history renders through cold resume', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-command-row')) // The Access chip submits `/permission ` — a host command with no // model call, so the settled row renders keylessly over this cold history. - // The row copy is the assertion: `permission · preset workspace-write`, + // The row copy is the assertion: `permission · preset read-only`, // where neither half repeats the other (the dispatched `/` and its // argument stay out of the title, and the settlement text never restates // the command's own name). - await page.getByRole('button', { name: 'Access mode, current: Full access' }).click() - await page.getByRole('menuitem', { name: 'Workspace Write' }).click() - await page.getByRole('button', { name: 'Access mode, current: Workspace Write' }).waitFor({ timeout: 10_000 }) + await page.getByRole('button', { name: 'Access mode, current: Workspace Write' }).click() + await page.getByRole('menuitem', { name: 'Read Only' }).click() + await page.getByRole('button', { name: 'Access mode, current: Read Only' }).waitFor({ timeout: 10_000 }) // Scoped to the row itself, so unrelated page text that happens to read // `permission` (a future resident slash menu) cannot satisfy or break it. - const row = page.locator('[data-variant="others"]').filter({ hasText: 'preset workspace-write' }) + const row = page.locator('[data-variant="others"]').filter({ hasText: 'preset read-only' }) await expect.poll(() => row.count(), { timeout: 10_000 }).toBe(1) expect(await row.getByText('permission', { exact: true }).count()).toBe(1) - expect(await row.getByText('/permission workspace-write', { exact: true }).count()).toBe(0) + expect(await row.getByText('/permission read-only', { exact: true }).count()).toBe(0) const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) .split(SEED_ID).join('{{seededId}}') await compareOrRefreshGolden(COMMAND_ROW_EXPECTED, snapshot, MODE) diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index a1d77657b8..7bdfcc1d59 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -57,7 +57,7 @@ describe('web e2e: settings modal and General preferences', () => { expect(await trigger.getAttribute('aria-expanded')).toBe('true') // General is active by default; Permission, Language and Appearance are functional. expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBe('true') - await dialog.getByRole('button', { name: 'Full access' }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: 'Workspace Write' }).waitFor({ timeout: 10_000 }) await expect.poll(() => dialog.getByText('语言', { exact: true }).count(), { timeout: 5_000 }).toBe(1) await expect.poll(() => dialog.getByText('外观', { exact: true }).count(), { timeout: 5_000 }).toBe(1) // Golden of the freshly opened dialog (default zh, General active). @@ -82,12 +82,12 @@ describe('web e2e: settings modal and General preferences', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-permission')) const existing = scaffold.ctx.sessions.create(SessionId('settings-permission-before')) expect(existing.events.find(event => event.type === 'permission/preset')?.data) - .toEqual({ preset: 'danger-full-access' }) + .toEqual({ preset: 'workspace-write' }) await page.getByRole('button', { name: '设置', exact: true }).click() const dialog = page.getByRole('dialog', { name: '设置' }) await dialog.waitFor({ timeout: 10_000 }) - const selector = dialog.getByRole('button', { name: 'Full access' }) + const selector = dialog.getByRole('button', { name: 'Workspace Write' }) await selector.waitFor({ timeout: 10_000 }) await expect.poll(() => selector.isEnabled(), { timeout: 5_000 }).toBe(true) await selector.click() @@ -98,7 +98,7 @@ describe('web e2e: settings modal and General preferences', () => { expect(document).toContain('permission:') expect(document).toContain('defaultPreset: read-only') expect(existing.events.find(event => event.type === 'permission/preset')?.data) - .toEqual({ preset: 'danger-full-access' }) + .toEqual({ preset: 'workspace-write' }) const created = scaffold.ctx.sessions.create(SessionId('settings-permission-after')) expect(created.events.map(event => [event.type, event.data])).toEqual([ diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index 0cad833303..671cc84906 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -10,6 +10,7 @@ import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox' import type {} from '@deepseek-ai/dsh-tools' import type {} from '@deepseek-ai/dsh-sandbox-policy' import type {} from '@deepseek-ai/dsh-user-approval' +import type {} from '@deepseek-ai/dsh-permission' import { launchWebScaffold, type WebScaffold } from './scaffold.ts' /** @@ -63,7 +64,7 @@ afterEach(async () => { scaffold = undefined }) -it('assembles the shipped Web catalog and keeps its access default', async () => { +it('assembles the shipped Web catalog with the confined access default', async () => { scaffold = await launchWebScaffold() const names = scaffold.ctx.tools.schemas().map(schema => schema.name).sort() expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS) @@ -76,8 +77,7 @@ it('assembles the shipped Web catalog and keeps its access default', async () => expect(writableRoots(scaffold.ctx.sandboxPolicy.resolve({ mode: 'workspace-write' }))).toEqual( expect.arrayContaining([canonicalPath('/tmp'), canonicalPath(tmpdir())]), ) - // The Web surface keeps its shipped access default; the base's confined one - // reaches the TUI. Pinning both keeps a base change from moving Web silently. - expect(scaffold.ctx.sandboxPolicy.defaultMode).toBe('danger-full-access') - expect(scaffold.ctx.approval.config.policy).toBe('never') + expect(scaffold.ctx.sandboxPolicy.defaultMode).toBe('workspace-write') + expect(scaffold.ctx.approval.config.policy).toBe('ask') + expect(scaffold.ctx.permission.defaultPreset).toBe('workspace-write') }, 120_000) diff --git a/apps/web/tests/snapshots/code-mode-round/ui.expected.md b/apps/web/tests/snapshots/code-mode-round/ui.expected.md index 183bd366a0..8f4c7e5bf2 100644 --- a/apps/web/tests/snapshots/code-mode-round/ui.expected.md +++ b/apps/web/tests/snapshots/code-mode-round/ui.expected.md @@ -39,7 +39,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md index 636b1e6d28..e4d5ac8426 100644 --- a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md +++ b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md @@ -54,7 +54,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md index 3f7fa52b2e..facb7b58cc 100644 --- a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md +++ b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md @@ -34,7 +34,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md index 7b6432ef87..8611ac5c0d 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md @@ -28,7 +28,7 @@ - textbox "Describe what you want to build" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md index 4aad3112e1..a9fb7901d7 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md @@ -28,7 +28,7 @@ - textbox "Describe what you want to build" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Plan mode on, press to turn off": Plan - button "Select model, current deepseek-v4-flash": - text: deepseek-v4-flash diff --git a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md index f58e5b77f2..5965797c69 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md @@ -26,7 +26,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/live-interactions/cancel.expected.md b/apps/web/tests/snapshots/live-interactions/cancel.expected.md index 3333237798..d1e4d2bbef 100644 --- a/apps/web/tests/snapshots/live-interactions/cancel.expected.md +++ b/apps/web/tests/snapshots/live-interactions/cancel.expected.md @@ -23,7 +23,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md index e6a93f2463..fb9337e978 100644 --- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md +++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md @@ -19,7 +19,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/live-interactions/loading.expected.md b/apps/web/tests/snapshots/live-interactions/loading.expected.md index b442b4345a..a5dfd08fb5 100644 --- a/apps/web/tests/snapshots/live-interactions/loading.expected.md +++ b/apps/web/tests/snapshots/live-interactions/loading.expected.md @@ -18,7 +18,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/live-interactions/retry.expected.md b/apps/web/tests/snapshots/live-interactions/retry.expected.md index f34dddd7dd..6380eaf5c6 100644 --- a/apps/web/tests/snapshots/live-interactions/retry.expected.md +++ b/apps/web/tests/snapshots/live-interactions/retry.expected.md @@ -28,7 +28,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/message-actions/ui.expected.md b/apps/web/tests/snapshots/message-actions/ui.expected.md index 613e9a3605..bf67498178 100644 --- a/apps/web/tests/snapshots/message-actions/ui.expected.md +++ b/apps/web/tests/snapshots/message-actions/ui.expected.md @@ -37,7 +37,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current deepseek-v4-flash": - text: deepseek-v4-flash - img diff --git a/apps/web/tests/snapshots/plan-review/approved.expected.md b/apps/web/tests/snapshots/plan-review/approved.expected.md index c971a6b2e4..5be3f83247 100644 --- a/apps/web/tests/snapshots/plan-review/approved.expected.md +++ b/apps/web/tests/snapshots/plan-review/approved.expected.md @@ -39,7 +39,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/question-composer/answered.expected.md b/apps/web/tests/snapshots/question-composer/answered.expected.md index fec84d06db..28297569ab 100644 --- a/apps/web/tests/snapshots/question-composer/answered.expected.md +++ b/apps/web/tests/snapshots/question-composer/answered.expected.md @@ -34,7 +34,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md index 829f21a70f..b9dee060ac 100644 --- a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md +++ b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md @@ -19,7 +19,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/queue-actions/editing.expected.md b/apps/web/tests/snapshots/queue-actions/editing.expected.md index 169dde2c51..3a70840713 100644 --- a/apps/web/tests/snapshots/queue-actions/editing.expected.md +++ b/apps/web/tests/snapshots/queue-actions/editing.expected.md @@ -32,7 +32,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/queue-actions/ui.expected.md b/apps/web/tests/snapshots/queue-actions/ui.expected.md index e2c91f7584..24edb57417 100644 --- a/apps/web/tests/snapshots/queue-actions/ui.expected.md +++ b/apps/web/tests/snapshots/queue-actions/ui.expected.md @@ -25,7 +25,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/seeded-history/command-row.expected.md b/apps/web/tests/snapshots/seeded-history/command-row.expected.md index 0173726c38..b916a3add2 100644 --- a/apps/web/tests/snapshots/seeded-history/command-row.expected.md +++ b/apps/web/tests/snapshots/seeded-history/command-row.expected.md @@ -41,11 +41,11 @@ - img - text: Context injection - img -- text: permission preset workspace-write +- text: permission preset read-only - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Workspace Write"': Workspace Write +- 'button "Access mode, current: Read Only"': Read Only - button "Select model, current deepseek-v4-flash": - text: deepseek-v4-flash - img diff --git a/apps/web/tests/snapshots/seeded-history/ui.expected.md b/apps/web/tests/snapshots/seeded-history/ui.expected.md index 0062b6cfab..a168d5e2a3 100644 --- a/apps/web/tests/snapshots/seeded-history/ui.expected.md +++ b/apps/web/tests/snapshots/seeded-history/ui.expected.md @@ -43,7 +43,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current deepseek-v4-flash": - text: deepseek-v4-flash - img diff --git a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md index e782d25c05..118e4ff3e1 100644 --- a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md +++ b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md @@ -11,8 +11,8 @@ - img - text: 关闭 - text: 权限 选择新会话的默认权限模式 - - button "Full access": - - text: Full access + - button "Workspace Write": + - text: Workspace Write - img - text: 语言 - button "中文": diff --git a/apps/web/tests/snapshots/steering/settled.expected.md b/apps/web/tests/snapshots/steering/settled.expected.md index 2ab2f5970d..72e28598e5 100644 --- a/apps/web/tests/snapshots/steering/settled.expected.md +++ b/apps/web/tests/snapshots/steering/settled.expected.md @@ -35,7 +35,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/apps/web/tests/snapshots/web-search-round/ui.expected.md b/apps/web/tests/snapshots/web-search-round/ui.expected.md index 37d53a0df6..4ff674462e 100644 --- a/apps/web/tests/snapshots/web-search-round/ui.expected.md +++ b/apps/web/tests/snapshots/web-search-round/ui.expected.md @@ -26,7 +26,7 @@ - textbox "Message the agent" - button "Commands": - img -- 'button "Access mode, current: Full access"': Full access +- 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": - text: DeepSeek-V4-Flash - img diff --git a/packages/credentials/credentials-local/README.i18n.yaml b/packages/credentials/credentials-local/README.i18n.yaml index 89a8576683..b5fb4b2f0e 100644 --- a/packages/credentials/credentials-local/README.i18n.yaml +++ b/packages/credentials/credentials-local/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/credentials/credentials-local/README.md -README.md: 126140b10719dc6f7bc458a118ba1feb1f440270 -README.zh.md: c22575115ab44b5e86a847ffe8f1fa1a795b580d +README.md: 02b883958faf8b695a3a2abf2df77790cc2fca86 +README.zh.md: 59c7fd5747f327e8998882ca4db1473173e793b5 diff --git a/packages/credentials/credentials-local/README.md b/packages/credentials/credentials-local/README.md index 126140b107..02b883958f 100644 --- a/packages/credentials/credentials-local/README.md +++ b/packages/credentials/credentials-local/README.md @@ -32,7 +32,7 @@ External edits publish `credentials/updated` per changed reference after the sna ## Security boundary -The document is `0600` under a `0700` directory, which stops other OS users — **not** the model. Tool processes (bash, the filesystem tools) run as the same user, so under the shipped `danger-full-access` default they can read this file exactly like any other file the user owns, and no sandbox mode singles it out. What the harness does hold to is narrower: it never hands the model a resolved path to the document, and never loads it into the process environment (see [app-boot's Personal config](../../ui/app-boot/README.md#personal-config)), so reaching the value takes a deliberate read of a path the agent was not given. +The document is `0600` under a `0700` directory, which stops other OS users — **not** the model. Tool processes (bash, the filesystem tools) run as the same user, and the shipped `workspace-write` file policy confines mutations rather than reads, so they can read this file exactly like any other file the user owns; no sandbox mode singles it out. What the harness does hold to is narrower: it never hands the model a resolved path to the document, and never loads it into the process environment (see [app-boot's Personal config](../../ui/app-boot/README.md#personal-config)), so reaching the value takes a deliberate read of a path the agent was not given. That is discretion, not a boundary. A deployment that must keep provider keys away from its own agent cannot get there with file permissions; an OS-keychain provider — a store the model's processes cannot read at all — is the deferred answer and belongs beside this provider as a sibling package. @@ -48,7 +48,7 @@ No direct invalidation; credentials never enter a request prefix. - **Multi-line entries refuse `set`/`unset`** — the line editor will not rewrite an entry it would corrupt; `describe` reports them `writable: false` and edits must go to the file directly. - **Same-reference concurrent writes are last-write-wins** — the writer lock and the read-modify-write keep concurrent writers from dropping each other's entries, but two writers editing one reference still resolve to the later write; there is no revision check. -- **A same-UID process can read the document** — see [Security boundary](#security-boundary): only a confining sandbox mode denies it, and an OS-keychain provider is deferred. +- **A same-UID process can read the document** — see [Security boundary](#security-boundary): the file-effect sandbox modes do not deny reads, and an OS-keychain provider is deferred. - **Unrepresentable values fail loud** — control characters, or a mix of both quote styles with backslashes, cannot round-trip the dotenv line format. - **Environment changes are invisible** — `process.env` is read live per resolution, but no event can announce a change there. - **Atomic, not crash-durable** — inherited from `dsh-atomic-write`; the store re-reads on boot. diff --git a/packages/credentials/credentials-local/README.zh.md b/packages/credentials/credentials-local/README.zh.md index c22575115a..59c7fd5747 100644 --- a/packages/credentials/credentials-local/README.zh.md +++ b/packages/credentials/credentials-local/README.zh.md @@ -32,7 +32,7 @@ dotenv 格式,用 `dotenv` 解析;写回用物理行级编辑器,保留一 ## 安全边界 -文档在 `0700` 目录下以 `0600` 权限存放,这挡得住其他 OS 用户,**挡不住**模型。工具进程(bash、文件系统工具)以同一用户身份运行,因此在出厂默认的 `danger-full-access` 下,它们读这个文件与读该用户拥有的任何其他文件毫无二致,也没有任何沙箱模式会把它单独挑出来。harness 真正守住的更窄:它绝不把该文档的解析后路径交给模型,也绝不把它载入进程环境(见 [app-boot 的个人配置](../../ui/app-boot/README.md#personal-config)),因此要拿到这个值,需要刻意去读一条并未交给 agent 的路径。 +文档在 `0700` 目录下以 `0600` 权限存放,这挡得住其他 OS 用户,**挡不住**模型。工具进程(bash、文件系统工具)以同一用户身份运行,而已交付的 `workspace-write` 文件策略限制的是修改而非读取,因此它们读这个文件与读该用户拥有的任何其他文件毫无二致;也没有任何沙箱模式会把它单独挑出来。harness 真正守住的更窄:它绝不把该文档的解析后路径交给模型,也绝不把它载入进程环境(见 [app-boot 的个人配置](../../ui/app-boot/README.md#personal-config)),因此要拿到这个值,需要刻意去读一条并未交给 agent 的路径。 这是审慎,不是边界。必须让提供方密钥远离自身 agent 的部署无法靠文件权限做到;OS 钥匙串 provider——一个模型的进程根本读不到的存储——才是延后的答案,它应当作为平级包与本 provider 并列。 @@ -48,7 +48,7 @@ dotenv 格式,用 `dotenv` 解析;写回用物理行级编辑器,保留一 - **多行条目拒绝 `set`/`unset`**——行编辑器不改写会被它破坏的条目;`describe` 把它们报为 `writable: false`,编辑必须直接落到文件上。 - **同一引用的并发写入是后写胜出**——写锁加读-改-写让并发写入者不会丢掉彼此的条目,但两个写入者编辑同一个引用时仍以较后的写入为准;没有修订检查。 -- **同 UID 进程可以读取该文档**——见[安全边界](#security-boundary):只有受限沙箱模式会拒绝它,OS 钥匙串 provider 仍是延后项。 +- **同 UID 进程可以读取该文档**——见[安全边界](#security-boundary):文件效果沙箱模式不会拒绝读取,OS 钥匙串 provider 仍是延后项。 - **无法表示的值响亮失败**——控制字符,或同时混用两种引号又含反斜杠的值,无法在 dotenv 行格式中往返。 - **环境变化不可见**——每次解析实时读取 `process.env`,但那里的变化不可能发出事件。 - **原子但不保证崩溃持久**——继承自 `dsh-atomic-write`;存储在启动时重新读取。