Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3c4fe31
Add named Tool inputs and user configuration
nedtwigg Sep 15, 2026
b253955
Validate Tool inputs before approval and explain failed launches
nedtwigg Sep 15, 2026
1fab497
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
9a4dca2
Simplify Tool input plumbing after review
nedtwigg Sep 16, 2026
92ccba5
Handle Tool approval failures without repeating grants
nedtwigg Sep 16, 2026
b5f2dfb
Retain early rejection of special Tool configuration files
nedtwigg Sep 16, 2026
0688fe0
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
28d4703
Protect Tool argv and quote for the destination shell
nedtwigg Sep 16, 2026
8978997
Requote persisted Tool argv for the restored shell
nedtwigg Sep 16, 2026
2c35026
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
de6c2d4
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
7c515b7
Validate pending Tool grant state as a boolean
nedtwigg Sep 16, 2026
7be519e
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
70cfd23
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
2179316
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
dea018a
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
a711a8a
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
8560939
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
cd5162d
Clarify user Tool misses and cross-file reuse warnings
nedtwigg Sep 16, 2026
e45220a
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
07947c1
Exercise saved-permission retry after stack integration
nedtwigg Sep 16, 2026
7991e2d
Merge branch 'tool-takeover' into dor-tool-inputs
nedtwigg Sep 16, 2026
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
33 changes: 21 additions & 12 deletions docs/specs/dor-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,27 @@ Source of truth: `surfaceKindFromParams` / `isToolParams` in `lib/src/components

## Declaring tools

**Must resolve a named Tool from the nearest ancestor `dormouse.yml`.** 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 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.

| Field | Behavior |
| --- | --- |
| `run` | Required command, typed into the configured shell after integration readiness |
| `run` | Required shell command string or argument list, typed into the configured shell after integration readiness |
| `render` | `iframe` by default, or `ab-screencast` |
| `port` | `announced` by default, or `auto`; [Serving](#serving) owns selection |
| `prespawn_dedupe` | Optional scalar or list of literal key elements with substitutions |

- **Must reject unknown `prespawn_*` fields and unknown substitutions**; unknown ordinary fields produce warnings. The substitution set is `$PROJECT_ROOT`, the declaring directory, and `$CWD`, the caller's resolved directory. (rationale)
- **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 warn when a repo-local key omits `$PROJECT_ROOT`**, while allowing intentional cross-checkout dedupe.
- Reserved: **Must reject `$PROJECT_ROOT` in the future user-global configuration**, which has no project root; see scope **dor-tools** under [Future](#future).
- **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.

**Must pass named-tool inputs as argument values, never substitute them into a shell-command string.** String `run` accepts no arguments and remains literal shell syntax. List `run` expands `$TARGET`, `$CWD`, and `$PROJECT_ROOT` within elements; a whole `$ARGS` element expands all input arguments. Without `$ARGS` or `$TARGET` in the list, append the inputs. **Must quote argv for the destination Session's shell**, using the current default only for new Sessions; takeover stores that quoted command for reruns.

**Must require exactly one existing regular local file 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. Reject URLs, directories, and missing files. 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`; `lib/src/host/tool-registry.test.ts`.
Source of truth: `lookupTool` in `lib/src/host/tool-trust.ts`; `parseToolFile` / `resolveDedupeKey` in `lib/src/host/tool-registry.ts`; `resolveToolInput` 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/components/Wall.test.tsx`.

## Identity and dedupe

Expand All @@ -58,7 +64,7 @@ Source of truth: `lookupTool` in `lib/src/host/tool-trust.ts`; `parseToolFile` /
- **Must serialize Tool launch requests and approval completion in the renderer**, covering lookup, matching, creation, and startup. The current lock serializes all Tool requests, not only matching keys.
- **Must retain the queue after integration until the new Tool command starts or completes**, or startup times out or is cancelled. A matching completion before waiting counts; integration alone does not prove injection occurred.
- **Must reveal a live matching Tool and report `existing` without sending input.** An idle match restarts its stored command in its own directory and reports `adopted`; a failed restart reports an error.
- **Must reuse and reveal a matching pending approval Surface unless `--fresh` is set**, matching Tool name, project root, CWD, and fresh intent; preserve its approval state and report `pending`.
- **Must reuse and reveal a matching pending approval Surface unless `--fresh` is set**, matching Tool name, project root, CWD, arguments, and fresh intent; preserve its approval state and report `pending`.
- **Must accept a short-lived keyed restart after observing its new completed command id**, matching the stored command and directory. A completion predating injection or belonging to another command does not prove restart.
- **Must apply runtime re-keys only to the announcing Tool**, without merging Surfaces, transferring state, or killing either side of a collision. (rationale)

Expand All @@ -77,13 +83,15 @@ Source of truth: `queueToolSpawn` / the `surface.tool` handler in `lib/src/compo
7. **Must record each grant as its own atomically written file**, so hosts sharing one state directory never lock or merge.
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.

Approval layout follows `docs/specs/layout.md` → Pane body.

**Must validate a bounded regular, non-symlink grant receipt for the requested key.** Missing, corrupt, or mismatched records grant nothing; a filename alone is never approval.

Reserved: **Must keep future implicit glob dispatch user-global and limited to user-global Tools**, and gate any future repo `prespawn_*` execution on the same approval; see scope **dor-tools** under [Future](#future).

Source of truth: `createToolHost` in `lib/src/host/tool-host.ts`; `FileToolTrustStore` / `lookupTool` in `lib/src/host/tool-trust.ts`; `resolveUpstreamUrl` in `lib/src/host/git-upstream.ts`; `ToolApproval` in `lib/src/components/wall/ToolApproval.tsx`; `resolveToolApproval` in `lib/src/components/Wall.tsx`. Tests: `lib/src/host/tool-trust.test.ts`, `lib/src/components/Wall.test.tsx`.
Source of truth: `createToolHost` in `lib/src/host/tool-host.ts`; `FileToolTrustStore` / `lookupTool` in `lib/src/host/tool-trust.ts`; `resolveUpstreamUrl` in `lib/src/host/git-upstream.ts`; `ToolApproval` in `lib/src/components/wall/ToolApproval.tsx`; `resolveToolApproval` in `lib/src/components/Wall.tsx`; `toolPendingFromParams` in `lib/src/components/wall/browser-surface.ts`. Tests: `lib/src/host/tool-trust.test.ts`, `lib/src/components/Wall.test.tsx`, `lib/src/components/wall/tool-surface.test.ts`.

## Serving

Expand Down Expand Up @@ -181,6 +189,8 @@ The Tool-specific local boundaries are `docs/specs/security-local.md` → Dor To

**Must persist the command and stable Tool metadata with `surfaceType: 'tool'`**, retaining the ordinary CWD field. Never persist a derived URL, browser session binding, conflict, or pending approval as runnable Tool state. Live notes follow `docs/specs/notepad.md` → Live resume.

**Must retain resolved argv for argument-list Tools and re-quote it for the shell selected at cold restore.** Update the restored command in terminal options and Tool pane/door metadata. Literal shell-string commands retain their saved text. Reject persisted argv containing terminal controls before restoring any PTY.

**Must cold-restore an approved Tool by starting its saved command through integration-gated shell readiness**, then rediscover its port. Agent-resume commands do not override the saved Tool command. Pending approvals restore as ordinary terminals and execute nothing. **Must rebuild visible Tool metadata from its pane row when layout geometry is unusable**, rather than starting the command in a plain terminal with no serving behavior.

**Must retain live Tool browser params and OSC announcements in volatile Workspace-transfer content**, applying them to the destination plan without mutating the durable record. A serving iframe Tool participates in the ordinary iframe move confirmation. **Must refuse transfer while a Tool awaits approval or its browser startup has no session binding.**
Expand All @@ -197,10 +207,9 @@ Source of truth: `PersistedToolMetadata` in `lib/src/lib/session-types.ts`; `sav

**Scope: dor-tools** — remaining design, in implementation order.

- **C — glob table + `dor open`.** The user-global tools file, glob rules
(pattern → tool name), `dor open <target>` as sugar over `dor tool`, argument
substitution in `prespawn_dedupe` so per-target viewers do not collapse into
one pane, and the loopback file/viewer endpoint a local *file* needs (the
- **C — glob table + `dor open`.** User-global glob rules
(pattern → tool name), `dor open <local-file>` as sugar over `dor tool`,
and the loopback file/viewer endpoint a local *file* needs (the
iframe proxy instruments only `http://` upstreams).
- **D1 — reaping without cooperation.** Idle-threshold reap +
rehydrate-from-args + `persist: "never"`: every stateless tool, no new API,
Expand Down
6 changes: 6 additions & 0 deletions docs/specs/dor-tool.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ YAML authors naturally collapse one-element lists to scalars. Overloading a scal

A misspelled substitution such as `$PROJECTROOT` retained as a literal silently makes distinct checkouts share a key. Rejecting unknown substitutions exposes the typo before reuse can target another checkout.

Argument-list commands let the renderer quote each value for the actual target shell. Keeping shell strings literal avoids needing a shell-template parser to distinguish an author-provided pipeline from punctuation in a filename. Canonical file targets make symlink aliases reuse the same document viewer.

A Session can keep running PowerShell after its user's default changes to Bash. Takeover therefore cannot use the default's quotation rules: apostrophes and quoted executable paths differ between those shells. Pending invocations also depend on their CWD, since identical relative filenames in two subdirectories identify different documents.

## Identity and dedupe

`pnpm storybook`, `pnpm run storybook`, and `pnpm storybook --quiet` are different command strings for the same intended tool. `dor ensure` already supplies exact-command/CWD identity. An explicit Tool key allows authors to choose their own scope without making the declaration of a short command name implicitly enable dedupe.
Expand Down Expand Up @@ -54,6 +58,8 @@ Hostile text printed by the designated command can contain an announcement. The

## Persistence and hosts

A Tool may take over a PowerShell Session even while the selected default is Bash, and the selected default may change before restart. Its already-quoted command string cannot safely move between those shells. Retaining resolved argv preserves literal filenames and lets cold restore quote for its actual shell without retaining an obsolete shell executable.

A derived URL or browser daemon binding belongs to one execution. Reusing it after cold restore can connect a Tool to another process that obtained the old port. The saved command and declaration metadata are sufficient to start again and discover the new endpoint.

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.
Expand Down
6 changes: 5 additions & 1 deletion docs/specs/security-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,11 @@ Source of truth: `context` in `standalone/sidecar/pty-core.js`; `attachRouter` i

**Must keep repo-local named Tools inert until the user grants trust through Dormouse chrome.** The control socket exposes lookup and launch, never a trust-grant verb. Pending approval spawns neither its terminal nor a helper. Approval workflow belongs to `docs/specs/dor-tool.md` → Trust.

**Must derive the grant key in the host**, using the canonical upstream URL or project-root folder; a renderer request cannot supply an arbitrary grant URL. **Must bound config reads and refuse symlinks on every host.**
**Must keep named-tool inputs as argv until the renderer quotes them for the target shell.** User configuration is the local user's authority; a project name cannot replace a user Tool during user-only lookup. Resolution belongs to `docs/specs/dor-tool.md` → Declaring tools.

**Must reject C0 and DEL characters in Tool argv, substituted argv, and local-file targets before launch**, including controls exposed by canonicalizing symlinks. Shell quotes do not protect terminal editing keys. String `run` remains explicit shell code. Source of truth: `hasShellInputControls` in `dor/src/commands/shell-quote.ts`; `resolveToolInput` in `lib/src/host/tool-input.ts`; `useDorControl` in `lib/src/components/wall/use-dor-control.ts`. Tests: `lib/src/host/tool-input.test.ts`, `lib/src/components/Wall.test.tsx`.

**Must derive the grant key in the host**, using the canonical upstream URL or project-root folder; a renderer request cannot supply an arbitrary grant URL. **Must bound config reads and refuse repo-config symlinks on every host.** The user config may follow a dotfiles symlink; its opened descriptor must still be a bounded regular file.

**An upstream grant trusts the claimed URL, not authenticated checkout provenance.** A supplied directory containing its own `.git/config` can claim an already-granted upstream; folder-only grants limit this sharing. **Must not describe the chrome gesture as a boundary against other processes running as the user**; the local account model is The dor control socket above.

Expand Down
7 changes: 1 addition & 6 deletions dor/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,18 +206,13 @@ export async function runCli(rawArgv: string[], options: CliOptions = {}): Promi
if (!check.ok) return fail(check.message);
}

// stricli discards the `--` escape sequence during parsing, so capture its
// presence here (pre-parse) for commands that must distinguish an empty
// command tail from none — e.g. `dor split --` vs bare `dor split`.
const hasArgumentEscape = args.includes('--');

const capture = createCaptureProcess(options.env);
await runStricli(APPLICATION, commandName ? [commandName, ...args] : [], {
process: capture.process,
forCommand: (): DorCommandContext => ({
process: capture.process,
options,
hasArgumentEscape,
commandArgs: args,
}),
});

Expand Down
6 changes: 6 additions & 0 deletions dor/src/commands/shell-quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

export type ShellCommandKind = 'cmd' | 'posix' | 'powershell';

/** Shell quotes cannot protect bytes that an interactive terminal interprets
* as editing keys, escape sequences, or line submission before shell parsing. */
export function hasShellInputControls(value: string): boolean {
return /[\x00-\x1f\x7f]/.test(value);
}

const POSIX_SAFE_ARG = /^[A-Za-z0-9_@%+=:,./-]+$/;
// No `,` or `@`, unlike the posix set: PowerShell's argument mode reads a comma
// as the array operator (`cat a,b.txt` passes two arguments), while an initial
Expand Down
2 changes: 1 addition & 1 deletion dor/src/commands/split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async function runSplitCommand(this: DorCommandContext, flags: SplitFlags, ...co
// Only a bare `dor split` (no `--`, no command) steals focus; a `--` tail
// and an initial command alike leave it on the caller. The CLI owns the
// whole decision so the host can honor the field as sent.
focusNeutral: this.hasArgumentEscape || command !== undefined,
focusNeutral: this.commandArgs.includes('--') || command !== undefined,
...workspaceParam(flags.workspace),
});
writeStdout(this, renderSplitResponse(response, flags.json === true));
Expand Down
Loading
Loading