# AGENTS.md ## Purpose This file defines repository-wide instructions for Coding Agents working on this project. More specific instructions may later be introduced by nested `AGENTS.md` files inside individual modules. ## Before Making Changes Before modifying the repository: 1. Read this file. 2. Read `docs/README.md`. 3. Read documentation relevant to the task. 4. Inspect the current repository state instead of assuming documentation is perfectly up to date. 5. Prefer extending existing conventions over introducing parallel structures. 6. Check `.agent/note/` for relevant project knowledge when applicable. ## Documentation Documentation is part of the implementation. When a change affects documented behavior, architecture, workflows, interfaces, engineering decisions, or development processes, update the corresponding documentation in the same change. Do not keep important project knowledge only in chat history, issue comments, or temporary Agent context. ## Documentation Locations - `docs/requirements/` — requirements - `docs/architecture/` — architecture and system design - `docs/decisions/` — Architecture Decision Records - `docs/plans/` — active and completed implementation plans - `docs/development/` — development workflow and engineering practices - `docs/reference/` — stable technical reference - `docs/troubleshooting/` — recurring problems and verified solutions - `.agent/note/` — durable Agent-oriented project knowledge - `.agent/templates/` — templates used by Coding Agents - `.agent/skills/documentation/SKILL.md` — documentation maintenance rules - `CHANGELOG.md` / `CHANGELOG.en.md` — the user-visible changes of every released version, one section per release. This is the only home for "what changed in version X": the roadmap progress ledger owns release *state*, the CHANGELOG owns release *content*. Each release updates it before the tag is created (see `docs/development/release-workflow.md`). ## Documentation Maintenance Documentation maintenance rules are defined in: `.agent/skills/documentation/SKILL.md` When creating, modifying, moving, or deleting project documentation, follow the Documentation Skill. ## Documentation Validation Before completing changes that affect documentation, run the repository documentation check. Use the canonical package script: `docs:check` The package manager adopted by this repository is pnpm, so the command is: ```bash pnpm docs:check ``` A change with failing documentation checks is incomplete. ## Source of Truth Avoid duplicating the same information across multiple documents. Each important concept should have one canonical location. Other documents should link to that source instead of copying its contents. ## Bilingual Documentation Chinese is the primary documentation language. For documents that require bilingual maintenance: - `.md` is the canonical Chinese version. - `.en.md` is the corresponding English version. The two files are treated as one logical document: when you update one, check whether the other needs to be synchronized, and vice versa. If the two versions ever conflict, the Chinese version wins — fix the English version. Do not produce low-quality machine translation; the English version should read naturally and accurately express the meaning of the Chinese version. All README documents and formal documents under `.agent/note/` must follow this convention. Do not create empty `.en.md` files for other document types unless bilingual maintenance is actually needed. The changelog is maintained as a bilingual pair too (`CHANGELOG.md` / `CHANGELOG.en.md`): it is a user-facing document that ships inside the npm tarball alongside both READMEs, so an English reader must not hit a Chinese-only release history. Other types — ADRs, plans, reference, and troubleshooting entries — stay Chinese-only unless a real need appears. ## Current Project State The project is an out-of-tree DSH plugin bundle (`@dsh-electron/dsh-plugin-multi-root-workspace`) that widens the workspace sandbox scope from one root to a primary root plus N additional roots, without modifying any upstream package. Milestone M1 (bundle composition and empty-root pass-through) is complete and verified: the plugin installs through `dsh plugin`, replaces the two provider rows, and behaves exactly like an uninstalled harness while no additional root is configured. See `docs/plans/completed/2026-09-12-m1-composition-and-passthrough.md` for the evidence and for the T0 findings. Milestone M2 (additional roots and dialect grants) is complete and verified: a registered additional root is enforced both by the in-process fence and by the host kernel dialect (Seatbelt / bwrap / Landlock) through one shared scope, the cross-provider allow matrix is pinned by tests, the workspace topology reaches the model through a runtime-context contribution, and the empty-root behavior stays byte-identical to an uninstalled harness. See `docs/plans/completed/2026-09-12-m2-additional-roots-and-dialect-grants.md`. Dialect grants are widened by recognizing the profile `super.confine` produced and cloning its grant spelling — never by deep-importing upstream internals (`docs/decisions/ADR-0003-dialect-grant-widening.md`). Milestone M3 (root registry, `/workspace-folders`, browser panel, journey e2e) is implemented and re-accepted after an external review: roots are persisted in the `multi_root_workspace` storage domain keyed by the canonical primary root, validated on every write and every read, and the registry feeds the M2 scope; the command and the sidebar Workspace Folders panel both manage them, the panel talking to the host over the plugin's own Connection RPC channel; `pnpm smoke:journey` drives a real agent turn across two Git repositories in both the `web` and `headless` compositions. The review's eight findings are fixed, each with a regression test — see the "external review rework" section of `docs/plans/completed/2026-09-12-m3-root-registry-command-and-ui.md`, plus `docs/decisions/ADR-0004-root-registry-persistence-and-validation.md` (rework decisions 9–15) and `docs/decisions/ADR-0005-out-of-tree-client-transport.md`. The `v0.1.1` hardening batch (**H1–H4**) landed on 2026-09-15 on `fix/v0.1.1-hardening` and reached `main` through PR #1; it is **released as `v0.1.1`**. **`v0.1.2`** is the support-matrix expansion release (`0.1.6-alpha.2`, `0.1.7-alpha.1`, and the `0.1.6-alpha.2` panel session probe), **released as `v0.1.2`**. **`v0.1.3`** is the support-matrix expansion (`0.1.7-alpha.2` and `0.1.7-rc.1`, no new adapter branch), **released as `v0.1.3`**. **`v0.1.4`** is the support-matrix expansion (`0.1.7-rc.2`, no new adapter branch), **released as `v0.1.4`**. **`v0.1.5`** prepares exact support for DSH `0.2.0-rc.1` and `0.2.0-rc.2`, with a CI matrix across all supported runtimes and enabled operating systems; it is released as `v0.1.5`. **`v0.1.6`** shipped DSH `0.2.1-alpha.1` / `.2` support, session-origin / effective-cwd separation (ADR-0012), and the B1–B3 consumers; **`v0.1.7`** shipped the second-phase B batches (B4–B9 plus the B1–B3 hardening and the R1–R4 review fixes), the Files-tab additional-root browsing (P1–P11), and exact compatibility for `0.2.1-alpha.3` through `0.2.1-rc.2`. **`v0.1.8`** fixed the DSH 0.2.0 `permissionPresets/catalog` outage (optional-package classification + de-gated provider rows); **`v0.1.9`** shipped the optional in-package Auto Review entry (ADR-0022, off by default). **`v0.1.10`** shipped additional-root Skills (ADR-0023: `.dsh/skills` / `.agents/skills` discovery in additional roots, `/alias-id:skill-name` namespaced invocation, per-root toggles plus a global setting), the Workspace Folders panel UI alignment, and exact compatibility for `0.2.1-rc.3`. **`v0.1.11`** shipped exact compatibility for `0.2.1-rc.4` and `0.2.2-alpha.2`, plus the Issue #24 hot-fix: `setRegistryAuthority()` now notifies listeners only on a real `available`/`unavailable` transition, breaking the Skills-invalidation → `skills/change` → client-requery feedback loop that kept a registry-contending Host at ~1 CPU core until V8 OOM (lease retry, takeover and fail-closed semantics unchanged). **`v0.1.12`** shipped the shared multi-Host registry authority (ADR-0025: the default bundle enables `sharedStorageRoot` — one owner opens the storage medium, followers reach it over kernel-authenticated local IPC) with per-operation admission ordering (ADR-0024), raised dependency security floors, and exact compatibility for `0.2.1-rc.5` (20 exact releases in the contract). **`v0.1.13`** ships F4 background recovery and diagnostics (ADR-0026), plus the Files-tab root-selection fix; the real TUI soak remains deferred. See `docs/reference/dsh-0.2.1-migration.md` for the `alpha.3`–`rc.3` interface review. Numbering, so that historical documents agree: **M1–M3** are the MVP milestones released as `v0.1.0`, **H1–H4** are the v0.1.1 batch, and **H1 is the same work as M4** under its second name; H4 Phase 1 and Phase 2 are both implemented. Progress, numbering and release state have exactly one home: the progress ledger in `docs/plans/active/2026-09-12-multi-root-workspace.md`. The user-visible changes of each released version have exactly one home as well: `CHANGELOG.md` / `CHANGELOG.en.md`, which every release updates in the same change. The batch's four items: - **H1 (= M4) — one Registry Authority Process per store.** A store-wide kernel lease beside the JSON document; a contended process is fail-closed and takes over via `refresh()`. See `docs/decisions/ADR-0007-registry-authority-lease.md`. - **H2 — panel authority is host-derived.** The client `primaryRoot` field is gone and every endpoint requires a live `sessionId`. See `docs/decisions/ADR-0008-panel-session-derived-authority.md`. - **H3 — DSH compatibility is a code contract rather than a documented agreement**: `src/compat/dsh-version.ts` holds an exact-version allowlist (`SUPPORTED_DSH_RELEASES`), `peerDependencies` names those same versions, `scripts/check-dsh-compat.mjs` (`pnpm compat:check`, first step of CI) fails when any of allowlist / peers / dev pin / installed tree drift apart, and the `multi-root-compat` row classifies the installation at boot. `0.1.5-rc.2`, `0.1.6-alpha.1`, `0.1.6-alpha.2`, `0.1.7-alpha.1`, `0.1.7-alpha.2`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2`, `0.2.1-alpha.1`, `0.2.1-alpha.2`, `0.2.1-alpha.3`, `0.2.1-alpha.4`, `0.2.1-alpha.5`, `0.2.1-rc.1`, `0.2.1-rc.2`, `0.2.1-rc.3`, `0.2.1-rc.4`, `0.2.1-rc.5`, and `0.2.2-alpha.2` are supported; the dev pin is `0.2.2-alpha.2` (cordis `4.0.6-alpha.1`). From `0.2.1-alpha.3`, `@deepseek-ai/dsh-storage-domain` pins `zod` at `4.4.3` exactly, so the domain schemas in `src/registry.ts` are built by the storage-domain's own resolved zod via `src/compat/storage-zod.ts` — do not reintroduce a package-level `import { z } from 'zod'` for domain schemas, and do not pin this package's `zod` to one upstream pin (the pre-`alpha.3` `^4.4.3` would split the tree on the other side). On the `0.2.2` line: `AgentRegistry` declares `inject = ['sessionProjections']` — any test stack mounting it must mount `SessionProjectionRegistry` first; tool execution lives outside `dsh-tools` in executors (`ctx.tools.setExecutor`, batch `ctx.tools.execute({calls})`, `@deepseek-ai/dsh-tools/pipeline` — alpha.2-only, probes stay structural); `SandboxExecutionPolicy.additionalWritableRoots` is upstream-native and composes with this plugin's own scope widening; 0.2.2+ lanes need Node >=24.9 (CI routes 0.1.*/0.2.0/0.2.1 to Node 22, newer releases to 24). Auto Review stays pinned to its exact `0.2.1-rc.2` baseline — `AUTO_REVIEW_RELEASE` does not move with the dev pin. `0.1.7-alpha.2`, `0.1.7-rc.1`, and `0.1.7-rc.2` kept the `0.1.7-alpha.1` shapes, so promoting them did not add an adapter branch. From `0.1.7-rc.1`, `dsh plugin add` refuses a plugin unless `peerDependencies` names that exact release. `0.1.7-alpha.2` through `0.1.7-rc.2` depend on cordis `~4.0.4`. `0.1.6-alpha.2` kept the `0.1.6-alpha.1` shapes (`confine` stays async, the instruction renderer name is unchanged), so promoting it did not require an adapter edit. `0.1.7-alpha.1` kept those two shapes and still required adapters: session format 4 rejects `kind: 'plugin'`, the tool-failure bit moved onto the message, panel icons use Regular names, in-process boot publishes `PluginPackages`, and bash execution is `execute().result()`. Its `dsh` depends on cordis `^4.0.3`; a probe that also installs `4.0.2` splits `@deepseek-ai/dsh-tools` and the scheduler Symbol misses. `0.2.1-rc.3` kept the `0.2.1-rc.2` shapes — its upstream delta is purely additive (new sidebar slots, navigation pins, and a DeepSeek `/models` listing) — so promoting it did not add an adapter branch. - **H4 — additional roots' own `AGENTS.md` / `CLAUDE.md` now reach the model** through the `multi-root-instructions` row, because upstream's discovery walks upward from the session cwd and can never reach them. Phase 1 injects the root's top-level files; Phase 2 injects the files of the subdirectories the session has actually worked in, driven by the persisted `session/event` feed rather than by upstream's `SessionMessageProjection` semantics. See `docs/plans/completed/2026-09-15-dsh-compat-contract.md` and `docs/decisions/ADR-0010-additional-root-instruction-scope.md`. Eleven invariants that must survive future changes: 1. **Re-resolving a path is not re-authorizing it.** A registration carries the canonical directory it was granted for (`recordedPath`); `ctx.multiRootScope` grants it only while `canonicalPath(path)` still equals that value, and reports `redirected` otherwise. Never make a grant follow a replaced directory or symlink. 2. **One write path, serialized.** Registry mutations run their whole read → validate → persist sequence inside the per-primary-root queue (`serialize`); the storage domain only serializes individual writes. Do not read a snapshot outside the queue. 3. **One revalidation entry point.** `registry.refresh()` re-stats, re-resolves, re-judges, republishes the scope, and writes nothing; the command's `list` and the panel's `list` both call it, and listing must remain enough to notice a directory that disappeared. `publish` stays idempotent. `refresh()` also retries the store-wide authority lease so a waiting process can take over after the previous holder exits. 4. **One response shape per panel endpoint.** `src/contract.ts` owns the endpoint → response mapping (`reveal` answers `{ revealed }` and every root-management endpoint answers a `RootsView`), and both halves validate the wire shapes at runtime. File browsing does not travel this channel: the Files-tab fork talks to the host `workspaceFiles` Remote namespace directly, and `src/workspace-files.ts` routes and bounds it — reads lexically under a registered-but-withheld root are refused `workspace-file/outside-workspace` (upstream `stat`/`read`/`readBytes`/`changes` carry no workspace confinement of their own). 5. **The client-graph anchor stays.** The web client-module scan reads this package's `dsh.client` only from a loader row mounted at the bare package name — subpath rows are never client rows. `cordis.patch.yml` must keep the `multi-root-client` row (`name` = the bare package name, backed by the barrel's no-op `apply`), or `lib/client.js` never reaches the browser and the `sidebar.footer.action` registration silently disappears. See `docs/troubleshooting/client-bundle-not-in-boot-graph.md`. 6. **One Registry storage owner per store; shared Hosts use ordered admission.** Only the process holding the store-wide kernel lease opens the JSON domain. With `sharedStorageRoot` (enabled by the bundle), other Hosts use kernel-authenticated IPC through their Registry facade; each new operation and every mutation are ordered at the owner, including its own calls. Scope snapshots and notifications never authorize a new operation alone. Without admission, or on uncertainty, preserve negative boundaries and allow only the established primary-root fallback (no shared temp). Synchronous confine/spawn keep their return shape and consume a short-lived admission; cached argv and expired async descendants cannot reuse it. The legacy mode without `sharedStorageRoot` retains contended fail-closed behavior. Teardown remains inside the authority-transition queue: drain mutations → close domain → release lease. See `docs/decisions/ADR-0025-shared-registry-authority.md`, `docs/decisions/ADR-0024-registry-operation-admission.md` and the historical ADR-0007. Platform evidence belongs in the existing progress ledger. 7. **The compatibility gate is a precondition, not a diagnostic — and the provider rows are the exception that keeps the profile alive.** `multi-root-providers`, `multi-root-registry`, `multi-root-instructions`, `multi-root-lsp`, `multi-root-workspace-files` and `multi-root-process-fence` all inject `multiRootCompat`, and cordis will not start a row whose injected service is missing — so an unsupported or mixed installation makes those gated rows simply not exist. Keep that injection on any new row that widens authority. `multi-root-fs` and `multi-root-sandbox` deliberately do NOT inject the gate: their widening is scope-driven and the gated registry can never populate the scope on a refused installation, so they degenerate to the upstream providers — and because upstream `fs-sandbox`/`sandbox` are already disabled by the patch, gating them would instead take `ctx.fs`/`ctx.sandbox`/`ctx.shell`/`permissionPresets` down and brick the profile (this was the `permissionPresets/catalog` failure: a false `mixed` verdict plus gated providers). A refused gate must degrade to "plugin not installed", never to a dead profile — and never to a widened one, so do NOT add gate-free widening: the scope stays empty without the registry. Version-dependent code belongs only in `src/compat/`, and it must probe *structure* (is the return value thenable? which renderer name is exported?) rather than compare version strings; packages the plugin never loads at run time (host-mounted services, type-only peers) must not testify from a plugin-private copy — see `HOST_SERVICE_PACKAGES` vs `RUNTIME_OPTIONAL_PACKAGES` vs `RUNNER_ANCHORED_PACKAGES` in `src/compat/dsh-version.ts` (the last is for copies executed by the standalone Windows ACL runner, inspected from the `dsh-sandbox-local` anchor). `DSH_MULTI_ROOT_COMPAT=warn` has exactly one legitimate caller, the upgrade lane. Promoting a release is a human act: a green upgrade run is evidence, never authorization. See `docs/decisions/ADR-0009-dsh-compat-contract.md`. 8. **Instructions are user context, never system authority, and revocation is explicit.** `multi-root-instructions` injects each additional root's **top-level** instruction file — and the instruction files of the subdirectories the session has worked in — from `agent/pre-step` as a `form: 'instructions'` user message (source kind `plugin` through session format 3, and this plugin's own `multi-root-workspace` from format 4, which rejects `kind: 'plugin'`) — not through `systemPrompt`, and not from a lifecycle event. Message construction loads the optional peer `@deepseek-ai/dsh-llm` **on demand** through `src/compat/llm-message.ts`, never with a static value import: the barrel is the module the carrier loader row mounts, so its load-time dependency set must equal the required-package set. Discovery is pinned to the root (`projectRoot` is the root, and every candidate must be canonically inside it), which is what keeps `$DSH_HOME`, the primary root (its own nested files included), and any ancestor from being injected a second time. The byte budget is shared across all additional roots, not per root. Touches come from the persisted `session/event` feed: a `tool/call` is paired with its `tool/result` by call id, and only a **successful** `read` / `write` / `edit` makes the directory chain from the root down to the touched file worth examining — a failed call never does. The directories a single touch makes worth examining are the touched file's parent directory and every ancestor of that parent up to the additional root, so an intermediate directory's `AGENTS.md` is visible too. Delivery state is per session and per scope (root + relative directory + file name) and lives in process memory, so after a resume this row may re-tell instructions it already told. When a root is removed, disappears, or is `redirected`, and when a delivered file vanishes, emit an explicit revocation or withdrawal: the earlier instructions are still in the conversation, so going silent does not retract them. With zero additional roots this row must contribute nothing at all. Nested discovery for the **primary** root stays upstream's job, on both supported releases. See `docs/decisions/ADR-0010-additional-root-instruction-scope.md`. 9. **Panel authority is host-derived from the session.** Every panel request requires `sessionId`; `resolvePanelPrimaryRoot` looks up `ctx.get('sessions')?.get(sessionId)` and returns `canonicalPath(session.header.cwd)`. There is no client `primaryRoot` field and no `sandboxPolicy` fallback. A missing or unknown session is rejected; a client with no current session shows "No active session" and does not call the host. The `/workspace-folders` command still uses `invocation.agent.session` (trusted host context). See `docs/decisions/ADR-0008-panel-session-derived-authority.md`. 10. **The writable set is the only authorization that reaches enforcement.** `resolve()`'s `writableRoots` = read-write members ∩ the effective tool cap (the `AsyncLocalStorage` flag set at `tools/execute` dispatch entry from `exec.name` — nesting only narrows, and outside any dispatch a non-empty `toolAccess` means capped). Only that set reaches the fs fence, every kernel dialect, and the Windows root-set SID. Membership projections (`additionalRoots`, `roots`) are unaffected: a read-only root stays readable and routable, it just never receives write authority. Never widen `writableRoots` past this intersection, never read tool identity from model arguments, and never treat read-only as read confinement. Under `danger-full-access` the deny side is symmetric: once restrictions apply (a non-empty deny set or an unavailable authority), `resolve()` also appends the configured `dshHome` to `deniedRoots` as `protectedHome` — `$DSH_HOME` holds the store, the lease and user profiles, and deny wins even when the primary root sits inside it. See `docs/decisions/ADR-0013-per-root-and-per-tool-access.md`. 11. **The provider brand gate prevents foreign-provider widening.** `ctx.fs` and `ctx.sandbox` must carry this plugin's `Symbol.for('@dsh-electron/dsh-plugin-multi-root-workspace/provider')` brand; `multi-root-providers` checks it at init and the registry, instructions, LSP and workspace-files rows inject it, so any foreign provider — regardless of composition order — leaves the multi-root rows absent and the scope empty. Never replace the brand check with instanceof or cordis-private state, and never warn-and-continue on a conflict. See `docs/decisions/ADR-0014-root-limit-and-provider-conflict.md`. Before touching the compatibility contract, the adapters, or anything that has to work on more than one upstream release, read `.agent/note/dsh-compat-contract.md` — it records the measured 0.1.5 → 0.1.6 differences and the quiet traps (a `confine` that is sync on one release and async on the next, two LLM wire protocols in the journey smoke, a local `git checkout` revert step that eats uncommitted manifest edits). Development environment, toolchain, commands, and the smoke mechanism are documented in `docs/development/plugin-development-workflow.md`. Requirements, design, upstream facts, and decisions live in `docs/requirements/`, `docs/architecture/`, `docs/reference/`, and `docs/decisions/`. Commit messages in this repository must NOT include "Generated with Devin" or "Co-Authored-By" trailers (user preference). Windows local verification: put a non-packaged pwsh (MSI or ZIP build — a Store/MSIX `pwsh.exe` is refused by the deny broker with a diagnostic) first on PATH. The broker install and the native ACL e2e need elevation; elevated runs create Administrators-owned directories, so `tests/working-directory.e2e.ts` and the journey's "pwsh saw the other repository's diff" fail with git "dubious ownership" — run those non-elevated. See `.agent/note/windows-deny-broker-packaged-child.md`. `ctx.multiRootScope.setAdditionalRoots()` remains the scope's only write port for roots and is what tests and smoke scripts use; in production the registry calls it. `ctx.multiRootScope.setRegistryAuthority()` is the registry's second, narrower port: it publishes only the `authority: 'available' | 'unavailable'` fact (invariant 6) and never touches the root table. Do not build a second data source. The browser half talks to the host over the Connection RPC channel `/multi-root-workspace` — not over a Typert Remote namespace (ADR-0005). The client UI's styling is the host's: `src/client/styles.ts` is the single stylesheet, classes are `mrfw-`-prefixed, and every color is a host `var(--dsw-*)` token — never a literal color (ADR-0006). Do not invent constraints that are not written down. B1–B3 extend the same scope to LSP, Windows ACL and additional-root file browsing. Before changing these consumers, read `docs/decisions/ADR-0011-multi-root-workspace-consumers.md` (including the hardening amendment); validation and release status belong only to the existing roadmap ledger. The process fence only ever confines AGENT terminals — `spawnTerminal` is prefixed solely when the spec env carries `DSH_PTY_SESSION_ID` (stamped by every supported terminal-bash backend); a spawn without it (the panel's controller) is the user's own terminal and passes through in every mode. Public method wrappers must preserve the caller context and restore property descriptors on disposal. Windows capabilities are per root set, never a primary-root SID granted to all additional roots; network and device paths (UNC, `\\?\`, `\\.\`, mapped drives) are refused on Windows as `unsupported-path`. `kernel:probe`, the three smokes, `pack:check`, and `smoke:packed` are mandatory gates on every OS — Windows runs them natively through pwsh. Dynamic working directories never change the Session origin, Registry key, or authorization set. Read `docs/decisions/ADR-0012-session-origin-and-effective-cwd.md` before changing cwd-sensitive consumers; the upstream interface review is `docs/reference/dsh-0.2.1-migration.md`. Platform and version execution evidence stays in the existing roadmap ledger. The second-phase P2 batch (**B4–B8**, branch `feat/b4-b8-p2`, released with `v0.1.7`) hardens the authorization model further: roots carry a per-root access level (`recordedPath` = read-write grant, `readOnlyRecordedPath` = read-only grant; domain version stays 1 so a downgrade can never escalate), `toolAccess` on the `multi-root-scope` row caps a tool's writes to additional roots through the `tools/execute` AsyncLocalStorage dispatch context, every registry mutation reclassifies from stored records before validating, stored records per primary root are capped (`maxAdditionalRoots`, default 16 / ceiling 64), a `multi-root-providers` brand gate fails closed when a foreign plugin owns `ctx.fs` or `ctx.sandbox`, and the model-facing topology is rendered from the session view per mode and platform. See `docs/plans/active/2026-10-11-b4-b8-p2.md`, `docs/decisions/ADR-0013-per-root-and-per-tool-access.md` and `docs/decisions/ADR-0014-root-limit-and-provider-conflict.md`.