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
37 changes: 9 additions & 28 deletions docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,7 @@ tab/eval/screenshot commands, and anything added later. It owns the Windows
recipe: cross-spawn rather than Node's own `spawn`, `windowsHide`, and
resolution on `exit` with an exit-time output snapshot (rationale).

**Never forward an argument containing a literal `%VAR%`** — `cmd.exe` expands
it through a `.cmd` shim, an unavoidable batch limitation; today's forwarded
arguments carry none.
**Must leave Windows shim quoting to `spawnAndCapture`.** Forwarded argv can contain literal percent expressions; never interpolate them into an independently constructed shell command. (rationale)

- **`dor agent-browser`, `dor playwright` and both browser hosts spawn the `PATH`-resolved
absolute path, never the bare name** — cross-spawn resolves a bare name
Expand Down Expand Up @@ -153,9 +151,7 @@ SIGKILLs the child; **Windows must end the whole tree** with
child is `cmd.exe` and the real CLI is its descendant (`treeKillCommand`, pinned
through its `isWindows` argument).

**Resolution.** `dor-lib-common`'s `exports` point at its built `dist`
(Node-type-free `.d.ts`, since `dor`'s `tsc` avoids `@types/node`); every
esbuild/Vite consumer inlines it. **The `dor` and `dormouse-lib` prebuilds must
`dor-lib-common`'s `exports` point at built `dist`; esbuild/Vite consumers inline it. **The `dor` and `dormouse-lib` prebuilds must
build `dor-lib-common` first**, or those `.d.ts` files are missing when either
typechecks.

Expand Down Expand Up @@ -219,6 +215,7 @@ spec carries:

- **The POSIX socket name is 8 random bytes rather than 16**, so the path clears
macOS's `sun_path` cap (rationale).
- **Must reject malformed control requests before reading their fields or forwarding them.** `standalone/sidecar/dor-control-server.test.js` pins continued service after a refused request.
- **A connection that says nothing at all is dropped after 10s.**
- The two proof domains are mirrored between client and server, pinned by
`lib/src/lib/mirrored-constants.test.ts`.
Expand Down Expand Up @@ -267,9 +264,7 @@ and each host's hop in `standalone/src/tauri-adapter.ts`,

## Handle Model

`Window ⊃ Workspace ⊃ Pane ⊃ Surface` (`docs/specs/glossary.md`). **Must treat a command's primary target as a Surface; `dor move` also takes a destination Workspace, while `--workspace <ref>` scopes the source** ([dor workspace](#dor-workspace)). `Reserved:` no command targets
another Window; a request reaches the window that owns its Surface instead
(§Standalone), and the ref grammar for one is [Future](#future).
`Window ⊃ Workspace ⊃ Pane ⊃ Surface` (`docs/specs/glossary.md`). **Must use Surface handles for Surface verbs and container refs for container verbs.** `dor move` takes a destination Workspace, while `--workspace <ref>` scopes the source ([dor workspace](#dor-workspace)). Cross-window request routing follows [Standalone](#standalone).

Invariants:

Expand Down Expand Up @@ -391,7 +386,7 @@ It picks the style (`cmd` / `posix` / `powershell`) with the same classifier
clipboard/drop path escaping uses
([mouse-and-clipboard.md](mouse-and-clipboard.md) §8.6).

**Every first-party command except the `dor agent-browser` and `dor playwright`
**Every public first-party command except the `dor agent-browser` and `dor playwright`
passthrough accepts `--json`**, emitting a stable object with the same handles
as its text output; single-Surface responses always carry both `surface_id`
(stable) and `surface_ref` (Workspace-stable short ref). Text output carries the
Expand Down Expand Up @@ -545,13 +540,7 @@ nonstandard port is the one case needing the scheme typed. This overrides
server on https just SSL-errors. **Reject** an input that is neither a URL nor a
`host:port`, including a purely numeric "host" like `800:600` (rationale).

**Resolution is CLI-side**, so `dor agent-browser` hands `agent-browser` a real URL rather
than a handle a binary would resolve differently. Only the `open` / `goto` /
`navigate` verbs resolve, matching the target by **shape, not position** since
`dor` can't know agent-browser's flag arity (`open --headed surface:3`
resolves); **only the first special-shaped argument is rewritten**, these verbs
taking a single target. A Surface handle requires a live control endpoint
(failing clearly outside Dormouse); the `host:port` inference does not.
**Must resolve navigation targets CLI-side before forwarding to the browser provider.** Only the first target of a navigation verb is eligible. Skip known option values; an unknown option leaves the argv unchanged rather than guessing its arity. A Surface handle requires a live control endpoint; `host:port` inference does not. The provider descriptors and `resolveOpenTargetArgs` own recognized verbs and option arities.

A Surface handle resolves through the `surface.resolveOpen` control method,
which runs the same host port scan as `dor list --ports` (visible panes **and**
Expand Down Expand Up @@ -588,13 +577,7 @@ Source of truth: `runBrowserCli` in `dor/src/commands/browser-cli.ts`; `BrowserV
before stricli parses provider arguments.** One runner drives both; what differs
is each provider's descriptor:

| | `dor agent-browser` | `dor playwright` |
| --- | --- | --- |
| Session flag forwarded | `--session <s>` | `--session=<s>`; native `-s` read as `--session` |
| Targets resolved in | `open`, `goto`, `navigate` | `open`, `goto` |
| Never binds a Surface | `close` | `close`, `detach`, `close-all`, `kill-all`, `delete-data`, `list`, `show`, `install`, `install-browser` |
| Informational flags | `--help`, `-h` | `--help`, `-h`, `--version`, `-v` |
| Runs in / with | the caller's cwd and executable | the binding's project cwd and pinned executable |
`BrowserCliDescriptor`, the provider descriptors, and `BROWSER_PROVIDERS` own session argv, navigation verbs, nonbinding/informational controls, and execution scope.

- **Exactly one identity flag**: `--key` (default `default`), `--session`, or
`--surface`, plus `--workspace`; any two fail (`--key and --surface are
Expand Down Expand Up @@ -642,7 +625,7 @@ stderr warning, when the bound one is gone**, and **must fail naming a bound cwd
that no longer exists** rather than report the CLI missing.

Source of truth: `runBrowserCli`, `resolveBinding` and `extractSessionFlags` in
`dor/src/commands/browser-cli.ts`; `runAgentBrowserCli` in
`dor/src/commands/browser-cli.ts`; `BROWSER_PROVIDERS` in `dor-lib-common/src/browser-providers.ts`; `runAgentBrowserCli` in
`dor/src/commands/agent-browser.ts`; `runPlaywrightCli` in
`dor/src/commands/playwright.ts`; `SURFACE_CONTROL_METHODS` in
`dor/src/protocol.ts`; `ResolveBrowserRequest`, `BrowserSurfaceRequest` and
Expand All @@ -662,9 +645,7 @@ stays unsupported.
named by its Workspace-stable `surface:N` ref, or rediscovered after layout
churn by `--command` / `--cwd` / `--port`, and `dor ensure`'s command+cwd match
is an implicit key that also lets an agent adopt a command the user started by
hand. Only browser Surfaces carry an explicit join key (`dor agent-browser --key <name>`,
`dor playwright --key <name>`), because their session is held externally by the browser
CLI.
hand. Browser join keys follow `docs/specs/dor-browser.md` → Managed identity; Tool keys follow `docs/specs/dor-tool.md` → Identity and dedupe.

The worked examples are `dor/skill.md`'s "## Recipes", which ships with the
CLI ([Agent Skill](#agent-skill)).
Expand Down
2 changes: 2 additions & 0 deletions docs/specs/dor-cli.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ redirects Unix stderr to a log or `/dev/null`, and daemon diagnostics discard
write errors. Closing capture's read ends does not signal or kill descendants;
a descendant that continues writing must tolerate a closed output sink.

A Windows reproduction in 2026-10 (Node 22.22.3, cross-spawn 7.0.6) passed a literal percent-delimited environment expression unchanged through simple and npm-shaped global/local batch shims. The earlier claim that forwarded arguments contain no such expression did not describe browser passthrough; bypassing the shared shim escaping would reintroduce expansion.

## Control-channel security

**Who the threat is.** Not the network — the channel is a local socket or named pipe. The attacker is a second account on the same box, or any process running as the user; interposing inherits the whole verb set at once — keystrokes in, screen and scrollback out, pane destroyed.
Expand Down
17 changes: 6 additions & 11 deletions docs/specs/dor-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,7 @@ Source of truth: `surfaceKindFromParams` / `isToolParams` in `lib/src/components

**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 |
| --- | --- |
| `run` | Required shell command string or argument list, typed into the configured shell after integration readiness |
| `render` | `iframe` by default, `agent-browser-screencast`, or `playwright-screencast` |
| `viewport` | Initial browser sizing; `docs/specs/dor-browser.md` → Viewport presets owns resolution and defaults |
| `port` | `announced` by default, or `auto`; [Serving](#serving) owns selection |
| `prespawn_dedupe` | Optional scalar or list of literal key elements with substitutions |
`ToolEntry` and `parseToolFile` own declaration fields and defaults. [Serving](#serving) owns port selection; `docs/specs/dor-browser.md` → Viewport presets owns sizing resolution.

- **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 or directory input. (rationale)
- **Must deliver the parsed file's warnings on the untrusted answer and on a built-in open**, not only on an already-trusted lookup — those are the paths a Tool's first run takes.
Expand All @@ -55,7 +49,7 @@ Source of truth: `surfaceKindFromParams` / `isToolParams` in `lib/src/components

**Must require exactly one existing local regular file or directory when `$TARGET` appears in the run list or dedupe key.** Resolve relative paths against the invocation CWD and follow symlinks to a canonical absolute path before substitution and reuse. **Must accept a `file:` URL only when its host is empty, `localhost`, or this machine's name** (case-insensitive, either side in its short form before the first dot), converting it with the host platform's `fileURLToPath`; reject other URLs, missing paths, and every other file kind (fifo, socket, device). Validate run and key inputs before showing approval. Pending approval distinguishes the original arguments and invocation CWD; [Trust](#trust) owns re-resolution and recovery. Input control-character restrictions belong to `docs/specs/security-local.md` → Dor Tool configuration.

Source of truth: `lookupTool` in `lib/src/host/tool-trust.ts`; `parseToolFile` / `resolveDedupeKey` in `lib/src/host/tool-registry.ts`; `resolveToolInput` / `resolveLocalToolTarget` in `lib/src/host/tool-input.ts`; `readUserToolFile` in `lib/src/host/tool-user-config.ts`; `toolRunCommand` in `lib/src/components/wall/use-dor-control.ts`; `lib/src/host/tool-host.test.ts`, `lib/src/host/tool-trust.test.ts`, `lib/src/host/tool-input.test.ts`, `lib/src/host/tool-open.test.ts`, `lib/src/components/Wall.test.tsx`.
Source of truth: `lookupTool` in `lib/src/host/tool-trust.ts`; `ToolEntry` / `parseToolFile` / `resolveDedupeKey` in `lib/src/host/tool-registry.ts`; `resolveToolInput` / `resolveLocalToolTarget` in `lib/src/host/tool-input.ts`; `readUserToolFile` in `lib/src/host/tool-user-config.ts`; `toolRunCommand` in `lib/src/components/wall/use-dor-control.ts`; `lib/src/host/tool-host.test.ts`, `lib/src/host/tool-trust.test.ts`, `lib/src/host/tool-input.test.ts`, `lib/src/host/tool-open.test.ts`, `lib/src/components/Wall.test.tsx`.

**Must resolve a Tool's initial viewport host-side with its declaration, including after approval.** Iframe Tools accept only `pane-sync`; automated Tools accept a preset or inline dimensions. **Must preserve live user/agent sizing when reusing a Tool**, rather than reapplying its declaration.

Expand Down Expand Up @@ -279,16 +273,17 @@ 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`, `state`, and `open` 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 invalid encoder inputs and serialized payloads exceeding the host's limit**, including JSON escaping that expands an otherwise valid field. (rationale)
- **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 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, C0/C1 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, open requests, 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. Both hosts forward each parse's as one `terminal:toolEvents`, which the owning renderer applies with `applyLiveToolEvents`; the fake adapter applies locally.
- **Must reconstruct announcements, state, and resets from raw replay without emitting replies or acting on an `open`**, preserving transferred announcements when since-mark replay has no command start, and clear the renderer record on Session disposal. Ordinary terminal announcements stay inert.
- Reserved: **Must retain `name`, `dehydrate`, and `persist` as inert parsed fields**, serving the announced-name and D1/D2 items under [Future](#future). Neither `persist: never` nor a `dehydrate` verb changes current persistence.
- **Must act on `open` only from live output of a Tool Session whose designated command is running**, as `dor open` from that Session (`--preview` when `preview` is true), resolved from the Session's local CWD, else the target's directory. The path is absolute, with no control character; the Tool gets no answer.
- **Must show a failed `open` in the preview slot**: a preview whose lookup fails runs the built-in error viewer there instead (`docs/specs/dor-tools-builtin.md` → Error viewer), and a failed activate is sent again as a preview unless a newer `open` from that Session followed it.
- Reserved: **Never assign an OSC 367 verb beyond `serve`, `state`, `open`, and `dehydrate`**; `dehydrate` belongs to D2 under [Future](#future), while existing title/progress protocols keep those roles.

Source of truth: `TerminalProtocolParser` / `collectTerminalToolEvents` in `lib/src/lib/terminal-protocol.ts`; `parseToolAnnounce` / `parseToolOpen` in `dor-tools-lib/src/osc.ts`; `applyLiveToolEvents` in `lib/src/lib/tool-events.ts`; `dispatchToolOpens` in `lib/src/lib/tool-open-requests.ts`; `surface.tool` in `lib/src/components/wall/use-dor-control.ts`; `recordToolAnnounce` in `lib/src/lib/tool-announce-store.ts`; `recordToolEvents` in `lib/src/lib/tool-events.ts`; `createOwnerPtyStream` in `lib/src/host/owner-pty.ts`. Tests: `dor-tools-lib/test/osc.test.mjs`, `lib/src/lib/tool-announce.test.ts`, `an OSC 367 open` in `lib/src/components/wall/preview-slot.test.tsx`, `lib/src/host/remote/sidecar-entry.test.ts`, `vscode-ext/test/message-router.test.ts`, `standalone/scripts/dev-agent-browser-announce.test.mjs`.
Source of truth: `TerminalProtocolParser` / `collectTerminalToolEvents` in `lib/src/lib/terminal-protocol.ts`; `serveSequence` / `stateSequence` / `openSequence` / `parseToolAnnounce` / `parseToolOpen` in `dor-tools-lib/src/osc.ts`; `applyLiveToolEvents` in `lib/src/lib/tool-events.ts`; `dispatchToolOpens` in `lib/src/lib/tool-open-requests.ts`; `surface.tool` in `lib/src/components/wall/use-dor-control.ts`; `recordToolAnnounce` in `lib/src/lib/tool-announce-store.ts`; `recordToolEvents` in `lib/src/lib/tool-events.ts`; `createOwnerPtyStream` in `lib/src/host/owner-pty.ts`. Tests: `dor-tools-lib/test/osc.test.mjs`, `lib/src/lib/tool-announce.test.ts`, `an OSC 367 open` in `lib/src/components/wall/preview-slot.test.tsx`, `lib/src/host/remote/sidecar-entry.test.ts`, `vscode-ext/test/message-router.test.ts`, `standalone/scripts/dev-agent-browser-announce.test.mjs`.

## Unsaved changes

Expand All @@ -311,7 +306,7 @@ Source of truth: `parseToolState` in `dor-tools-lib/src/osc.ts`; `getToolDirty`

### Closing unsaved Tools

The iframe save channel, connected only to a `builtin:file` frame (`docs/specs/dor-tools-builtin.md` → Editing files), binds its window, proxy origin, and a per-mount connection nonce. Save completion carries the request id and current dirty state; a timeout or disconnected editor never permits a Save closure. **Must ignore a save-channel message naming another `dorTool` version or malformed for its kind**; a save error reaches the prompt control-stripped and bounded.
The iframe save channel, connected only to a `builtin:file` frame (`docs/specs/dor-tools-builtin.md` → Editing files), binds its window, proxy origin, and a per-mount connection nonce. Save completion carries the request id and current dirty state; a timeout or disconnected editor never permits a Save closure. **Must bind each save completion to its accepted connection generation and discard it after reconnect or close**, even when a replacement connection reuses the request id or nonce. (rationale) **Must ignore a save-channel message naming another `dorTool` version or malformed for its kind**; a save error reaches the prompt control-stripped and bounded.

**Must offer Save / Discard / Cancel before closing dirty Tools through Dormouse**: Pane closure, standalone window/app teardown, iframe reload or renderer change, and Workspace movement to another Window; **a Workspace close asks once, before any Surface closes.** Discard authorizes that action without declaring the edit clean; Save proceeds only after successful acknowledgement and no newer edits. A Tool without a connected save handler must be saved in its own UI or discarded. **Never prompt for a command close or move**: `dor kill`, `dor workspace close` (even `--force`), and cross-window `dor workspace move` (even `--dangerously-destroy-iframe-page-state`) refuse a dirty Tool. VS Code webview/host closure, forced termination, and crashes cannot be vetoed; drafts are not persisted.

Expand Down
Loading
Loading