Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/specs/dor-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ Source of truth: `runFilePicker` in `dor/src/commands/open-picker.ts`; `listOpen
- **Must resolve a Tool's document-link request through ordinary open dispatch and trust checks**, relative to the current canonical document's directory. The request chooses the current Surface or another Surface; it never requests a preview or a fresh instance. Existing matching Tools are focused through ordinary reuse without changing either Surface's history.
- **Must keep Back/Forward history on the navigating Surface**, covering every file/folder handler, including user Tools. Traversal reopens the recorded canonical target with its selected handler and lookup directory in that Surface even when another Surface shows the target; a new in-place visit after Back drops the forward branch. Pin a preview on a committed in-place file visit.
- **Must obtain Save / Discard / Cancel before replacing a dirty document**, using [Closing unsaved Tools](#closing-unsaved-tools). A same-document anchor or reuse of another Surface leaves the draft alone. Never cache drafts or undo state in history.
- **Must ask in the external-link dialog before a document link opens a page that runs its own script**, Back/Forward included (`docs/specs/security-local.md` -> "Terminal output"); a cancel is a navigation failure, and a same-document anchor never asks.
- **Must validate the destination before interrupting the current Tool**, and keep the current location/history on lookup failure or cancelled consent. Refuse overlapping navigation from one Surface and stale requests whose source run changed. When the interrupt or the new run fails, keep or restore the source's run, never typing over user input. Report navigation failures on the source, without opening the preview error viewer.
- **Must retain history through minimize, renderer changes, and live Workspace transfer, but never in durable saves**. Only the current document restarts after cold restore. Ending designation, Break, or closing discards history. Refuse Workspace transfer during navigation or pending reading-position restoration.
- **Must release pending reading-position restoration on timeout**, even when the destination never serves a page or never answers; renderer rebinding must not renew the deadline.
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/dor-tools-builtin.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Source of truth: `dor-tools-builtin/viewer/markdown.tsx`; `safeCreateDOM` in `do

### Relative links

- **Must follow a relative link on plain click**, and offer Open in another surface and Edit link on its context menu. Cmd/Ctrl-click chooses another Surface. Dispatch follows `docs/specs/dor-tool.md` → Document navigation. (rationale)
- **Must follow a relative link on plain click**, and offer Open in another surface and Edit link on its context menu. Cmd/Ctrl-click chooses another Surface. Dispatch, and any confirmation the host asks first, follows `docs/specs/dor-tool.md` → Document navigation. (rationale)
- **Must open a local PNG or JPEG image's file the same way on Edit or double-click**, with a link's context menu whose edit item opens it.
- **Must let an enclosing link own double-click and context-menu behavior.**
- **Must treat only a scheme-less relative reference as a document link**, resolved from the opened canonical file's directory with parent references allowed and its query ignored; HTTP(S)/mail links keep their existing handling.
Expand Down
8 changes: 6 additions & 2 deletions docs/specs/security-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
| Guarantee | Feature | Pinned by |
| --- | --- | --- |
| **Terminal output cannot write your clipboard or steal focus.** File access requires a user action or the gated OSC 367 `open` of a Tool run Dormouse launched. OSC 52 only offers a copy format; links require confirmation, naming any app scheme, or an allowed local preview; deceptive links and dangerous Windows handlers have no open action. | | `lib/src/lib/terminal-protocol.test.ts`, `lib/src/lib/mouse-selection.test.ts`, `lib/src/lib/copy-editor.test.ts`, `lib/src/lib/external-links.test.ts`, `lib/src/lib/terminal-link-activation.test.ts`, `lib/src/components/ExternalLinkModalHost.test.tsx`, `lib/src/lib/tool-launch-latch.test.ts`, `lib/src/components/wall/preview-slot.test.tsx` |
| **A link in a document you are viewing cannot open a page that runs its own scripts without asking.** Dormouse's own dialog shows the file's full path first; Markdown, text, and images still open on a click. | | `lib/src/components/wall/preview-slot.test.tsx`, `lib/src/components/wall/tool-navigation.test.ts`, `lib/src/components/ExternalLinkModalHost.test.tsx` |

The attacker is any program writing to a PTY.

Expand All @@ -19,6 +20,8 @@ Output reaches the screen, Session state, and bounded terminal reports — rende

**An `OSC 8` hyperlink opens only after a confirmation dialog**, except a local `file:` link whose display text names its target, which previews through the user's `open` rules (`docs/specs/dor-tool.md` -> "Terminal links"); any scheme but `http`, `https`, `mailto`, and `file` gets a dialog naming it as another app, and a target whose display text names a different host gets **no open action at all** (`docs/specs/mouse-and-clipboard.md` -> "OSC 8 hyperlinks"). File opens use `docs/specs/dor-tool.md` -> "Opening local files".

A Tool's document link names its own target, anywhere on disk under any link text (`docs/specs/dor-tool.md` -> "Document navigation").

Notification text: `docs/specs/alert.md` -> "Text And Security".

**Agent recovery is the one path from terminal output to typed input**: at shutdown each host interrupts every live PTY and scans its later output for an agent's resume hint, which the next cold restore types unconfirmed (`docs/compatible-agents.md` -> "Capture", "Cold restore"). It types a rebuilt invocation, never sliced output; what a terminal still chooses is `docs/specs/security.md` -> "What is not defended".
Expand All @@ -34,6 +37,7 @@ Notification text: `docs/specs/alert.md` -> "Text And Security".
- **FAIL IF** `S130` a host launches an external URL without revalidating it through `normalizeExternalUri` in `lib/src/lib/external-links.ts`, consent notwithstanding — VS Code's in the extension host (`vscode-ext/src/message-router.ts`), standalone's in `standalone/src/tauri-adapter.ts` and `openUrl` in `standalone/src/updater.ts` — or that function accepts a scheme in `PSEUDO_SCHEMES` or `REFUSED_APP_SCHEMES`, or the latter drops `ms-msdt`, `search-ms`, `ms-officecmd`, or `ms-appinstaller` (rationale). Pinned by `vscode-ext/test/message-router.test.ts` and `standalone/src/tauri-adapter.test.ts`.
- **FAIL IF** `S131` an unconfirmed `OSC 8` activation reaches the preview path when its display text is not a whole-component suffix of its decoded target, or the host opens a `file:` URL whose host is not empty, `localhost`, or this machine: `localFileLinkPreviewPath` in `lib/src/lib/external-links.ts`, `activateTerminalLink` in `lib/src/lib/terminal-link-activation.ts`, `resolveLocalToolTarget` in `lib/src/host/tool-input.ts`. Pinned by `lib/src/lib/external-links.test.ts`, `lib/src/lib/terminal-link-activation.test.ts`, and `lib/src/host/tool-input.test.ts`.
- **FAIL IF** `S132` an OSC 367 `open` reaches `surface.tool` from replay, from a Session that is not a Tool, from output outside the run the host launched in that PTY (`docs/specs/dor-tool.md` → OSC 367), or naming anything but an absolute path free of controls; if anything output reports, a shell-integration command line included, can begin that run; if a write but the host's own command launch can carry `launch`; or if a control-socket request can set its `oscOpen` flag, or its failure reaches the error viewer's argv with a control character. Read `parseToolOpen` in `dor-tools-lib/src/osc.ts`; `ToolLaunchLatch` in `lib/src/lib/tool-launch-latch.ts` and its one user, `createOwnerPtyStream` in `lib/src/host/owner-pty.ts`; the `launch` writes in `typeToolCommand` in `lib/src/components/wall/use-dor-control.ts`, `typeCommandWhenPromptReady` in `lib/src/lib/terminal-lifecycle.ts`, and `watchHelper` in `lib/src/lib/helper-terminal.ts` and each host's arm (`onPtyLaunch` in `lib/src/host/remote/sidecar-entry.ts`, `vscode-ext/src/message-router.ts`); `dispatchToolOpens` in `lib/src/lib/tool-open-requests.ts` (reached from `applyLiveToolEvents`, never from `parseReplay` in `lib/src/lib/platform/replay-parse.ts`); the `oscOpen` argument of `dispatchDorControlRequest` in `lib/src/lib/platform/dor-control-dispatch.ts`; and the `oscOpen` gate in `lib/src/components/wall/use-dor-control.ts`. Pinned by `lib/src/lib/tool-launch-latch.test.ts` and `lib/src/components/wall/preview-slot.test.tsx`.
- **FAIL IF** `S269` a document-link `open` — one carrying a disposition, Back/Forward through a Surface's history included — runs or focuses a target `builtin:file` would serve as a page running that file's own script, under any handler but `builtin:code` or `builtin:folder`, before the user confirms its full resolved path in host UI the Tool's page cannot reach; or a cancelled, replaced, or aborted confirmation lets it proceed; or `runsOwnScript` misses a format the viewer CSP lets run script. Read `documentLinkNeedsConfirmation` in `lib/src/components/wall/tool-navigation.ts`, `runsOwnScript` in `dor-tools-builtin/src/file-viewer-format.ts`, its call in the `surface.tool` handler of `lib/src/components/wall/use-dor-control.ts`, and `confirmDocumentLinkOpen` in `lib/src/lib/external-link-confirmation.ts`. Pinned by `lib/src/components/wall/preview-slot.test.tsx`, `lib/src/components/wall/tool-navigation.test.ts`, and `lib/src/components/ExternalLinkModalHost.test.tsx`.
- **FAIL IF** `S133` cold restore types a command `normalizeResumeCommand` in `lib/src/lib/resume-patterns.ts` did not rebuild at the typing boundary (`restoreTerminal` in `lib/src/lib/terminal-lifecycle.ts`), its grammar admits more than the invocations `docs/compatible-agents.md` -> "Detection" names — another argument, whitespace, or a shell metacharacter — or capture scans output from before its first interrupt (`captureAgentRecovery` in `lib/src/host/recovery-capture.ts`). Pinned by `lib/src/lib/coding-agents.test.ts`, `lib/src/lib/terminal-registry.alert.test.ts`, and `lib/src/host/recovery-capture.test.ts`.

## Browser panes
Expand Down Expand Up @@ -165,7 +169,7 @@ The attacker is another local account reading disk; what the remote stack leaves
No standalone log call (`docs/specs/standalone.md` -> "Logging") carries PTY bytes, but the log records the `dor` socket path (rationale).

- **FAIL IF** `S162` `write_file_atomically` in `standalone/src-tauri/src/lib.rs` stops restricting the directory and the file it writes to the owning user on **every** platform `restrict_to_owner` has an arm for — `0700`/`0600` on unix, and on Windows a DACL protected from inheritance carrying exactly one ACE for the current user — or the mode stops reaching the temp file *before* any bytes are written, or **any** of its callers stops going through it. Enumerate them from the file rather than from this line: every writer under the state root is one, the legacy-transcript scrub and `arrivals.json` included. The one exception is the macOS hang sample `/usr/bin/sample` writes into `hangs/`; it goes through `write_owner_only_with`, which restricts the directory before the child runs and the file after. Pinned by the Rust tests in the same file (rationale).
- **FAIL IF** `S163` a standalone log handle opens other than through `open_log` in `standalone/src-tauri/src/lib.rs`, or `open_log` stops leaving the log owner-only on **every** platform — unix: `0600`, an existing file tightened, a symlink refused; Windows: `restrict_to_owner`'s DACL — or the Linux default is not a per-user directory whenever `XDG_STATE_HOME` or `HOME` is absolute. Pinned by the Rust tests in the same file (rationale).
- **FAIL IF** `S163` a standalone log handle, read or write, opens other than through `open_log` in `standalone/src-tauri/src/lib.rs`, or `open_log` stops leaving the log owner-only on **every** platform — unix: `0600`, an existing file tightened, a symlink or anything but a regular file refused; Windows: `restrict_to_owner`'s DACL — or the Linux default is not a per-user directory whenever `XDG_STATE_HOME` or `HOME` is absolute. Pinned by the Rust tests in the same file (rationale).

Source of truth: `SESSION_STATE_KEY` in `vscode-ext/src/session-state.ts`, `ensureToken` in `vscode-ext/src/peer-link.ts`, `default_log_location` in `standalone/src-tauri/src/lib.rs`, `createManagedVoiceHost` in `lib/src/host/managed-voice-host.ts`.

Expand All @@ -190,7 +194,7 @@ The attacker is a file planted in a folder the user opens: Windows looks a bare
- **FAIL IF** `S165` a host, `dor`, or a built-in Tool spawns a program by bare name on Windows: a system program goes by its path under `%SystemRoot%` (`cmd.exe` by `ComSpec`), a user-installed one such as git by the file `docs/specs/dor-cli.md` -> "Spawning External Binaries" resolves on `PATH`, or not at all. Search `standalone/sidecar/`, `lib/src/host/`, `vscode-ext/src/`, `dor/src/`, `dor-lib-common/src/`, and `dor-tools-builtin/src/`. Pinned by `standalone/sidecar/pty-core.test.js`, `standalone/sidecar/clipboard-ops.test.js`, and `lib/src/host/git-upstream.test.ts`.
- **FAIL IF** `S166` the VS Code pty host forks in any cwd but the extension's own directory. Pinned by `vscode-ext/test/pty-manager.test.ts`.
- **FAIL IF** `S167` git run in a repository the user has not trusted can run a program that repository's config names: every git spawn in the directories above must lead with `UNTRUSTED_REPO_GIT_ARGS` and run only a subcommand its comment says it covers. Pinned by `lib/src/host/git-cli.test.ts`, `dor/test/open-picker.test.mjs`, and `dor-tools-builtin/test/folder-viewer.test.mjs`, each planting a `core.fsmonitor`.
- **FAIL IF** `S168` a host spawns a browser `binaryPath` the gate did not accept, or for Playwright loads `playwright-core` beside one, or `dor` runs such a pinned executable; the gate accepts, beyond the host's own override value by exact match, a UNC or device path, a relative path but the bare executable name, a `..` segment, a control character, or a file name other than the provider's executable; or the webview sends or stores one it refuses. The path crosses from the webview realm, so a same-user writer is no reason to rate a break below WARNING. Read `isAllowedBrowserBinary` in `dor-lib-common/src/browser-providers.ts`, `runWithBinaryFallback` in `lib/src/host/agent-browser-host.ts`, `findPlaywrightInstall` in `lib/src/host/playwright-install.ts`, the pin in `dor/src/commands/browser-cli.ts`, and the callers of `isAllowedBinaryFor` in `lib/src/lib/agent-browser-binary.ts`. Pinned by `lib/src/lib/agent-browser-binary.test.ts`, `lib/src/components/Wall.test.tsx`, and `dor/test/playwright.test.mjs`.
- **FAIL IF** `S168` a host spawns a browser `binaryPath` the gate did not accept, or for Playwright loads `playwright-core` beside one, or `dor` runs such a pinned executable; the gate accepts, beyond the host's own override value by exact match, a UNC or device path, a relative path but the bare executable name, a `..` segment, a C0 or C1 control character or DEL, or a file name other than the provider's executable; or the webview sends or stores one it refuses. The path crosses from the webview realm, so a same-user writer is no reason to rate a break below WARNING. Read `isAllowedBrowserBinary` in `dor-lib-common/src/browser-providers.ts`, `runWithBinaryFallback` in `lib/src/host/agent-browser-host.ts`, `findPlaywrightInstall` in `lib/src/host/playwright-install.ts`, the pin in `dor/src/commands/browser-cli.ts`, and the callers of `isAllowedBinaryFor` in `lib/src/lib/agent-browser-binary.ts`. Pinned by `lib/src/lib/agent-browser-binary.test.ts`, `lib/src/components/Wall.test.tsx`, and `dor/test/playwright.test.mjs`.
- **FAIL IF** `S169` the Playwright host loads `@playwright/cli` or `playwright-core` from outside the launcher's own installation: a directory above it, a shim script path that climbs out of the shim's directory, or a `playwright-core` neither nested in the CLI package nor beside it in its `node_modules`, though Node's lookup reaches further. Read `findPlaywrightInstall` in `lib/src/host/playwright-install.ts`. Pinned by `lib/src/host/playwright-install.test.ts`.

Source of truth: `windowsSystemPath` in `standalone/sidecar/windows-system.js`; `resolveBinaryPath` in `dor-lib-common/src/resolve-binary.ts`; `ensureChild` in `vscode-ext/src/pty-manager.ts`; `UNTRUSTED_REPO_GIT_ARGS` in `dor-lib-common/src/git.ts`.
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/security-local.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Why session snapshots earn the strongest protection on disk. They are `Persisted

Why the peer-link token is listed here. It is a `randomUUID()` in the VS Code extension's global storage, and its own comment says it is the only thing between another local process and this installation's terminals — a local credential at rest that the remote spec's credentials table does not cover, because nothing about it is remote. Both of its controls are unix-only: `peerDirIsSafe()` returns true immediately on `win32`, and Node's `mode: 0o600` there touches only the read-only attribute, so unlike `burrow_state_dir` no DACL work is done for it.

Why the standalone log is guarded at all. The socket path arrives via the sidecar's stderr, which Rust appends verbatim. Linux used to fall back to `env::temp_dir()`, which with `TMPDIR` unset is `/tmp` at `1777`, and still does without a usable `HOME`: another account could pre-plant `dormouse.log` as a symlink, which the truncating open followed, or as a world-writable file of its own, which then collected the log. Kernel `fs.protected_symlinks` / `fs.protected_regular` blunt both only where a distribution enables them (2026-10 audit, #1027). macOS's `$TMPDIR` is the per-user `/var/folders/.../T` at `0700` (measured 2026-09). The open is hardened as well as the directory moved because `$DORMOUSE_LOG_FILE` can point anywhere.
Why the standalone log is guarded at all. The socket path arrives via the sidecar's stderr, which Rust appends verbatim. Linux used to fall back to `env::temp_dir()`, which with `TMPDIR` unset is `/tmp` at `1777`, and still does without a usable `HOME`: another account could pre-plant `dormouse.log` as a symlink, which the truncating open followed, or as a world-writable file of its own, which then collected the log. Kernel `fs.protected_symlinks` / `fs.protected_regular` blunt both only where a distribution enables them (2026-10 audit, #1027). macOS's `$TMPDIR` is the per-user `/var/folders/.../T` at `0700` (measured 2026-09). The open is hardened as well as the directory moved because `$DORMOUSE_LOG_FILE` can point anywhere. The read takes the same open because the webview asks for the log's tail to attach to an update report: a symlink at the log's name would hand it another file's tail, and a FIFO would hang the read.

## Local-file viewer

Expand Down
Loading
Loading