Skip to content
Draft
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
150 changes: 69 additions & 81 deletions docs/specs/dor-browser.md

Large diffs are not rendered by default.

153 changes: 53 additions & 100 deletions docs/specs/dor-cli.md

Large diffs are not rendered by default.

22 changes: 12 additions & 10 deletions docs/specs/dor-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Source of truth: `surfaceKindFromParams` / `isToolParams` in `lib/src/components

**Must resolve a named Tool from the nearest ancestor `dormouse.yml`, then fall back to user Tools when that name is absent.** `--global` skips project discovery. Malformed project files fail lookup. If only the user file exists, an unknown name reports that file and its Tool names. The host owns discovery, bounded reads, YAML parsing, and substitutions; the renderer receives the resolved result. Canonical field shapes are `ToolEntry` in `lib/src/host/tool-registry.ts`.

**Must read user Tools from `$XDG_CONFIG_HOME/dormouse/dormouse.yml` when that environment value is absolute, otherwise `~/.config/dormouse/dormouse.yml`.** Both local hosts use this location. User Tools require no project grant; malformed or unreadable user configuration fails lookup. Project and user Tools occupy separate reuse scopes.
**Must read user Tools from `$XDG_CONFIG_HOME/dormouse/dormouse.yml` only when that environment value is absolute**, else `~/.config/dormouse/dormouse.yml`; both local hosts use this location. User Tools require no project grant; malformed or unreadable user configuration fails lookup. Project and user Tools occupy separate reuse scopes.

| Field | Behavior |
| --- | --- |
Expand All @@ -44,6 +44,7 @@ Source of truth: `surfaceKindFromParams` / `isToolParams` in `lib/src/components

- **Must reject unknown `prespawn_*` fields and unknown substitutions**; unknown ordinary fields produce warnings. `$PROJECT_ROOT` is the declaring directory, `$CWD` the caller's resolved directory, and `$TARGET` the canonical local file input. (rationale)
- **Must preserve scalar `prespawn_dedupe` as a one-element literal list**, never interpret it as a command to execute. Reserve separate fields for future computed keys. (rationale)
- **Must reject an empty `prespawn_dedupe` list, and `$TARGET` in a `prespawn_dedupe` whose `run` is a shell-command string** — a string `run` takes no inputs, so there is no target to key on.
- **Must warn when a repo-local key omits `$PROJECT_ROOT`, or a `$TARGET` run has a key without `$TARGET`.** Allow intentional cross-checkout or cross-file dedupe.
- **Must reject `$PROJECT_ROOT` in user configuration**, which has no project root.

Expand Down Expand Up @@ -79,7 +80,7 @@ Source of truth: `queueToolSpawn` / the `surface.tool` handler in `lib/src/compo
4. **Must require `trust-recorded` before re-resolving the named entry**, then stage the command, renderer, port strategy, and key before exposing its terminal. Rejected grants or failed re-resolution retain approval and display an error until retry; blank reasons use a fallback. Closed Surfaces must not start later or show stale errors.
5. **Must recheck the resolved key before launching an approved Tool**, honoring its original `--fresh` intent. Close a redundant approval through the ordinary close coordinator before revealing or restarting the match; a failed closure retains the approval and sends no command.
6. **Must close a declined approval through the ordinary close coordinator and record no denial.** Archive failure may retain the pane. (rationale)
7. **Must record each grant as its own atomically written file**, so hosts sharing one state directory never lock or merge.
7. **Must record each grant as its own atomically written file** under the host's state directory, so hosts sharing one never lock or merge. **A host that supplies no state directory keeps grants in memory for that process only**, rather than inventing a location the user cannot find to revoke.
8. **Never content-hash grants or re-prompt solely because the config changed.** (rationale)

**Must offer Retry and Close after post-grant lookup failure**, preserving the error with no PTY. Retry repeats only lookup; Close retains permission; the footer states both. **Never restore pending approval once launch clears its marker**, including after PTY/minimization failure.
Expand Down Expand Up @@ -149,19 +150,19 @@ Source of truth: `toolCommand` in `dor/src/commands/tool.ts`; `dor/test/snapshot

**Must accept exactly one existing local regular file for `dor open`**, resolved by the `$TARGET` rules in [Declaring tools](#declaring-tools).

**Must select the first matching entry of the user file's ordered `open` list**, whose entries contain `match` and `tool`. `--tool` explicitly selects a handler. Every association must name an argument-list Tool in that same user file or `builtin:file`. Never discover project configuration during this lookup; project `open` rules are ignored with a warning during explicit project-tool lookup.
**Must select the first matching entry of the user file's ordered `open` list.** Every association must name an argument-list Tool in that same user file or `builtin:file`. **Never discover project configuration during this lookup**; project `open` rules are ignored with a warning during explicit project-tool lookup.

**Must match patterns without `/` against the canonical filename, and patterns with `/` against both paths relative to the canonical CWD and canonical absolute paths**, separators normalized to `/`, with bundled picomatch: case-sensitive, dotfiles only by explicit pattern. Use the supplied CWD if canonicalization fails; matching never changes the Tool's run directory or `$CWD`. A miss names the user config path and suggests `--tool`. (rationale)
**Must use the supplied CWD when canonicalization fails**, and never let matching change the Tool's run directory or `$CWD`. A miss names the user config path and suggests `--tool`. (rationale)

**Must pass the canonical file path as the selected Tool's one input.** Reuse follows [Identity and dedupe](#identity-and-dedupe), `$TARGET` in the key providing per-file identity; placement follows [Take-over](#take-over).

**Must reject declared Tool names beginning with `builtin:` in either configuration scope.** Built-in handler names cannot be shadowed.

**Must use `builtin:file` for supported files when no user rule matches.** An explicit unknown handler or malformed user configuration fails without fallback. Selecting `builtin:file` for an unsupported format reports that limitation and suggests a user Tool. Built-in identity is the canonical file path in its own scope, separate from user and project Tools.
**Must fail without fallback on an explicit unknown handler or malformed user configuration**; `builtin:file` named for an unsupported format reports that limitation and suggests a user Tool. Built-in identity is the canonical file path in its own scope, separate from user and project Tools.

**Must prefer known extensions over filename-based text fallbacks; source extensions remain escaped previews.**

**Must run the built-in viewer as a Tool-owned `dor` process**, serving HTML, images, media, and escaped text/source previews. Markdown is source text; custom viewers may render it. Text previews and HTML/CSS dependency inspection are limited to 8 MiB per file. Text/source previews grant only their opened file and skip dependency inspection. (rationale) Oversized HTML and referenced CSS still stream without dependency inspection. The grant contains at most 256 files: the opened document and statically referenced relative HTML/CSS assets within its directory tree; exceeding that bound fails the open without serving a partial grant. Never expand the grant through root-relative, external, or dynamic references; requests can read only granted paths.
**Must run the built-in viewer as a Tool-owned `dor` process.** Text/source previews grant only their opened file and skip dependency inspection. (rationale) Oversized HTML and referenced CSS still stream without dependency inspection. **The grant contains at most 256 files** — the opened document and statically referenced relative HTML/CSS assets within its directory tree — and exceeding that bound fails the open without serving a partial grant. **Never expand the grant through root-relative, external, or dynamic references**; requests can read only granted paths.

**Must require a user Tool for PDFs**, including files named `README.pdf`. (rationale)

Expand Down Expand Up @@ -202,6 +203,7 @@ Source of truth: `toolTakesOverCaller` / `toolRerunsInCaller` / `callerStillPlac
**Must consume OSC 367 at the PTY owner's parser**, including malformed and unknown verbs, and emit no reply. `serve` and `state` are implemented verbs. The escape registry is `docs/specs/terminal-escapes.md`.

- **Must sanitize and bound the payload before retaining it.** `ToolAnnounce` / `parseToolAnnounce` and `ToolState` / `parseToolState` own the field shapes and validation limits.
- **Must reject a payload naming a version this contract does not speak.** `state` requires `v: 1`; `serve` reads an omitted `v` as 1 and refuses any other value — a future v2's rejection path.
- **Must treat an optional serve `path` as a path/query on the discovered port, never as another authority.** Accept at most 2,048 characters starting with one `/`, with no backslash, ASCII whitespace/control, or DEL; invalid paths are ignored and the default is `/`. The port still must belong to the designated Session's process tree. Live binding memory includes the path; durable saves omit it.
- **Must forward parsed announcements, state reports, and command-start resets in stream order to the owning renderer.** A start clears the previous command's announcement and unsaved state; later reports in that chunk survive. Standalone uses `terminal:protocolEvents`; VS Code uses nullable `terminal:toolAnnounce` and `terminal:toolState` scoped to the owning webview, with null clearing the corresponding record. The fake adapter applies locally.
- **Must reconstruct announcements, state, and resets from raw replay without emitting replies**, preserving transferred announcements when since-mark replay has no command start, and clear the renderer record on Session disposal. Ordinary terminal announcements stay inert.
Expand Down Expand Up @@ -261,16 +263,16 @@ Source of truth: `PersistedToolMetadata` in `lib/src/lib/session-types.ts`; `sav
rehydrate-from-args + `persist: "never"`: every stateless tool, no new API,
no Windows question.
- **D2 — dehydrate/rehydrate.** The `367;dehydrate` verb +
`DORMOUSE_DEHYDRATE`; the `dehydrate` flag is reserved in the serve payload
from the shipped `serve` payload. The Windows graceful-stop is needed here
only.
`DORMOUSE_DEHYDRATE`, opted into by the `dehydrate` flag the shipped `serve`
payload already reserves. The Windows graceful-stop is needed here only.
- **The announced `name`.** Wire the reserved [OSC 367](#osc-367) `name` into
the title-candidates channel and `dor list`'s location column.
- **Later** — `prespawn_*` beyond the dedupe literal: a computed key, and
`prespawn_port`. Pocket/remote browser view (rides the browser-surface
staging in `docs/specs/remote-api.md`; reserve the kind on the wire now). An in-pane terminal/browser strip (decide against the
glossary's reserved multiple-Surfaces-per-Pane). A `boots: web` hint if the
terminal flash grates. `--has terminal` / `--has browser` for `dor list`.
terminal flash grates. `--has terminal` / `--has browser` *filter flags* for
`dor list`, whose rows already carry the fields.

### Dehydrate and rehydrate

Expand Down
53 changes: 11 additions & 42 deletions docs/specs/dor-tool.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,42 +42,16 @@ A hardcoded Storybook port can disagree with the port it obtains under contentio

### September 2026 innerdogfood QC record

The `dor-tool-qc` run began at `4c7f9012` and used the real standalone sidecar,
staged CLI, PTYs, and iframe proxy. At that historical baseline the Tools flag
could reject creation and standalone `dor open` split; both behaviors were
subsequently superseded by always-enabled Tools and eligible inline opening.

Observed passes covered project approval (pending dedupe, decline/re-prompt,
folder-only permission, failed spawn without a PTY, repair and Retry); literal
argv and canonical symlink targets; three concurrent keyed invocations sharing
one Tool, fresh instances, idle/fast-command restart with stable refs; automatic
single-port serving, three-port refusal, and announced port/path selection.
User-rule ordering and explicit overrides worked; malformed user configuration
failed, and project associations did not intercept file opens. Text/Markdown
source, HTML/CSS/image, SVG, audio, and awkward filenames rendered; URL,
directory, missing/unsupported-file and oversized-text cases failed usefully.

Approval controls remained usable at 249×203 pixels. Terminal Context,
minimize/reveal, exit/refocus, and iframe/screencast round trips passed. A clean
harness reload preserved every ID, kind, URL, and Workspace; cross-Workspace
identity stayed scoped. The four final viewer processes and earlier fixture
listeners exited when their Tools closed; both owned harnesses stopped and
private credential captures were deleted.

Full `pnpm test` and `pnpm build` passed during the run, plus 139 focused UI
checks. The PDF-policy follow-up passed 168 CLI tests, 20 host dispatch/proxy
tests, and spec/public-doc lints; staged hosts contained no PDF renderer assets.
These counts describe that run, not the current test inventory.

One development-state reset made Tools appear as terminals while root tests
and builds ran beside the harness. Investigation confirmed that
`e2e-lint-selftest` temporarily mutates Vite inputs, including invalid root
package JSON; the exact metadata-loss trigger was not captured. A clean
restart and stable-build reload passed. This run did not exercise Tool transfer,
native-window movement, native Tauri/VS Code rendering, Windows shells, or cold
restore. Screenshots and raw JSON were local ignored artifacts, not portable
verification evidence. The reusable recipe is `docs/testing/dor-tool-qc.md`.

The `dor-tool-qc` run (from `4c7f9012`, on the real standalone sidecar, staged
CLI, PTYs, and iframe proxy) exercised project approval, keyed reuse, serving,
open-rule dispatch, and the built-in viewer's formats and failures. Two findings
constrain later edits: at that baseline the Tools feature flag could reject
creation and standalone `dor open` split, both since superseded by
always-enabled Tools and eligible inline opening; and Chromium's native PDF
plugin failed inside the normal iframe sandbox (see "Opening local files"). The
run did not cover Tool transfer, native-window movement, native Tauri/VS Code
rendering, Windows shells, or cold restore. The reusable recipe is
`docs/testing/dor-tool-qc.md`.

The September 2026 integration reuses Terminal Context for the Tool's primary terminal. The auxiliary helper's automatic refresh, Reset, and Promote semantics do not describe a serving command, whose Session also owns the browser and remote terminal identity. Sharing the presentation avoids introducing a second navigation mechanism or a second shell.

Expand All @@ -89,8 +63,7 @@ A CSS source preview escapes its contents, so its URLs cannot load assets. Scann

Keeping the built-in viewer in the Tool's process tree reuses port discovery, kill, restart, and Workspace transfer. An OSC path carries the per-run URL capability without saving that secret in the restart command. Holding the selected file descriptors bounds what the server can read after launch; it trades automatic replacement-file refresh for a grant whose contents cannot widen through path replacement.


The VS Code host supports Node 18, which lacks native glob matching. Bundled picomatch keeps association behavior the same across hosts. Patterns with separators test both the CWD-relative and canonical absolute path: files above the CWD otherwise start with `../` and can miss patterns intended to cover an absolute directory. Canonicalization also gives symlink aliases one matching identity.
The VS Code host supports Node 18, which lacks native glob matching. Bundled picomatch keeps association behavior the same across hosts. Patterns with separators test both the CWD-relative and canonical absolute path: files above the CWD otherwise start with `../` and can miss patterns intended to cover an absolute directory. Canonicalization also gives symlink aliases one matching identity. Canonicalizing only the target mixed physical and logical paths under a symlinked CWD, so relative slash patterns missed files inside that directory. An absolute target can still be opened after its caller's CWD disappears; matching falls back to the supplied directory in that case.

## Take-over

Expand All @@ -115,7 +88,3 @@ A derived URL or browser daemon binding belongs to one execution. Reusing it aft
Routing `dor tool` to a native editor on one host would change its result from a Surface handle to a host-specific side effect. Native file opening remains a separate operation.

A Workspace transfer carries the live browser binding separately from its durable record. The arrival record can reach disk while the windows coordinate, whereas the content channel stays in memory; reusing the saved-record projection alone would reopen a Tool browser and lose its current page state. Pending approvals and unfinished browser startup still own asynchronous work in the source window, so the move waits for the user to resolve the approval or retry after startup.

## Opening local files

The VS Code host supports Node 18, which lacks native glob matching. Bundled picomatch keeps association behavior the same across hosts. Patterns with separators test both the CWD-relative and canonical absolute path: files above the CWD otherwise start with `../` and can miss patterns intended to cover an absolute directory. Canonicalization also gives symlink aliases one matching identity. Canonicalizing only the target mixed physical and logical paths under a symlinked CWD, so relative slash patterns missed files inside that directory. An absolute target can still be opened after its caller's CWD disappears; matching falls back to the supplied directory in that case.
2 changes: 1 addition & 1 deletion docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ not a create: a Workspace that arrives mounts from the record it brought.
plain iframe or serving iframe Tool's document cannot leave its webview, so it reopens at its
saved URL, and a Workspace holding one — Doored ones included — asks with the
Close's typed confirmation before it leaves; agent-browser Surfaces reconnect and ask
nothing (`iframeSurfaceIds` on the Wall handle; `standalone/src/workspace-drag.test.ts`).
nothing (`iframeSurfaceRefs` on the Wall handle; `standalone/src/workspace-drag.test.ts`).

**Must show drag refusals over Window content in a dialog** until dismissal, retry, or Workspace departure. Source of truth: `onDropOnOtherWindow` in `standalone/src/workspace-drag.ts`; `lib/src/components/WorkspaceStrip.test.tsx`.

Expand Down
6 changes: 3 additions & 3 deletions docs/specs/standalone.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,9 @@ Source of truth: `route` in `standalone/src-tauri/src/routing.rs`,
broadcasting rang every sibling's AlertManager for a pane none of them shows.
`Destroyed` SIGTERMs whatever the departing window still owned, which is what
the close ack-timeout path never killed.
- **A `dor` request naming a Surface no window owns is answered with an error**,
never handed to a sibling — acting on the wrong terminal is worse than failing
(`docs/specs/dor-cli.md` → Standalone).
- **A `dor` request naming a Surface no window owns is answered with an error**
— `No Dormouse window owns surface '<id>'` — never handed to a sibling, since
acting on the wrong terminal is worse than failing.
- **`pty_request_init`, `pty_graceful_kill` and `capture_agent_recovery` target
the invoking window's own PTYs**, and take no ids at all: a window tearing down
must not interrupt or kill a sibling's terminals, and a set it could name is a
Expand Down
Loading
Loading