diff --git a/.changeset/child-approvals-main-session.md b/.changeset/child-approvals-main-session.md new file mode 100644 index 00000000..240581e9 --- /dev/null +++ b/.changeset/child-approvals-main-session.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust`: an approval request from a subagent or workflow child (for example a deep-research researcher's `web_fetch` in the default ask mode) is now asked in the main interactive session instead of being refused. The status line names the workflow or subagent, the tool, and the target; `y` allows once, `a` remembers it like a main-session grant, `n` returns the refusal to the child. Requests queue, cancelled children close theirs, and headless runs keep the refusal (#220). diff --git a/.changeset/isolated-rust-launch-preview.md b/.changeset/isolated-rust-launch-preview.md new file mode 100644 index 00000000..aae4906f --- /dev/null +++ b/.changeset/isolated-rust-launch-preview.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add an opt-in `codsh --rust` launch preview beside the unchanged legacy client. Locally packed native candidates reuse licensed upstream Rust welcome/input components with an isolated dsh Home/Profile, offline startup, explicit unavailable-execution feedback, and terminal cleanup. Include source/dependency notices and installed-product PTY verification. Refuse filesystem-resolved legacy Home aliases and ambiguous case-only overlaps with initially absent Homes before any writes; refuse unresolved symlinks in explicit/default legacy Homes, while retaining valid separate legacy links. Interpret `DSH_HOME` with released dsh's blank/tilde/lexical-normalization rules. Preserve literal `GROK_HOME` and conservatively refuse every parent-traversal (`..`) component, including separate Homes, instead of guessing symlink traversal. Protect default Homes even with overrides. Refuse any unresolved non-ASCII Home component before writes instead of approximating Unicode filesystem identity, including separate missing names; retain native resolution for existing Unicode directories and missing ASCII children beneath them. Compare device/inode ancestry with missing suffixes anchored to existing directories so firmlink aliases cannot evade isolation through different realpath strings; retain separate aliased Homes and fail closed on unavailable directory identity. Include installed-product path-matrix regressions and separate missing/resolvable-Home controls. This does not enable the dsh adapter, publish native releases, or change the default client. diff --git a/.changeset/memory-remember-session-toggle-fix.md b/.changeset/memory-remember-session-toggle-fix.md new file mode 100644 index 00000000..8e680635 --- /dev/null +++ b/.changeset/memory-remember-session-toggle-fix.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Fix `codsh --rust`: an empty `/remember` (the two-step draft prompt) no longer resets this session's `t` memory toggle back to `config.toml`'s value on save or cancel. Turning memory on with `t` after this session's first prompt was already sent now says it is too late for any prompt here. `/new` drops that toggle and follows `config.toml` again, so the notice does not claim the change carries into the next new session. diff --git a/.changeset/rust-background-commands.md b/.changeset/rust-background-commands.md new file mode 100644 index 00000000..c1b6d327 --- /dev/null +++ b/.changeset/rust-background-commands.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` runs background shell commands through dsh jobs. Ctrl+B moves the running command to the background, a command past `[toolset.bash] foreground_block_budget_ms` moves on its own (or is killed at its timeout with `auto_background_on_timeout = false`), and send-now moves a running command instead of killing it. The status line counts running commands, a message interrupts a blocking `job_output` wait, `/tasks` (Ctrl+G) shows command output and stops one with `x`, and a finished command wakes an idle session with a `◎ Task completed` turn. `/new`, session switches, and quit stop the session's commands; a resumed history never shows them as running. Plain `-p` and editor ACP are unchanged. diff --git a/.changeset/rust-capability-matrix.md b/.changeset/rust-capability-matrix.md new file mode 100644 index 00000000..0f4d426e --- /dev/null +++ b/.changeset/rust-capability-matrix.md @@ -0,0 +1,19 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Three-platform capability matrix for the installed Rust client (#201): +`scripts/rust-capability-matrix.py` and the `capability-matrix` job record OS / +terminal versions and the real effect of keys, mouse, shell, cancel, screen +modes, terminal restore, the real clipboard (macOS pasteboard, X11 `xclip`, +Windows `clip.exe` UTF-16LE), voice doctor without fixtures, sandbox profiles, +loopback SSH, real tmux, and the Windows ConPTY harness. Missing devices and +refused features are `refused` / `unavailable`, never a silent pass. Related: +`docs/rewrite/platform-capabilities.md`, `scripts/rust-clipboard-pty-test.py`, +`scripts/rust-tmux-pty-test.py`. Windows `/copy` sends UTF-16LE to `clip.exe` +so CJK survives; an unreachable Linux display is no longer reported as an +empty clipboard. macOS `/copy` forces a UTF-8 locale for `pbcopy` so CJK is not +turned into MacRoman, and a closed terminal window now ends the client on +macOS too (stdin readable with nothing pending counts as a hangup when no +POLLHUP arrives). diff --git a/.changeset/rust-clone-grove-substitute.md b/.changeset/rust-clone-grove-substitute.md new file mode 100644 index 00000000..0b41c3fb --- /dev/null +++ b/.changeset/rust-clone-grove-substitute.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust clone` clones with plain git in place of the reference Grove lazy clone, behind the reference gates (`GROK_CLONE`, `GROK_GROVE` / `[cli] grove`, Grove's `[clone] enabled`): depth 1 of one branch with blobs on demand, `--full-history`, `--cone`, git credentials or `GROVE_AUTH_TOKEN` (never `codsh --rust login`), a missing or empty target only, nothing left behind after a failure or Ctrl-C, and `--remote ssh://…` to clone on another host. A Grove worktree request (`GROK_WORKTREE_TYPE`, `[cli] grove_worktree`, `GROK_GROVE`) is recorded and falls back to a plain git worktree. diff --git a/.changeset/rust-deep-research-fixes.md b/.changeset/rust-deep-research-fixes.md new file mode 100644 index 00000000..c89a90ba --- /dev/null +++ b/.changeset/rust-deep-research-fixes.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust`: web search and fetch registration now follows the effective settings (`config.toml` with its env overrides, requirements and `--disable-web-search`) instead of launcher environment variables, so a setup configured only in `config.toml` gives the session, `/deep-research` researchers and in-session web search their `web_search`/`web_fetch` tools. A deep-research run that completes with a partial result reads `complete (result: partial)` in the completion notice, the tasks pane and the workflow block, as `/workflow runs` already said. `codsh --rust -p "/deep-research "` runs the built-in workflow in the foreground and prints its result (no query prints the usage), and the `streaming-messages-json` init line lists the session's tools and `deep-research`. diff --git a/.changeset/rust-deep-research.md b/.changeset/rust-deep-research.md new file mode 100644 index 00000000..c679f53e --- /dev/null +++ b/.changeset/rust-deep-research.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust`: `/deep-research ` runs the built-in deep-research workflow, the reference script pinned byte for byte from grok-build. It plans questions, researches them in parallel with web search, has separate verifier agents open each cited source, and writes a cited report. Only verified claims are reported; failed branches, missing web services, rate limits, contradicted sources and failed citation checks mark the result partial (`Result status: partial`), and a stop, the agent budget or a missing query end the run as cancelled, budget-limited or blocked. The built-in takes precedence over project, personal and plugin workflows of the same name and cannot be saved over. It uses the configured web search substitute; it was tested only with the mock model and fake local web services. diff --git a/.changeset/rust-dotenv-temp-pin.md b/.changeset/rust-dotenv-temp-pin.md new file mode 100644 index 00000000..53f13289 --- /dev/null +++ b/.changeset/rust-dotenv-temp-pin.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Stop a workspace deny glob from pinning `/tmp` when the workspace lives under the resolved `/private/tmp` write root. Renaming a workspace directory onto a fresh `/tmp` sibling stays allowed, while a directory inside `secrets/**/*.key` stays pinned. diff --git a/.changeset/rust-dsh-auth-enterprise-services.md b/.changeset/rust-dsh-auth-enterprise-services.md new file mode 100644 index 00000000..9bb59a0b --- /dev/null +++ b/.changeset/rust-dsh-auth-enterprise-services.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add substitute login, logout, and managed-config setup to `codsh --rust`. Identity tokens stay isolated from model keys and other services; organization pins refuse API-key-only use until a matching session exists; unsigned or unverifiable organization policy is refused, and official grok.com entitlements are not reproduced. Startup and inspect refresh an expired session or clear it without blocking login or an otherwise valid API key. Slash login keeps the live dsh client when a settings write fails, and replaces it when the settings patch changes. A usable identity session is passed to the executing dsh child, while other parent `GROK_AUTH_*` variables are not. Logout revokes the session at the identity provider before clearing it, and keeps the local session when revocation fails. A signature for another principal is refused at setup and when the sidecar is loaded, including a deployment-key caller with no team. Fail-closed policy with no pubkey and no sidecar is refused. diff --git a/.changeset/rust-dsh-cancel-live-turn.md b/.changeset/rust-dsh-cancel-live-turn.md new file mode 100644 index 00000000..d56d2c8c --- /dev/null +++ b/.changeset/rust-dsh-cancel-live-turn.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Cancel a live dsh turn from the isolated Rust client without losing an unrelated draft. Empty-draft Ctrl+C sends ACP session/cancel; Esc never cancels a turn or pending approval. Cancelled tools cannot run from a late allow or process teardown. diff --git a/.changeset/rust-dsh-config-enter-reload.md b/.changeset/rust-dsh-config-enter-reload.md new file mode 100644 index 00000000..fb0cdd4d --- /dev/null +++ b/.changeset/rust-dsh-config-enter-reload.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Empty Enter reloads isolated config and connects without submitting, and credential writes patch one ref while keeping existing records. diff --git a/.changeset/rust-dsh-context-compaction.md b/.changeset/rust-dsh-context-compaction.md new file mode 100644 index 00000000..6fda9418 --- /dev/null +++ b/.changeset/rust-dsh-context-compaction.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Inspect dsh context occupancy and advertised model limits without fabricating zeros, and run manual/automatic compaction through dsh so resume keeps the same checkpoint, retained tools/todos, and original records on failure. diff --git a/.changeset/rust-dsh-editor-acp.md b/.changeset/rust-dsh-editor-acp.md new file mode 100644 index 00000000..2fb2ef82 --- /dev/null +++ b/.changeset/rust-dsh-editor-acp.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Editors can attach to the isolated Rust client with `codsh --rust agent stdio`. The process speaks ACP/JSON-RPC 1 and forwards sessions, prompts, config, approval, and cancel to dsh. `session/load` resumes the same dsh session and replays saved history without running tools again. A denied tool whose call id lives on the nested session message is replayed as failed. Model and reasoning changes are written to `$GROK_HOME/model-selection.toml` before the next prompt and restored on load and on a terminal resume, including an advertised model that is not a catalog id. An unknown model is refused. Terminal `/dontAsk` and `/acceptEdits` share the editor permission mode, and that mode is written before dsh starts. Unsupported `x.ai` methods return method-not-found instead of a success stub. Closing the editor releases the write owner so a terminal can resume the same session. diff --git a/.changeset/rust-dsh-effective-config-fix.md b/.changeset/rust-dsh-effective-config-fix.md new file mode 100644 index 00000000..2fb96aee --- /dev/null +++ b/.changeset/rust-dsh-effective-config-fix.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Write isolated dsh credentials owner-only, apply overlay `api_key`, inspect the telemetry values actually forced off for dsh, and keep first-run config fields visible. diff --git a/.changeset/rust-dsh-effective-config.md b/.changeset/rust-dsh-effective-config.md new file mode 100644 index 00000000..24570811 --- /dev/null +++ b/.changeset/rust-dsh-effective-config.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add first-run compatible `config.toml` mapping into isolated dsh settings, with inspectable origins. `codsh --rust inspect` / `inspect --json` report CLI, environment, overlay, file, and default sources. Invalid files are preserved; old credentials and official login/telemetry are not used. diff --git a/.changeset/rust-dsh-explicit-memory.md b/.changeset/rust-dsh-explicit-memory.md new file mode 100644 index 00000000..12cd3c55 --- /dev/null +++ b/.changeset/rust-dsh-explicit-memory.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Browse, save, search, edit, and forget local memory notes with `/memory`, `/remember`, and `codsh --rust memory clear`. `/remember` appends to the workspace `MEMORY.md` after confirmation. The memory modal highlights the selected row, hides the preview under 80 columns, and cannot delete `MEMORY.md`. `[memory] enabled = false` keeps notes out of the next prompt until the session toggle. `/new`, switch, fork, and rewind drop that toggle. `/cd` then `/new` reads the new workspace. The first turn of a new session injects a bounded global and workspace note, plus keyword session matches. `index.sqlite` is a SQLite FTS5 keyword index rebuilt from the notes; a foreign database is left unchanged. Workspace clear removes `MEMORY.md`, `sessions/`, and `index.sqlite`. A session toggle does not rewrite config, and disabling memory does not delete or upload the files. diff --git a/.changeset/rust-dsh-file-attachments.md b/.changeset/rust-dsh-file-attachments.md new file mode 100644 index 00000000..d5a4d5fb --- /dev/null +++ b/.changeset/rust-dsh-file-attachments.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +The isolated Rust client attaches workspace files from the `@` picker, a single line or line range, or a pasted path. Dotfiles and gitignored files, including nested `.gitignore` files, `**` patterns such as `**/*.log`, and patterns that contain `/` (`logs/*.log`, `/secret.rs`, anchored at the directory that owns that `.gitignore`), stay hidden until the query starts with `!`. Pasting one of those paths leaves the text in the draft and does not read or send the file. Pasted prose that only names a path stays text. A prompt sent during a turn is queued with its chips, and Alt+Up restores that prompt once. A removed chip is not sent. A missing file, a file over 256 KiB, a permission failure, or a file that changed after preview stays in the composer and does not send its bytes. dsh receives the admitted text, and a resumed session shows the same `@path` mention. diff --git a/.changeset/rust-dsh-file-search.md b/.changeset/rust-dsh-file-search.md new file mode 100644 index 00000000..91627e80 --- /dev/null +++ b/.changeset/rust-dsh-file-search.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Search and list files through dsh grep and glob, and continue a long read with the offset dsh returns. A denied path is left out of the model result and the tool card, including a shell search of that file. An empty search, a binary file, a file that changed after it was read, and a missing language server are reported as those failures and do not invent contents or locations. A missing language server says that no LSP provider handles the file. diff --git a/.changeset/rust-dsh-file-tool-approval.md b/.changeset/rust-dsh-file-tool-approval.md new file mode 100644 index 00000000..9113e151 --- /dev/null +++ b/.changeset/rust-dsh-file-tool-approval.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Ask once before real dsh file write/edit tools run from the isolated Rust client. The UI shows the pending operation and dsh-supplied diff, allows that call once, and rejects or ends input without writing. Missing files, tool errors, and stale or duplicate approval replies are reported as failures. diff --git a/.changeset/rust-dsh-fork-rewind-review.md b/.changeset/rust-dsh-fork-rewind-review.md new file mode 100644 index 00000000..bee2eecd --- /dev/null +++ b/.changeset/rust-dsh-fork-rewind-review.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Refuse /rewind during a live turn, accept /fork --no-worktree, apply the fork model only on fork, store confirm/fork UI prefs in `$GROK_HOME/config.toml`, and reset minimal native history on rewind/fork. diff --git a/.changeset/rust-dsh-fork-rewind.md b/.changeset/rust-dsh-fork-rewind.md new file mode 100644 index 00000000..ed430061 --- /dev/null +++ b/.changeset/rust-dsh-fork-rewind.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Fork and rewind conversations through dsh seed/projection without restoring files. Isolated `codsh --rust` supports `/rewind`, `/undo`, `/fork`, `--fork-session`, and refuses `--restore-code`. diff --git a/.changeset/rust-dsh-fullscreen-minimal.md b/.changeset/rust-dsh-fullscreen-minimal.md new file mode 100644 index 00000000..b9f02dd0 --- /dev/null +++ b/.changeset/rust-dsh-fullscreen-minimal.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Switch the isolated Rust client between official fullscreen alternate-screen and minimal native-history rendering in process. `/minimal` and `/fullscreen` keep the dsh session, draft, running turn, and pending approval; session-scoped `--minimal`/`--fullscreen` do not rewrite `[ui] screen_mode`. diff --git a/.changeset/rust-dsh-hooks.md b/.changeset/rust-dsh-hooks.md new file mode 100644 index 00000000..b3991b34 --- /dev/null +++ b/.changeset/rust-dsh-hooks.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Run Grok-compatible command hooks from `codsh --rust` at SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, and SessionEnd. Exit 2 or `decision: deny` blocks the prompt or tool. A timeout, crash, or other non-zero exit is a recorded failure, not a success. Hook stdout and stderr stay hook output. An allowing hook cannot skip permission checks or widen a sandbox or permission deny. Untrusted project hooks stay skipped. diff --git a/.changeset/rust-dsh-image-draft-lifecycle.md b/.changeset/rust-dsh-image-draft-lifecycle.md new file mode 100644 index 00000000..770bf87d --- /dev/null +++ b/.changeset/rust-dsh-image-draft-lifecycle.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +In the isolated Rust client an unsent composer draft lives only in the running process, as in the reference: it is not written to `$GROK_HOME`, a new launch in any project starts with an empty composer, a sent prompt and its image never come back, and an exec screen-mode relaunch resumes the session without the draft. `--resume` shows a sent image turn with its `[Image #N]` placeholders, without the text-only `` path, and sends nothing again. On a text-only model the attach notice says the model cannot see images and gets only the saved path, whose attribute is escaped. Rules, session rules, and the first-turn memory note wrap that user text once; they are not copied onto the `` element or an attached file body. On macOS an empty bracketed paste (Cmd+V on an image-only clipboard) reads the clipboard image; on Windows it and Alt+V say the clipboard image read is not available yet and attach nothing. Whitespace alone inserts nothing, and pasting or dropping the absolute path or `file://` URL of an image file attaches it as an image instead of a binary `@file` mention. diff --git a/.changeset/rust-dsh-image-input.md b/.changeset/rust-dsh-image-input.md new file mode 100644 index 00000000..a9e9cbe4 --- /dev/null +++ b/.changeset/rust-dsh-image-input.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +The isolated Rust client pastes a clipboard image as an atomic `[Image #N]` chip. Hovering the chip, or resting the cursor on it, shows the sniffed size (png, gif, jpeg, or webp), byte length, digest, and saved path. That notice is metadata, not a terminal-graphics picture. A model that declares `image` in `input_modalities` receives an ACP image block. A text-only model receives the saved path under the isolated attachment store and does not get a hidden vision route. A queued image is rebuilt for the model selected when it sends. An empty clipboard, a corrupt file, and a file over 256 KiB stay in the composer with a notice. Removing the chip drops it from the next submit. `/minimal`, `/fullscreen`, and closing the model menu restore the parked bytes with the placeholder. The packed launcher forwards `CODSH_CLIPBOARD_IMAGE` and `GROK_CLIPBOARD_NO_NATIVE_READ`. `[model.] provider` selects the dsh provider; it defaults to the catalog id. Entries that share a provider and the same key, backend, URL, and headers are written as one provider with several models. A reused provider with different credentials or a different backend is refused. diff --git a/.changeset/rust-dsh-legacy-config-import.md b/.changeset/rust-dsh-legacy-config-import.md new file mode 100644 index 00000000..2939ba5a --- /dev/null +++ b/.changeset/rust-dsh-legacy-config-import.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add explicit `codsh --rust import` for selected legacy dsh providers and preferences. Preview lists conversions, conflicts, and unsupported items from current `settings.yaml` sources without copying tokens, credential files, or trust grants. The imported model follows `agent-default-model` when that model is listed. Inline keys are not copied, a missing `apiKeyEnv` is not invented, and existing nested settings are kept. UI density maps to isolated compact mode; apply writes only into the isolated Home. diff --git a/.changeset/rust-dsh-managed-policy-trust.md b/.changeset/rust-dsh-managed-policy-trust.md new file mode 100644 index 00000000..074e008b --- /dev/null +++ b/.changeset/rust-dsh-managed-policy-trust.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add managed defaults, locked requirements, and workspace folder trust to `codsh --rust`. Inspectable origins keep locked values from being bypassed; untrusted project Hooks/plugins/instructions stay inactive until an explicit grant. diff --git a/.changeset/rust-dsh-mcp-local.md b/.changeset/rust-dsh-mcp-local.md new file mode 100644 index 00000000..20adfcf1 --- /dev/null +++ b/.changeset/rust-dsh-mcp-local.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +The isolated Rust client supports local MCP servers. `codsh --rust mcp list|add|remove|enable|disable|doctor` manages `[mcp_servers]` in `$GROK_HOME/config.toml`, and trusted folders add `.grok/config.toml` and `.mcp.json` servers (untrusted ones are listed but not started). dsh's own MCP client mounts the servers for each session; a missing program, a startup crash, or a refused `initialize` is reported by name while the session starts with the rest. Tools keep dsh's `mcp____` names, and Grok's `search_tool` and `use_tool` are added. Every call runs once through the same permission rules (`server__tool`), Hooks, and cancellation. Output above `[mcp] max_output_bytes` (default 20000) is cut with the full text saved to disk, startup and per-tool timeouts are enforced, and `/mcps` shows state, failures, and tools and can enable, disable, or restart servers. SSE, OAuth, plugin-provided servers, and managed MCP policy are not handled yet. diff --git a/.changeset/rust-dsh-model-protocol-effort.md b/.changeset/rust-dsh-model-protocol-effort.md new file mode 100644 index 00000000..8e3d9adb --- /dev/null +++ b/.changeset/rust-dsh-model-protocol-effort.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Map configured model backends and reasoning effort into isolated dsh, and let `/model` `/effort` select only advertised catalog options. Same-named models on different protocols stay distinct; unsupported options are refused; usage and context stay unknown unless the provider or config supplies them. diff --git a/.changeset/rust-dsh-mouse-search-reading.md b/.changeset/rust-dsh-mouse-search-reading.md new file mode 100644 index 00000000..26ea54e1 --- /dev/null +++ b/.changeset/rust-dsh-mouse-search-reading.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add fullscreen mouse selection, transcript search, turn jump, and reading-position restore to the isolated Rust client. Selection survives rebuild, hit testing uses the painted gutter, and scrollback Ctrl+D half-pages instead of quitting. `/find` and `/jump` refuse in minimal with the `/fullscreen` remedy. diff --git a/.changeset/rust-dsh-permission-rules.md b/.changeset/rust-dsh-permission-rules.md new file mode 100644 index 00000000..6dfd19f1 --- /dev/null +++ b/.changeset/rust-dsh-permission-rules.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add allow/ask/deny rules, permission modes, and remembered project grants to `codsh --rust`. Explicit deny and hook blocks survive always-approve, including brace groups, quoted or backslash-escaped words, `eval`, ANSI-C `bash -c` scripts, and a path-qualified executable matched by basename without regard to case (`/bin/rm`, `./rm`, `RM.EXE`). Wrappers such as `timeout`, `sudo`, `nohup`, and `xargs` peel to the inner command without eating the command name while `env -S` prompts. `sort -o` (including attached `sort -oFILE` and clustered `sort -uoFILE`), `--output`, and unique `sort --compress-program` prefixes are not read-only. Frozen git read-only subcommands auto-allow; git writes do not, including `git branch `, `-f`/`--force`, `-u`/`--set-upstream-to` (including the attached form `git branch -uorigin/main` and a bare `-u` or `-t` with no operand), unique prefixes of `branch --delete`/`--move`/`--copy`/`--force`, and `--output` on `diff`, `log`, `show`, `blame`, and `rev-list`, plus `cat-file --filters`. A leading word such as `time /bin/rm`, `exec /bin/rm`, or `builtin rm` cannot hide a denied command, and a shell option that takes the next word (`bash -o errexit -c`) is consumed before the script is read. An unquoted `*`, `?`, or `[` in a command word is not expanded, so a pathname glob such as `./r*`, `./*m`, or `./r?` (including behind `time`, `exec`, `builtin`, `command`, `sudo`, or `bash -o errexit -c`) is not auto-approved. `git branch --track` and a unique prefix such as `--tr` are writes even with no operand. Literal `git branch` and `sort file` stay read-only. A parameter expansion (`$x`, `"$x"`, `${x}`, `$1`, `"$1"`, `$@`, `$*`) is unsplittable: it is not peeled as a literal command, not auto-approved, and not glob-allowed. Claude settings load from `~/.claude` and walk to the repo root. Read/Edit deny follows in-path symlinks. `y` is once, `a` remembers a path-scoped project grant, and `/revoke-approvals` forgets those grants. diff --git a/.changeset/rust-dsh-plugin-install-lifecycle.md b/.changeset/rust-dsh-plugin-install-lifecycle.md new file mode 100644 index 00000000..115c7cea --- /dev/null +++ b/.changeset/rust-dsh-plugin-install-lifecycle.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add isolated `codsh --rust plugin` marketplace add/list/update/remove and plugin install/update/uninstall with inspectable provenance. Failed or untrusted installs leave no success record and do not grant execution. A present `strict_known_marketplaces` list binds catalog load, named install, the catalog entry's clone URL, and later git update. Layers are strictest-wins. Git URL comparison folds scheme and host only, including GitHub, and keeps the repository path case-sensitive. diff --git a/.changeset/rust-dsh-privacy-feedback.md b/.changeset/rust-dsh-privacy-feedback.md new file mode 100644 index 00000000..1760f27f --- /dev/null +++ b/.changeset/rust-dsh-privacy-feedback.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Keep isolated Rust telemetry, trace upload, session tracking, and content sharing off unless a substitute destination is configured. Feedback draft text is posted only when content sharing is on; trace upload and session tracking send their own redacted counters. `/feedback` uses one Write/Drafts form in every mode. Official hosts are matched by hostname. Diagnostic previews redact prompts and secrets and stay separate from model traffic. `GROK_LOG_FILE` and `GROK_HOOKS_LOG` are not forwarded. diff --git a/.changeset/rust-dsh-prompt-edit.md b/.changeset/rust-dsh-prompt-edit.md new file mode 100644 index 00000000..9202d07d --- /dev/null +++ b/.changeset/rust-dsh-prompt-edit.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +The isolated Rust client keeps the official textarea for prompt editing. Typing `/` in a nonempty draft stashes that draft so a slash command can run, then restores it. Slash completion starts from an empty `/`. Multiline, undo, history search, slash/HISTFILE completion, prompt Vim (`[ui] simple_mode=false`), paste, and `$VISUAL`/`$EDITOR`/`vi` external editing submit the resulting text through dsh. An unsent draft survives resize, and a submit that does not start a turn puts that draft back. A narrow screen still shows `Execution unavailable` beside that draft. `/edit-prompt` requires an empty composer; Ctrl+G in minimal preserves a draft. Next-prompt ghost text is not wired, so Tab and Right do not accept it. Suggestion rows stay blocked. diff --git a/.changeset/rust-dsh-rendered-content.md b/.changeset/rust-dsh-rendered-content.md new file mode 100644 index 00000000..ede82607 --- /dev/null +++ b/.changeset/rust-dsh-rendered-content.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Render real dsh Markdown, tables, code, mermaid labels, thoughts, and tool cards/diffs through official `xai-grok-markdown` in the isolated Rust client, keeping heading, code, table, and diff colors on the session. Pretty mode matches official markdown, including `Vec`, comparisons, fenced Rust, and inline HTML tags, and keeps a ZWJ emoji in one cell. Full content pages the body without painting that body again in the notice. Long bodies fold; Tab then expand/raw/full-content/`y` keep original bytes, and Esc closes full content back to the fold. `/transcript` opens them in `$PAGER`. A failed tool paints `failed` and `[error]` in a failure color instead of success. diff --git a/.changeset/rust-dsh-resume-single-owner.md b/.changeset/rust-dsh-resume-single-owner.md new file mode 100644 index 00000000..06831b37 --- /dev/null +++ b/.changeset/rust-dsh-resume-single-owner.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Resume the same dsh session after exit and refuse a second write owner. Isolated `codsh --rust --continue` / `--resume ` restore persisted conversation, mark unfinished tools as unknown, and do not replay side effects. Write ownership uses an exclusive lock and is released only after the dsh child is reaped. diff --git a/.changeset/rust-dsh-rules-skills-commands.md b/.changeset/rust-dsh-rules-skills-commands.md new file mode 100644 index 00000000..8c5a204d --- /dev/null +++ b/.changeset/rust-dsh-rules-skills-commands.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +The isolated Rust client discovers compatible project rules, skills, agent definitions, and custom commands after folder trust and includes that context in the dsh prompt. Closer skill directories outrank broader ones. Nested `SKILL.md` files return only when the walk depth is greater than five, and a child of a directory that already has `SKILL.md` is still recorded. A configured `[skills] paths` directory is depth 0, so its children start at depth 1 and a sixth child is not loaded. Flat `commands/*.md` files are slash commands. A skill or command named `login`, `logout`, or `feedback` keeps the built-in on the bare slash and is offered only as `/local:name` (or the ancestor, repo, or user qualifier). `--rules` appends session rules and `--system-prompt-override` replaces the dsh prompt. Untrusted project assets stay inactive. Collisions, truncation, gitignored files, and invalid extra rule paths are visible in `inspect`. User-invocable skills and custom commands appear in the slash menu; `/reload-assets` rescans an added, removed, or empty directory. `codsh --rust` forwards `GROK_CLAUDE_SKILLS_ENABLED` and `GROK_CURSOR_SKILLS_ENABLED`, so setting either off stops that vendor skill scan. `inspect --json` always includes top-level `assets.skills` and `assets.commands`. A native candidate staged before that catalog still returns those arrays; the launcher fills them from `.grok` and skips `.claude` or `.cursor` when the matching flag is off. diff --git a/.changeset/rust-dsh-session-export-share-delete.md b/.changeset/rust-dsh-session-export-share-delete.md new file mode 100644 index 00000000..b4a900e0 --- /dev/null +++ b/.changeset/rust-dsh-session-export-share-delete.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Export a selected session as Markdown and reject extra arguments before writing. Share only to the explicitly selected substitute, over http or https, without following redirects. HTTPS uses the native TLS connector and a configured extra CA; an untrusted certificate uploads nothing. A symlink at the sessions root, a project directory, a session directory, or the log is not read or uploaded. Session deletion stays blocked because released dsh persistence has no deletion operation, so no session data is removed. Disk usage still reports the isolated home without deleting files. diff --git a/.changeset/rust-dsh-session-search-dashboard.md b/.changeset/rust-dsh-session-search-dashboard.md new file mode 100644 index 00000000..fd8e1813 --- /dev/null +++ b/.changeset/rust-dsh-session-search-dashboard.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Search, rename, and resume dsh sessions from the welcome list, `/resume`, the agent dashboard, and `codsh --rust sessions`. Manual titles outrank generated titles. A refused switch keeps the current write owner. A same-directory resume that is already active does not close the live session, and the open picker or dashboard shows `already active`. The launcher forwards the dashboard startup settings. diff --git a/.changeset/rust-dsh-settings-themes-status.md b/.changeset/rust-dsh-settings-themes-status.md new file mode 100644 index 00000000..11a2adb3 --- /dev/null +++ b/.changeset/rust-dsh-settings-themes-status.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Connect the isolated Rust client's `/settings`, `/theme`, compact mode, timestamps, and status line to effective `$GROK_HOME/config.toml`. Theme preview/Escape does not persist; minimal mode keeps the terminal palette; status-line scripts time out and clean up on exit; locked requirements show their source. diff --git a/.changeset/rust-dsh-shared-server.md b/.changeset/rust-dsh-shared-server.md new file mode 100644 index 00000000..950bbf9d --- /dev/null +++ b/.changeset/rust-dsh-shared-server.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +The isolated Rust client can share sessions between clients. `codsh --rust agent serve` serves ACP over an authenticated WebSocket on `127.0.0.1:2419` by default, and `codsh --rust agent leader` runs a per-user leader on a 0600 socket that `agent --leader stdio` (or `[cli] use_leader`) starts or reuses. One dsh process runs each live session: another client attaches with `session/load` and gets saved turns, the running turn, and any pending approval without a second executor. The first approval answer wins and late answers are told they are stale, a concurrent prompt is refused, option changes are broadcast, and a dsh exit is reported without retrying the turn. A client disconnect does not cancel work. A non-off sandbox profile keeps the session out of the leader. `codsh --rust leader list|info|kill` manages leaders. Nothing listens unless one of these commands is run. `agent headless`, `--remote`, and Cursor worker mode need official services and are refused. diff --git a/.changeset/rust-dsh-shell.md b/.changeset/rust-dsh-shell.md new file mode 100644 index 00000000..a8076eed --- /dev/null +++ b/.changeset/rust-dsh-shell.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Run a shell command through dsh's bash tool from the isolated Rust client. The card shows stdout, stderr, and the exit code, including 0. A denied command does not run, and cancelling a running command is reported as aborted rather than success. An interactive terminal session is unavailable: the acp profile does not mount dsh's persistent terminal, and window resize is not a dsh tool. diff --git a/.changeset/rust-dsh-streamed-turn.md b/.changeset/rust-dsh-streamed-turn.md new file mode 100644 index 00000000..847e213a --- /dev/null +++ b/.changeset/rust-dsh-streamed-turn.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Drive real dsh turns from the isolated Rust client over ACP/JSON-RPC. Prompts submitted in the Rust UI execute through `dsh --profile acp` in the isolated Home; streamed answers, provider thoughts, empty replies, and failures are shown as dsh reports them. Protocol mismatch is refused. Mock models stay at the dsh provider boundary. diff --git a/.changeset/rust-dsh-subagents.md b/.changeset/rust-dsh-subagents.md new file mode 100644 index 00000000..4bd14a8b --- /dev/null +++ b/.changeset/rust-dsh-subagents.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` runs typed subagents through dsh. `general-purpose`, `explore`, `plan`, `[subagents.roles]`, and agent files set each child's tools and model; children inherit the parent's permissions, `max_concurrent`, `limit_behavior`, and `max_depth` are honored, and `--no-subagents` or `--disallowed-tools Agent(type)` remove them. The tool block and status line follow each child, and `/tasks` (Ctrl+G in fullscreen) lists children, opens a read-only child transcript, and cancels one child. diff --git a/.changeset/rust-dsh-voice-input.md b/.changeset/rust-dsh-voice-input.md new file mode 100644 index 00000000..9b96a2e5 --- /dev/null +++ b/.changeset/rust-dsh-voice-input.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +The isolated Rust client dictates into the current draft only after `/voice` or an enabled Ctrl+Space / F8 press. Transcripts are not submitted. A slash command typed while recording parks the draft and restores it instead of replacing it with `/`. One Esc while that completion is open leaves the recording and restores the parked draft. Audio goes to a configured substitute speech-to-text URL; official hosts are refused. `/voice doctor` lists input devices without recording. Live microphone capture stays unverified; a fixture file exercises the real substitute route. Linux and Windows capture are marked unverified. diff --git a/.changeset/rust-dsh-web-search-fetch.md b/.changeset/rust-dsh-web-search-fetch.md new file mode 100644 index 00000000..515ba9ae --- /dev/null +++ b/.changeset/rust-dsh-web-search-fetch.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +The isolated Rust client can search and fetch through explicitly configured substitutes. Both tools stay off, and are not advertised, until search names a model and fetch is enabled. Domain policy, proxy routing, loopback exceptions, and private-address blocks apply at startup. Official hosts are refused. Authentication, rate-limit, redirect, cancellation, and network failures return no page text. Sources and truncation stay visible in the command, JSON, and dsh tool result. diff --git a/.changeset/rust-dsh-worktrees.md b/.changeset/rust-dsh-worktrees.md new file mode 100644 index 00000000..2b67557b --- /dev/null +++ b/.changeset/rust-dsh-worktrees.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust -w [NAME]` (with optional `--worktree-ref`) starts a session in a new git worktree on its own branch, carrying your uncommitted changes without touching the checkout, and `-w -r ` continues a session there under a new id. `codsh --rust worktree list|show|apply|rm|gc|db` and `/worktree` manage worktrees; `apply` is explicit and reports conflicts instead of overwriting your edits. Subagents can run with `isolation: "worktree"`, keeping their edits out of your checkout until you apply them. diff --git a/.changeset/rust-filesystem-sandbox.md b/.changeset/rust-filesystem-sandbox.md new file mode 100644 index 00000000..88430e51 --- /dev/null +++ b/.changeset/rust-filesystem-sandbox.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add filesystem confinement to `codsh --rust`. `--sandbox`, `GROK_SANDBOX`, or `[sandbox] profile` applies Seatbelt on macOS or Landlock on Linux to the client and its dsh child. `off` stays unconfined. A profile that cannot be enforced refuses startup, including a Linux profile that must deny a path inside a write root. On macOS every existing ancestor of a protected path up through the write root that contains it, including `$GROK_HOME` and a hook directory's grandparent, cannot be renamed onto another write root. The macOS profile denies keychain Mach services unless a keychain database is an explicit grant, and it does not grant a read of all of `/dev`. When the user and project `sandbox.toml` files define one custom profile differently, startup uses the user definition and warns with both paths. An explicit profile name does not trust an untrusted project's `.grok/sandbox.toml`: a custom profile defined only there refuses startup, while a user `$GROK_HOME/sandbox.toml` definition stays usable and still wins. A malformed, unreadable, or symlinked untrusted project file does not veto that user definition; a trusted malformed project file still refuses startup. A relative deny glob stays inside the workspace, `[!…]` and `[^…]` both negate on macOS, and unsupported POSIX classes, empty segments, a trailing slash, a `.` or `..` segment, or a `**` that is not its own path segment refuse startup. `[sandbox] profile` uses the same loader as inspect: a signed requirements pin beats `--sandbox`, `GROK_SANDBOX`, and `GROK_CONFIG_PATH`; a managed default does not. `[sandbox]` is accepted in signed fail-closed user and trusted project config. The status line names the active profile and write roots. Protected config and hook files are not rewritten; a permission-mode change stays in the session. Linux and Windows are not claimed from a macOS run, and child-network blocking is unchanged. diff --git a/.changeset/rust-glob-tail-rename.md b/.changeset/rust-glob-tail-rename.md new file mode 100644 index 00000000..de2d5feb --- /dev/null +++ b/.changeset/rust-glob-tail-rename.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Pin directories inside a deny glob, including directories created after launch, so renaming one onto another write root cannot carry a matched file out from under the Seatbelt regex. A rename that stays inside the glob remains denied. diff --git a/.changeset/rust-goal-rounds.md b/.changeset/rust-goal-rounds.md new file mode 100644 index 00000000..22ecd556 --- /dev/null +++ b/.changeset/rust-goal-rounds.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` adds `/goal [--budget ]` with `status`, `pause`, `resume`, and `clear`: dsh's own goal driver runs the goal rounds and a dsh extension adds independent verifier subagents for every completion claim (the model's claim alone never completes a goal), a token budget kept apart from workflow agent limits, pause causes, and a visible stop reason, while the Rust client shows goal rounds and progress in the transcript and status line. diff --git a/.changeset/rust-headless-json.md b/.changeset/rust-headless-json.md new file mode 100644 index 00000000..e4186942 --- /dev/null +++ b/.changeset/rust-headless-json.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Plain prompts accept `--output-format plain|json|streaming-json|streaming-messages-json`. `json` prints one object. The streaming formats print one JSON object per line and end with `end` or `result`. `--include-partial-messages` adds `stream_event` deltas and only changes `streaming-messages-json`. Tool arguments, tool results, and reasoning are copied from dsh. Usage and cost are copied only when dsh sends them; otherwise the terminal object says `usage_absent` and does not invent a bill. Interrupt, truncation, and a model error finish without reporting a successful `end_turn`. A tool approval with no terminal is rejected inside dsh and exits 1. Every JSON format prints an error object (`type: error`, or `result` with `is_error` and not `subtype: success`) and does not say `end_turn`. A partial `message_start` omits `usage` when dsh sent none. diff --git a/.changeset/rust-image-generation.md b/.changeset/rust-image-generation.md new file mode 100644 index 00000000..51834f1b --- /dev/null +++ b/.changeset/rust-image-generation.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` generates and edits images through an image service you configure: `[models] image_gen` (and optionally `image_edit`) names a `[model.]` with `base_url` and `supports_image_generation` / `supports_image_edit`, speaking the reference `xai` JSON format or the `openai` Images format (for example a local stable-diffusion.cpp `sd-server`). Without one, neither tool exists; official hosts are refused. The model's `image_gen` / `image_edit` tools and `/imagine ` run through dsh, and every request asks first with a card naming the prompt, the host, how many reference images are sent, and that codsh does not know the service's price. `[Image #N]` chips, absolute paths, `file://` and `data:` URLs are edit references (Read deny rules apply). The row shows elapsed time, Ctrl+C cancels and saves nothing, and a refusal, URL-only or malformed reply, HTTP error or timeout saves nothing and says why. Results are checked and saved atomically as `/images/.`; `/images` lists them and `/images open [N]` opens one, including after `--resume`. `codsh --rust image generate|edit|list` uses the same service outside a session. diff --git a/.changeset/rust-linux-prebuilt.md b/.changeset/rust-linux-prebuilt.md new file mode 100644 index 00000000..f472aa0f --- /dev/null +++ b/.changeset/rust-linux-prebuilt.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +The Linux Rust client no longer needs `libssl` (OpenSSL is compiled in), ships for arm64 as well as x64, and is checked before it starts: an older glibc, a musl system such as Alpine, or a missing shared library is refused with what to install, and `codsh --rust install-check` shows the glibc it found. diff --git a/.changeset/rust-macos-perf.md b/.changeset/rust-macos-perf.md new file mode 100644 index 00000000..24a1bdb4 --- /dev/null +++ b/.changeset/rust-macos-perf.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` responds faster: Ctrl+Q quits at once even while dsh is still starting (and no dsh is left behind), text typed during startup is kept, a large paste no longer looks up each of its lines on disk, bursts of typed keys are painted once, a finished answer is shown before the session is read back for auto-compaction, synchronous dsh requests return as soon as they are answered, and the launcher remembers a verified client instead of hashing it on every start. Closing the terminal window now ends the client and dsh instead of leaving them spinning. `scripts/rust-perf-bench.py` measures the installed client against the pinned reference on macOS with thresholds frozen before the candidate runs. diff --git a/.changeset/rust-memory-capture.md b/.changeset/rust-memory-capture.md new file mode 100644 index 00000000..63f8b216 --- /dev/null +++ b/.changeset/rust-memory-capture.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` captures memory: the end of a session saves a metadata summary to `sessions/`, `/flush` (and an idle flush) appends a model-written summary of the last 20 messages to the daily session log, and `/dream` (and the gated automatic Dream at launch) merges the session logs into the workspace `MEMORY.md` under a cross-process lock. Each call names the route, what was sent and the token usage; the cost is not reported. Existing logs are appended to, a `MEMORY.md` edited while Dream ran is kept, and the previous `MEMORY.md` and consolidated logs move to `sessions/.archive/`. `/memory` then `s` shows content-free diagnostics. `GROK_MEMORY_LOG` writes content-free event lines, `--memory-flush` flushes after a `-p` turn, and `[memory_v2] enabled = true` is refused because that store is not implemented. diff --git a/.changeset/rust-memory-followups.md b/.changeset/rust-memory-followups.md new file mode 100644 index 00000000..94884fd4 --- /dev/null +++ b/.changeset/rust-memory-followups.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` memory fixes from the real-model check of #186: the first `/flush` after a restart or `--resume` now sends the delta prompt against the session's last flush on disk instead of re-summarizing (and duplicating) the whole window; first-turn recall uses the reference keyword search (stop words dropped, any keyword matches, best match first), so a conversational question finds session logs before any Dream; `[memory_v2] enabled = true` no longer adds an `unknown security/policy field` warning to `inspect` next to its refusal; and a `Dream is already running` refusal is cleared once the running job reports. diff --git a/.changeset/rust-monitors.md b/.changeset/rust-monitors.md new file mode 100644 index 00000000..68b2ce0d --- /dev/null +++ b/.changeset/rust-monitors.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` adds monitors. The model's `monitor` tool runs a long-lived script as a dsh job (reference limits: `timeout_ms` default and cap 10 hours, `persistent` for the session's lifetime); each line it prints reaches the model as a ``, batched and rate limited like the reference, and a flooding script is stopped and killed. An idle session wakes with a `◎ Monitor event` turn within dsh's three-wake budget; an event that arrives while the model writes its answer waits for one grouped wake, so events never add model requests beyond that budget. The script's exit is reported once. The status line counts monitors, the Ctrl+G tasks pane lists them and stops the selected one with `x`, and `/new`, session switches, and quitting stop them. Subagents, plain `-p`, editor ACP, and the shared server do not offer the tool. diff --git a/.changeset/rust-network-env-sandbox.md b/.changeset/rust-network-env-sandbox.md new file mode 100644 index 00000000..aa341c0a --- /dev/null +++ b/.changeset/rust-network-env-sandbox.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Enforce network isolation and shell environment filtering for `codsh --rust` sandbox profiles. `restrict_network` (built-in `read-only` and `strict`, or a custom profile) denies network with macOS Seatbelt `(deny network*)` for the client and its children. A profile that asks for network isolation on a platform that cannot apply it, including Linux Landlock and Windows, refuses startup instead of continuing with network open. dsh's per-call file mode is not a network sandbox. `[shell_environment_policy]` filters the environment of a shell child this client starts, and an unknown inherit value or an inexpressible pattern refuses startup. A macOS run does not claim Linux network enforcement. dsh's own bash tool still inherits the dsh process, because a second Seatbelt profile cannot be applied inside the first. diff --git a/.changeset/rust-new-session-keeps-route.md b/.changeset/rust-new-session-keeps-route.md new file mode 100644 index 00000000..2997d73b --- /dev/null +++ b/.changeset/rust-new-session-keeps-route.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`/new` and a dashboard dispatch keep the configured provider, model, effort, permissions, and settings patch. They do not fall back to another provider. `/cd` reloads trusted workspace config before that next session starts, and that workspace model and effort replace a saved selection. A failed settings write keeps the live client. diff --git a/.changeset/rust-pin-isolated-grok-home.md b/.changeset/rust-pin-isolated-grok-home.md new file mode 100644 index 00000000..34be3127 --- /dev/null +++ b/.changeset/rust-pin-isolated-grok-home.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` ignores an inherited `GROK_HOME` again and always uses `~/.codsh-rust/.grok`. A preview launched with `GROK_HOME` pointing at another Home no longer creates that directory or writes prompt history and drafts into it. Web search and fetch settings are read from the isolated `config.toml`. diff --git a/.changeset/rust-plain-automation.md b/.changeset/rust-plain-automation.md new file mode 100644 index 00000000..d6c4a6d6 --- /dev/null +++ b/.changeset/rust-plain-automation.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Run one plain prompt through the isolated Rust client and released dsh. `-p`/`--single`, `--prompt-file`, and `--prompt-json` print the final answer on stdout. `--verbatim` sends that user content unchanged and does not expand custom slash commands; rules and enabled first-turn memory still apply as their own leading block. A plain turn has no time limit. A relative `--prompt-file` is read after `--cwd` is entered, so it names a file inside that directory, and an inherited plain mask or step bound never reaches an interactive session. The tool mask is applied before the first model request, with public tool ids mapped to dsh names and deny winning over allow. `Agent` removes both registered subagent spawn tools (`subagent` and `subagent_fork`); `Agent(type)` in any letter case is refused before any provider call, from flags or an inherited `CODSH_PLAIN_TOOLS`, until subagent types exist. `--cwd`, `--sandbox`, `--no-memory`, and `--disable-web-search` work for plain prompts and interactive sessions; `--cwd` is entered before the sandbox write roots are computed. Headless-only `--tools`, `--disallowed-tools`, `--max-turns`, and `--verbatim` warn and are ignored without a plain prompt. `-m` is `--model`, `-v` is `--version`, and `-c`/`--continue` and `-r`/`--resume` resume a session. `--max-turns` stops before the next model step. `help` and `completions` work for bash, zsh, fish, powershell, and elvish. Unknown options and missing values exit 2; SIGINT and SIGTERM exit 130 and 143, also during startup. JSON output and other specialized flags stay later tickets. diff --git a/.changeset/rust-plan-questions-todos.md b/.changeset/rust-plan-questions-todos.md new file mode 100644 index 00000000..0cdd6af7 --- /dev/null +++ b/.changeset/rust-plan-questions-todos.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` gains dsh plan mode, `ask_user_question`, and todos: `/plan [task|off]`, Shift+Tab (normal → plan → always-approve), `enter_plan_mode`, a plan gate that allows only the session `plan.md` in every permission mode, the plan review (approve with comments, request changes, line comments, copy, quit), `/view-plan`, the question card with timeout and dismissal, the todos pane (Ctrl+T), `--no-plan`, `--no-ask-user`, and the hidden `--todo-gate`. Plain prompts and editor sessions get the no-operator answer and an approved plan review. diff --git a/.changeset/rust-plugin-content.md b/.changeset/rust-plugin-content.md new file mode 100644 index 00000000..446b469b --- /dev/null +++ b/.changeset/rust-plugin-content.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Enabled plugins now take effect in `codsh --rust`. `plugin enable|disable` (or Space in `/plugins`) adds or withdraws an installed, trusted plugin's rules, skills and commands (`/plugin:name`, plus the bare name when nothing native, built-in, or in another plugin owns it), `plugin:agent` agent types, and command hooks through the existing asset discovery and the dsh hook runner, with `GROK_PLUGIN_ROOT` / `GROK_PLUGIN_DATA` set for hooks. Project plugins also need workspace trust, enabling never grants tool permissions, and a live session picks up a change on its next prompt; update, disable, and uninstall withdraw the old content. `plugin list --json`, `inspect`, and the expanded `/plugins` row show each plugin's state (`active`, `disabled`, `blocked`, `missing`, `shadowed`), contributions, and per-plugin problems, so one broken plugin file does not hide the others. Plugin MCP servers are not started yet. diff --git a/.changeset/rust-plugin-mcp.md b/.changeset/rust-plugin-mcp.md new file mode 100644 index 00000000..872dcf54 --- /dev/null +++ b/.changeset/rust-plugin-mcp.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust`: an enabled, trusted plugin's MCP servers (`.mcp.json` or manifest `mcpServers`) now mount through the same MCP discovery, dsh client, and approval gate as your own servers, below every other source and labelled `plugin: ` in `mcp list`, `/mcps`, `/plugins`, and `plugin list --json`. Installing or enabling grants no tool permission; disabling, updating, or uninstalling a plugin forgets its servers' remembered approvals and withdraws their tools before a live session's next prompt. diff --git a/.changeset/rust-plugin-workflows.md b/.changeset/rust-plugin-workflows.md new file mode 100644 index 00000000..dc8f91ea --- /dev/null +++ b/.changeset/rust-plugin-workflows.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust`: enabled plugins can ship Rhai workflows. `.rhai` files in a plugin's `workflows/` directory join the saved-workflow catalog after project and personal workflows while the plugin is active. They always run as `/:`, and as the bare `/` when nothing else owns that name. Plugin listings, `/workflows` and `/workflows :` show them with the plugin's version, license, trust and source; installing runs nothing. Disabled or removed plugins' workflows are refused with the reason. A run started from a plugin keeps its launch script and origin, so updating the plugin changes only later launches, and a paused run resumes only while its plugin is active. diff --git a/.changeset/rust-prebuilt-install.md b/.changeset/rust-prebuilt-install.md new file mode 100644 index 00000000..74384bd0 --- /dev/null +++ b/.changeset/rust-prebuilt-install.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` from an installed package: the Rust client's dsh plugins now load harness packages from the running dsh, so `npm install -g @deepseek-ai/dsh codsh-cli` starts sessions outside this workspace. The launcher verifies the prebuilt client for this platform (manifest, SHA-256, executable CPU, and that it belongs to this `codsh-cli` version) and refuses a missing, damaged, wrong-CPU, half-updated client or a too-old dsh with the reinstall/rollback command, never falling back to another runtime. New `codsh --rust install-check [--json]` shows that verdict without starting anything; the Rust Home records the version that last used it and announces an update or rollback once; `codsh update` warns people who use `codsh --rust` when the new package cannot run it here. `build:rust` records the package version and dsh floor in `artifact.json` and accepts `--target`; `check:rust-package` verifies what `npm pack` would ship without publishing. diff --git a/.changeset/rust-queue-steer-btw.md b/.changeset/rust-queue-steer-btw.md new file mode 100644 index 00000000..0093a9df --- /dev/null +++ b/.changeset/rust-queue-steer-btw.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add queue management, send-now, steer, and `/btw` side questions to `codsh --rust`. Follow-ups typed during a turn are queued and drained one per turn in order (after the turn ends or a Ctrl+C cancel, never during a pending approval or compaction); `Ctrl+;`/`Ctrl+'` or ↑ on an empty prompt opens a pane to edit in place, reorder, delete, or send a row now. `Ctrl+Enter`/`Ctrl+I` (Apple Terminal also `Ctrl+O`, VS Code-family `Ctrl+L`) and Enter on an empty prompt cancel the running turn quietly and run that row next. `[ui] follow_up_behavior = "steer"` injects plain text follow-ups into the running dsh turn through a private, token-checked control socket, and an unclaimed steer returns to the queue. `/btw ` (also mid-message) answers from the session context with no tools and never enters the conversation; `/queue` lists the queue; `[ui] combine_queued_prompts` joins adjacent prompts. diff --git a/.changeset/rust-remote-mcp.md b/.changeset/rust-remote-mcp.md new file mode 100644 index 00000000..b7f61efb --- /dev/null +++ b/.changeset/rust-remote-mcp.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` starts remote MCP servers (streamable HTTP and legacy SSE) through its own proxy, signs in to OAuth-protected servers with `mcp login ` / `/mcps auth ` (discovery, dynamic registration, PKCE, refresh, revocation), keeps image results for dsh, and answers MCP elicitations on a TUI card or through editor `x.ai/mcp/elicit`; editors also get `x.ai/mcp/auth_status`, `auth_trigger`, and `read_resource`. diff --git a/.changeset/rust-remote-org-identity.md b/.changeset/rust-remote-org-identity.md new file mode 100644 index 00000000..38c02023 --- /dev/null +++ b/.changeset/rust-remote-org-identity.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +A remote host can require an organization identity for `codsh --rust --remote ssh://…`: `[remote_access] identity = "required"` in its `requirements.toml` makes the leader proxy admit only an access token that your own OpenID Connect provider (RFC 7662 introspection) reports active for the configured issuer, audience, and teams, checked again on every request and on a timer, with `deny_subjects`, `locked`, and fail-closed policy errors. The client sends its `codsh --rust login` session only to remotes listed in `[[remote_identity]]` with that audience, refreshes it before expiry, and both sides audit a token fingerprint, never the token. Without a policy a remote works as before. diff --git a/.changeset/rust-remote-workspace.md b/.changeset/rust-remote-workspace.md new file mode 100644 index 00000000..0ca7357b --- /dev/null +++ b/.changeset/rust-remote-workspace.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust --remote ssh://[user@]host[:port]/abs/path` drives a session on another host over SSH: the client runs `codsh --rust agent --leader stdio` there with public-key auth and a pinned host key (`BatchMode=yes`, `StrictHostKeyChecking=yes`, no agent, X11, or port forwarding, no local environment or keys), and the remote config, credentials, permission policy, and sandbox execute every turn. Local-policy flags, `@file` attachments, and images are refused, and local rules, memory, and MCP servers are not sent. A lost connection keeps the turn running there and `/reconnect` attaches to it without re-running anything; a remote restart is reported as an interrupted turn with unknown effects and is never retried. `/remote` and `codsh --rust remote check [--json]` report what the real remote offers. The official Computer Hub, cloud workspaces, and the Cursor worker stay refused because they need private infrastructure. diff --git a/.changeset/rust-resume-transcript-markers.md b/.changeset/rust-resume-transcript-markers.md new file mode 100644 index 00000000..c38bba69 --- /dev/null +++ b/.changeset/rust-resume-transcript-markers.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Fullscreen resume paints the same turn markers as minimal mode. An interrupted or cancelled turn, an empty answer, a still-open turn, and a compaction sentence stay on the transcript, the full-content view, and a later `--resume`. A completed tool whose status is unknown is not marked interrupted. An error line is not painted twice. diff --git a/.changeset/rust-sandbox-deny-resolution.md b/.changeset/rust-sandbox-deny-resolution.md new file mode 100644 index 00000000..012dc7e2 --- /dev/null +++ b/.changeset/rust-sandbox-deny-resolution.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Keep a custom sandbox profile's `deny` list when it extends `devbox`; only the global hook, config, and trust write protection is skipped. Resolve every deny path and deny-glob literal prefix through its deepest existing ancestor so Seatbelt denies the resolved path (for example under `/tmp` or a symlinked directory, including files created after launch), treat a workspace whose name contains `[`, `*`, or `?` as a literal glob root, and refuse startup when a deny path sits under a dangling symlink or contains a control character. The sandbox report lists deny globs. dsh's own per-call bash sandbox cannot nest inside the applied Seatbelt policy, so every bash command used to refuse under a profile; codsh now starts dsh with its per-call file mode at `danger-full-access` while a profile is applied, leaving approvals unchanged, and the kernel policy confines bash children and child agents. Pin a deny glob's literal-prefix directory against rename so its subtree cannot be moved out of the anchored regex. A committed probe confirms the launchd escape (`launchctl submit` / `bootstrap gui/$UID`) is kernel-blocked under a profile, matching the reference `mach-lookup` rules. `codsh --rust inspect` and `inspect --json` do not apply the sandbox, so a broken config still prints every diagnostic, including the resolved profile, instead of stopping at the first error. diff --git a/.changeset/rust-saved-workflows.md b/.changeset/rust-saved-workflows.md new file mode 100644 index 00000000..c3e8b429 --- /dev/null +++ b/.changeset/rust-saved-workflows.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` now loads saved Rhai workflows from `/.grok/workflows` (trusted folders, taking precedence) and `$GROK_HOME/workflows`. `/workflows` lists them with what is hidden or invalid, `/ [args]` and `/workflow [--agent-budget N] [--effort LEVEL] [args]` launch one in the background, the model can launch one by name and sees the listing, and `/workflow save ` keeps a finished run as a project workflow without replacing files. A running or resumed run keeps the script it launched with. diff --git a/.changeset/rust-scheduled-prompts.md b/.changeset/rust-scheduled-prompts.md new file mode 100644 index 00000000..70256273 --- /dev/null +++ b/.changeset/rust-scheduled-prompts.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` adds scheduled prompts. `/loop [interval] ` asks the model to schedule the prompt through dsh's `scheduler_create` tool (60-second minimum, 50 loops per session, 7-day expiry, overlap skip). Each fire runs as an independent background dsh subagent and reports its final status once, waking an idle session; the next fire starts fresh with that status. The status line counts loops, and the Ctrl+G tasks pane lists them and deletes the selected loop with `x`. Plain `-p`, editor ACP, the shared server, and sessions without subagents offer no scheduler. diff --git a/.changeset/rust-scheduled-restart.md b/.changeset/rust-scheduled-restart.md new file mode 100644 index 00000000..af56debb --- /dev/null +++ b/.changeset/rust-scheduled-restart.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` saves scheduled loops with their session and restores them on `--continue`, `--resume`, or `/resume`. A loop whose fires were missed while nothing ran fires once, an expired loop is removed without firing, and a fire that was running when its process ended is shown as `outcome unknown` and never run again. `durable: true` is accepted when the session has an owner. Only the client holding the session owner lock saves and fires loops, so a second client or a lost lock does not fire a loop a second time (fires are not exactly-once: an interrupted one is reported unknown, not retried); deletes and durable expiries are reported only after they are saved. Loop rows show saved, durable, paused, the last outcome, and a changed permission mode; with subagents off saved loops are listed as paused. diff --git a/.changeset/rust-searxng-web-search.md b/.changeset/rust-searxng-web-search.md new file mode 100644 index 00000000..55f82b46 --- /dev/null +++ b/.changeset/rust-searxng-web-search.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Web search can use a keyless SearXNG JSON substitute (`protocol = "searxng"`) beside the existing Responses-shaped substitute. The request is `{base}/search?q=...&format=json`. Domain allow and deny apply to result URLs and cannot be widened by a model argument. Official hosts stay refused. A local fixture covers the protocol, and one real SearXNG process on localhost was the live verification target. diff --git a/.changeset/rust-session-migration.md b/.changeset/rust-session-migration.md new file mode 100644 index 00000000..3566626a --- /dev/null +++ b/.changeset/rust-session-migration.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add `codsh --rust import sessions` to copy selected legacy codsh sessions into the isolated Rust client. Listing and preview write nothing; `--apply` opens the old dsh Home read-only and writes each session (messages, tool calls and results, titles, image and file attachments, subagent sessions) as a new session id in `~/.codsh-rust/dsh`, verified before it counts, with provenance in `session-migrations/.json` shown by `/session-info`. The old client keeps resuming its byte-identical originals and never has to read the new format; there is no live sync between the two. Skipped events, undisplayed content, unfinished turns, and missing attachments or subagent logs are reported (missing data needs `--allow-partial`); damaged logs, unsupported formats, and unknown required events are refused; a failed or interrupted copy is removed. Re-importing an unchanged session reports the existing copy, and a legacy session that changed since is a conflict until `--again`. diff --git a/.changeset/rust-ship-browser-graph.md b/.changeset/rust-ship-browser-graph.md new file mode 100644 index 00000000..819af510 --- /dev/null +++ b/.changeset/rust-ship-browser-graph.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +The optional Ship plugin for `codsh --rust` now has the browser graph: `/ship` prints one `Ship graph · · Status · 待认领/已认领/已关闭 · N of M decision answers recorded · ` line (again only when it changes) whose loopback URL opens the same live Web panorama as legacy `/ship`, joined from the same spec, snapshot, answer, and local wayfinder files on every poll, so the terminal and the page agree and a lost graph cache loses no answer. The server listens on `127.0.0.1` under a random path only, stops when the session ends, dsh exits, or the plugin is uninstalled, and resuming the session reopens the same URL so an open page reconnects. Command hooks can now print a Claude Code-style `systemMessage` and receive `CODSH_HOOK_HOST_PID`. The legacy page fetches `graph.json` relative to itself; legacy `/ship` is otherwise unchanged. diff --git a/.changeset/rust-ship-extension.md b/.changeset/rust-ship-extension.md new file mode 100644 index 00000000..d6dbd109 --- /dev/null +++ b/.changeset/rust-ship-extension.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` can now install Ship as an optional first-party plugin: `codsh --rust plugin install bundled:ship --trust` followed by `plugin enable ship` adds `/ship`, which runs the legacy pre-flight and wayfinder phase through dsh with the legacy contract, keeps the legacy spec, `.ship.json` snapshot, and `.ship.answers.json` records and guards, resumes a cancelled run with a bare `/ship`, and refuses specs past wayfinding with a pointer to legacy `codsh`. Nothing Ship-related is installed, enabled, bound to a key, or written until you opt in, and legacy `codsh` `/ship` is unchanged. diff --git a/.changeset/rust-ship-full-flow.md b/.changeset/rust-ship-full-flow.md new file mode 100644 index 00000000..8a616743 --- /dev/null +++ b/.changeset/rust-ship-full-flow.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +The optional Ship extension for `codsh --rust` now runs the whole `/ship` flow while dsh executes every agent: grill, to-spec and tickets with both gates auto-Confirmed (gate 1 seals the Mission Contract), a parallel landing wave in isolated worktrees with serial `merge --no-ff` and the legacy conflict validation, a separate final verification turn, and Merge-back. The Stop hook continues each phase, a bare `/ship` resumes any phase, a failed or cancelled child is never merged or ticked (its worktree is kept and the ticket is dispatched again on resume), a missing sealed contract stops the run, and a lost run state is rebuilt from the files. The Rust client now shows the text of a Stop-hook-continued model call, and a hook that exits without reading its input (or is killed by a cancel) no longer crashes dsh. diff --git a/.changeset/rust-stress-resources.md b/.changeset/rust-stress-resources.md new file mode 100644 index 00000000..c83080af --- /dev/null +++ b/.changeset/rust-stress-resources.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` quits faster with background work running: dsh no longer stays alive for an extra second after quit because its credentials watcher fell back to watching the whole dsh home (an empty owner-only `.credentials.yaml` is created in the isolated dsh home when none exists; an existing file is never touched, and `/logout` does not count the empty file as a stored credential). The long-lived dsh process keeps Node 22's young-generation size under newer Node, so its memory no longer grows by ~40–55 MiB over the first turns of a session. `scripts/rust-stress-bench.py` measures concurrency and long-run resources (background subagents and commands, workflow pause/resume, a 30-turn session, quit and crash under load) against the pinned reference on Linux, macOS and Windows with thresholds frozen before the candidate runs. diff --git a/.changeset/rust-subagent-messages.md b/.changeset/rust-subagent-messages.md new file mode 100644 index 00000000..d729069a --- /dev/null +++ b/.changeset/rust-subagent-messages.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` adds subagent messages and continuation. They are off by default and turned on with `[features] active_agent_messages = true` or `GROK_ACTIVE_AGENT_MESSAGES=1`. With the flag on, the model's `send_subagent_message` tool steers, queues, or interjects a message into a running child. An interject also ends the child's blocking `job_output` wait early. The same tool wakes a completed child as the same identity in a dsh job (`· attempt N`), and a child can message its parent or a sibling. Unknown or foreign ids, cancelled children, oversize text, and spent quotas get the reference refusal text, and the transcript shows `Message sent to …` / `Message rejected · …` rows. The read-only child view shows each message as a `◎ Message from parent` turn. The `subagent` tool's `resume_from` starts a new child that continues a completed child of this session, with its transcript and pinned model (the row reads `continues "…"`). Nothing survives a restart, at most 32 finished children stay resident, and worktree-isolated, workflow, scheduler, and goal-verifier children are excluded. diff --git a/.changeset/rust-terminal-clipboard-doctor.md b/.changeset/rust-terminal-clipboard-doctor.md new file mode 100644 index 00000000..0e160a1f --- /dev/null +++ b/.changeset/rust-terminal-clipboard-doctor.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Add honest clipboard delivery, a terminal doctor, `wrap`, and focus-gated notifications to `codsh --rust` (ticket 155). Every copy (`y`/`⇧Y`, cards, memory paths, `/export`, and the new `/copy [N] [path]`) tries the native tool, tmux's buffer, and OSC 52, always writes `$GROK_HOME/last-copy.txt` (or `GROK_COPY_FILE`), and only says "Copied!" when a trusted route succeeded; otherwise it says unconfirmed or unreachable and names the backup. `codsh --rust doctor [--json]` and `/doctor` report terminal, tmux, color, newline-key, and clipboard facts with finding ids and an unverified list; `doctor fix` appends tmux settings to your tmux config only after confirmation, with a backup and an undo command, and never reloads tmux. `codsh --rust wrap ` runs e.g. `ssh host` in a local PTY, forwards its OSC 52 copies to this machine, and restores terminal modes when it exits or the connection drops. `[ui.notifications]` (method, condition, idle threshold, events, hooks) uses DECSET 1004 focus reports, an SSH session shows a one-time `/doctor` tip, and `GROK_EXIT_TIMEOUT_SECS` bounds a hanging quit. macOS/Windows clipboard tools, real terminals, real tmux, and real SSH are not verified here. diff --git a/.changeset/rust-update-installer.md b/.changeset/rust-update-installer.md new file mode 100644 index 00000000..6149710e --- /dev/null +++ b/.changeset/rust-update-installer.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust update` updates (or with `--to`, rolls back) the install with the package manager that installed it — npm, pnpm, Yarn or Bun — and verifies the new Rust client before reporting success; `codsh update` uses the same installer selection. `codsh --rust --version` now reports the `codsh-cli` version, and releases carry the macOS (arm64, Intel) and Linux x64 clients built on their own CI runners. diff --git a/.changeset/rust-usage-cost.md b/.changeset/rust-usage-cost.md new file mode 100644 index 00000000..10283765 --- /dev/null +++ b/.changeset/rust-usage-cost.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +Show session token usage in `codsh --rust`. `/usage` (alias `/cost`), `/session-info`, the status line command payload, headless `json` / `streaming-json` / `streaming-messages-json` results, and the new `codsh --rust usage [turn]` command read one ledger folded from the dsh session log: input with cache reads and writes, output with reasoning, totals, model calls, API time, per-model rows, and subagent sessions folded into the turn that spawned them. The whole log is folded, so a resume never double-counts; a fork keeps its inherited history as in the reference, and a `-p` result reports only its own turns. A call with no reported usage, an interrupted or failed turn, or a missing or running subagent log marks the ledger incomplete instead of adding zeros. Auxiliary calls (session title, compaction summary, `/btw`, memory) are not counted. dsh reports no cost and no price table is used, so cost is shown as not available and headless output says `cost_status: "unknown"`; nothing is estimated and `$0` is never shown. Checked with the keyless mock provider and real dsh on Linux only. diff --git a/.changeset/rust-video-generation.md b/.changeset/rust-video-generation.md new file mode 100644 index 00000000..8ef4d04a --- /dev/null +++ b/.changeset/rust-video-generation.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': minor +'codsh-bundle': minor +--- + +`codsh --rust` generates videos through a video service you configure: `[models] video_gen` names a `[model.]` with `base_url` and `supports_video_generation`, speaking the reference `xai` async video API or the native job API of a local stable-diffusion.cpp `sd-server` (`protocol = "sdcpp"`). Without one, no video tool exists; official hosts are refused. The service's durations, resolutions, tools and (sdcpp) frame rate and format are declared in config, shown in the tool schemas, and checked before any request, so an unsupported value fails with the reason. The model's `image_to_video` / `reference_to_video` tools and `/imagine-video ` run through dsh, and every job asks first with a card naming the length, resolution, prompt, host, how many images are sent, and that codsh does not know the service's price. Each job is recorded under `/video-jobs/` before it starts; the row shows queued/generating state and elapsed time; failed, expired, refused, malformed, timed-out and cancelled jobs are distinct and save nothing, and Ctrl+C records whether the service confirmed the cancel. Finished videos are checked and saved atomically as `/videos/.`; `/videos` lists jobs and videos, `/videos open|status|cancel` open a video, finish a job nobody follows (including after `--resume`, which names unfinished jobs) or cancel it. `codsh --rust video generate|reference|list|status|cancel` uses the same service outside a session. diff --git a/.changeset/rust-web-cancel-and-policy.md b/.changeset/rust-web-cancel-and-policy.md new file mode 100644 index 00000000..5ce1d669 --- /dev/null +++ b/.changeset/rust-web-cancel-and-policy.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Cancel an in-flight web search or fetch from dsh without returning a late body. Redirects recheck the full allow entry, an http proxy tunnels with CONNECT, and managed web defaults stay user-overridable under a requirements pin. diff --git a/.changeset/rust-web-framing-and-body.md b/.changeset/rust-web-framing-and-body.md new file mode 100644 index 00000000..9dfbac98 --- /dev/null +++ b/.changeset/rust-web-framing-and-body.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Read chunked web responses by chunk size so a payload containing the terminator is not truncated, and finish a zero-size chunk after its trailer fields and blank line. Pass the fetched page as its own JSON field, and label managed-only web settings as managed. diff --git a/.changeset/rust-windows-prebuilt.md b/.changeset/rust-windows-prebuilt.md new file mode 100644 index 00000000..5cedc4c7 --- /dev/null +++ b/.changeset/rust-windows-prebuilt.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +`codsh --rust` now ships a native Windows x64 client (`win32-x64`, static MSVC runtime) and runs in a Windows console: typing (including CJK and characters that arrive as Alt codes), file approvals, dsh's `pwsh` shell tool, cancelling a command together with its process tree, resume, and install/update/rollback of the Rust Home. The codsh sandbox profiles are refused on Windows; the shared server, `--remote` and `wrap` are not available there. diff --git a/.changeset/rust-workflow-background-runs.md b/.changeset/rust-workflow-background-runs.md new file mode 100644 index 00000000..6fc6b15c --- /dev/null +++ b/.changeset/rust-workflow-background-runs.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Workflows in the isolated Rust client now run in the background of their session. The `workflow` tool returns as soon as the script starts, each run gets a session-unique display name (`name`, `name-2`, …), and its completion comes back once as a notice turn with the status and result. `/workflow runs` lists the session's runs with phases, agent counts, and elapsed time; `/workflow pause|resume|stop ` (and the tool's `pause`, `stop`, and `resume` sources) control one by name. Pause and stop cancel the run's children. Resume replays the run's journal from its original script and args, so finished agents are not run again, but an unfinished step runs again and nothing is exactly-once. A budget stop resumes only with a higher agent budget. Runs resume only in the process that started them: after a restart they are listed and refused, and an active one shows as interrupted. The tasks pane lists workflow runs, the status line counts active ones, and a session runs at most 4 at once. A plain `-p` prompt still waits for its run. Launching a saved workflow by name and `/workflow save` are refused. diff --git a/.changeset/rust-workflow-parallel-budget.md b/.changeset/rust-workflow-parallel-budget.md new file mode 100644 index 00000000..8b44348d --- /dev/null +++ b/.changeset/rust-workflow-parallel-budget.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Complete the workflow host contracts in `codsh --rust`. `output_schema` asks a workflow child for a ```json answer that matches the schema (validated with the reference `jsonschema` crate), gives a missed answer one correction turn in the same dsh child, and returns the parsed JSON or `success: false`. `write_scratch_file`, `read_scratch_file`, and `git_diff_since` work with the reference limits, with scratch files kept under the session directory. The live-children cap of a run is configurable with `[subagents] workflow_max_concurrent` or `GROK_WORKFLOW_MAX_CONCURRENT_AGENTS` and stays separate from the cumulative `agent_budget`; a panel over the budget is still refused whole. Failed, refused, cancelled, and never-started children are never reported as successful, and every child is closed when the run ends, fails, or is cancelled. `resume_from` stays refused. Only Linux was exercised. diff --git a/.changeset/rust-workflow-rhai.md b/.changeset/rust-workflow-rhai.md new file mode 100644 index 00000000..7ba05d9f --- /dev/null +++ b/.changeset/rust-workflow-rhai.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Run Rhai workflows in `codsh --rust`. The model's `workflow` tool runs a reference-format script (inline, or a `.rhai` file in a trusted project or `$GROK_HOME/workflows`) in the vendored reference engine, and every `agent()` / `parallel()` call starts a real dsh subagent with the script's model, effort, agent type, and capability. Args, the cumulative agent budget, phases, logs, and the result follow the reference; syntax errors, invalid metadata, child failures, endless scripts, and Ctrl+C end the run within bounds. The tool block shows the run name, phase, and agent count, and `/tasks` tags its children. Runs are foreground only; named workflows, resume/pause/stop, `output_schema`, scratch files, and `git_diff_since` are refused with explicit errors, and child agents cannot start workflows. The engine limits are not a security sandbox. Only Linux was exercised. diff --git a/.changeset/session-title-typed-prompt.md b/.changeset/session-title-typed-prompt.md new file mode 100644 index 00000000..74d749b9 --- /dev/null +++ b/.changeset/session-title-typed-prompt.md @@ -0,0 +1,6 @@ +--- +'codsh-cli': patch +'codsh-bundle': patch +--- + +Fix `codsh --rust` session titles: a session whose first turn carried local memory (or project rules, agent definitions, or an expanded skill body) was titled ` Local memory notes the` or ` …`, because the title came from the whole first message codsh sent. `/resume`, `sessions list`, the dashboard, `/rename --auto`, and exports now use the words you typed, dsh's stored fallback title is no longer shown, and resuming such a session shows its first turn instead of dropping it. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83590353..1c87b043 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,18 @@ permissions: id-token: write jobs: + # codsh-cli carries the prebuilt Rust client for each platform in + # native/-/ (ticket 66 / #198). The directory is gitignored, + # so each platform is built on its own runner here and staged below before + # anything is published; a package missing one of them fails the check + # instead of shipping `codsh --rust` broken on that platform. + native: + uses: ./.github/workflows/rust-native.yml + with: + retention-days: 30 + release: + needs: native runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -25,6 +36,17 @@ jobs: cache: pnpm registry-url: https://registry.npmjs.org - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + pattern: native-* + path: natives + merge-multiple: true + - name: Stage the Rust client for every platform + run: | + mkdir -p packages/cli/native + for archive in natives/native-*.tgz; do tar -C packages/cli/native -xzf "$archive"; done + node scripts/build-ship-extension.mjs + pnpm run check:rust-package -- --require darwin-arm64,darwin-x64,linux-x64,linux-arm64,win32-x64 - name: Create release PR or publish to npm uses: changesets/action@v1 with: diff --git a/.github/workflows/rust-native.yml b/.github/workflows/rust-native.yml new file mode 100644 index 00000000..d553fdcf --- /dev/null +++ b/.github/workflows/rust-native.yml @@ -0,0 +1,103 @@ +# Build the prebuilt Rust client for every platform codsh-cli ships (#198; +# Linux arm64 #199, Windows x64 #200). +# +# Reusable: rust-platforms.yml calls it for evidence on the rewrite branch and +# ci/** branches, and release.yml calls it so a publish carries every native +# directory instead of none. Each job runs `pnpm run build:rust` on a machine +# of that platform (macOS targets need Apple clang and the SDK) and uploads the +# staged `packages/cli/native/` as a tarball, which keeps the executable +# bit that artifact zips drop. Nothing is packed, published, tagged or signed +# here; macOS binaries carry only the linker's ad-hoc signature. +name: Rust native build + +on: + workflow_call: + inputs: + retention-days: + type: number + default: 14 + +permissions: + contents: read + +jobs: + native: + name: native ${{ matrix.key }} + strategy: + fail-fast: false + matrix: + include: + - key: darwin-arm64 + os: macos-15 + - key: darwin-x64 + os: macos-15-intel + # The oldest supported Ubuntu images keep the glibc floor low (#199); + # OpenSSL is compiled in (vendored), so libssl is not a requirement. + - key: linux-x64 + os: ubuntu-22.04 + - key: linux-arm64 + os: ubuntu-22.04-arm + # Native Windows (#200): MSVC toolchain, schannel TLS, no sandbox. + - key: win32-x64 + os: windows-2022 + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - name: Rust 1.98.1 + run: | + rustup toolchain install 1.98.1 --profile minimal + rustup default 1.98.1 + rustc -vV + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + key: ${{ matrix.key }} + - run: pnpm install --frozen-lockfile + - name: Build and stage the Rust client + run: pnpm run build:rust + - name: Describe the staged client + shell: bash + run: | + set -x + uname -a + node -p 'process.platform + "-" + process.arch + " " + process.version' + cat packages/cli/native/${{ matrix.key }}/artifact.json + if [ "$RUNNER_OS" = Windows ]; then + ls -l packages/cli/native/${{ matrix.key }}/codsh-rust.exe + packages/cli/native/${{ matrix.key }}/codsh-rust.exe --version + # Imported DLLs: system ones only (no OpenSSL, no MSVC runtime DLLs beyond the UCRT). + vswhere="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" + dumpbin="$("$vswhere" -latest -find 'VC/Tools/MSVC/**/bin/Hostx64/x64/dumpbin.exe' | head -1)" + "$dumpbin" -nologo -dependents packages/cli/native/${{ matrix.key }}/codsh-rust.exe | tee dependents.txt + # The C runtime is linked statically (crt-static): no VC++ Redistributable needed. + if grep -qiE 'vcruntime|msvcp|api-ms-win-crt' dependents.txt; then echo "the client imports the MSVC runtime"; exit 1; fi + exit 0 + fi + file packages/cli/native/${{ matrix.key }}/codsh-rust + ls -l packages/cli/native/${{ matrix.key }}/codsh-rust + packages/cli/native/${{ matrix.key }}/codsh-rust --version + if [ "$RUNNER_OS" = macOS ]; then + sw_vers + otool -L packages/cli/native/${{ matrix.key }}/codsh-rust + vtool -show-build packages/cli/native/${{ matrix.key }}/codsh-rust || true + codesign -dv packages/cli/native/${{ matrix.key }}/codsh-rust 2>&1 || true + else + ldd packages/cli/native/${{ matrix.key }}/codsh-rust + objdump -T packages/cli/native/${{ matrix.key }}/codsh-rust | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1 + fi + - name: Pack the staged directory + run: tar -C packages/cli/native -czf native-${{ matrix.key }}.tgz ${{ matrix.key }} + - uses: actions/upload-artifact@v4 + with: + name: native-${{ matrix.key }} + path: native-${{ matrix.key }}.tgz + retention-days: ${{ inputs.retention-days }} + if-no-files-found: error diff --git a/.github/workflows/rust-platforms.yml b/.github/workflows/rust-platforms.yml new file mode 100644 index 00000000..ba5becfd --- /dev/null +++ b/.github/workflows/rust-platforms.yml @@ -0,0 +1,625 @@ +# Platform evidence for the Rust client (#198 macOS install/update/rollback; +# later #199 Linux, #200 Windows, #201/#202/#209). Runs on the rewrite +# integration branch and on ci/** branches pushed for a ticket's evidence. +# It builds every native directory, checks the multi-platform package layout, +# and installs the packed product on runners whose Rust toolchain has been +# removed. It publishes, tags and releases nothing. +name: Rust platforms + +on: + push: + branches: + - rewrite/grok-dsh-20260920 + - 'ci/**' + +permissions: + contents: read + +concurrency: + group: rust-platforms-${{ github.ref }} + cancel-in-progress: true + +jobs: + native: + uses: ./.github/workflows/rust-native.yml + + package: + name: package layout (all natives) + needs: native + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + pattern: native-* + path: natives + merge-multiple: true + - name: Stage every native directory + run: | + mkdir -p packages/cli/native + for archive in natives/native-*.tgz; do tar -C packages/cli/native -xzf "$archive"; done + node scripts/build-ship-extension.mjs + - name: Check the package a release would publish (dry run) + run: | + mkdir -p packed + pnpm run --silent check:rust-package -- --require darwin-arm64,darwin-x64,linux-x64,linux-arm64,win32-x64 --json | tee packed/package-check.json + - name: Pack codsh-cli (not published) + run: | + (cd packages/cli && npm pack --ignore-scripts --pack-destination ../../packed) + ls -l packed + tar -tzf packed/codsh-cli-*.tgz | grep -E '^package/native/[^/]+/(codsh-rust|artifact.json)$' + - uses: actions/upload-artifact@v4 + with: + name: codsh-cli-package + path: packed/ + if-no-files-found: error + + macos-install: + name: macOS install ${{ matrix.name }} + needs: [native, package] + # Every platform on the integration branch; on a ci/** evidence branch + # only the platform its name carries (ci/198-macos-…), to spare runners. + if: github.ref_name == 'rewrite/grok-dsh-20260920' || contains(github.ref_name, 'macos') + strategy: + fail-fast: false + matrix: + include: + - name: arm64 + os: macos-15 + rosetta: false + - name: x64 (Intel) + os: macos-15-intel + rosetta: false + - name: x64 Node under Rosetta + os: macos-15 + rosetta: true + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + # dsh for the install test comes from this checkout, as it does locally. + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + pattern: native-* + path: natives + merge-multiple: true + - uses: actions/download-artifact@v4 + with: + name: codsh-cli-package + path: packed + - name: Stage the natives a release would carry + run: | + mkdir -p packages/cli/native + for archive in natives/native-*.tgz; do tar -C packages/cli/native -xzf "$archive"; done + node scripts/build-ship-extension.mjs + - name: Remove the Rust toolchain + run: | + rm -rf "$HOME/.cargo" "$HOME/.rustup" + # The image also links Homebrew's rustup; take every entry off PATH. + for tool in cargo rustc rustup rustup-init; do + while found=$(command -v "$tool"); do + echo "removing $found" + rm -f "$found" 2>/dev/null || sudo rm -f "$found" + hash -r + done + done + if command -v cargo || command -v rustc || command -v rustup; then echo "a Rust toolchain is still on PATH"; exit 1; fi + echo "no cargo, rustc or rustup on PATH" + - name: Use an x64 Node under Rosetta + if: matrix.rosetta + run: | + /usr/bin/pgrep -q oahd || sudo softwareupdate --install-rosetta --agree-to-license + echo "CODSH_TEST_TOOL_NODE=$(node -p process.execPath)" >> "$GITHUB_ENV" + version=$(node -p process.version) + curl -fsSLO "https://nodejs.org/dist/$version/node-$version-darwin-x64.tar.gz" + curl -fsSL "https://nodejs.org/dist/$version/SHASUMS256.txt" | grep " node-$version-darwin-x64.tar.gz\$" | shasum -a 256 -c - + mkdir -p "$RUNNER_TEMP/node-x64" + tar -C "$RUNNER_TEMP/node-x64" --strip-components=1 -xzf "node-$version-darwin-x64.tar.gz" + echo "$RUNNER_TEMP/node-x64/bin" >> "$GITHUB_PATH" + - name: dsh from the registry, as a user installs it + # The version this codsh-cli was tested with (codsh.testedDsh), with + # the job's own Node (x64 under Rosetta: dsh loads per-arch addons). + run: | + tested=$(node -p "require('./packages/cli/package.json').codsh.testedDsh") + node -p process.arch + npm install -g --prefix "$RUNNER_TEMP/dsh-registry" --no-audit --no-fund "@deepseek-ai/dsh@$tested" + echo "CODSH_INSTALL_TEST_DSH=$RUNNER_TEMP/dsh-registry/lib/node_modules/@deepseek-ai/dsh" >> "$GITHUB_ENV" + echo "CODSH_TESTED_DSH_VERSION=$tested" >> "$GITHUB_ENV" + - name: Record the environment + run: | + mkdir -p evidence + { + sw_vers + uname -a + echo "hardware: $(sysctl -n machdep.cpu.brand_string) hw.optional.arm64=$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" + echo "node: $(command -v node) $(node -p 'process.version + " " + process.platform + "-" + process.arch')" + echo "node translated by Rosetta: $(node -e "const r=require('child_process').spawnSync('sysctl',['-in','sysctl.proc_translated'],{encoding:'utf8'});process.stdout.write((r.stdout||'').trim()||'0')")" + echo "npm: $(npm -v) python3: $(python3 -V)" + echo "cargo on PATH: $(command -v cargo || echo none)" + } | tee evidence/environment.txt + - name: Launcher explains a missing x64 client under Rosetta + if: matrix.rosetta + run: | + copy="$RUNNER_TEMP/arm64-only" + mkdir -p "$copy/native" + cp -R packages/cli/bin packages/cli/package.json "$copy/" + cp -R packages/cli/native/darwin-arm64 "$copy/native/" + set +e + node "$copy/bin/codsh.mjs" --rust install-check > evidence/rosetta-missing.txt 2>&1 + status=$? + set -e + cat evidence/rosetta-missing.txt + test "$status" = 1 + grep -q 'Rosetta' evidence/rosetta-missing.txt + - name: Install the packed release tarball into a clean prefix + run: | + prefix="$RUNNER_TEMP/clean-prefix" + export npm_config_cache="$RUNNER_TEMP/npm-cache" + tarball=$(ls "$PWD"/packed/codsh-cli-*.tgz) + echo "installing $tarball" + # The README's one-liner, with the packed tarball for codsh-cli. + npm install -g --prefix "$prefix" --no-audit --no-fund "@deepseek-ai/dsh@$CODSH_TESTED_DSH_VERSION" "$tarball" + HOME="$RUNNER_TEMP/clean-home" && mkdir -p "$HOME" && export HOME + cd "$RUNNER_TEMP" + PATH="$prefix/bin:$PATH" "$prefix/bin/codsh" --rust install-check --json | tee "$GITHUB_WORKSPACE/evidence/clean-install-check.json" + node -e "const r=require('$GITHUB_WORKSPACE/evidence/clean-install-check.json'); if (!r.ok || !r.dsh.entry.startsWith('$prefix') || r.dsh.version !== process.env.CODSH_TESTED_DSH_VERSION) { console.error('install-check did not use the registry dsh'); process.exit(1) }" + cd "$GITHUB_WORKSPACE" + "$prefix/bin/codsh" --rust --version | tee evidence/clean-version.txt + grep -q "codsh-rust $(node -p "require('./packages/cli/package.json').version") " evidence/clean-version.txt + - name: Install, update, break and roll back the packed product + run: | + python3 scripts/rust-install-test.py | tee evidence/rust-install-test.txt + cp /tmp/codsh-rust-install-out-*/result.json evidence/rust-install-result.json + - name: Installed-product PTY, isolation and network audit + if: ${{ !matrix.rosetta }} + run: python3 scripts/rust-pty-test.py --output "$PWD/evidence/rust-pty" + - name: First-phase flows on the installed product (turn, approval, cancel, resume, legacy Homes) + if: ${{ !cancelled() && !matrix.rosetta }} + run: | + status=0 + for test in turn permission cancel resume legacy-home; do + echo "::group::rust-$test-pty-test.py" + if python3 "scripts/rust-$test-pty-test.py" > "evidence/rust-$test-pty-test.txt" 2>&1; then echo "PASS rust-$test-pty-test.py"; else echo "FAIL rust-$test-pty-test.py"; status=1; fi + tail -n 20 "evidence/rust-$test-pty-test.txt" + echo "::endgroup::" + done + exit $status + - uses: actions/upload-artifact@v4 + if: always() + with: + name: macos-evidence-${{ matrix.os }}-${{ matrix.rosetta && 'rosetta' || 'native' }} + path: evidence + if-no-files-found: warn + + linux-platform: + name: Linux ${{ matrix.name }} + needs: [native, package] + if: github.ref_name == 'rewrite/grok-dsh-20260920' || contains(github.ref_name, 'linux') + strategy: + fail-fast: false + matrix: + include: + - name: x64 Ubuntu 22.04 + os: ubuntu-22.04 + - name: x64 Ubuntu 24.04 + os: ubuntu-24.04 + - name: arm64 Ubuntu 22.04 + os: ubuntu-22.04-arm + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + pattern: native-* + path: natives + merge-multiple: true + - name: Stage the natives a release would carry + run: | + mkdir -p packages/cli/native + for archive in natives/native-*.tgz; do tar -C packages/cli/native -xzf "$archive"; done + node scripts/build-ship-extension.mjs + - name: Remove the Rust toolchain + run: | + rm -rf "$HOME/.cargo" "$HOME/.rustup" + for tool in cargo rustc rustup; do + while found=$(command -v "$tool"); do rm -f "$found" 2>/dev/null || sudo rm -f "$found"; hash -r; done + done + if command -v cargo || command -v rustc || command -v rustup; then exit 1; fi + echo "no cargo, rustc or rustup on PATH" + - name: dsh from the registry, as a user installs it + run: | + tested=$(node -p "require('./packages/cli/package.json').codsh.testedDsh") + npm install -g --prefix "$RUNNER_TEMP/dsh-registry" --no-audit --no-fund "@deepseek-ai/dsh@$tested" + echo "CODSH_INSTALL_TEST_DSH=$RUNNER_TEMP/dsh-registry/lib/node_modules/@deepseek-ai/dsh" >> "$GITHUB_ENV" + - name: Platform run (install-check, install/update/rollback, turn, approval, cancel, resume) + run: python3 scripts/rust-platform-test.py --output "$PWD/evidence" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: linux-evidence-${{ matrix.os }} + path: evidence + if-no-files-found: warn + + linux-containers: + name: Linux clean system ${{ matrix.image }} + needs: [native, package] + if: github.ref_name == 'rewrite/grok-dsh-20260920' || contains(github.ref_name, 'linux') + strategy: + fail-fast: false + matrix: + include: + # Supported: glibc at or above the recorded floor, no libssl, no Rust. + - image: node:22-bookworm-slim + expect: ok + python: apt + - image: node:24-trixie-slim + expect: ok + python: apt + - image: fedora:41 + expect: ok + python: dnf + # Refused with the fix, before any Home is created. + - image: node:22-bullseye-slim + expect: glibc + - image: node:22-alpine + expect: libc + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + pattern: native-* + path: natives + merge-multiple: true + - name: Stage the natives a release would carry + run: | + mkdir -p packages/cli/native + for archive in natives/native-*.tgz; do tar -C packages/cli/native -xzf "$archive"; done + node scripts/build-ship-extension.mjs + - name: Run in ${{ matrix.image }} + env: + IMAGE: ${{ matrix.image }} + EXPECT: ${{ matrix.expect }} + PYTHON: ${{ matrix.python }} + run: | + mkdir -p evidence + cat > evidence/run.sh <<'SCRIPT' + set -eu + case "${PYTHON:-}" in + apt) apt-get update -qq && apt-get install -y -qq python3 >/dev/null ;; + dnf) dnf install -y -q nodejs npm python3 >/dev/null ;; + esac + cat /etc/os-release | head -2 + node -p 'process.version + " " + process.arch + " glibc=" + (process.report.getReport().header.glibcVersionRuntime || "none")' + ldd --version 2>&1 | head -1 || true + for tool in cargo rustc rustup; do if command -v "$tool"; then echo "unexpected $tool"; exit 1; fi; done + ls /usr/lib/x86_64-linux-gnu/libssl.so.3 /usr/lib64/libssl.so.3 2>/dev/null || echo "no libssl.so.3 on this system" + if [ "$EXPECT" = ok ]; then + # The dsh a user installs on this system (codsh.testedDsh from the registry). + tested=$(node -p "require('./packages/cli/package.json').codsh.testedDsh") + npm install -g --prefix /tmp/dsh-registry --no-audit --no-fund "@deepseek-ai/dsh@$tested" >/dev/null + export CODSH_INSTALL_TEST_DSH=/tmp/dsh-registry/lib/node_modules/@deepseek-ai/dsh + python3 scripts/rust-platform-test.py --only install-check,install --output "$PWD/evidence/platform" + else + work=$(mktemp -d) + (cd packages/cli && npm pack --ignore-scripts --pack-destination "$work" >/dev/null) + npm install -g --prefix "$work/prefix" --offline --ignore-scripts --no-audit --no-fund "$work"/codsh-cli-*.tgz >/dev/null 2>&1 + export HOME="$work/home" && mkdir -p "$HOME" + set +e + "$work/prefix/bin/codsh" --rust install-check --json > evidence/install-check.json + check=$? + "$work/prefix/bin/codsh" --rust -p hello > evidence/launch.txt 2>&1 + launch=$? + set -e + cat evidence/install-check.json evidence/launch.txt + test "$check" = 1 && test "$launch" = 1 + node -e "const r=require('./evidence/install-check.json'); if (r.runtime.code !== process.env.EXPECT) { console.error('expected', process.env.EXPECT, 'got', r.runtime.code); process.exit(1) }" + test ! -e "$HOME/.codsh-rust" && echo "no Rust Home was created" + fi + SCRIPT + docker run --rm -e EXPECT -e PYTHON -v "$PWD:$PWD" -w "$PWD" "$IMAGE" sh evidence/run.sh 2>&1 | tee evidence/container.log + exit "${PIPESTATUS[0]}" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: linux-container-evidence-${{ strategy.job-index }} + path: evidence + if-no-files-found: warn + + # Native Windows (#200): the packed product installed with npm on a runner + # without a Rust toolchain, driven through ConPTY (pywinpty) inside cmd.exe. + windows: + name: Windows ${{ matrix.os }} + needs: [native, package] + if: github.ref_name == 'rewrite/grok-dsh-20260920' || contains(github.ref_name, 'windows') + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pnpm install --frozen-lockfile + - run: python -m pip install pywinpty==2.0.15 + - uses: actions/download-artifact@v4 + with: + name: codsh-cli-package + path: packed + - name: Remove the Rust toolchain + shell: pwsh + run: | + Remove-Item -Recurse -Force "$env:USERPROFILE\.cargo", "$env:USERPROFILE\.rustup" -ErrorAction SilentlyContinue + foreach ($tool in 'cargo', 'rustc', 'rustup') { + if (Get-Command $tool -ErrorAction SilentlyContinue) { Write-Error "$tool still on PATH"; exit 1 } + } + "no cargo, rustc or rustup on PATH" + - name: Windows run through ConPTY + shell: pwsh + run: | + $package = (Get-ChildItem packed\codsh-cli-*.tgz | Select-Object -First 1).FullName + python scripts/rust-windows-pty-test.py --package $package --output "$PWD\evidence" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: windows-evidence-${{ matrix.os }} + path: evidence + if-no-files-found: warn + + macos-perf: + # Interaction performance on the installed product against the pinned + # reference (#202). Only on ci/** branches whose name carries "perf": + # a full run takes most of an hour of a macOS runner. The reference binary + # is downloaded, SHA-256 checked and never uploaded. + name: macOS perf (reference baseline, frozen thresholds, candidate) + needs: [native, package] + if: contains(github.ref_name, 'perf') + runs-on: macos-15 + timeout-minutes: 150 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: actions/download-artifact@v4 + with: + name: codsh-cli-package + path: packed + - name: Install the packed product with the registry dsh, as a user does + run: | + tested=$(node -p "require('./packages/cli/package.json').codsh.testedDsh") + prefix="$RUNNER_TEMP/perf-prefix" + npm install -g --prefix "$prefix" --no-audit --no-fund "@deepseek-ai/dsh@$tested" "$(ls "$PWD"/packed/codsh-cli-*.tgz)" + echo "PERF_LAUNCHER=$prefix/lib/node_modules/codsh-cli/bin/codsh.mjs" >> "$GITHUB_ENV" + mkdir -p "$RUNNER_TEMP/perf-check-home" + HOME="$RUNNER_TEMP/perf-check-home" "$prefix/bin/codsh" --rust install-check + - name: Fetch the pinned reference (checked, not redistributed) + run: | + mkdir -p "$RUNNER_TEMP/reference" + curl -fsSL -o "$RUNNER_TEMP/reference/grok" https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-1.0.34-macos-aarch64 + chmod +x "$RUNNER_TEMP/reference/grok" + shasum -a 256 "$RUNNER_TEMP/reference/grok" + - name: Bench driver + run: | + python3 -m venv "$RUNNER_TEMP/perf-venv" + "$RUNNER_TEMP/perf-venv/bin/pip" install --quiet 'pyte==0.8.2' + - name: Reference baseline, freeze, candidate (alternating with reference controls) + run: | + "$RUNNER_TEMP/perf-venv/bin/python" scripts/rust-perf-bench.py \ + --reference "$RUNNER_TEMP/reference/grok" --launcher "$PERF_LAUNCHER" \ + --runs "${PERF_RUNS:-30}" --cold-runs "${PERF_COLD_RUNS:-30}" --purge --output "$PWD/evidence/perf" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: macos-perf + path: evidence/perf + if-no-files-found: warn + + stress: + # Concurrency and long-run resource stress against the pinned reference + # (#209): concurrent background subagents + a background command, an + # inline workflow with pause/resume/stop, a 30-turn session, quit and + # crash under load. Thresholds are frozen from this runner's reference + # samples before the candidate starts. Only on ci/** branches whose name + # carries "stress". The reference binary is downloaded, SHA-256 checked + # and never uploaded. + name: Stress (${{ matrix.name }}) + needs: [native, package] + if: contains(github.ref_name, 'stress') + strategy: + fail-fast: false + matrix: + include: + - name: linux-x64 + os: ubuntu-22.04 + - name: macos-arm64 + os: macos-15 + - name: windows-x64 + os: windows-2022 + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: actions/download-artifact@v4 + with: + name: codsh-cli-package + path: packed + - name: Install the packed product with the registry dsh, as a user does + shell: bash + run: | + tested=$(node -p "require('./packages/cli/package.json').codsh.testedDsh") + prefix="$RUNNER_TEMP/stress-prefix" + npm install -g --prefix "$prefix" --no-audit --no-fund "@deepseek-ai/dsh@$tested" "$(ls "$PWD"/packed/codsh-cli-*.tgz)" + if [ "$RUNNER_OS" = "Windows" ]; then + echo "STRESS_LAUNCHER=$prefix/node_modules/codsh-cli/bin/codsh.mjs" >> "$GITHUB_ENV" + else + echo "STRESS_LAUNCHER=$prefix/lib/node_modules/codsh-cli/bin/codsh.mjs" >> "$GITHUB_ENV" + fi + - name: Bench driver + shell: bash + run: | + python -m pip install --quiet 'pyte==0.8.2' 'psutil==7.2.2' + if [ "$RUNNER_OS" = "Windows" ]; then python -m pip install --quiet pywinpty==2.0.15; fi + - name: Fetch the pinned reference (checked, not redistributed) + shell: bash + run: | + reference=$(python scripts/rust-stress-bench.py --fetch-reference "$RUNNER_TEMP/reference") + echo "STRESS_REFERENCE=$reference" >> "$GITHUB_ENV" + - name: Reference baseline, freeze, candidate + shell: bash + run: | + python scripts/rust-stress-bench.py --reference "$STRESS_REFERENCE" --launcher "$STRESS_LAUNCHER" \ + --runs "${STRESS_RUNS:-5}" --crash-runs "${STRESS_CRASH_RUNS:-2}" --output evidence/stress + - uses: actions/upload-artifact@v4 + if: always() + with: + name: stress-${{ matrix.name }} + path: evidence/stress + if-no-files-found: warn + + capability-matrix: + # Three-platform capability matrix (#201). Records OS/terminal versions and + # runs the installed-product checks: keys, mouse, shell, cancel, screen + # modes, terminal restore, the real clipboard (pasteboard / X11 under + # xvfb / Windows), voice doctor without fixtures, sandbox profiles, SSH, + # real tmux and the Windows ConPTY harness. Missing devices and refused + # features are recorded as refused/unavailable, never as a silent pass. + # Runs on the integration branch and on branches whose name has "matrix". + name: Capability matrix (${{ matrix.name }}) + needs: [native, package] + if: github.ref == 'refs/heads/rewrite/grok-dsh-20260920' || contains(github.ref_name, 'matrix') + strategy: + fail-fast: false + matrix: + include: + - name: linux-x64 + os: ubuntu-22.04 + - name: macos-arm64 + os: macos-15 + - name: windows-x64 + os: windows-2022 + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + pattern: native-* + path: natives + merge-multiple: true + - uses: actions/download-artifact@v4 + with: + name: codsh-cli-package + path: packed + - name: Stage the natives a release would carry + shell: bash + run: | + mkdir -p packages/cli/native + for archive in natives/native-*.tgz; do tar -C packages/cli/native -xzf "$archive"; done + node scripts/build-ship-extension.mjs + - name: Terminal tools (tmux, X11 clipboard, OpenSSH) + if: runner.os == 'Linux' + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq tmux xclip xvfb openssh-server openssh-client >/dev/null + tmux -V; xclip -version 2>&1 | head -1; /usr/sbin/sshd -V 2>&1 | head -1 || true + - name: Terminal tools (tmux) + if: runner.os == 'macOS' + run: | + command -v tmux || brew install tmux + tmux -V + - name: Install the packed product with the registry dsh, as a user does + shell: bash + run: | + tested=$(node -p "require('./packages/cli/package.json').codsh.testedDsh") + prefix="$RUNNER_TEMP/matrix-prefix" + npm install -g --prefix "$prefix" --no-audit --no-fund "@deepseek-ai/dsh@$tested" "$(ls "$PWD"/packed/codsh-cli-*.tgz)" + if [ "$RUNNER_OS" = "Windows" ]; then + echo "CODSH_MATRIX_LAUNCHER=$prefix/node_modules/codsh-cli/bin/codsh.mjs" >> "$GITHUB_ENV" + else + echo "CODSH_MATRIX_LAUNCHER=$prefix/lib/node_modules/codsh-cli/bin/codsh.mjs" >> "$GITHUB_ENV" + fi + - name: Windows ConPTY driver + if: runner.os == 'Windows' + shell: pwsh + run: | + python -m pip install pywinpty==2.0.15 + New-Item -ItemType Directory -Force evidence | Out-Null + python scripts/rust-windows-pty-test.py --package (Get-ChildItem packed/codsh-cli-*.tgz).FullName --output "$PWD\evidence" + - name: Capability matrix + if: ${{ !cancelled() }} + shell: bash + run: | + mkdir -p evidence + if [ "$RUNNER_OS" = "Linux" ]; then + xvfb-run -a python scripts/rust-capability-matrix.py --output "$PWD/evidence" + else + python scripts/rust-capability-matrix.py --output "$PWD/evidence" + fi + - uses: actions/upload-artifact@v4 + if: always() + with: + name: capability-matrix-${{ matrix.name }} + path: evidence + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index d4b01cae..420c68d6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,10 @@ lib/ .dev-home/ .env .claude/ +rust/target/ +packages/cli/native/ +packages/cli/extensions/ship/hooks/ship-hook.mjs +packages/cli/extensions/ship/commands/ +packages/cli/extensions/ship/hooks/ship-web.mjs +packages/cli/extensions/ship/web/ +__pycache__/ diff --git a/CONTEXT.md b/CONTEXT.md index d3b8f7bd..991295bf 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -3,6 +3,673 @@ A terminal coding agent composed on the dsh plugin runtime, whose interaction design deliberately aligns with the best of today's agent CLIs. +## Parallel rewrite boundary + +The language and interaction rules below describe the legacy Launcher/Bundle. +For the separately developed Rust/dsh path, [ADR-0002](docs/adr/0002-frozen-grok-rewrite-reference.md) +selects frozen Grok 1.0.34 behavior instead of Claude-first arbitration and +allows minimal native scrollback alongside fullscreen. It does not change the +legacy Viewport, keybindings, data or tests. The itemized reference register +lives under `docs/rewrite/reference/`; declarations are not parity evidence. + +`codsh --rust` is the explicit parallel Rust client. Its locally packed native +client reuses licensed upstream Rust input, welcome layout, and the official +fullscreen/minimal renderers, and submits prompts to released dsh over +ACP/JSON-RPC (`dsh --profile acp`) in the isolated Home. `codsh --rust agent stdio` +exposes that same session to an editor: standard new/load/prompt/config/approval/cancel +are real, `session/load` is dsh resume plus read-only replay (a denied nested +tool result stays failed), model and reasoning changes persist before the next +prompt and are restored on resume even when the advertised model is not a +catalog id, and unadvertised +`x.ai/*` methods return method-not-found. Closing the editor releases the write +owner. `agent serve` (authenticated WebSocket, loopback by default) and +`agent leader` (per-user 0600 socket, reached by `agent --leader stdio`) put +several clients on one hub: one dsh process per live session, `session/load` +attaches to a live session instead of starting a second executor, the first +approval answer wins, a concurrent prompt is refused, and a dsh exit is reported +without retrying. Nothing listens unless one of those commands runs, and a +non-off sandbox profile keeps a session out of the leader. `--remote +ssh://host/abs/path` (ticket 190) makes the ACP child an OpenSSH client (public +key, pinned host key, `BatchMode=yes`, no forwarding, allowlisted env) that runs +`codsh --rust agent --leader stdio` there: the remote hub and its dsh execute, +local policy flags and local context are refused, the client takes no local +owner lease and reads no local history, `session/load` replay rebuilds the +transcript, `_codsh/prompt_complete` finishes a turn that was running when it +attached, and a stopped remote leader or dsh is an interrupted turn with unknown +effects. The official Computer Hub, cloud workspaces, and the Cursor worker stay +refused. A host whose `[remote_access] identity = "required"` (ticket 207, +`remote_identity.rs`) puts a gate in the `agent --leader stdio` proxy: only +`initialize` and `authenticate` (method `codsh-org-identity`, token in +`_meta."codsh/identity"`, never forwarded to the leader) pass until an RFC 7662 +introspection says the token is active for the configured issuer, audience, and +teams; every later request and a timer check again, and a denial sends +`_codsh/remote_access_revoked`, cancels the connection's prompts, and closes. The +client sends its `codsh --rust login` session only to `[[remote_identity]]` +destinations with the advertised audience, re-reads auth.json before each request, +and both sides audit a SHA-256 fingerprint, never the token. `codsh --rust clone` (ticket 191) is plain git standing in for the +Grove lazy clone: off until `GROK_CLONE`/`GROVE_CLONE`, `GROK_GROVE`/`[cli] +grove`, or Grove's `[clone] enabled`; depth-1 single-branch partial clone +(`--full-history`, `--cone`), git's own credentials or `GROVE_AUTH_TOKEN` as an +https header kept out of `.git/config`, a hidden staging directory renamed onto +a missing or empty target so failure and cancel leave nothing, and `--remote` +running the same clone on the SSH host. A Grove worktree request +(`GROK_WORKTREE_TYPE`, `[cli] grove_worktree`, `GROK_GROVE`) is recorded and +falls back to a plain git worktree; there is no daemon, projection, or mount. Local MCP servers are +mounted by dsh's own MCP client from a per-process plan codsh-rust writes +(`$DSH_HOME/mcp/run-*/plan.json`, `CODSH_MCP_PLAN`); a server that fails to +start is dropped by name and the session still starts. Remote (http/sse) +servers reach dsh as stdio through codsh's remote proxy (`__mcp-remote-proxy`), +which owns the HTTP transport, OAuth tokens (`mcp login`, `/mcps auth`, editor +`x.ai/mcp/auth_trigger`), result projection, and MCP elicitation; elicitations +and resource reads cross to the TUI card or editor hub through owner-only +files under `/bridge/`, so dsh is still the only MCP client and executor. Grok's `search_tool` and +`use_tool` are a dsh plugin that re-enters dsh's tool pipeline, so permission, +Hooks, and cancellation see the real `server__tool` once. dsh remains the only +executing agent core and durable session owner; the Rust process does not link +the official agent runtime or own tools. Protocol mismatch, empty answers, +mid-stream failure, disconnect, and cancellation are reported truthfully. +`Ctrl+C` clears a draft first; an empty draft cancels the running dsh turn. Esc +does not cancel. `--continue` / `--resume ` restore the same dsh session and, in both +fullscreen and minimal, keep `[interrupted]`, `[cancelled]`, `[empty answer]`, +and the compaction sentence on the transcript; a finished unknown tool is not +inferred to be interrupted; +`--fork-session`, `/fork`, and `/rewind` copy conversation through dsh +seed/projection without restoring files or replaying tools. A second write +owner is refused. `/minimal` and `/fullscreen` switch render mode +in process: fullscreen uses the alternate screen, minimal writes committed +history to the native terminal buffer, `/rewind` and `/fork` replace that +native buffer instead of appending discarded turns. Official `xai-grok-markdown` renders Markdown, tables, code, mermaid labels, thoughts, and dsh tool cards/diffs, and the session keeps those colors. Pretty paint matches official markdown, so `Vec`, comparisons, fenced Rust, and inline HTML tags stay, keeps a ZWJ cluster in one cell, and shows `failed` plus `[error]` in a failure color. Esc closes full content without leaving the expanded tail on the folded transcript. Tab then `l`/`r`/Enter/`y` fold, expand, show raw markdown, open full content, or copy original bytes. `/expand` reprints the last folded block in minimal; `/transcript` opens the exact transcript in `$PAGER`. Display fold state does not rewrite model history or copied bytes. And the active session, +draft, running turn, and pending approval survive. `--minimal` / `--fullscreen` and +`GROK_SCREEN_MODE` are session-scoped and do not rewrite isolated +`[ui] screen_mode`. Fullscreen `/find`, `/jump`, click-vs-drag selection, Vim +scrollback keys, and mouse-capture toggle follow the frozen Grok contracts; +minimal refuses overlays that do not exist there. `Ctrl+Q` quits. `Ctrl+D` +quits except in fullscreen scrollback, where it half-pages. Prompt editing stays on the official textarea rather than a +second input model: typing `/` in a nonempty draft stashes that draft so the +slash command can run, then restores it. Slash completion starts +from an empty `/`, multiline chords, history search, slash/HISTFILE completion, +prompt Vim (`[ui] simple_mode=false`), paste, and `$VISUAL`/`$EDITOR`/`vi` +round-trips submit the resulting text through dsh. An unsent draft survives +resize. A submit that does not start a turn, including first-run with no +provider, puts the cleared draft back. A narrow screen still shows +`Execution unavailable` beside that draft. `/edit-prompt` requires an +empty composer. Next-prompt AI ghost text is not wired: the host passes no +suggestion, so Tab and Right do not accept ghost text. Suggestion rows stay +blocked. `@` attaches a workspace file. Dotfiles and `.gitignore` matches, +including nested `.gitignore` files, `**` patterns such as `**/*.log`, and +patterns that contain `/` (`logs/*.log`, `/secret.rs`, anchored at the +directory that owns that `.gitignore`), +stay hidden until the query starts with +`!`. A chip can name one line, a line range, or a quoted path with spaces. +Pasting a workspace path is a drop, except a dotfile or `.gitignore` match, +which stays text and is not read. Pasted prose that names a path stays text. +Backspace removes that chip and Ctrl+Z puts it back. Enter during a turn +queues the draft and its chips; Alt+Up restores the oldest queued prompt into +an empty composer. The Rust queue follows the Grok reference rather than the +legacy Queue panel below: rows are Prompts, `!` lines, or `/` commands, held +by the composer and drained one per turn when the turn ends or is cancelled +(never while an approval, compaction, or an in-place edit is pending). +`Ctrl+;`/`Ctrl+'` or ↑ on an empty prompt opens the pane (`e` edit in place, +Enter send now, `x` delete, `Shift+J/K` move). Send-now is cancel-and-send: +`Ctrl+Enter`/`Ctrl+I` (Apple Terminal also `Ctrl+O`; VS Code family `Ctrl+L`) +or Enter on an empty prompt cancels the turn through dsh without a +`[cancelled]` marker and runs that row next. `[ui] follow_up_behavior = +"steer"` hands plain text rows to the running dsh turn through a private +control socket (0700 directory, 256-bit token, unlinked after the one +handshake, env removed from dsh's own environment before tools run); dsh's +agent `steer` delivers it at the next step and an unclaimed steer returns to +the Queue. `/btw` asks the side model from the session's messages over the +same channel with no tools and never appends to the session; its panel is +Surface state only. `[ui] combine_queued_prompts` joins adjacent Prompts. Submit reads the file at that moment. A removed chip is +not sent. A missing file, a file over 256 KiB, a permission failure, or a +change since preview stays in the composer and does not send bytes. dsh +receives the admitted text, and resume shows the same +`@path` mention. `chips=false` means this draft has no attachment. The isolated Home is `~/.codsh-rust/dsh`, Profile `rust`; +inherited legacy configuration/credential files are not imported automatically. +`codsh --rust import --preview` / `--apply` copies selected current dsh +providers and preferences from `$DSH_HOME/settings.yaml`, +`code-cli-thinking.json`, and `code-cli-ui.json` into the isolated Home; it +maps `code-cli-ui.json` density `compact`/`comfortable` to `[ui] compact_mode` +and lists `coding-cli-runner` bell/notify/bang preferences as unsupported. +A multi-model route imports the `agent-default-model` selection; inline `apiKey` +values, missing `apiKeyEnv`, and `headers`/`compat` stay out of the isolated +file. Existing nested settings are preserved. It does not treat outdated `code-cli-settings.json` as a provider source and never +copies tokens, credential files, or original trust/execution grants. +`codsh --rust import sessions [ID]... [--all] [--apply]` copies selected legacy +codsh sessions (read-only from the old dsh Home) into the isolated Home under +new ids with a provenance record in `session-migrations/`; the old Home is +never written, there is no dual-writer sync, and gaps, skipped events, and +refusals (damaged, unsupported format, unknown required event) are reported. +Configured +`env_key` values are passed through. Nonessential telemetry, trace upload, +session tracking, and content sharing default off. Opt-in uploads require a +substitute `endpoints.telemetry_url`, `endpoints.feedback_base_url`, or +`endpoints.trace_upload_url`; official grok.com, api.x.ai, and Sentry hosts are +refused by parsed hostname. `/feedback` opens the same Write/Drafts form in +every screen mode; Enter sends, `/feedback ` sends immediately, and a +failed submit keeps the draft for edit or delete. Draft text is included only +when `privacy.share_content` is on. `privacy.share_session` attaches the session +id to an enabled trace upload and nothing else. `export` writes one session's +stored transcript as Markdown and does not claim redaction; an extra argument +is rejected before a file is created. A symlink at the sessions root, a +project directory, a session directory, or the log file is not read, so +export and share stay inside the real sessions tree. `share` posts that transcript only to +the explicitly selected substitute over http or https. HTTPS uses the same native-tls +connector as other substitute calls, plus a configured `GROK_EXTRA_CA_BUNDLE` or +`SSL_CERT_FILE` root; an untrusted certificate uploads nothing. It does not follow a redirect. Session +deletion is blocked: released dsh persistence has create, open, flush, stat, +and list, and no deletion operation, so CLI delete, `/delete`, the resume +picker, and the dashboard remove nothing. The picker says that asking to +delete is blocked. `du` reports isolated-home sizes +and deletes nothing. Diagnostic previews and +`GROK_DEBUG_LOG` contain kind/ok/count only. `GROK_LOG_FILE` and +`GROK_HOOKS_LOG` are unwired: the launcher does not pass them, and no Rust +path reads them. Web search and fetch stay off until `$GROK_HOME/config.toml` +names a substitute. Search and fetch are separate. Search calls +`[models] web_search`. `protocol = "responses"` is one OpenAI Responses +request and needs a credential. `protocol = "searxng"` is a keyless GET of +`{base}/search?q=...&format=json`; result URLs are filtered by the configured +domain policy and a model argument cannot widen it. Fetch is a public HTTP read, optionally through +an `http` CONNECT `toolset.web_fetch.proxy_endpoint`. Official hosts are refused. +Domain policy, including an empty fetch allowlist and a path or port on an +allow entry, loads at startup and is checked again on every redirect. A name +with any private address is refused, and the connection uses an approved +address. A disabled side is not registered. Enabled `web_search` and +`web_fetch` are dsh tools and call the configured substitute. Failures do +not invent page text. Cancellation closes the request and drops a late body. +Search citations and fetch status, content type, the page, and truncation +travel as fields. The page is not scraped back out of the CLI text. Model provider traffic is not +telemetry. Requirements pins can force `web_fetch` and `models.web_search` +off. Managed values for those keys are user-overridable, not locks. +User settings enter through `$GROK_HOME/config.toml` and `codsh --rust inspect`; +`[ui] theme`, `auto_dark_theme`, `auto_light_theme`, `compact_mode`, +`show_timestamps`, `screen_mode`, `confirm_before_rewind`, +`ui.fork_secondary_model`, and `[ui.status_line]` use that same file. +`/settings` (`/config`) edits those live appearance and status-line controls; +`/theme` (`/t`) previews fullscreen themes and Escape restores without saving. +Minimal mode uses the terminal palette and refuses `/theme`. Status-line +scripts time out at 10s, clear `BASH_ENV`/`ENV`, and kill leftover process +groups on exit. Locked requirements show their source and cannot be edited. +applicable model/provider fields, including `provider`, `api_backend`, and +reasoning effort, are translated into isolated dsh `settings.yaml` rather than +competing with it. `provider` defaults to the catalog id. Entries that share a +provider and the same key, backend, URL, and headers are one provider with +several models. A reused provider with different credentials or a different +backend is refused, not written as a second YAML key. `/model` and `/effort` change only advertised catalog options; unknown +backends and efforts are refused, never treated as equivalent or silently +swapped. Usage and context stay unknown unless the provider or config actually +supplies them. Session usage is one ledger folded from the dsh session log (whole log, so a +resume never double-counts; a fork keeps inherited history as in the +reference; subagent children fold into the spawning turn; auxiliary title, +compaction, `/btw`, and memory calls are excluded), shared by `/usage`, +`/session-info`, the status line payload, headless output, and +`codsh --rust usage`. A call with no reported usage marks it incomplete, never +zero. Cost is unknown: dsh reports none and no price table is used. `/context` and `/compact` are dsh-backed: occupancy is a dsh +estimate, advertised limits follow the selected model's `context_window`, and +compaction mutates the dsh session log rather than a second history. Optional +`/compact` instructions travel only on the summarizer call (`purpose=compaction`) +with a recorded destination. Automatic thresholds and pruning map into dsh +`thresholdRatio` with a compatible `retainRatio` / tool-result pruner +settings; percents that would fail dsh plugin load (`retainRatio >= +thresholdRatio`, including `0`) are warned and remapped; unsupported +Grok-only prune ages stay warnings, not silent no-ops. Managed defaults live in +`$GROK_HOME/managed_config.toml`; `$GROK_HOME/requirements.toml` locks values so +later CLI, environment, overlay, workspace, or user layers cannot bypass them. +Unknown security fields fail closed with diagnostics. Workspace trust is stored +in `$GROK_HOME/trusted_folders.toml`; untrusted project Hooks/plugins/instructions +stay inactive until `--trust` or an interactive grant. The same decision gates +`.grok/sandbox.toml`: naming a custom profile does not trust that file, a +project-only definition refuses startup while the workspace is untrusted, and +a user `$GROK_HOME/sandbox.toml` definition stays usable and still wins. The +untrusted project body is not applied; a malformed, unreadable, or symlinked +untrusted project file does not veto that user definition. A trusted malformed +project file still refuses startup. `devbox` skips only the global +hook/config/trust write protection; a profile extending it keeps its `deny` +list. Deny paths and glob literal prefixes are resolved to the paths Seatbelt +checks, and one that cannot be resolved or expressed refuses startup. +`inspect` does not apply the profile: it reports the resolved name and every +config error. A session launch still refuses a profile the kernel cannot apply. dsh's per-call +Seatbelt cannot nest inside a profile, so while one is applied codsh starts +dsh with its per-call file mode at `danger-full-access` and unchanged +approvals; the kernel policy confines bash children and child agents. A deny +glob's literal prefix is pinned against rename, and so is a directory inside +the glob tail (including one created after launch), because Seatbelt matches +the resolved path. The ancestor walk stops at the resolved write root, so a +workspace under `/tmp` does not pin `/tmp` itself. The launchd escape is +kernel-blocked, matching the reference `mach-lookup` rules. `restrict_network` +is a kernel network deny on macOS (`(deny network*)` for this process and its +children), not a dsh file mode. Linux Landlock network and Windows confinement +are different mechanisms: a profile that asks for network isolation refuses +startup on a platform that cannot apply it. `[shell_environment_policy]`, when active, is the environment of a shell +child this client starts and of the dsh process spawned afterwards. dsh's +bash tool is built from that process environment and only adds keys, so it +sees the same filter; with no policy, dsh keeps the launch allowlist. A +second Seatbelt profile is still not applied inside the first. The acp profile's persistent terminal tools are not mounted: a patch +cannot add a plugin the profile does not already depend on, so an interactive +terminal session is unavailable. Window resize is not a dsh tool. A one-shot +bash command shows stdout, stderr, and the exit code, including 0. Ctrl+C +cancels it; dsh reports that as an aborted tool, not as success. + After trust, compatible +rules, skills, agent definitions, and custom commands are discovered in the +frozen order (closer skill directories outrank broader ones; nested +`SKILL.md` files return only when the walk depth is greater than five, and a +child of a directory that already has `SKILL.md` is still recorded; a +configured `[skills] paths` directory is depth 0, so its children start at +depth 1 and a sixth child is not loaded; flat +`commands/*.md` files are slash commands) and included in the dsh prompt. +A skill or command named `login`, `logout`, or `feedback` does not take the +bare slash: that name stays the built-in, and the asset is `/local:name` (or +`/ancestor:name`, `/repo:name`, `/user:name`). +The launcher forwards `GROK_CLAUDE_SKILLS_ENABLED` and +`GROK_CURSOR_SKILLS_ENABLED`; either set off stops that vendor scan. +`--rules` appends a session `` block; `--system-prompt-override` +replaces file rules and `--rules` while the typed prompt is still sent. `/reload-assets` rescans them. +Global rules still load when the project is untrusted. `paths.extra_skill_dirs` +is not a skill discovery root. Isolated plugin +marketplace add/list/update/remove and plugin install/update/uninstall copy +files into `$GROK_HOME/installed-plugins` with inspectable provenance. +Installation does not enable execution. Enable/disable (`plugin enable|disable`, +Space in `/plugins`) is the only switch for plugin content: `plugin::active_roots` +(enabled, not disabled, install-trusted or workspace-trusted for project +plugins, present, not shadowed) feeds `assets::discover` (plugin rules before +project rules, `/plugin:name` skills and commands, `plugin:agent` agents; workflows go to `workflow_catalog`) and +`plugin::hook_env` writes `CODSH_PLUGIN_HOOKS` for `rust-acp-hooks.mjs`, which +runs those command hooks under the same contract. Enabling never widens tool +permissions (`executionGranted` stays false). A live TUI session replaces the +dsh child on the next prompt when plugin hooks or agent types changed; rules, +skills, and commands are rescanned every prompt. Plugin MCP servers (ticket +204): `plugin::mcp_sources` feeds active plugins' `.mcp.json`/manifest +`mcpServers` into `mcp::discover` as `Source::Plugin` at the lowest priority +(shadowed ones are kept in `Discovery.shadowed`); relative stdio commands and +cwd are anchored in the plugin root, and plan entries carry `plugin`, +`pluginRevision` (commit@updated_at), and `pluginData`. The plugin view reads +the same discovery (`fill_mcp_states`) and, in the TUI, the mounted plan +(`annotate_live_mcp`). `AcpClient` keeps the plan each session mounted; +`mcp::plugin_servers_differ` compares its plugin entries with a fresh plan so +the TUI drops the dsh child before the next prompt and the editor server +resumes the idle session in a fresh dsh (`EditorServer::remount`; a stale spare +runtime is discarded). Every mutating `plugin::run` fingerprints plugin-won +servers before and after and `permission::revoke_mcp_allows` removes remembered +`server__*` allows (never denials) for each server whose plugin, revision, or +transport changed or vanished. No new MCP client or permission path exists. +Official marketplace auto-register is +off unless `GROK_OFFICIAL_MARKETPLACE_AUTO_REGISTER` is set. Git marketplace +catalogs are read from `$GROK_HOME/marketplace-cache` after add/update. +Each marketplace plugin installs into its own dest under `installed-plugins`. +Git updates persist the clone HEAD in inspect provenance. `marketplace.require_sha` / +`GROK_MARKETPLACE_REQUIRE_SHA` refuse unpinned remote install and update. +`marketplace remove` +clears trust and enable lists for those plugins. `extra_known_marketplaces` +pins sources with first-pin-wins. A present `strict_known_marketplaces` list +binds catalog load, named install, the catalog entry's clone URL, and later +git update: unlisted git sources are dropped with a warning, local paths are +refused unless an admin `extra_known_marketplaces` pin names that path, and an +empty or malformed list refuses every add and install. Layers are +strictest-wins, so a later user or workspace list cannot widen an earlier +lockdown. Git URL comparison folds scheme and host only, including GitHub, +and strips one trailing `.git`. The repository path stays case-sensitive. +`/plugins` and `/marketplace` open the directory; Ctrl+L is not bound here. +Ship for `codsh --rust` (ticket 195) is an ordinary first-party plugin, so the +Rust core has no Ship logic. `packages/cli/extensions/ship` holds the manifest, +`hooks/hooks.json`, and README; `scripts/build-ship-extension.mjs` (called by +`pnpm run build:rust`) bundles `packages/bundle/src/ship-extension*.ts` into +`hooks/ship-hook.mjs` and writes `commands/ship.md`, the legacy +`shipPromptFor(undefined)` contract with `$ARGUMENTS` pointing at the +`Arguments:` line of the command expansion. The launcher sets +`CODSH_BUNDLED_EXTENSIONS` to its `extensions/` directory and +`plugin install bundled: --trust` installs from there as a local source; +nothing installs or enables it implicitly, and it binds no key. The +UserPromptSubmit/PostToolUse hook reuses the legacy `ship-answers`, +`ship-snapshot`, and `plan` modules: a `/ship` prompt opens a run for the +workspace in `$GROK_PLUGIN_DATA/runs/`, answered `ask_user_question` results +are encoded as legacy answer records (held until wayfinder writes the spec, +then flushed after the snapshot is sealed), later tool calls recheck the +frozen original, and any other prompt ends the run. Plan mode and subagent +sessions are ignored. Ticket 208 adds the rest of the flow in +`ship-extension-runner.ts`, made at hook boundaries while dsh runs every +agent: Stop continues the turn with the next phase contract +(`shipContinuationFor`, capped by the hook runner's 8 continuations), a +PreToolUse deny auto-Confirms gate 1/2 (sealing the Mission Contract) and +gate 2/2, Stop claims the landing wave and asks for one worktree-isolated +`subagent` per ticket, each PostToolUse result serial-merges the Ready-set +(the conflict flow and validation are the legacy `ship-conflict` ones), a +PostToolUseFailure or a cancel leaves the ticket claimed and unmerged, and +after the separate verification turn the runner merges back. A later-phase +spec resumes instead of being refused; a lost run state is re-inferred from +the files, and a missing sealed contract stops the run. Spec, snapshot, +answers, contract, and ticket files stay the source of truth and are the +same files legacy `codsh` reads; there is no Goal (`Goal-Id` stays blank) or +Rhai. The Rust client keeps the text of a Stop-continued model call in the +running turn (`live_message_turn`), and the hook runner ignores EPIPE from a +hook that exits early or is killed by a cancel. The browser graph (ticket 196) is the same Web panorama page +(`ship-web-app.tsx`, built into the plugin's `web/`), served by +`hooks/ship-web.mjs`, a detached loopback server per workspace owned by the +dsh process that ran `/ship` (`CODSH_HOOK_HOST_PID`). It listens on +`127.0.0.1` only and answers GET/HEAD only under a random 128-bit path prefix +(`//`, `//index.html`, `graph.json`, two assets) for a loopback +Host naming its port. `graph.json` is joined on each request from the run +state, spec, snapshot, answers, and local wayfinder tickets, never from the +graph cache; the hooks rebuild `.ship.graph.json` like the legacy runner +and print one `Ship graph · · Status · 待认领/已认领/已关闭 · N of M +decision answers recorded · ` line (a hook `systemMessage`) when it +changes. SessionEnd, the owner's exit, or removing the plugin data (uninstall) +stops the server; its record in `$GROK_PLUGIN_DATA/web/` (0600) keeps the port +and token so SessionStart for the same session reopens the same URL. +`codsh --rust login` / `logout` / `setup` use configured substitute identity +or management services. Independent API-key use does not require login unless +`GROK_DISABLE_API_KEY_AUTH` or a team pin (`auth.force_login_team_uuid` or +top-level `force_login_team_uuid` in locked `requirements.toml`) requires a matching identity session. +Startup and inspect refresh an expired `auth.json`, or clear it when refresh fails. Clearing an unrefreshable token does not block `login` or independent API-key use. `/login` reloads that session and reapplies settings; it replaces dsh when credentials, readiness, or the settings patch changed, and keeps the live client when the settings write fails. `/new` and a dashboard dispatch start a new dsh session on that same path: the current provider, model, effort, permissions, environment, and settings patch are applied again, and a failed settings write keeps the live client. They do not fall back to another provider. `/cd` then either command reloads trusted workspace config for the new directory before that patch is written. That workspace model and effort replace a saved selection for the new directory. The session memory toggle does not carry across. A usable identity session is handed to the executing dsh child (`GROK_AUTH_PATH`, `GROK_AUTH_ACCESS_TOKEN`, `GROK_AUTH_PROVIDER_COMMAND`); other `GROK_AUTH_*` parent variables are not. `/logout` asks the configured identity provider to revoke the session before deleting `auth.json`. A revocation failure keeps the local session. `/logout` drops the live connection. +Session tokens stay in `$GROK_HOME/auth.json` and are not transferred to model +providers or other services. Official grok.com entitlements are not reproduced. +Unsigned or unverifiable managed policy is refused, including a signature for +another principal, a deployment-key caller with no team, an on-disk sidecar +that does not name this caller, and fail-closed files with no pubkey and no sidecar. +Permission modes, allow/ask/deny rules, and remembered project grants are compiled +into isolated `$DSH_HOME/permission-policy.json` and enforced in the dsh +`tools/pre-execute` plugin before a real tool body runs. Released dsh does not +run Grok lifecycle hooks, so the client plugin runs command hooks at +SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, and SessionEnd. +Exit 2 and `decision: deny` block; other failures are recorded and are not +success. Hook stdout and stderr stay hook output. An allow does not skip +permission checks, and a hook cannot widen a sandbox or permission deny. +Untrusted project hooks stay skipped. Typed subagents run as dsh children +(`rust-acp-subagents.mjs`): the Rust client resolves `[subagents]`, roles, agent +files, and `GROK_*` overrides into `CODSH_SUBAGENT_POLICY`; the plugin turns a +type's capability into a dsh tool allow-list (unclassified tools only under +`all`), preflights a type model, admits by session with queue or fail, and +reports one lifecycle (queued, start, activity, end, refused) on stderr that +feeds the tool block, status line, and task list. Cancel requests travel as +files in a private control directory. Git worktrees (`rust-worktree.mjs`) have +one implementation shared by `-w`, `codsh --rust worktree`, `/worktree`, and +subagent `isolation: "worktree"`: a plain `git worktree` per id under +`$GROK_HOME/worktrees` (dsh gets `CODSH_WORKTREE_HOME`) with a JSON registry +beside it. The source checkout is only read; apply is explicit and merges per +file against the worktree base; an isolated child sees a parent whose session +header cwd is the worktree, and the permission listener checks its calls under +both the worktree and the mapped checkout path. +A child's approval (#220) rides the same `approval/request` waterfall as the +main session. dsh-acp answers only for sessions it owns, so +`rust-acp-child-approval.mjs` (wired in `rust-acp-control.mjs`) takes a +child's request when the terminal UI owns its root session and the control +channel is up: `child_approval` carries the child, its label, type, workflow +and phase (from the subagent plugin's lineage, `codsh.rust.subagent-lineage`), +the tool, and its input; the Rust client queues it (`child_approval.rs`) +behind the main prompt and answers `child_approval_answer` with +`allowed-once` or `rejected`, recording an `a` grant itself exactly as the +main prompt does. The child's aborted call, its disposal, or the root's, close +the request (`child_approval_closed`); a closed channel settles it as +`unavailable`, and headless keeps the old refusal. +Subagent messages and continuation (ticket 173) stay in the same plugin, with +the pure pieces in `rust-acp-subagent-messages.mjs`. `[features] +active_agent_messages` / `GROK_ACTIVE_AGENT_MESSAGES` (off by default) arrives as +`activeAgentMessages` in the policy. With it on, the plugin registers +`send_subagent_message` for the parent and for tool-started children, and it +denies dsh's `send_message` / `interrupt_agent`, which reach only continuable +children and this client never starts any. A message is a dsh `agent-message` +user message. `steer` goes to `child.steer`, `queue` becomes a follow-up, and +`interject` is prepended to the child's inbox and aborts its blocking +`job_output` waits. A child not yet started takes parked messages when it +starts. A completed, non-cancelled, non-isolated child keeps its dsh handle +(at most 32 resident, oldest released, all released when the parent agent is +disposed) and wakes as the same identity: `continueChild` runs the turn inside +a dsh job owned by the parent agent, through admission, and the job's result +ends with `[subagent_id: …]`. `resume_from` starts a new child through the +`fork` provider seeded from the source's `snapshotEvents()`, pinned to the +source's type, provider, model, and effort. Unclaimed messages are counted per +child (8) and in total (64) until the target's driver emits +`agent/inbox/claimed`. The Rust board receives `message` and `resume` events +for the transcript row (`Message sent to …`, `Message rejected · …`), +`· attempt N`, and `continues "…"`, and `rust-acp-session-read.mjs` renders an +`agent-message` as the child view's `◎ Message from …` turn. Nothing touches +disk and nothing survives a restart. Workflow, scheduler, and verifier children +never get the tool and cannot be targeted. +Plan mode, questions, and todos (ticket 179) stay dsh state: `ctx.planMode` +logs the plan projection, `exit_plan_mode` asks through `ctx.userQuestions`, and +`todo/write` feeds the `todos` projection. `rust-acp-plan.mjs` adds +`enter_plan_mode` (the ordinary approval prompt), the plan file under +`$GROK_HOME/sessions///plan.md`, a prepended pre-execute +deny plus a monotonic guard that refuse every edit but the plan file while plan +mode is on (bash and subagents are not covered), `--no-plan` / `--no-ask-user` +tool removal, and the `--todo-gate` reminder (at most two per prompt). +`rust-acp-interaction.mjs` is the `user-questions/request` answerer: with +`CODSH_INTERACTION=tui` it sends the question over the control socket and +waits for the card (answer, dismissal, plan quit, timeout, or cancel; a late +answer is stale); without a terminal a question gets the no-operator text and a +plan review is approved. `plan_state` and `todos` follow session events. The +Rust client only renders the card, review, status flag, and todos pane and sends +`plan_set` for `/plan` and Shift+Tab. +Rhai workflows +(`rust-acp-workflow.mjs`) are a `workflow` tool that `rust-acp-subagents.mjs` +registers: the vendored +reference engine (`rust/upstream/xai-workflow`) runs in the native binary's +hidden `__workflow-engine` subcommand (dsh gets its path in +`CODSH_WORKFLOW_ENGINE`), speaks JSON lines on stdio, resolves the script, +budget, and agent options, and asks the host for each agent call; the host +starts it as a dsh child through the subagent planner (type, capability, model, +effort, isolation) with a per-run live cap, reports `event: "workflow"` lines +for the tool block, and never offers the tool to a child. The live cap +(`[subagents] workflow_max_concurrent`, `GROK_WORKFLOW_MAX_CONCURRENT_AGENTS`, +sent as `workflowMaxConcurrent` in the subagent policy) is separate from the +engine's cumulative `agent_budget`. The engine owns the reference host +contracts in `rust/src/workflow_host.rs`: `output_schema` compiles with the +`jsonschema` crate and rewrites the prompt with the output contract; the host +keeps a contract child open (`open: true` in the reply) so the engine can send +one `resume_agent` correction turn to the same dsh child, then a `close` line +ends it. Scratch files live under the session's plan directory +(`workflows//scratch`, sent as `scratchDir`), and `git_diff_since` runs +git in the engine. Open children are closed when the run ends. Since ticket 183 +a run is a background run of its session: the plugin's run manager +(`createWorkflowRuns`, helpers in `rust-acp-workflow-runs.mjs`) answers the +tool once the engine's `started` line arrives, gives the run a session-unique +display name, and keeps `run.json`, the immutable `script.rhai` and +`launch.json` (args), and the engine's `journal.jsonl` under +`workflows//`. The engine takes `journal: {path, resume, +pruneHostError}` on its start line and replays journaled agent results on a +resume; the host re-runs anything not journaled, so nothing is exactly-once. +Pause and stop abort the engine and its children; resume, only in the same dsh +process (restored runs refuse, an active one becomes `interrupted`), waits for +the old engine and starts the stored script again. A run that ends or stops at +its budget is reported once per launch epoch as a `followup` message +(`source.plugin: rust-acp-workflow`, `form: notice`), which +`rust-acp-background.mjs` puts back when a cancel discards it and announces as +a `notice` line. `/workflow` reaches the manager through the control channel +(`{type: "workflow"}` → `workflow_result`); the Rust client shows the reply in +the hint line, and the board's `event: "workflow"` lines (run id, `call`, +`session`, status, `elapsedMs`) feed the block title, the tasks pane's +Workflows section, and the status line. A plain `-p` prompt sets +`CODSH_WORKFLOW_FOREGROUND=1`, so there the tool waits for its run. +Since ticket 184 the saved catalog lives in `rust/src/workflow_catalog.rs`: +`scan` reads `/.grok/workflows` (trusted folders only) then +`$GROK_HOME/workflows`, keeps the first definition of a name (project wins), +and records shadowed, same-scope duplicate, and invalid files; it parses +`meta` only and never runs a script. The engine answers `{op: "catalog"}` and +`{op: "save"}` start lines without starting a run, and resolves +`{type: "name"}` sources from a fresh scan, so the run's stored +`script.rhai` is whatever the file held at launch. The Rust client scans the +same catalog for `/workflows`, the slash menu, and `/` (sent as a +`workflow` control message with `launch`); the plugin parses slash arguments +(`parseNamedArgs`), injects a `form: snapshot` reminder for a host-side +launch, adds the reference listing to a top-level agent's first step when it +changes, and saves a run through the engine (create-new, no symlinks, trusted +project only). Ticket 206 adds the built-in scope (see below). Ticket 205 adds +plugin workflows: `plugin::workflow_sources` lists installed plugins whose +layout has `workflows` directories (manifest `workflows`, default +`workflows/`) with state, trust, and provenance, without asset discovery; +`scan_with` adds an active plugin's files after project and personal ones. +The reference registry has no plugin scope, so the ticket 166 asset rule +applies: `:` always resolves, the bare name only when no +project/personal entry or other active plugin has it (two plugins make it +ambiguous, one plugin defining a name twice makes the qualified name +ambiguous), and an inactive plugin's names are refused with its status. +The engine's `started` line carries `origin` (scope, path, plugin name, +version, commit, source, license); the plugin keeps it in `launch.json` and +`run.json`, shows it in `/workflow runs`, and before resuming a plugin run +asks the engine for the catalog and refuses unless that plugin is active. +The resumed run still replays its stored `script.rhai`. +Ticket 206 adds `BUILTIN_WORKFLOWS` to `workflow_catalog.rs`: the reference +`deep_research.rhai`, pinned unmodified under `rust/upstream/workflows/` with +its digest in `import.json`, is merged before the project and user scopes, so +it shadows same-named files (reported as hidden) and takes the bare name from +plugins; there is no bundled scope. Its entry carries `scope: "builtin"` and +`upstream` (commit, path) in the catalog and the run `origin`, and +`save_project` refuses its name. The Rust client needs no new command: the +catalog makes `/deep-research` a workflow slash command, and the plugin +answers `launch: "deep-research"` with the reference replies (usage, args +`{query}`, launch reminder). The script's children use the ordinary dsh tools: +read-only children get `web_search`/`web_fetch` only when the configured +substitute enables them. A completed run whose result map has a `status` +(`partial`/`verified`) keeps it as `resultStatus`, shown as `Result status:` +in the completion reminder and `/workflow runs`, and as `complete (result: +X)` in the completion notice (`statusWords`), the plugin's `workflow` event +(`result`), the tasks row and the block title (`WorkflowRun::status_words`). +The dsh `tool-web` row is not the launcher's: `rust.mjs` writes a fail-closed +placeholder and `config::apply_to_dsh` (and the test-seam patch) appends +`web_tools_yaml`, the effective `web.search/fetch.enabled`, after it, so a +config-only setup gets both tools (this also covers #171's in-session +search). Headless `-p "/deep-research "` is the built-in command: +`run_plain_turn` asks the plugin for the visible tool names (control `tools`) +for the init line, sends `workflow_launch` instead of a prompt, waits for the +run's `workflow_result`, and stops the run on a signal; `rust-acp-workflow` +waits in the foreground when `CODSH_WORKFLOW_FOREGROUND=1`. +Ticket 186 adds memory capture in `memory_capture.rs` over the legacy store +of `memory.rs` (the default; `[memory_v2] enabled = true` is refused and +`memory-v2/` is never touched). The client owns the files, the Dream lock +(`.dream-mutex`, `.dream-consolidated`), the gate, the status file +(`.memory-status.json`) and the reference prompts; dsh owns the model call. +`/flush`, `/dream`, the idle flush, the gated automatic Dream and +`--memory-flush` send one `memory_model` control request (system prompt, +closing user message, optional flush window of whole tool exchanges) to +`rust-acp-control.mjs`, which calls the session's model through dsh without +appending to the session and answers `memory_model_result` (text, usage, +route, messages and characters sent) or `memory_model_error`; +`memory_model_cancel` aborts it on `/new`, a session switch, or quit. One job +runs at a time. The session-end summary is written by `AcpClient` from a +ledger of live (not replayed) updates. Local deviations from the reference: +append rather than overwrite a session log, a Dream conflict when +`MEMORY.md` changed while the model ran, a backup and an archive instead of +deletion, honest outcome text, and the delta flush surviving a restart (the +previous flush is read back from the session's newest flush log instead of +living only in memory). First-turn recall follows the reference keyword +search (`memory_keywords.rs`: stop words dropped, keywords OR'ed, BM25 order). The pre-compaction flush is not wired +because dsh owns compaction. +Background commands stay dsh jobs +(`rust-acp-background.mjs`): in the interactive client, `CODSH_BASH_POLICY` +(from `[toolset.bash] auto_background_on_timeout` and +`foreground_block_budget_ms`) makes a parent's foreground `bash` call run as a +dsh job that the tool call waits on. A result inside the budget is the normal +foreground result; past the budget, on Ctrl+B, or on send-now the call returns +the job id with the reference "moved to background" text and the command keeps +running; a turn cancel kills it and fails the call like dsh does. Children, +plain `-p`, and editor ACP keep dsh's own foreground bash. The plugin reports +job start, end, output, waits, completion notices, and agent status on stderr; +the Rust client feeds the status line, the tasks pane (`x` stops a command +through the control channel), and wake turns started by dsh's tool-jobs +completion notice. Closing a dsh session disposes its owner and stops its +commands; the client waits briefly for that before it kills the process group. +A restored history never shows a command as running. +Monitors stay dsh jobs too (`rust-acp-monitor.mjs`, registered by the +background plugin only when the interactive client sets `CODSH_MONITOR=1`): +the parent gets the reference `monitor` tool (`command`, `description`, +`timeout_ms` default and cap 36,000,000, `persistent`), never a child. The +script starts through dsh's bash executor with the session's sandbox, working +directory, and managed environment (stderr merged), is gated like a bash call, +and is a `ctx.jobs` job of kind `monitor`, so `job_output`, `job_list`, and +`job_kill` work on it and it stops with its session. Output follows the +reference pipeline (200 ms polls, trimmed lines, 500-byte line and 3000-byte +batch caps, a 10-event bucket refilled every 2 s, auto-stop after 30 s of +suppression, which here also kills the script). Each event is a +`` user message for the owning agent: an idle agent is woken +within dsh's budget of three wakes without a user message; a running agent +gets it at its next step only while a tool call runs or another message +extends the turn, and otherwise it is held and sent as one grouped wake when +the agent goes idle (after a cancel it waits for the next message), so an +event never adds an unbudgeted model step. The exit is dsh's own completion +notice. The client shows `◎ Monitor event · …` turns (messages claimed at one +step share a turn), counts monitors in the status line, and lists them in the +tasks pane, where `x` stops one and the model is told not to restart it. +Scheduled prompts stay dsh subagents (`rust-acp-scheduler.mjs`, registered by +the subagent plugin only when the interactive client sets `CODSH_SCHEDULER=1`): +dsh offers the parent the reference `scheduler_create`, `scheduler_delete`, and +`scheduler_list` tools (60-second minimum, 50 tasks per session, 7-day expiry, +overlap skip) and never offers them to a child. `/loop [interval] ` sends +the reference `/loop` instruction as the turn; each fire is a background +subagent of the default type started through the subagent planner, so type, +capability, approvals, and the live cap apply, and its status returns once as a +job completion that wakes an idle session. A fire does not resume the previous +child's transcript; it starts fresh with the previous fire's final status. +Tasks are saved per session (#178), durable or not, as in the reference, which +keeps all scheduler state in the session's resources: dsh writes +`$DSH_HOME/codsh-schedules/.json` atomically only after the Rust +client, holding the session owner lock (`session_owner.rs`), hands over the +lock token with `schedule_owner` on the control channel. The plugin re-reads +the lock before every save and fire; a changed lock (`OWNER_LOST`) or a closed +control socket (`CLIENT_GONE`) stops the session's tasks without writing, so +two processes never fire one loop. Resume restores the file: an overdue task +fires once (missed intervals collapse), an expired one is removed unfired, and +a fire recorded as in flight becomes `lastResult.status = "unknown"`, is never +replayed, and the next fire's frame says it was interrupted. Deletes and +durable expiries are acknowledged only after the absence is saved; a durable +create that cannot be saved is rolled back, and `durable: true` without an +owner is refused with `scheduler_durability_unavailable`. An unreadable file +is never overwritten. The creation permission mode is saved and a change is +flagged, not re-approved: fires run under the current mode. With subagents +off the plugin registers a paused scheduler (no tools, no fires) so saved +loops stay visible and deletable. The plugin reports `event: "schedule"` lines +on stderr; the Rust client keeps the loop rows for the status line and the +tasks pane, whose `x` sends `schedule_delete` through the control channel. +Plain `-p`, editor ACP (the `x.ai/scheduled_task_*` and +`x.ai/scheduler/delete` extensions are listed as unsupported), and the shared +server have no scheduler and leave saved loops untouched; a fork or rewind is a +new session id and starts without them. +Goals (ticket 180) are dsh's own goal driver plus `rust-acp-goal.mjs`; the +Rust client runs no loop of its own. `/goal` parses like the reference +(`status`, `pause`, `resume`, `clear` as the whole input, otherwise an +objective with an optional trailing `--budget `) and goes to the plugin +over the control channel; `CODSH_GOAL_POLICY` carries `[goal] enabled`, +`GROK_GOAL`, `GROK_GOAL_VERIFIER_N`, and `GROK_GOAL_CLASSIFIER_MAX`. The plugin +prepends a `tools/pre-execute` gate on `update_goal complete`: it refuses while +a dsh job still runs, otherwise it runs N verifier subagents (execute +capability, through the subagent plugin) and applies the panel rule; a refusal +denies the call with the gaps and keeps the goal active, too many refusals block +it as `verification-limit`, and no verifier (subagents off, policy error, spawn +failure) blocks it as `verification-unavailable` (fail closed; the reference +fails open). The token budget is a sidecar under `$DSH_HOME/codsh-goals/` +counting provider usage of the root session and its subagents while the goal is +active; it is checked when the agent goes idle and before each goal round's +step, and blocks the goal as `budget-limited`. It is separate from the workflow +agent-count budget. The plugin records why a goal paused (`/goal pause`, an +interrupted round, the model), marks a user turn during an active goal as a +takeover, and reports state, rounds, and stop notices on stderr; the client +shows them in the status line, names wake turns `◎ Goal round N/M`, and keeps +goal lines that arrive during session/resume. Session replay shows goal rounds +the same way. Deny and hook blocks have +no side effects; unsplittable shell, including a parameter expansion such as `$x`, `${x}`, `$1`, `"$1"`, `$@`, or `$*`, and Read/Edit path rules on operands cannot be +glob-allowed or auto-approved as read-only. Wrappers, including `sudo`, `nohup`, +and `xargs`, peel to the inner command +without eating the command name; `env -S` prompts. Brace groups and ANSI-C +`bash -c` scripts are inspected, so deny still matches the inner command. +Command basenames match without regard to case, so `RM.EXE` is still `rm`. +Attached or clustered `sort -o` (`sort -oFILE`, `sort -uoFILE`) and unique +long-option prefixes such as +`sort --compress-pro` are not read-only. +Frozen git read-only subcommands auto-allow; git writes do not, including an +attached upstream such as `git branch -uorigin/main`, and a bare `git branch -u` +or `-t` with no operand. A leading word such as `time`, `exec`, or `builtin` +cannot hide a denied command. A shell option that takes the next word, such as +`bash -o errexit -c`, is consumed before the script is read. An unquoted `*`, +`?`, or `[` in a command word is not expanded, so a pathname glob such as +`./r*` is not auto-approved. `git branch --track` and a unique prefix such as +`--tr` are writes even with no operand; literal `git branch` and `sort file` +stay read-only. Claude settings +load from `~/.claude` and every `.claude` from the repo root to the working +directory. Read/Edit deny/ask follow in-path symlink targets. Missing or corrupt +`permission-policy.json` refuses +mutating tools. `y` is once, `a` remembers a path-scoped project grant, and +`/revoke-approvals` forgets those grants. +Plain `codsh` still selects the legacy Launcher/Bundle; this is not the default +cutover. The Viewport language below remains the legacy Surface contract. + ## Language ### Product shape @@ -13,6 +680,17 @@ dsh, registers the Bundle into a profile, and boots it. The found dsh must meet the harness floor published on the launcher (`codsh.requiresDsh`). _Avoid_: wrapper, shim, cli package +**Native artifact**: +The prebuilt Rust client inside the Launcher package, one directory per +platform (`native/darwin-arm64`, `native/darwin-x64`, `native/linux-x64`, …) +holding `codsh-rust`, `artifact.json` (target, SHA-256, the Launcher version it +belongs to, the dsh floor) and its license/notice files. `codsh --rust` runs it +only after the manifest, hash, executable header and version all match; a +mismatch is refused with the reinstall and rollback commands, never a fallback +to the legacy runtime. `codsh --rust install-check` shows the same verdict +without starting anything. +_Avoid_: binary package, download + **Bundle**: The `codsh-bundle` npm package — the interactive surface and agent preset, installed into dsh profiles, never globally. @@ -108,7 +786,11 @@ every other folder is one row that opens the rest, because the session wanted is almost always in the folder they are in. Rows are ordered by when the session was last touched — not when it began — and each names its title, that age, how many messages it holds, and, only for a session from elsewhere, the -folder it belongs to. +folder it belongs to. Without a manual or generated title, the title is the +first prompt the person typed. Whatever the client laid in front of those +words for the model — first-turn memory and recalled session logs, rules, +agent definitions, an expanded skill body — is context, never the title, and a +resumed transcript shows that turn as typed rather than dropping it. _Avoid_: session picker, history list **Region pointer**: @@ -408,34 +1090,130 @@ speaks when the head row is off the screen. _Avoid_: tooltip, status hint **Pasted image**: -The clipboard image Ctrl+V attaches behind an `[Image #N]` token in the box — -one backspace removes the token whole, and a deleted token drops its image. -At submit an image-capable model gets it as a first-class attachment block. A -text-only model always gets the original saved under -`$DSH_HOME/attachments/pasted/`; an explicit `CODSH_VISION_*` sidecar adds a -verbatim description first, otherwise a `deepseek-official` text model borrows -`deepseek-v4-flash-vision-exp` for that description automatically. Failure -keeps the file-only path. The file context and any description ride the same -message so they survive `--resume`; the selected conversation model never -changes. +The clipboard image Ctrl+V attaches on macOS behind an `[Image #N]` +token in the box — one backspace removes the token whole, and a deleted token +drops its image. Cmd+V reaches the client as a bracketed paste: on macOS an +empty one (an image-only clipboard) reads the clipboard image the same way, and +whitespace alone inserts nothing. The Windows read (Alt+V, or that empty paste) +is not implemented: it says so and attaches nothing, left to the platform +tickets (#200/#201). A paste that is only absolute +paths or `file://` URLs of image files (a Finder drop) attaches those files; +prose, relative names, and mixed paths keep the text and `@file` rules. At +submit, a model whose `input_modalities` includes `image` +gets an ACP image block. A model that does not declare `image` gets the +original saved under the isolated dsh home `attachments/pasted/` and a +`` path in the same message, and the attach notice says that +model cannot see images. Rules and the first-turn memory note wrap the user's +text once and are not copied onto that element or an attached file body. The isolated client does not call +a second vision provider. An empty clipboard, a file that is not png, jpeg, +webp, or gif, and a file over 256 KiB stay in the composer with a notice. +`GROK_CLIPBOARD_NO_NATIVE_READ` disables the macOS pasteboard read whenever it +is set. The packed launcher forwards that switch and `CODSH_CLIPBOARD_IMAGE`, +so the session reads the controlled file, including an empty one. A model or +screen-mode switch keeps the same bytes, not only the placeholder. Closing the +model menu puts that draft back. The draft is process state, as in the +reference: nothing of it reaches disk, a new launch in any project starts +empty, a sent prompt never returns, and an exec relaunch resumes the session +without it. Resume shows a sent image turn by its placeholders, never its +`` path, and sends nothing again. _Avoid_: upload, embed +**Image service**: +The substitute that `image_gen` / `image_edit` call in `codsh --rust` +(ticket 187). It exists only when `[models] image_gen` (and optionally +`image_edit`) names a `[model.]` marked `supports_image_generation` / +`supports_image_edit`; official hosts are a configuration error and a chat +model is never used in its place. `image_gen.rs` owns config, the `xai` +(reference JSON) and `openai` (OpenAI Images, e.g. stable-diffusion.cpp +`sd-server`) wire formats, reply checks, and atomic numbered saves under +`/images/`; `codsh --rust image run --json` is the job runner. +`rust-acp-image.mjs` registers the two dsh tools only when the client sets +`CODSH_IMAGE_GEN` / `CODSH_IMAGE_EDIT`, resolves `[Image #N]` from the latest +user message (image block, else its `` path), spawns the runner, +and kills it on cancel. Approval is the ordinary dsh ask +(`rust-acp-file-approval.mjs`): every call asks, a Read deny rule refuses a +reference path, and the Rust client titles the card and row from the saved +tool input (prompt, host, reference count, unknown price). `/imagine` sends +the reference instruction verbatim; the session reader and picker show it +as `/imagine `. The editor ACP server gets no image env and so no +image tools. +_Avoid_: image model (for the chat model), Imagine account + +**Video service**: +The substitute that `image_to_video` / `reference_to_video` call in `codsh +--rust` (ticket 188). It exists only when `[models] video_gen` names a +`[model.]` marked `supports_video_generation`; official hosts are a +configuration error and a chat model is never used in its place. `video_gen.rs` +owns config, the declared capabilities (`video_durations`, +`video_resolutions`, `video_tools`, sdcpp `video_fps` / `video_output_format`), +validation before any request, the `xai` (reference async +`/videos/generations` + `/videos/{id}`) and `sdcpp` (stable-diffusion.cpp +`/sdcpp/v1/vid_gen` + `/sdcpp/v1/jobs/{id}`) protocols, and the **video job +record** in `/video-jobs/.json`, written before the start +request. A job's status is one of submitting, queued, generating, downloading, +completed, failed, expired, refused, cancelled (with the service's cancel +answer), timed_out, unknown, or lost; only completed saves a file, atomically +under `/videos/`. `codsh --rust video run --json` is the job runner; +`/videos status` and `video status` query a job nobody follows and finish it. +`rust-acp-video.mjs` registers the tools only when the client sets +`CODSH_VIDEO_I2V` / `CODSH_VIDEO_R2V`, resolves `[Image #N]` references, +spawns the runner detached, and on cancel sends SIGTERM and waits for the +runner's cancel answer. Approval is the ordinary dsh ask; the Rust client titles +the card and live row from the saved tool input and job record (length, +resolution, host, image count, unknown price). `/imagine-video` sends the +reference instruction verbatim with the attached images and shows as typed. +_Avoid_: video model (for the chat model), render farm + +**Clipboard delivery**: +The result of one copy in `codsh --rust` (ticket 155). Every copy tries the +native tool, tmux's paste buffer inside tmux, and OSC 52 where the route +rules enable it, and always writes the backup file (`$GROK_HOME/last-copy.txt` +or `GROK_COPY_FILE`). It is *confirmed* only when a trusted leg succeeded (a +local native tool, or OSC 52 to a terminal documented to accept it with no +multiplexer between), *tmux buffer* when only tmux holds it, *unverified* when +OSC 52 went out and nobody can confirm it, and *unreachable* otherwise; the +last two name the backup file and never say "Copied!". A native tool over SSH +or in a display-less container writes the remote clipboard and is never +confirmed. +_Avoid_: copied (for an unverified or unreachable copy) + +**Terminal doctor**: +`codsh --rust doctor [--json]` and `/doctor`: detected terminal facts with the +variable that identified each, clipboard routes and the expected delivery, +tmux options read with `tmux show-options`, findings with stable ids, and the +list of things this build has not verified. Findings never change the exit +code. `doctor fix` edits only the user's tmux config, after confirmation, +with a backup, the file's mode and line endings kept, and a printed undo +command; it refuses conflicting or conditional assignments and never runs +`tmux source-file`. +_Avoid_: auto-fix, repair + +**Wrap**: +`codsh --rust wrap `: runs a command (usually `ssh host`) in a local +pseudo-terminal, marks it with `GROK_OSC52_SINK`/`LC_GROK_OSC52_SINK`, copies +its OSC 52 writes to the local clipboard, refuses OSC 52 reads, and restores +the terminal modes and termios it left behind when it exits or drops. Exit +code is the command's, or 128+signal. +_Avoid_: tunnel, proxy + +**Terminal notification**: +An OSC 9/99/777 or BEL sequence written for a `[ui.notifications]` event +(`turn_complete`, `approval_required`, `agent_error`, `session_ready`) when +the condition holds. With `unfocused` it fires only after DECSET 1004 focus +reports say the terminal has been unfocused for `idle_threshold_secs`, and a +focus report in the meantime drops it; a terminal that never reports focus +counts as focused. Hooks run the configured command with `GROK_EVENT`, +`GROK_MESSAGE`, and `GROK_SESSION_ID`. +_Avoid_: desktop notification (codsh only writes the sequence) + **Image preview card**: -The card centered over the transcript while the cursor rests against an -`[Image #N]` token — it says what is attached, and shows it. A terminal with an -inline-graphics protocol is handed the image itself: Kitty graphics for -Ghostty, kitty, and WezTerm, `OSC 1337` for iTerm2. Sending the protocol a -terminal does not implement fails silently, and a multiplexer forwards neither, -so both are read off the environment rather than assumed; whatever is left gets -a half-block mosaic, resampled in a child process so no native decoder is -loaded here. The payload never travels as row text — a base64 image measures as -thousands of columns and is cut mid-sequence by the width every row is fitted -to, which leaves the terminal eating the rest of the frame as string data — so -the rows reserve blank cells and the frame paints the picture over them at an -absolute position. Transcript around the card is dimmed so the picture is what -reads; the card itself stays undimmed. Ctrl+O and a click on the card open the original in the -platform viewer. Card and picture come down together: a Kitty placement is not -cell content, so clearing its rows would leave it on screen. +The notice shown while the pointer rests on an `[Image #N]` chip, or while the +cursor rests on or just after that chip. It names the chip, the sniffed pixel +size (png, gif, jpeg, and webp), the byte length, a short digest, and the saved +path when one exists. Frozen guide 03 is this metadata line, not a picture. +This client does not speak Kitty graphics, `OSC 1337`, or a half-block mosaic, +and it does not open a platform viewer. Those legacy protocols are not part of +this rewrite. The image bytes stay out of the row text. _Avoid_: thumbnail, attachment chip **Todo readout**: @@ -869,7 +1647,10 @@ rebinding when `/ship` continues or a spec appears; zoom and selection survive updates. Connection failures retain the last graph with an automatic retry notice. The URL is pinned on the Panorama teaser and overlay title; `/ship` does not open a browser. Off a TTY the URL is printed once. On narrow screens, node -details sit below the graph. +details sit below the graph. The `codsh --rust` Ship plugin serves the same page +under a secret path prefix (the app fetches `graph.json` relative to the page) +and joins the graph from the persisted files on each request; its terminal +line carries the teaser buckets and the answer count the page shows. Distinct from the Panorama overlay and the Panorama teaser. Not a second store. _Avoid_: dashboard, hub, site, graph UI, second port per `/ship` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0cee3a30..0ef55d59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,6 +60,859 @@ live updates, and reconnect behavior on desktop and mobile; test both `/` and `/index.html`. Check English interface labels without translating user source text, and answer persistence across graph-cache rebuilds and resumed runs. +## Frozen Grok rewrite reference (#133) + +The parallel rewrite's research register is in +[`docs/rewrite/reference/`](docs/rewrite/reference/README.md). It does not alter +legacy behavior or import the new Rust client. Run its portable checks with: + +```sh +node scripts/reference-inventory.mjs check +node scripts/reference-mapping.mjs docs/rewrite/reference .scratch/reference-inventory.json +diff -u docs/rewrite/reference/inventory.json .scratch/reference-inventory.json +pnpm exec vitest run scripts/reference-inventory.spec.mjs scripts/reference-evidence.spec.mjs +``` + +To reproduce reference observations on macOS, use Python 3.10+ and the exact +installed Grok 1.0.34/build 3736acbc8658 binary. The driver creates a temporary +HOME/GROK_HOME/workspace, denies personal-home reads and nonessential networking, +and never reads real auth or sessions. Output directories must be new. + +```sh +python3 scripts/reference-probe-test.py +python3 scripts/reference-probe.py --binary /absolute/path/to/grok-1.0.34 --output .scratch/reference-run --samples 5 +python3 scripts/reference-model-probe.py --binary /absolute/path/to/grok-1.0.34 --output .scratch/reference-model.json +node scripts/reference-baseline.mjs .scratch/reference-run/observations.json .scratch/reference-baseline.json +``` + +The model probe serves deterministic SSE on one loopback port, with a synthetic +key and no paid calls. The main probe denies all network access. Both require +`sandbox-exec`; do not remove confinement to make a probe run on another platform. +Native Linux/Windows drivers and full performance workloads remain downstream. + +Reconcile discovery using a clean public source checkout pinned at the recorded +commit (this is research, not an application import): + +```sh +git clone https://github.com/xai-org/grok-build.git .scratch/reference-source +git -C .scratch/reference-source checkout --detach a28ee2b2063426e8816e380ccea528b9de95e5da +node scripts/reference-inventory.mjs extract docs/rewrite/reference/observations.json .scratch/reference-source .scratch/reference-discovery.json docs/rewrite/reference/model-observations.json .scratch/reference-source-evidence.json +diff -u docs/rewrite/reference/discovery.json .scratch/reference-discovery.json +diff -u docs/rewrite/reference/source-evidence.json .scratch/reference-source-evidence.json +``` + +Every discovered item needs a story, owning ticket, observable acceptance scenario +and evidence or explicit blocker. A source declaration or guide is not runtime +verification; planned acceptance scenarios are not passing tests. Add newly +found behavior instead of weakening the extraction or shrinking the register. +The validator resolves capture/guide/schema/source-fragment evidence and enforces +the binary pin; changing a locator and recomputing the inventory digest is not +verification. Audit semantic ownership: terminal gestures need their actual +UI/session behavior, and headless input options need provider-wire assertions. +`reference-mapping.mjs` and `reference-scenarios.mjs` are the committed mapping +source. Regeneration uses captured ticket metadata, exact guide heading ancestry, +namespace defaults and narrow contextual overrides; it needs no scratch generator +or GitHub access. The checker rejects owner/scenario drift even when IDs and stories +are internally consistent; regeneration alone is not an independent semantic oracle. +Expected-owner regressions must be grounded in the quoted contract. Repeated TOML +settings retain their functional namespace owner as well as contextual enterprise +owners; broad `ui`, `toolset` and `compat` groups do not imply appearance or file +search behavior. Classify by context, not isolated words: hook prompt +blocks and sandbox write protection need real effects, MCP headers/stdio belong to +MCP, model headers to providers, ACP updates to protocol/session replay, and plan +feedback to plan review rather than telemetry. ACP `x.ai/review/comment` instead +records cloud code-review events and requires consent/destination acceptance. +Section extraction and owner context share a fence-aware Markdown scan, including +nested blockquote containers: normalize syntax for TOML fields while preserving +original quotation bytes and line locators. Code comments cannot hide subsequent +prose, and fenced examples remain section evidence, not behavior paragraphs. Audit extraction deltas for actual +prose preservation rather than preserving misclassified code-fragment identities. +Environment extraction recognizes literal reads/setters, named constants, env-map +lookups and key loops, enclosing environment-variable tables, assignment-form hints +such as `COLORTERM=truecolor`, and documented process reads, credential inputs and +launcher-resolution controls without vendor-prefix or underscore requirements. +Literal source names are not shell identifiers: preserve case, punctuation, +whitespace, leading underscores and single letters across reads, child injection, +env maps and loops. `container` is not `CONTAINER`; `PROGRAMFILES(X86)` and +`CARGO_BIN_EXE_xai-grok-pager` retain their full names in qualified/assignment forms. +Empty names, equals signs and NUL are not valid literal keys. New source identities require contextual mapping +and source-evidence/count digest updates, not new runtime availability claims. +Guide-qualified variables and assignment forms resolve to canonical functional +owners; a path mentioning a variable is not itself an environment identity. +Workflow budget/lifecycle subcontracts keep #182/#183 even under a slash owner; +actual memory-v2 capture/Dream controls use #186 and campaign patches use #139/#141. +Keep documented gates separate from newer source-only overrides in acceptance. +Audit control effects across canonical/config/example/alias representations: scrolling +and mouse capture are input behavior, ghost text differs from its model routing, +legacy metadata saves differ from model-backed capture, and pruning is compaction. +Diagnostics require real file/filter/destination checks and status-line environment +sanitization requires rc-file canaries. Fetch proxy/domain/enablement tests must not +be replaced by search-policy acceptance; mixed rows may require both scenarios. +Privacy and feedback acceptance (`scripts/rust-privacy-pty-test.py`) drives the +packed `codsh --rust feedback` command and the live `/feedback` form: local +draft save/edit/delete, failed submit retention, explicit submit to a loopback +substitute, redaction when `privacy.share_content` is off, a trace-upload POST +only when that switch is on, refused official hosts, and a network audit that +must not contact an unconfigured host. Diagnostic payloads are kind/ok/count. +`GROK_LOG_FILE` and `GROK_HOOKS_LOG` stay unwired: the launcher allowlist does +not forward them, and the Rust client does not read them. +`GROK_CLAUDE_SKILLS_ENABLED` and `GROK_CURSOR_SKILLS_ENABLED` are on that +allowlist, as are `CODSH_CLIPBOARD_IMAGE` and `GROK_CLIPBOARD_NO_NATIVE_READ`. +The image PTY sets the clipboard file so the packed session does not read the +host pasteboard. `CODSH_IMAGE_PTY_ONLY=vision,text-only,restart` runs a subset +of `scripts/rust-image-pty-test.py` while debugging; the full run is the +acceptance evidence. A packed `codsh --rust inspect` with both set off must not list +`.claude` or `.cursor` skills (`scripts/rust-launcher.spec.mjs`). +Do not treat model `base_url` traffic as telemetry. +Keep shell completion separate from next-prompt suggestions, diagnostic logging from +hook authority, and sandbox auto-approval from confinement. Filesystem confinement +for `codsh --rust` is `python3 scripts/rust-filesystem-sandbox-test.py`: it builds +nothing itself and runs the debug or staged `codsh-rust` with `--sandbox project`. +The probe must show Seatbelt (macOS) or Landlock (Linux) denying an outside write, +a rename, a rename of `$GROK_HOME`, a hook parent, or a deeper hook ancestor onto a write root, a symlink escape, a hook +retarget, a protected `config.toml` write, and +a `*.pem` glob, a workspace `**/.env` and `certs/**/*.pem` (without denying the sibling or a same-prefix path), and negated `[!a]` / `[^b]` classes, while an allowed sibling write succeeds. It also runs a +`devbox`-based profile whose `deny` list must still hold, absolute globs and an +exact not-yet-existing file under `/tmp` and under a symlinked directory (denied +at the resolved path), a workspace named `ws[12]*?` whose relative glob must not +hit the sibling `ws1ab`, refusals for a deny under a dangling symlink or with +a control character, a glob whose literal-prefix directory cannot be renamed +out of the deny (a directory inside the tail cannot be renamed either, because +Seatbelt does not see the destination), a directory inside the glob tail +(including one created after launch) that cannot be renamed onto `/tmp` or +another in-workspace write root, +a workspace `**/.env` whose ancestor walk stops at the resolved write root +(renaming a workspace directory onto a fresh `/tmp` sibling stays allowed, +`outside/.env` stays readable, and the generated profile does not pin `/tmp` +or `/private/tmp` alone), +an absolute deny glob whose nested directory cannot be renamed to a sibling +outside that glob, and a +`inspect --json` probe that prints every config error (including an unknown +field and unsigned `fail_closed`) instead of stopping at the sandbox precheck, +and a launchd escape probe: a sandboxed child running `launchctl submit` and +`launchctl bootstrap gui/$UID` must not get an unconfined process to read a +denied file. That probe uses a unique `codsh.sandboxprobe.` user-domain +label, targets a throwaway secret, and tears the job down (the harness sweeps +`launchctl list` afterward); unsandboxed both launchctl paths do exfiltrate +the secret, so the block is real and matches the reference nono `mach-lookup` +rules. Its fixture is created +outside `/tmp`, `/private/tmp`, `/var/tmp`, and `TMPDIR`, because those directories +are write roots. `python3 scripts/rust-sandbox-pty-test.py` is the +installed-product check: it packs `codsh`, runs `codsh --rust --sandbox session` +in a PTY with the mock model (`DSH_CODE_CLI_MOCK_TOOL=sandbox-session`), and has +dsh's `read`/`edit`/`write`, `bash` (`cat`, redirection, `mv`, `python3`), and +a real `subagent` (its own file tools and `bash`) attempt reads, writes, and +renames of a denied file, a write-denied hook, and its directory. Denials must +be kernel `EPERM` with the protected bytes and names unchanged, while allowed +edits and writes land and the bash approval card still appears; a +`--sandbox off` run of the same calls is the control. dsh's nested +`sandbox-exec` fails under Seatbelt (`sandbox_apply: Operation not +permitted`), so codsh passes dsh `$DSH_HOME/codsh-kernel-sandbox.yml`, which +sets only its per-call file mode to `danger-full-access`. The Seatbelt profile still allows global Mach lookups apart +from the keychain services; whether an unconfined service reached that way can +act on a denied path has not been probed and is not claimed either way. A macOS result does not +mark Linux or Windows supported. Linux refuses a profile that must write-deny a +path inside a write root, because Landlock cannot express that exception. +`python3 scripts/rust-network-env-sandbox-test.py` is the ticket 12 probe. It +checks a denied outbound TCP connect (EPERM to 127.0.0.1), an allowed connect, +a secret hidden from and shown to `sh -c env`, a cancel path that kills the +probe process group, and a startup refusal when the policy cannot be applied. +A macOS run does not claim Linux Landlock network. Terminal/editor/platform +aliases and background model/admission/login/goal/compaction controls need observable +functional effects. Memory prose must retain capture, queue/lease diagnostics and +telemetry privacy only where quoted; merged source/binary paragraph identities can +carry different contracts without proving either runtime behavior. +`config-audit.json` records the full generic-fallback review, not a hand-picked +list of environment-name fixes. New generic-only config/env rows must receive a +reviewed classification and effect-specific scenario; the checker rejects an +unreviewed `PARITY-139` fallback. Source-only build/test/internal declarations use +scoped `DISCOVERY-133-*` research and explicit blockers, never effective-config +parity. Preserve frozen public contracts even when the newer source disagrees. +Canonical/documented fields and explicit environment aliases must include the +same core owners/scenarios, while retaining valid context-specific additions. +`reference-config-audit.spec.mjs` tests these invariants on independent fixtures +and samples every classified family; source extraction also checks audit consumer +quotations byte-for-byte against the pinned checkout. Do not regenerate original +captures or source provenance for an ownership-only correction. +Alias ownership follows explicit “Also ENV” +config-reference relationships before lexical namespaces; incidental mentions are +not aliases. Local hook identity variables must not inherit remote-workspace +ownership. `/vim-mode` is scrollback navigation, not prompt Vim editing, and +`/import-claude` imports configuration rather than session histories. Overlay +allowlists require effect-based security acceptance. Source/build controls remain +provisional, not released capabilities. Interactive help/docs/diagnostics require real terminal acceptance; +feature-gated panes retain explicit availability blockers until exercised. +The portable checker requires independent source-only coverage as well as captured +behavior; paired deletion from discovery and inventory must fail. Source evidence +contains command enum/argument relationships that seed hidden-command help probes. +The driver defaults to the checked-in `source-evidence.json`; use +`--source-evidence ` for a freshly regenerated artifact. All command-tree probes append +`--help`, never execute the underlying operation, and retain rejected source-only +paths as unverified. The offline probe also covers compatibility options, paired +FPS runs, fullscreen/minimal tutorial navigation, and help/docs palette/reader +filtering, scrolling, aliases, dismissal, debug FPS toggling and error recovery. +It does not open the personal browser or verify live dock resources. For only the +supplemental small-command observations, run `reference-probe.py` with +`--small-commands-only --binary /absolute/path/to/grok-1.0.34 --output .scratch/reference-small`. +This avoids regenerating valid performance/headless captures. Its two PTYs record +announcement usage, unsupported-graphics GBOOM refusal, dashboard location picker +opening/dismissal via Ctrl+L, the `/cd` autocomplete placeholder and invalid path, +minimal dashboard refusal, typing and clean quit. Ticket 27 adds the installed +`sessions list`/`sessions search` contract, `/resume` title-or-content filtering across workspaces, +manual `/rename` priority, configured-model titles, and the fullscreen dashboard. +Foreign Claude/Codex/Cursor session roots stay gated and are not native dsh history. It does not establish populated +banners, new-agent cwd, overlay rendering or argument passthrough. The model probe +rejects malformed structured output and missing terminal events. +Freeze measured numeric performance thresholds before collecting candidate data. +Run the ordinary typecheck and full unit suite for changes to these workflows. + +## Isolated Rust client (#134–#139) + +Rust 1.98.1 (verified toolchain, edition 2024), Cargo, Node 22.19+, and the ordinary pnpm workspace +are required to build a local native candidate. `rust/Cargo.lock` pins the +selected UI dependency closure. The complete upstream agent closure is not +built: it would initialize official execution/auth/services and also requires +DotSlash/protoc. This slice imports the upstream text editor/inline terminal +crates and extracts offline wide/stacked welcome layout; see +`rust/upstream/import.json` and `rust/upstream/MODIFICATIONS` for provenance. +The client reuses the official fullscreen alternate-screen renderer and the +imported inline crate for minimal native-scrollback mode. `/minimal` and +`/fullscreen` switch in process without restarting dsh; session-scoped CLI flags +do not rewrite `[ui] screen_mode`. This is not a claim of complete Grok parity. +Frozen behavior remains 1.0.34, while the imported public source declares 1.0.35 +(correspondence unproven). + +```sh +pnpm run build:rust +cargo check --manifest-path rust/Cargo.toml --locked --workspace +pnpm run test:rust +cargo clippy --manifest-path rust/Cargo.toml --locked --workspace --all-targets -- -D warnings +cargo fmt --manifest-path rust/Cargo.toml --all -- --check +pnpm run typecheck +pnpm test +pnpm run build +pnpm exec vitest run --config vitest.e2e.config.ts e2e/wrapper.e2e.ts e2e/pty-input.e2e.ts e2e/pty-session.e2e.ts +pnpm run test:rust:pty +pnpm exec vitest run scripts/rust-acp-protocol.spec.mjs +python3 scripts/rust-cancel-pty-test.py +python3 scripts/rust-resume-pty-test.py +python3 scripts/rust-config-pty-test.py +python3 scripts/rust-model-pty-test.py +python3 scripts/rust-compact-pty-test.py +python3 scripts/rust-trust-pty-test.py +python3 scripts/rust-auth-pty-test.py +python3 scripts/rust-permission-pty-test.py +python3 scripts/rust-screen-pty-test.py +python3 scripts/rust-fork-pty-test.py +python3 scripts/rust-plugin-pty-test.py +python3 scripts/rust-plugin-content-pty-test.py +python3 scripts/rust-ship-pty-test.py +python3 scripts/rust-prompt-pty-test.py +python3 scripts/rust-voice-pty-test.py +python3 scripts/rust-nav-pty-test.py +python3 scripts/rust-content-pty-test.py +pnpm exec vitest run scripts/rust-subagents.spec.mjs +python3 scripts/rust-subagent-pty-test.py +pnpm exec vitest run scripts/rust-subagent-messages.spec.mjs +python3 scripts/rust-subagent-message-pty-test.py +pnpm exec vitest run scripts/rust-worktree.spec.mjs +python3 scripts/rust-worktree-pty-test.py +pnpm exec vitest run scripts/rust-plan.spec.mjs +python3 scripts/rust-plan-pty-test.py +cargo build --manifest-path rust/Cargo.toml --locked -p codsh-rust +pnpm exec vitest run scripts/rust-workflow.spec.mjs +python3 scripts/rust-workflow-pty-test.py +pnpm exec vitest run scripts/rust-goal.spec.mjs +python3 scripts/rust-goal-pty-test.py +pnpm exec vitest run scripts/rust-plugin-mcp.spec.mjs +python3 scripts/rust-plugin-mcp-pty-test.py +pnpm exec vitest run scripts/rust-mcp-remote.spec.mjs +python3 scripts/rust-mcp-remote-pty-test.py +``` + +Workflows (ticket 181) run the vendored reference engine (`cargo test --manifest-path rust/Cargo.toml -p xai-workflow`, and `workflow::` unit tests in `codsh-rust` for the JSON-line host protocol, source resolution, and option checks). `scripts/rust-workflow.spec.mjs` uses the debug binary as the engine and drives released dsh over ACP with the keyless `subagents` mock mode: in a parent prompt `WORKFLOW_CALL ` makes the model call the `workflow` tool once and answer `PARENT_WORKFLOW ok|error: `, and the children answer by prompt marker (`CHILD_SAY ` echoes, `CHILD_MODEL` reports the route and effort dsh used, `CHILD_FAIL` ends with a provider error, `CHILD_WORKFLOW` tries a nested workflow, `CHILD_SLOW` waits for a cancel). It covers args, parallel results, per-agent model/effort/capability, trusted and untrusted `script_path`, syntax and metadata errors, child and host failures, every refused option, the budget, `validate_only`, an endless script, cancellation without leftover engines, nested refusal, and disabled subagents. Ticket 182 adds `CHILD_JSON FIRST<> RETRY<>` (answers the second text once told it missed the output contract) and `CHILD_HOLD ` (counts HOLD turns live at once and reports `peak=N`); `CODSH_REVIEW_TRACE` records each model turn with its user and assistant texts. The ticket 182 tests cover schema pass, retry in the same child and failure after the retry, the live cap against a wider panel, budget independence, a failure mix, a cancel with queued children, a killed engine, scratch and `git_diff_since`, and ask mode, a deny rule, `capability_mode` and a disabled type for workflow children; `workflow::`/`workflow_host::` unit tests cover the schema, scratch, git and agent-run limits. Ticket 183 turns those tests to background runs: the mock answers a parent turn whose last user message holds a workflow completion reminder with `PARENT_WORKFLOW_NOTICE` and the reminder, and `CHILD_ONCE ` waits 60 seconds on its first turn per tag and answers `CHILD_ONCE_DONE ` at once afterwards (so a resumed run shows which children ran again). The spec plays the Rust client's control socket for `/workflow` and covers unique names across sessions, the 4-run cap, user versus model stop, pause with real children stopped and resume from the journal, a budget stop resumed under a higher cap, a restart after SIGKILL that refuses every resume, and the plain-prompt wait (`CODSH_WORKFLOW_FOREGROUND=1`). `scripts/rust-workflow-pty-test.py` (staged binary) types a `WORKFLOW_CALL` for a saved `triage.rhai` and checks the start reply, the completion notice turn, the workflow block title, `/workflow runs`, the `/tasks` tags and Workflows section, `/workflow pause`, `resume`, and `stop` on a running workflow by name (engine and child gone after pause and stop), a syntax error, and the `-p` path, plus a headless panel under the configured cap (config file, then environment), a schema retry, and a scratch note. Ticket 184 adds saved workflows: `workflow_catalog::` unit tests cover precedence, trust, invalid and symlinked files, the listing caps, and every save refusal; in the spec, the mock answers a prompt holding `WORKFLOW_CONTEXT` with `PARENT_WORKFLOW_CONTEXT` and every saved-workflow listing and slash-launch reminder the session received, and a completion notice is found in any user message since the last reply. The spec covers launch by name (project over personal when trusted, personal when not), the listing and its change note, an invalid file named by name, `/` and `/workflow ` with text or JSON args, `--agent-budget` and `--effort` reaching the children, an edited file not changing a running or resumed run, and `/workflow save` (success, existing file, duplicate handle, untrusted folder, unwritable directory). The PTY test adds `/workflows` and `/workflows ` (details and an invalid file's reason), completing `/digest` from the slash menu and running it, an invalid `/`, `/workflow save`, and `-p` by name. Ticket 205 adds plugin workflows: `workflow_catalog::` unit tests pass plugin sources to `scan_with` and cover qualified and bare names below project and personal workflows, two plugins offering one name, a name defined twice in one plugin, invalid plugin files, and every inactive state; a `plugin::` test covers the `workflows` layout, contributions, and `workflow_sources` through enable and uninstall. The spec installs real plugins with the debug binary (`plugin install --trust`, `enable`, `update`, `disable`, `uninstall`) and covers install running nothing, the disabled refusal, the listing, launch by `demo:review` and by the bare name with real children, the origin kept in `run.json` and `launch.json`, a personal workflow taking the bare name, disable and uninstall refusing new launches, two plugins with one name, and a paused run that resumes its original script after an update and is refused while its plugin is disabled or uninstalled. The PTY test installs a plugin through the launcher in a third session and checks `/workflows` and `/workflows demo:digest`, `/lint` and `/demo:digest` running real children, the runs overview's source line, and `-p` by qualified name before and after `plugin disable`. The built-in deep-research workflow (ticket 206) has its own block in the same spec: a Node fake SearXNG (`/search?format=json`, HTTP 429 for RATELIMIT) and page server on loopback, reached through `$GROK_HOME/config.toml` and the Rust `web` command (`startAgent({ web })` sets `CODSH_WEB_SEARCH`/`CODSH_WEB_FETCH` and `CODSH_RUST_BIN`), and mock children recognised by the reference prompt openings (planner, researcher, verifier, synthesizer; question markers RESEARCH_PLANNER_FAIL, RESEARCH_BRANCH_FAIL, RESEARCH_SLOW, RESEARCH_VERIFIER_BADIDS, RESEARCH_BAD_CITATION). It covers a verified run with traceable sources and `report.md`, a partial run (failed branch, rate limit, contradicted claim, citation fallback), missing search or fetch with a failed planner, stop, agent budget, the no-query pause, precedence over a project file, and the save refusal; `workflow_catalog::` unit tests check the pinned copy against `rust/upstream/import.json`. The PTY script runs `/deep-research` and a `-p` run by name against a Python fake of the same services. No test calls a real model or live web search. + +Worktrees (ticket 174) use temporary real git repositories only. `scripts/rust-worktree.spec.mjs` covers `rust-worktree.mjs` directly (dirty carry, clean `--worktree-ref`, suffixes for a taken branch or directory, non-git and unborn refusals, merge/overwrite/conflict/delete/binary/exec-bit apply, symlink and symlinked-directory refusals, `rm` and `gc` safety, `db rebuild`, and the two-view permission check) and drives released dsh with the `subagents` mock for `isolation: "worktree"`: the child's edit stays in its worktree until applied, an unchanged worktree is removed after completion and after cancel, a checkout deny rule covers the worktree copy, and a non-git parent starts no child. `scripts/rust-worktree-pty-test.py` needs `pnpm run build:rust` and runs `-w`, `/worktree list|apply`, an isolated child in the TUI, a CLI conflict, `-w -r`, a subdirectory offset, a non-git refusal, `rm`, and `gc` through the repo launcher. It was run on Linux only. + +Subagents (ticket 172) have two layers of evidence. `scripts/rust-subagents.spec.mjs` drives released dsh over ACP with the keyless `subagents` mock mode: type capability allow-lists, permission inheritance in ask mode, depth, disabled and unknown types, a model override and a missing model, cancel through the control directory, parent `session/cancel`, the `fail` and `queue` limits, and one background delivery through `job_output`. `scripts/rust-subagent-pty-test.py` runs the Rust client in a PTY against the repo launcher and the staged binary (`pnpm run build:rust` first): an explore child that loses `write`, a general-purpose child that writes, the Ctrl+G and `/tasks` list, a read-only child view, Ctrl+C cancelling only the child, `x` cancelling a background child, `/compact`, `--resume`, child sessions kept out of `sessions list`, and the headless `--no-subagents` and `Agent(type)` flags. It does not require macOS; it has been run on Linux only. + +Subagent messages and continuation (ticket 173) have two layers too. `scripts/rust-subagent-messages.spec.mjs` unit-tests the outcome, disposition, and size helpers, then drives released dsh over ACP with the keyless `subagents` mock mode and a scripted parent (`STEPS [...]` in the prompt runs tools in order, with `{{id:N}}` / `{{job:N}}` naming earlier results): the flag off by default, steer / queue / interject delivery to a running child (interject ends the child's blocking `job_output` wait early), waking a completed child as the same identity, `resume_from` seeding a new child with the old transcript and pinned model, every refusal (unknown or foreign id, cancelled, oversize, per-child and total quotas), child-to-parent and sibling messages, and the refusal inside workflow, scheduler, and verifier children. `scripts/rust-subagent-message-pty-test.py` runs `codsh --rust` with `GROK_ACTIVE_AGENT_MESSAGES=1` and checks the `Message sent to …` row, `· attempt 2`, `continues "…"`, the child view's `◎ Message from parent` turn, a rejected unknown id, and that the tool is absent by default. + +Plan mode, questions, and todos (ticket 179) have two layers as well. `scripts/rust-plan.spec.mjs` runs released dsh over ACP with the keyless `interaction` mock mode (prompt keywords ASK_ONE, ASK_MULTI, PLAN_ENTER, PLAN_EXIT, PLAN_EMPTY, PLAN_EDIT_OTHER, PLAN_EDIT_FILE, TODOS, STATUS) and plays the Rust client's end of the control socket: the headless no-operator answer, `--no-plan` / `--no-ask-user` removing tools, the `--todo-gate` limit of two, `plan_set`, the plan gate under always-approve and ask, the plan file, approving and quitting the review, the empty-plan review, single and multi-select answers, the timeout and a stale late answer, `enter_plan_mode` approval and decline, and plan mode surviving `session/resume`. `scripts/rust-plan-pty-test.py` drives the real Rust client in fullscreen and minimal (the status flag, review keys, `/view-plan`, the card, the todos pane and Ctrl+T, Shift+Tab, and headless flags). It runs on Linux and macOS. + +Goals (ticket 180) have two layers. `scripts/rust-goal.spec.mjs` unit-tests the verdict parser, panel rule, policy, and usage counting, then drives real dsh over a Unix control socket with the keyless `goal` mock mode (objective markers CLAIM_FROM_k, NEVER_CLAIM, WRITE_ROUND_k, BURN, SLOWROUND, BGJOB, and verifier markers VERIFY_PASS, VERIFY_FAIL, VERIFY_SILENT, VERIFY_SPLIT, VERIFY_FILE, VERIFY_SLOW): achieved, refused then achieved, the verification limit and resume, split panels, a silent verifier, subagents off, the token budget, pause during a round and during verification, ACP cancel, a user takeover, a running background job, goal mode off, `session/resume` persistence, and usage errors. The model's own claim never completes a goal in any of them. `python3 scripts/rust-goal-pty-test.py` runs on Linux and macOS after `pnpm run build:rust`: a refused and then verified completion with named goal rounds, `/goal status`, pause during a round, the paused state after quit and `--resume`, resume and clear, a budget stop that refuses resume, Ctrl+C pausing a round, and `GROK_GOAL=0`. + +Plugin MCP servers (ticket 204) have three layers. `plugin::` unit tests install real plugin directories into a temp Home and check that only a trusted, enabled plugin contributes servers, a user definition and an earlier plugin shadow a later one, `${CLAUDE_PLUGIN_ROOT}`/`${GROK_PLUGIN_DATA}` expand, a relative command resolves in the plugin and one that escapes it is invalid, manifest path and inline forms load, a broken `.mcp.json` does not hide other plugins, disable forgets only that server's remembered allows (denials and other servers' grants stay), and the mounted-plan comparison asks for a remount on enable, update, and disable but not for unrelated MCP config edits. `scripts/rust-plugin-mcp.spec.mjs` uses a plugin that ships the keyless fixture `e2e/fixtures/rust-mcp-fixture.mjs` behind a relative `./bin/server` and drives real dsh: an untrusted install is refused, installed-but-disabled mounts nothing, enabled lists the tools and `mcp list` says `plugin: demo`, a headless call without a rule and an ACP reject have no side effect, an ACP approval writes into the plugin data directory once, a remembered project approval covers the tool in a new session, `plugin disable` forgets it and the open ACP session loses the tools before its next prompt (direct and `use_tool` calls fail), re-enabling asks again, uninstall removes the server, and a second case checks a user server beating two plugins, a missing program as a partial failure consistent between `plugin list --json` and the tool list, `mcp disable|enable` on a plugin server, `mcp remove` refusing with a plugin hint, the first plugin taking a name back, and `plugin update` remounting the new definition. `python3 scripts/rust-plugin-mcp-pty-test.py` (Linux; macOS not run) enables the plugin from `/plugins`, checks the expanded row (`mounts with the next prompt`, `connected (2 tools)`, `still mounted; withdrawn with the next prompt`), `/mcps` showing `[plugin: demo]`, an `a` approval being remembered and then forgotten on disable, and the next prompt's `unknown tool` for the old name. Real plugin marketplaces and paid MCP servers were not used. + +`build:rust` stages the host binary under ignored `packages/cli/native/-` +with SHA-256, selected dependency metadata and license/notice files. `npm pack` +from `packages/cli` includes it; install that tarball locally to try `codsh --rust`. +No download-on-launch, release, global install, or default cutover occurs. +Cross-target packaging/CI publication and native Linux/Windows verification are +later tickets, not established by a macOS build. Packages lacking the artifact +fail explicitly. The regular legacy `build` does not add native artifacts. + +Prebuilt packaging (ticket 66) keeps every platform's client inside the one +`codsh-cli` package, so the fixed `codsh-cli`/`codsh-bundle` Changesets group and +`pnpm run release` are unchanged; splitting into per-platform optional packages +would be a separate, owner-approved release-layout change. Because +`packages/cli/native/` is not committed, `release.yml` now calls the reusable +`.github/workflows/rust-native.yml` (darwin-arm64 on `macos-15`, darwin-x64 on +`macos-15-intel`, linux-x64 on `ubuntu-22.04` and linux-arm64 on +`ubuntu-22.04-arm` for a glibc 2.35 floor), stages the four directories plus +the generated Ship extension, and runs `check:rust-package -- --require +darwin-arm64,darwin-x64,linux-x64,linux-arm64` before the Changesets step; a +missing or mismatched platform stops the publish. On Linux the `openssl` crate +is built `vendored` (static OpenSSL 3, license in +`licenses/openssl-src-*/OPENSSL-LICENSE.txt`), so the client needs only glibc +and `libgcc_s`. `build:rust` reads the ELF's DT_NEEDED libraries and newest +`GLIBC_*` version into `artifact.json` `runtime`; `check:rust-package` fails if +they differ from the binary, and the launcher refuses an older glibc, a +non-glibc (musl) system or a missing library with the distribution packages to +install, before any Home is created (#199). +The Windows client (#200) is `x86_64-pc-windows-msvc`, built on `windows-2022` +by `rust-native.yml` with `+crt-static`, staged as `native/win32-x64` and +required by `release.yml`. Windows-specific code paths: `dunce` canonical paths +(no `\\?\` verbatim prefixes reach dsh or the Homes), the ACP control channel on +a loopback TCP port with a token instead of a Unix socket, `taskkill /T /F` for +process trees, a session-owner lock on one byte far past the record (mandatory +byte-range locks would make the owner file unreadable), key releases handled +only for the voice chord (Windows reports a release for every key), a release +with no press before it (an Alt code, or a ConPTY character that is not on the +keyboard layout) typed as the character, and no kitty keyboard push/pop. A +Linux box can check the Windows build with `rustup target add +x86_64-pc-windows-gnu` and `cargo clippy --target x86_64-pc-windows-gnu`; the +real run is `scripts/rust-windows-pty-test.py` on a Windows runner (Python with +`pywinpty`), which the `windows` job of `rust-platforms.yml` runs on the +integration branch and on branches whose name contains `windows`. It installs +the packed tarball with registry dsh into a clean prefix after removing the Rust +toolchain and drives the installed client through ConPTY; the evidence artifact +is `windows-evidence-`. +Interaction performance (#202) is measured by `scripts/rust-perf-bench.py` +(Python 3 with `pyte`) against the pinned reference binary (SHA-256 checked, +never redistributed). One run measures the reference in both screen modes, +freezes `thresholds.json` from those samples (latency ceiling max(p95 × 1.2, +p95 + 16.7 ms), throughput floor median × 0.9, resource ceiling p95 × 1.2) and +logs its SHA-256 before any candidate process starts, then measures the +installed candidate alternating with reference control sessions and judges it +against the frozen file only; thresholds are never loosened after the fact. +Both products talk to one loopback model fixture, so model time is reported +apart from client and dsh time. The `macos-perf` job of `rust-platforms.yml` +runs it on `macos-15` for `ci/**` branches whose name contains `perf` (most of an +hour); `docs/rewrite/perf/` keeps the frozen thresholds and the before/after +reports with the method and the breakdown. The pinned reference is the macOS arm64 build; elsewhere, pass `--only candidate` +(measurements only, no judging). +Concurrency and long-run resource stress (#209) is measured by +`scripts/rust-stress-bench.py` (Python 3 with `pyte`, and `pywinpty` on +Windows) against the same pinned reference on Linux, macOS and Windows. One +run freezes `thresholds.json` from this runner's reference samples (latency +ceiling max(p95 × 1.2, p95 + 16.7 ms), memory-growth ceiling max(p95 × 1.2, +p95 + 32 MiB)) and logs its SHA-256 before any candidate process starts, then +measures the installed candidate under the same load and judges it against +the frozen file only. Hard checks (no leftover process and no model traffic +after quit or crash; resume honesty) are not derived from the reference. The +`stress` job of `rust-platforms.yml` runs it for `ci/**` branches whose name +contains `stress`; `docs/rewrite/perf/stress.md` keeps the method, load and +per-platform notes. +`rust-terminal-pty-test.py` also checks that a closed terminal ends the client, +dsh and the launcher (with and without SIGHUP), and that Ctrl+Q while dsh is +still starting quits at once without leaving dsh behind and that text typed +during startup is kept. +The three-platform capability matrix (#201) is `scripts/rust-capability-matrix.py` +plus the `capability-matrix` job of `rust-platforms.yml` (Linux, macOS, Windows) +and `docs/rewrite/platform-capabilities.md`. It records the OS and terminal, +runs the installed-product checks this platform supports (keys, mouse, shell, +cancel, screen, terminal restore, real clipboard, voice doctor, sandbox, SSH, +tmux, Windows ConPTY), and writes `capability-matrix.json`. Cells are `ok`, +`refused` or `unavailable` — a missing microphone or a refused sandbox profile +is never a silent pass. Branches whose name contains `matrix` also run the job. +On Linux run the matrix under `xvfb-run -a` so the X11 clipboard cell is +exercised. +`build:rust` passes the `codsh-cli` version to the build +(`CODSH_PACKAGE_VERSION`), so `codsh --rust --version`, the ACP `clientInfo` and +`artifact.json` all name the package version; a plain `cargo build` reports the +crate version with `-dev`. `artifact.json` now +also records `version` (the `codsh-cli` version), `requiresDsh`, `binary` and +`format`; the launcher refuses a staged client whose version differs from its +own package (an interrupted update), whose SHA-256 or executable header +(Mach-O/ELF/PE and CPU, universal Mach-O accepted) does not match its directory, +or whose directory is missing, and prints the reinstall/rollback commands. +`pnpm run build:rust -- --target ` stages another target +(`aarch64-apple-darwin`, `x86_64-apple-darwin`, `x86_64-unknown-linux-gnu`, …) +when this machine has that Rust target and its linker/SDK; on an Apple silicon +Mac, `rustup target add x86_64-apple-darwin` then that command stages the Intel +build. Linux cannot build the macOS targets: `aws-lc-sys` needs Apple clang and +the SDK, and the client links AppKit/CoreFoundation, so macOS artifacts are +staged on a Mac or a GitHub Actions macOS runner (ad-hoc signed by the Apple linker; no Developer ID signing or +notarization is performed or claimed). Before a release that promises macOS +prebuilds, run `pnpm run check:rust-package -- --require darwin-arm64,darwin-x64`: +it lists what `npm pack` would ship (dry run; nothing is packed, uploaded or +published) and verifies each staged directory for its own platform. + +The `rust-acp-*.mjs` dsh plugins load harness packages (`@deepseek-ai/dsh-llm`, +`@deepseek-ai/dsh-tools`) from the running dsh through `rust-acp-dsh.mjs`, +because after `npm install -g @deepseek-ai/dsh codsh-cli` those packages live +under dsh's own `node_modules`, not above `codsh-cli`; a bare import only worked +in this workspace. The launcher also resolves the optional LSP/ask-user plugins +beside that dsh, passes a dsh found on PATH as its real path, refuses a dsh +package older than `codsh.requiresDsh` before creating the Rust Home, and +records the running version in `~/.codsh-rust/codsh-version.json` so an update +or a rollback to an older `codsh-cli` is announced once. `codsh update` runs the +new package's `codsh --rust install-check` when `~/.codsh-rust` exists and, if +it fails, prints the command that returns to the previous version. + +```sh +pnpm run build:rust +pnpm exec vitest run scripts/rust-artifact.spec.mjs +python3 scripts/rust-install-test.py +pnpm run check:rust-package +``` + +`scripts/rust-artifact.spec.mjs` covers the header reader, every refusal, the +dsh floor, the version stamp, the plugins loading from a dsh laid out like a +global npm install (no harness package above `codsh-cli`), and launcher +refusals that leave no Rust Home behind. `scripts/rust-install-test.py` packs +`packages/cli`, installs it with `npm install -g` into a fresh prefix beside a +dsh (this checkout's, or `CODSH_INSTALL_TEST_DSH=` for one +installed from the registry), and runs without `CODSH_ACP_PATCH` against a +local OpenAI-compatible fixture: `install-check`, a headless turn, a PTY turn +with terminal restoration, an in-place update that keeps sessions, refusals for +a half-updated, damaged, wrong-CPU and missing client (no dsh start, no Home +change), an old and a missing dsh, a rollback, `codsh update` to a package +without this platform's client, and `codsh --rust update` (`--check`, the update +itself, and `--to` back). Fake `cargo`/`rustc`/`rustup`/`grok` on PATH must +never run and legacy `~/.dsh`/`~/.grok` canaries stay byte-identical. It runs on +Linux and macOS. `.github/workflows/rust-platforms.yml` (on pushes to the +rewrite branch and `ci/**`) builds every native directory, packs the +multi-platform `codsh-cli` (never published), and on `macos-15` (arm64), +`macos-15-intel` and `macos-15` with an x64 Node under Rosetta removes the Rust +toolchain, installs that tarball into a clean prefix, and runs this script; +the two native jobs also run `scripts/rust-pty-test.py`. The Linux jobs run +`scripts/rust-platform-test.py` (a clean `install-check`, this install test, and +the turn, approval, cancel and resume PTY tests, which now run on Linux as well +as macOS) on `ubuntu-22.04`, `ubuntu-24.04` and `ubuntu-22.04-arm` without a +Rust toolchain, and its `install-check,install` steps inside clean +`node:22-bookworm-slim`, `node:24-trixie-slim` and `fedora:41` containers +(no libssl); `node:22-bullseye-slim` (glibc 2.31) and `node:22-alpine` (musl) +must be refused before any Home is created. `platform-report.json` records the +OS release, kernel, C library, Node and terminal, every step, and an +`untested` list; #200 and #201 reuse it. `scripts/rust-update.spec.mjs` +covers installer selection (`CODSH_INSTALLER`, then the pnpm/Yarn/Bun global +directory, then `npm_config_user_agent`, then npm) and `codsh --rust update` +against a fake registry and package manager. + +`test:rust:pty` requires macOS, Python 3 and clang. It packs and locally installs +the product in a temporary prefix, uses synthetic HOME/DSH_HOME/workspace canaries, +operates the actual Rust UI through a PTY, and checks termios, screen/paste/cursor +restoration after normal exit, cancellation, signals and malformed Profile startup. +It also requires a case-insensitive test volume and checks twenty installed-product +PTY refusals for `DSH_HOME`/`GROK_HOME` aliases: existing root/dsh/Profile, reverse +spelling, a missing child, and genuinely absent root/dsh/Profile/mixed-case paths. +Each must fail before terminal entry or any directory/file mutation. Four separate +missing-Home controls (including a similar prefix) must still launch without +creating the configured legacy Home. `scripts/rust-legacy-home-pty-test.py` is the +focused form: after a submitted prompt, an inherited `GROK_HOME` or `DSH_HOME` that +names a missing or populated separate Home stays absent or unchanged, and prompt +history lands in `~/.codsh-rust/.grok`. The launcher uses native canonicalization +for existing paths and compares device/inode ancestry, not realpath strings alone. +Each missing suffix stays anchored to its nearest existing directory. Suffixes are +compared relative to shared directory identities; merely sharing an ancestor does +not make separate siblings overlap. Metadata errors, non-directories and unavailable +inode identity fail closed. No platform mount-prefix rewrite is used. +The macOS installed matrix requires a real Data-volume firmlink alias. Firmlink +fixtures live under `/tmp` so `/private/tmp` and `/System/Volumes/Data/private/tmp` +have different native realpath strings and the same device/inode. The matrix +exercises both alias directions, absent/partly-existing/existing roots, root/dsh/Profile +and case variants, legacy ancestors, default links and separate prefix-neighbor/nested +controls. Full UI controls verify separate aliased Homes without touching legacy data. +A non-directory Home path is refused because directory identity cannot be established. +When either path has missing components, it conservatively +refuses a case-folded potential overlap on every platform. It neither assumes +case sensitivity for unresolvable suffixes nor creates files to probe the volume. +Every unresolved non-ASCII component is refused before suffix reconstruction, +including separate names; no Unicode folding table or normalization heuristic is +used as a filesystem oracle. Existing Unicode directories keep native identities, +and missing ASCII children beneath resolved separate Unicode ancestors still work. +The installed matrix covers long-s and ligature aliases at root/child paths for both +variables and default links, with genuinely absent/existing roots. Separate ASCII, +Unicode, composed/decomposed, emoji and invisible-character controls distinguish +intentional missing-name refusal from existing-path support. Native realpath, +device/inode and Profile samefile evidence accompany complete tree snapshots. +Existing distinct paths keep their native identities. Node's JavaScript realpath +fallback is insufficient because it preserves case spelling on the verified host. +Eighteen further PTY checks cover dangling explicit/default legacy links, root/child +targets, dangling ancestors, chains, absolute targets, separate missing targets and +cycles for both Home variables. `ENOENT` must be distinguished from a dangling +symlink with `lstat` before reconstructing missing path components; unresolved +symlink ambiguity fails before writes. Native cycle errors fail closed. Six actual +UI controls retain resolvable links to separate legacy Homes (including missing +children under valid links). Snapshot assertions record link text without following +links, along with directories and content hashes. No test uses personal Home data. +The path matrix runs both variables with existing/missing absolute, relative, +literal tilde, Unicode/space paths, directory/symlink/dangling/missing `..` +traversals, and unset/empty/blank/overridden default Homes. It calls the released +`dsh-home-paths` resolver as its dsh oracle; Grok's pinned `xai-dirs/src/lib.rs` +keeps nonempty overrides verbatim. The launcher matches dsh's blank/tilde/lexical +rules but refuses all `GROK_HOME` parent-traversal components before writes, +even when they might be separate: no custom symlink traversal is attempted. +Default Homes stay protected when overridden. Evidence records native filesystem +identities, full synthetic tree snapshots and raw PTY output, not just path strings. +Packing/installing is offline with isolated npm configuration; product fixtures +live temporarily under the output directory. Keep `TMPDIR` at a valid system +temporary directory **outside the repository** when running the existing unit/e2e +suites: their outside-repository fixtures must not discover this worktree's Git root. +The network observer interposes socket/connect/connectx/sendto/sendmsg/DNS calls +without replacing their results; a compiled loopback UDP positive control proves +socket and outbound-send observation. +A Node preload preserves only this audit injection across the launcher's sanitized +child environment. A separate uninstrumented run denies network and synthetic +legacy-home reads with `sandbox-exec`. This is socket-boundary evidence, not +privileged packet capture. +No real credentials, session history, official account or paid service is used. +Logs/screens/result JSON go to a new `.scratch/rust-pty-*` directory; `--output` +can choose another new directory. Do not reuse personal data or a personal Home. + +The isolated Home still creates `~/.codsh-rust/dsh/profiles/rust/package.json` +with an empty bundle composition. Interactive `codsh --rust` then starts released +`dsh --profile acp` over ACP/JSON-RPC stdio in that Home; dsh owns execution and +durable sessions. The mock model, when used, is a dsh provider-boundary fixture +(`CODSH_ACP_PATCH`, `DSH_CODE_CLI_MOCK_TOOL`), not a stub of the Rust client or +dsh core. Plain automation is `python3 scripts/rust-plain-pipe-test.py`: it packs +the CLI and runs no-TTY `-p`/`--prompt-file` prompts, `--cwd`, `--continue`, +tool denial, `--max-turns`, invalid flags, and SIGINT/SIGTERM through released +dsh. It also checks the first-request tool list (including `Agent` removing `subagent` and `subagent_fork`, `--disallowed-tools Agent(explore)` keeping `subagent` while other names are masked, typed `Agent(...)` refusals from `--tools`, `Agent()`, and an inherited `CODSH_PLAIN_TOOLS`, and `--disable-web-search`), first-turn memory with `--no-memory` and `--verbatim`, rules still reaching the model under `--verbatim`, a relative `--prompt-file` read from inside `--cwd`, Ctrl+C during connect sending no prompt, `--cwd` with `--sandbox` write roots, title resume, a non-mock provider, help, shell completions, and one interactive PTY session where `--cwd` and `--disable-web-search` apply and headless-only flags warn. `CODSH_PLAIN_LONG_TURN=1` adds a 190 s mock turn that proves there is no wall-clock cap; it is off by default because it takes over three minutes. Sandbox write targets are created beside the repo, outside the temp roots, and removed afterwards. JSON output formats are refused here. Unknown options exit 2. Model/protocol/effort checks (`python3 scripts/rust-model-pty-test.py`) +drive a loopback OpenAI-compatible fixture at the provider boundary and assert +the actual request path, model id, auth, and effort; same-named models on +different backends are not treated as equivalent. Enter submits the draft through dsh when connected. Missing dsh, ACP +protocol mismatch, empty answers, mid-stream failure, and disconnect are shown +as failures or empty results, never as success. File read/write/edit run through +released dsh tools. The Rust UI correlates `session/request_permission` with the +tool-call id, shows the pending operation and dsh-supplied diff, allows once with +`y`, remembers this project only with `a`, and rejects with `n` without writing. +`/revoke-approvals` forgets this project's remembered grants. Allow/ask/deny rules, +remembered project grants, locked always-approve, and hook deny are enforced before the +dsh tool body. Unsplittable shell and Read/Edit path rules on operands cannot +bypass deny; always-approve skips grants and non-shell ask; a corrupt policy +file refuses mutating tools. Wrappers peel to the inner command without eating +the command name; `env -S` prompts; Read/Edit deny follows in-path symlinks; +remembered file grants are path-scoped. Missing files, tool errors, cancelled +approvals, and duplicate replies are observable failures. `Ctrl+C` clears a +draft without cancelling; an empty draft sends ACP `session/cancel` to dsh for a +running turn, including pending approval and in-flight tools. Esc never cancels. +Late allow replies and process teardown cannot execute a cancelled action; unknown +tool results display as cancelled. After cancel, a new prompt still works. +`test:rust:pty` now also runs `scripts/rust-turn-pty-test.py`, +`scripts/rust-file-pty-test.py`, `scripts/rust-cancel-pty-test.py`, +`scripts/rust-resume-pty-test.py`, `scripts/rust-config-pty-test.py`, +`scripts/rust-model-pty-test.py`, `scripts/rust-compact-pty-test.py`, +`scripts/rust-trust-pty-test.py`, `scripts/rust-screen-pty-test.py`, +`scripts/rust-fork-pty-test.py`, `scripts/rust-settings-pty-test.py`, +`scripts/rust-import-pty-test.py`, `scripts/rust-plugin-pty-test.py`, +`scripts/rust-auth-pty-test.py`, `scripts/rust-permission-pty-test.py`, +`scripts/rust-prompt-pty-test.py`, `scripts/rust-content-pty-test.py`, +`scripts/rust-voice-pty-test.py`, `scripts/rust-assets-pty-test.py`, and +`scripts/rust-session-data-pty-test.py`, `scripts/rust-memory-pty-test.py`, and +`scripts/rust-legacy-home-pty-test.py` against +the packed native candidate. The session-data PTY checks Markdown export, +explicit share to a loopback substitute, and `du`. Rust unit tests also post that +share over localhost HTTPS: the configured test CA is accepted, a missing or wrong +CA fails without uploading, and an HTTPS redirect is not followed. `sessions delete`, +`/delete` cancel, the resume picker, and dashboard delete must report the +dsh persistence blocker and leave every session, including the other workspace, +in place. A redirect from the selected share URL must not be followed. Voice PTY uses a local substitute speech-to-text +server and `CODSH_VOICE_FIXTURE`; it does not open the microphone. `/voice doctor` +must not record. A slash command typed while recording parks the draft and +restores it; it must not leave `/` or drop that text. A changed draft drops a +late transcript. Linux and Windows capture stay unverified. +`python3 scripts/rust-web-pty-test.py` drives search and fetch through the +packed client and real dsh sessions. Search uses a local Responses-shaped +substitute and a local SearXNG JSON fixture; the SearXNG request is a keyless +GET and result URLs stay inside the configured domain list. One fetch uses +the public `http://example.com/` page. A real SearXNG process is not started +by that script. Image generation (ticket 187): `pnpm exec vitest run +scripts/rust-image-gen.spec.mjs` drives real dsh with the keyless `image` mock +mode (`IMAGE_TOOLS`, `IMAGE_GEN [ratio=R] `, `IMAGE_EDIT | :: +`, `IMAGE_PAIR a THEN b`, and the `/imagine` instruction) against a +fake native runner, and `python3 scripts/rust-image-gen-pty-test.py` (Linux and +macOS) drives the packed client against a loopback xAI-shaped fake image +service: approval card reject/allow, `/imagine`, a Ctrl+V `[Image #1]` edit, +refusal, malformed bytes, URL-only and HTTP 402 replies, a hung request +cancelled with Ctrl+C, `/images` and open, resume, the disabled launch, and +the official-host refusal. Neither script calls a real or paid image service. +Video generation (ticket 188): `pnpm exec vitest run +scripts/rust-video-gen.spec.mjs` drives real dsh with the keyless `video` mock +mode (`VIDEO_TOOLS`, `VIDEO_I2V {json}`, `VIDEO_R2V {json}`, and the +`/imagine-video` instruction) against a fake native runner, and `python3 +scripts/rust-video-gen-pty-test.py` (Linux and macOS, part of +`test:rust:pty`) drives the packed client against loopback fakes of the xAI +async video API and the stable-diffusion.cpp job API: approval card +reject/allow, the live job row, `/imagine-video` with a Ctrl+V image, an +unsupported duration, failed, expired, refused, malformed and foreign-host +results, a timeout recovered later with `/videos status`, Ctrl+C with each +cancel answer, `/videos` and open, the resume notice, the disabled launch, and +the official-host refusal. Neither script calls a real or paid video service. +Web tool registration (ticket 206): `python3 scripts/rust-web-config-pty-test.py` +(Linux and macOS, part of `test:rust:pty`) packs and installs the client and +runs it with web search and fetch configured only in `config.toml` (no +`CODSH_WEB_*`, no `CODSH_ACP_PATCH`) against a loopback OpenAI-shaped fake +model and a fake SearXNG and page: the session offers and runs `web_search` +and `web_fetch`, the `-p` init line lists them with `deep-research`, +`GROK_DISABLE_WEB_SEARCH=1` leaves only fetch, and stray `CODSH_WEB_*` values +without configuration register neither. +Child approvals (#220): `python3 scripts/rust-child-approval-pty-test.py` +(Linux and macOS, part of `test:rust:pty`) packs and installs the client, runs +it in the default ask mode with no allow rules and no `CODSH_WEB_*`, and has a +trusted project workflow's child call `web_fetch` against a loopback fake +model and page: the main session's status line names the workflow, the child, +the tool, and the URL; `y` fetches, `n` returns the refusal to the child, `a` +writes the domain grant and the next fetch runs without asking, `/workflow +stop` closes a waiting request, and a `-p` run keeps the refusal. +`python3 scripts/rust-plugin-content-pty-test.py` runs on Linux +and macOS through the repo launcher and the `pnpm run build:rust` binary: a +temp plugin with a rule, skill, command, agent, and PreToolUse hook is +installed with `--trust`, `/plugins` Space enables it, the next prompt must +carry the skill, and the replaced dsh child must let the plugin hook deny a +real bash call; Space again must withdraw both. `pnpm exec vitest run +scripts/rust-plugin-content.spec.mjs` covers install, enable, update, disable, +uninstall, a broken plugin beside a good one, and project plugins behind +workspace trust with real dsh and the keyless mock. +`python3 scripts/rust-ship-pty-test.py` runs on Linux and macOS after `pnpm run build:rust` (which also runs `scripts/build-ship-extension.mjs` +to generate `packages/cli/extensions/ship/hooks/ship-hook.mjs` and +`commands/ship.md`; both are gitignored): with the `ship-wayfinder` mock it +checks that a fresh Home has no `/ship` and no Ship state, installs +`bundled:ship --trust` (still disabled), enables it, answers the wayfinder +question card and checks the sealed snapshot and answer records, cancels a +card (nothing recorded), resumes with a bare `/ship`, refuses a conflicting +idea, resumes a spec past wayfinding at grill (the Stop hook continues the +turn), checks Stop writes nothing, and checks `plugin disable ship` removes +`/ship`. `python3 scripts/rust-ship-full-pty-test.py` (same prerequisites plus +agent-browser, below) runs the whole flow with the `ship-full` mock in a real +git repository: gates, a parallel landing wave in worktrees with a merge +conflict whose first resolution fails validation, final verification, and +fast-forward Merge-back; then Ctrl+C during the wave (nothing merged or +ticked), a removed sealed Mission Contract (the run stops), and a deleted run +state (the files resume it). It compares the terminal, the git history, the +records, and the browser graph at 1440x900 and 390x844, reading what the +model saw from the mock's request trace. `CODSH_SHIP_FULL_ONLY=full|recover` +runs one scenario, `CODSH_SHIP_FULL_KEEP=1` keeps the temp home, and +`CODSH_SHIP_FULL_TMP_PREFIX` sets the temp prefix. `pnpm exec vitest run +scripts/rust-ship-extension.spec.mjs packages/bundle/tests/ship-extension.spec.ts +packages/bundle/tests/ship-extension-runner.spec.ts` +covers the generated plugin and the hook logic; the legacy `e2e/pty-ship*.e2e.ts` +suites stay the reference for the full legacy flow. The plugin's browser graph +is checked with `python3 scripts/rust-ship-web-pty-test.py`, which drives real +dsh in a PTY and headless Chromium through the agent-browser CLI at 1440x900 +and 390x844 (the checklist in "Web panorama" above: `/` and `/index.html`, +navigation, expand/collapse, relations, long and missing answers, live +updates, reconnect after the session ends and resumes, a corrupt cache, and +uninstall), and saves screenshots to the printed directory. agent-browser is +not a repo dependency: install it anywhere (for example `npm i -g +agent-browser`, or into a temp prefix) and set `AGENT_BROWSER` to its binary; +set `AGENT_BROWSER_EXECUTABLE_PATH` to use an installed Chrome instead of +`agent-browser install`. Never point it at your own browser profile. The assets test +trusts a fixture repo, checks that ordered rules, a skill, and a flat custom +command change the dsh request, rescans an added skill, requires the deleted +skill to be absent from the next dsh reply, checks `--rules`, and checks an +empty directory. Untrusted inspect must omit project files. A non-user-invocable +skill must stay out of the menu. The memory PTY uses a synthetic isolated home: +`/remember` writes the confirmed note into `MEMORY.md` only after `y`, `/memory` +opens a modal that keeps the selected row highlighted, a width under 80 columns +hides the preview until Enter, `x` cannot delete `MEMORY.md`, `[memory] enabled = false` +stays off until `t`, `/new` drops that session toggle, +`GROK_MEMORY=0` hides the browser +without deleting the file, `x` deletes only a session file, and `memory clear --workspace --yes` removes that +scope's `MEMORY.md`, `sessions/`, and `index.sqlite`. An empty `/remember` (the +two-step draft, no inline text) must keep the live `t` toggle on save or cancel +instead of re-deriving `config.toml`'s value (ticket 53 / issue 185), in both +directions (`t off` over an enabled config, `t on` over a disabled one). It +asserts on the mock model's actual received request (via the mock adapter's +echoed reply, not just UI state): the first turn's request contains a saved +note's text when memory is on, omits it when memory is off, and after `/cd` to +another project plus `/new` contains only that project's own note, never the +previous project's. Turning memory on with `t` after a session's first turn +already ran (`memory_injected`, not a bare `turns.is_empty()` check that +`/clear` would fool) reaches no prompt here and must say so, instead of "for +this session"; `/new` drops the toggle and follows `config.toml` again, so +the notice must not claim it carries to the next new session either. A later +prompt in that same session must still omit the note. `index.sqlite` is a +SQLite FTS5 database rebuilt from the notes. A damaged file is reported and +replaced only after the new database is complete. A foreign SQLite file is +left unchanged. The bundled SQLite amalgamation comes from `rusqlite` 0.37 / +`libsqlite3-sys` 0.35 (SQLite 3.50.2, public domain) and is compiled with FTS5. A skill or command named `login`, `logout`, or +`feedback` must keep the built-in on the bare slash and appear only as +`/local:name`. Nested `SKILL.md` files stop when the walk depth is greater than +five, and a child of a directory that already has `SKILL.md` is still recorded. +A configured `[skills] paths` directory is depth 0, so a sixth child is not +loaded. +Official `xai-grok-markdown` tests run with `cargo test --manifest-path rust/Cargo.toml --workspace`. Packed content PTY covers markdown, tables, mermaid, thoughts, fold, raw, full content, copy-original, `$PAGER`, resume, diffs, and failed tools. The visible fullscreen frame must match official markdown: keep `Vec`, comparisons, fenced Rust, and inline HTML tags, keep a ZWJ emoji together, and paint `failed` plus `[error]` for a missing or rejected tool. A settled full-content page shows the fenced function and the unclosed-fence marker once; the notice under the transcript is not a second copy of that body. +Prompt editing must keep the official textarea, prove Unicode/paste/resize, +history selection, slash/HISTFILE completion cancel, both simple and prompt-Vim +modes, and an actual `$VISUAL` round-trip that does not submit on save or failure. +`/context` and `/compact` are dsh-backed: occupancy and advertised model limits +must not be fabricated, manual/automatic compaction uses the dsh session log, +failed compact must keep the ACP session and original records, cancel must print +`Compaction cancelled.` and accept a following prompt, and resume must hide +replaced history while answering a new prompt. The config test covers `inspect` / +`inspect --json`, CLI/env/overlay/file precedence, invalid TOML preservation, +first-run missing credentials, generated dsh `settings.yaml` mapping, restart +after a config change, unmanaged settings conflict, and refusal to automatically +import legacy `~/.dsh` / `~/.grok` credentials. Explicit `codsh --rust import` +(`scripts/rust-import-pty-test.py`) previews current dsh `settings.yaml` / +`code-cli-thinking.json` / `code-cli-ui.json` sources, lists conversions, +conflicts and unsupported items, maps UI density onto `[ui] compact_mode`, +copies selected providers without tokens or trust grants, keeps the model named +by `agent-default-model`, and leaves source files and nested isolated settings +unchanged on preview, cancel, failure, and repeat apply. The auth test covers `login` / +`logout` / `setup` help without creating Home, independent API-key use, +organization pins that refuse API-key-only ready (`GROK_DISABLE_API_KEY_AUTH`, +empty team list, locked `requirements.toml` `[auth]` or top-level +`force_login_team_uuid`), external-provider login with owner-only `auth.json`, logout that revokes the +identity session and does not revoke model/MCP credentials, unsigned +managed-policy refusal, a signature bound to another principal, fail-closed +policy with no pubkey and no sidecar, a local signed substitute management +service, and `/login` `/logout` in a real PTY. The packed test also records +that a ready identity session is handed to the dsh child and that an +undocumented `GROK_AUTH_*` parent variable is not. +Under an organization pin the PTY starts unready, `/login` must show `Connected to dsh ACP` before the next prompt, and `/logout` must drop that connection. +Public ACP framing, including +file-tool permission, `session/cancel`, `session/list`, `session/resume`, and +dsh-backed conversation fork/rewind, is covered by +`scripts/rust-acp-protocol.spec.mjs`. +`--continue` / `--resume ` restore the same dsh session through ACP +`session/resume` plus a read-only persistence projection; `--fork-session`, +`/fork`, and `/rewind` seed a new append-only child without restoring files. +`/rewind` while a turn is streaming is refused; `/fork --no-worktree` copies +conversation only; `--worktree` stays out of this slice. `[ui] confirm_before_rewind` +and `ui.fork_secondary_model` live in `$GROK_HOME/config.toml` (default +`~/.codsh-rust/.grok/config.toml`), the same user file as screen mode. +`ui.fork_secondary_model` applies to `/fork` and `--fork-session`, not rewind. +A second client is refused when it cannot take write ownership. Interrupted +tools are displayed as unknown and are not replayed. `session/load` remains +unsupported by dsh ACP. The editor entry `codsh --rust agent stdio` accepts +editor `session/load` by resuming through dsh and replaying the read-only +projection; it does not make dsh itself implement `session/load`. A nested +tool-result whose call id is only on `message.source` replays as failed. +`session/set_config_option` writes `$GROK_HOME/model-selection.toml` before +the next prompt. Terminal resume and editor `session/load` apply that file, +including an advertised ACP value that is not a catalog id. An unknown model +is refused. Terminal `/dontAsk` and `/acceptEdits` write the same +session mode file, and resume writes that mode into the policy before dsh starts. Proprietary +`x.ai/*` methods return method-not-found. The repeatable check is the +`serves an editor` case in `scripts/rust-acp-protocol.spec.mjs`. The shared +hub behind `agent serve` / `agent leader` / `agent --leader stdio` is checked by +`scripts/rust-shared-server.spec.mjs` against real dsh: WebSocket auth (401/404, +Bearer and `server-key`), two clients on one session, first-answer-wins +approval with a stale notice, a refused concurrent prompt, a disconnect during +an approval followed by a reattach that answers it (the file is edited once), +config broadcast, a killed dsh reported as `_codsh/runtime_exited`, and leader +auto-start, `[cli] use_leader`, `leader list|info|kill`, idle exit, and a lost +leader failing the waiting prompt. It needs Node 22 (dsh's engine) and a built +`rust/target/debug/codsh-rust`. The remote workspace (`--remote ssh://…`, +ticket 190) is checked by `scripts/rust-remote.spec.mjs` and +`scripts/rust-remote-pty-test.py` against a local unprivileged OpenSSH server on +127.0.0.1 (temp host key, one authorized key, no forwarding) and real dsh on +the "remote" side: key and pinned-host-key auth (an unknown host key and a wrong +key are refused), a remote turn that edits the remote file and not the local +one, no local credential or env reaching the remote login, remote `--continue` +and `--resume` prefixes, a stricter remote policy winning, refused local-policy +flags and non-text prompts, a killed connection followed by `/reconnect` that +attaches to the still-running turn (its command ran once), and a remote leader +restart reported as unknown effects with no retry. Both need OpenSSH: set +`CODSH_TEST_OPENSSH=` to a tree with `usr/bin/ssh`, `usr/sbin/sshd`, and +`usr/bin/ssh-keygen` (for example `apt-get download openssh-client +openssh-server` plus `dpkg -x`; add `libwrap0` and `libwtmpdb0` when they are +missing), or have ssh/sshd on PATH. Without them the spec is skipped and the PTY +script prints SKIP with the reason. Organization identity for remote access +(ticket 207) is checked by `scripts/rust-remote-identity.spec.mjs` (fixtures in +`scripts/rust-identity-fixtures.mjs`) against a real Ory Hydra (Apache-2.0, +sqlite build) with the organization's login and consent app on loopback, the +same OpenSSH server with the org key restricted to a forced command, and real +dsh: login through Hydra, no token sent without a `[[remote_identity]]` +destination, an unauthenticated client refused, team, audience, and issuer +mismatches, `deny_subjects`, `locked`, an unreadable policy, a user config that +cannot turn the policy off, refresh of a short-lived token, logout and +administrator revocation, a mid-turn revocation that cancels the remote command, +and audit logs without the token. It needs OpenSSH as above and Hydra: download +`hydra_-linux_sqlite_64bit.tar.gz` from github.com/ory/hydra releases, +check it against that release's `checksums.txt`, and set +`CODSH_TEST_HYDRA=` (or have `hydra` on PATH); without +it the spec is skipped with the reason. `codsh --rust clone` (ticket 191) is checked by +`scripts/rust-clone.spec.mjs` (fixtures in `scripts/rust-clone-fixtures.mjs`) +and `scripts/rust-clone-pty-test.py` against temporary local bare repositories +only: file://, a loopback `git http-backend` behind a Basic-token check, and +the same unprivileged OpenSSH server for ssh clones and `--remote` clones. It +covers the gate order, depth/branch/tags, `--full-history`, cones, deepen, +directory conflicts, a repeated clone that fetches nothing, failure and Ctrl-C +cleanup (also a closed `--remote` connection), `GROVE_AUTH_TOKEN` versus +`codsh --rust login`, the Grove worktree request with its plain-git fallback, +and real dsh editing a file in the fresh clone, locally and on the SSH host. +The SSH cases use `CODSH_TEST_OPENSSH` as above and are skipped without it. Local MCP is checked by +`scripts/rust-mcp.spec.mjs` with the keyless stdio fixture +`e2e/fixtures/rust-mcp-fixture.mjs` and real dsh: a configured server's tool +writes its file exactly once after an ACP approval and not after a reject, +`use_tool` asks as the real tool, `search_tool` returns `server__tool` names +and schemas, tool errors stay errors, `session/cancel` reaches the server as +`notifications/cancelled`, deny rules and PreToolUse Hooks match +`server__tool` (also through `use_tool`), a missing program, a startup crash, +and a refused `initialize` are reported while the session starts, a crash +mid-call errors and dsh reconnects, `tool_timeout_sec` cancels the call, +oversized output is cut and saved, CLI enable/disable/add/remove change the +next session's tools, and repo servers start only while the folder is trusted. +The mock mode is `DSH_CODE_CLI_MOCK_TOOL=mcp` (`MCP_TOOLS`, `MCP_CALL + THEN ...`). `/mcps` in the terminal is covered for a plugin server by +`scripts/rust-plugin-mcp-pty-test.py` on Linux; it was not run on macOS. Remote +MCP (ticket 168) has four layers. `mcp_auth::`, `mcp_remote::`, `mcp_bridge::`, +`elicit_form::`, and `elicit_ui::` unit tests use in-process loopback HTTP +servers for discovery, registration, PKCE, refresh, revocation, the credential +store, streamable HTTP and SSE framing, 401/404/redirect handling, result +projection, schema checks, and card keys. `scripts/rust-mcp-remote.spec.mjs` +runs real dsh against the keyless loopback fixture +`e2e/fixtures/rust-mcp-remote-fixture.mjs` (streamable HTTP, legacy SSE, and an +OAuth authorization server with auto-consent; `$BROWSER` is a script that +fetches the URL): `mcp login`/`logout`, token refresh, session loss, image +blocks, the session placeholder header, refused redirects, headers kept out of +the plan, and editor `x.ai/mcp/elicit` (form, URL, error, invalid content, +cancel), `auth_status`, `auth_trigger`, and `read_resource`. Its last case is +the real integration: the MCP TypeScript SDK 1.30.0 example +`simpleStreamableHttp.js --oauth --oauth-strict` (the SDK dsh's own MCP client +ships, lockfile-pinned) with its demo authorization server, signing in, +calling `greet`, answering `collect-user-info`'s form from the editor, and +reading a resource. `scripts/rust-mcp-remote-pty-test.py` drives `/mcps auth`, +the MCP card (form accept, `d` decline, URL accept), and `/mcps logout` in a +real terminal on Linux. Hosted MCP providers, macOS, and Windows were not +checked. A GUI editor was not launched. `--restore-code` is refused. +Fullscreen uses the alternate-screen lifecycle; minimal emits committed turns +into native history through the official inline renderer. `/rewind` and `/fork` +in minimal reset that native buffer the same way compact does, so discarded +turns are not left in scrollback. In-place `/minimal` and `/fullscreen` keep +the dsh session, draft, running turn, and pending approval; +`--minimal`/`--fullscreen` do not rewrite isolated `[ui] screen_mode`. +Fullscreen `/find` searches the transcript overlay, `/jump` previews turns and +restores the prior reading position on Esc, and click-to-fold does not fire on +a drag that copies. Fullscreen paints only the visible transcript rows on a +dedicated Rect that mouse hit-testing uses; the gutter is the painted `>`/` ` +prefix, not an inserted bar. Rebuild remaps a selection by turn identity and +drops it when that anchor is gone. Folds persist across rebuild by turn identity. +`/vim-mode` persists `[ui] vim_mode` without changing `ui.simple_mode`. +`ui.mouse_reporting_toggle` / `GROK_MOUSE_REPORTING_TOGGLE` lets Ctrl+R +(scrollback focused) or `/toggle-mouse-reporting` flip capture; teardown always +disables mouse reporting. The hint names `$GROK_HOME/config.toml`. Frozen View +`Ctrl+F` remains the content viewer (ticket 153); transcript search is `/find`. +Vim `y` copies the selected block and `Y` copies block metadata. `inspect` +labels nav prefs as `config` or `env`, including `inspect --json`. +`scripts/rust-nav-pty-test.py` clicks a thought fold, copies a dragged span +(OSC 52), restores `read=` on Esc, scrolls while a turn is still streaming, +and requires wheel input to change `read=`. Scrollback Ctrl+D half-pages +instead of quitting; `Ctrl+Q` still quits. Empty-session Tab still cycles the +welcome menu. `/find [text]` opens browse mode so `n`/`N` step matches. Ctrl+P +refuses the command palette (ticket 154) rather than a silent no-op. Minimal +`/find` and `/jump` refuse with the `/fullscreen` remedy. Dock +(`features.dock` / `GROK_DOCK`) is an explicit unsupported gate, not a fake pane. +`/settings` and `/theme` persist appearance and status-line choices into the +same user `config.toml`; preview/Escape must not write; locked requirements +show their source; status-line scripts must time out and clean process groups. +Queue delivery across a switch remains a later ticket. + ## Documentation site `site/` is a static GitHub Pages site. `index.html` and `zh.html` are the short diff --git a/README.md b/README.md index 8a0928cb..0c7ad965 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,1262 @@ codsh Common flags: - `codsh -p "task"` — Run a non-interactive task directly +- `codsh --rust -p "task"` — One plain dsh answer on stdout, then exit - `codsh --continue` — Continue the last session - `codsh --resume ` — Resume a specific session - `codsh update` — Update launcher and profile runtime +## Isolated Rust client (local candidates) + +`codsh --rust` explicitly selects the parallel Rust client; plain `codsh` keeps +using the existing version. The Rust UI submits prompts over ACP/JSON-RPC to a +real `dsh --profile acp` process in the isolated Home. `codsh --rust agent stdio` +is the editor entry for that same dsh session: ACP version 1, `session/new`, +`session/load` (dsh resume plus read-only history replay), `session/prompt`, +`session/cancel`, `session/set_config_option` (model, reasoning effort, and +`permission_mode`), and approval requests. Model and reasoning changes are +written to `$GROK_HOME/model-selection.toml` before the next prompt and restored +on `session/load` and on a terminal resume, including an advertised model that +is not a `config.toml` catalog id. A model that is neither is refused. Terminal +`/dontAsk` and `/acceptEdits` set the same session modes, and that mode is on +the policy file before dsh starts. Proprietary `x.ai/*` methods, +`session/delete`, `session/fork`, and `session/set_mode` return JSON-RPC +method-not-found. Closing the editor releases the write owner; a second client +is refused while it is held. In Zed, add a custom agent server whose command is +the installed `codsh` with arguments `--rust`, `agent`, `stdio`. This checkout +did not launch Zed or another GUI editor; the repeatable check is an external +ACP client over stdio. + +Sharing is opt-in; a plain launch starts no service and opens no port. +`codsh --rust agent serve` serves the same ACP over an authenticated WebSocket +(`/ws`, default `127.0.0.1:2419`). The secret comes from `--secret`, +`GROK_AGENT_SECRET`, or is generated and printed once; clients send +`Authorization: Bearer ` or `?server-key=`, and a non-loopback +`--bind` prints a warning. `codsh --rust agent leader` runs a per-user leader on +`$GROK_HOME/leader.sock` (mode 0600, same-user peers only). `agent --leader stdio`, +or `[cli] use_leader = true`, starts or reuses it for an editor; `--no-leader` +wins. One dsh process runs each live session. `session/load` of a session the +process already runs attaches without a second executor: saved turns, the +running turn, and a pending approval are sent again. Approval requests reach +every attached client; the first answer wins, and a late one gets +`_codsh/stale_response`. A second prompt during a turn is refused, not queued. +Option changes reach the other clients as `config_option_update`. A client +disconnect never cancels a turn. A dsh exit is reported as +`_codsh/runtime_exited`; the running turn's effects are unknown and nothing is +retried. A sandbox profile other than `off` keeps the session in the editor's own +process instead of the leader, and a leader client cannot bring its own model or +permission flags. `codsh --rust leader list|info|kill` inspects and stops +leaders. `agent headless`, `agent serve --remote `, `--grok-ws-url`, and +Cursor worker mode need official services and are refused. The leader is +Unix-only; it was checked on Linux, not on macOS or Windows. + +A remote workspace runs over SSH: `codsh --rust --remote +ssh://[user@]host[:port]/abs/path` (interactive, `-p`, `--continue`, +`--resume `) starts `codsh --rust agent --leader stdio` on that host +(`--remote-command` changes the remote start command) and drives its session +from this terminal. Authentication is an SSH public key with a pinned host key: +`BatchMode=yes` and `StrictHostKeyChecking=yes`, so an unknown host key or an +unauthorized key is refused and nothing prompts for a password +(`--remote-identity FILE`, `--remote-known-hosts FILE`, `--remote-ssh PROG`). +The remote host's own config, credentials, permission policy, and sandbox run +every turn. Nothing local is forwarded: no agent, X11, or port forwarding, no +local environment or provider keys, no local MCP servers, rules, memory, or +model selection. The session directory is a remote path. Local files are never +sent as remote files: `@file` attachments and images are refused, and the client +offers the remote no filesystem access. `--model`, `--permission-mode`, +`--always-approve`, `--auto`, `--allow`, `--deny`, `--sandbox`, and other +local-policy flags are refused with `--remote`. A lost connection leaves the +turn running there; the transcript marks it interrupted, a prompt is not sent +while disconnected, and `/reconnect` attaches to it again without re-running +anything. After a remote restart the running turn has no result, its external +effects are reported as unknown, and nothing is retried; `/reconnect` resumes the +saved session. `/remote` and `codsh --rust remote check [--json]` show +what the real remote reports (leader, sandbox, whether reattach works) and the +features a remote session does not have: steer, `/btw`, plan, subagents, +background commands, goal, workflow, the `/resume` picker, fork, rewind, export, +memory, and plugins. The official Computer Hub (`workspace start --hub-url`), +cloud workspaces (`x.ai/cloud/*`), and the Cursor worker need private +infrastructure and stay refused. Checked on Linux against a local OpenSSH +server, not against another machine, macOS, or Windows. + +An organization can require its identity for remote access (ticket 207). The +reference sends its signed-in organization bearer to the official Computer Hub; +codsh maps that to a deployable OpenID Connect provider you run (checked with +Ory Hydra) and the SSH remote above. On the remote host, `requirements.toml` +(or `managed_config.toml`) sets `[remote_access] identity = "required"`, +`issuer`, `audience`, `introspection_url` (RFC 7662; https, or http on +loopback; official x.ai/grok.com endpoints are refused; optional +`introspection_client_id` with an owner-only `introspection_secret_file`), +`teams`, `deny_subjects`, `recheck_secs` (default 30), and `locked` with +`lock_message`. A user's `config.toml` cannot turn it off, and a policy that +cannot be read or is incomplete refuses every connection. The leader proxy then +answers nothing but `initialize` and `authenticate` until the client presents +an access token that the provider reports active, for that issuer and audience, +of an admitted team, and not a refresh token; it asks the provider again on +every request, and on a timer while connected, so expiry, revocation at the +provider (including `codsh --rust logout`), `deny_subjects`, and +`locked` end the connection and cancel the turns it started (a turn left +running by an earlier, already closed connection is not stopped by a later +revocation; `codsh --rust leader kill` on the host stops it). On the client, +`codsh --rust login` with that provider (`GROK_OIDC_ISSUER`, +`GROK_OIDC_CLIENT_ID`, `GROK_OIDC_AUDIENCE`) is not enough by itself: the token +is sent only to remotes listed in `[[remote_identity]] target = "ssh://host[:port][/path]"` +with the same `audience`, only when the remote asks for that issuer, and never +when its JWT `aud` names another audience. It is refreshed before it expires and +sent again after a refresh. Both sides keep an owner-only JSONL audit +(`$GROK_HOME/remote-identity.log` on the client, `remote-access.log` on the +host) with the purpose, destination, subject, and a 12-hex SHA-256 fingerprint +of the token, never the token; errors never contain it. `remote check` and +`/remote` show the result. Limits: the check is in codsh's leader proxy, so a +key that can open a shell bypasses it; restrict the organization key to a forced +command such as `restrict,command="/path/org-agent.sh"` whose script runs +`codsh --rust agent --leader stdio` with `SSH_CONNECTION` passed through. `agent +serve` sockets and a remote clone carry no identity (a host that requires one +refuses the clone). Without a policy a remote works exactly as before. There is +no official Computer Hub, SSO, or paid account behind this; checked on Linux +with Hydra and OpenSSH on loopback, not on macOS or Windows. + +`codsh --rust clone [-b BRANCH] [--cone PATH]... [--full-history] [DIR]` +clones with plain git in place of the reference Grove lazy clone. It is off +until a reference gate turns it on: `GROK_CLONE` or `GROVE_CLONE`, then +`GROK_GROVE` or `[cli] grove` in `$GROK_HOME/config.toml`, then `[clone] +enabled` in Grove's `~/.config/grove/config.toml` (your real home); otherwise +it exits 2 and says which setting turns it on. What stands in for each Grove +behavior: + +| Grove (reference) | codsh substitute | Difference | +| --- | --- | --- | +| depth-1 bootstrap of the selected branch; `--full-history` for all history, branches, and tags | `git clone --depth=1 --single-branch --no-tags`; `--full-history` is `--no-single-branch` with tags | same shape; `git fetch --deepen=N origin` / `--unshallow origin` deepen only the selected branch, as documented. A local-path URL makes git ignore depth, and the summary says so (use `file://`) | +| lazy blobs from the content store | partial clone `--filter=blob:none` | blobs of the checkout are fetched at clone time and others when git needs them; a server without filter support sends everything, and the summary says so | +| `--cone` projection | `--sparse` plus `git sparse-checkout set --cone` | a real sparse checkout, not a projection | +| daemon, FUSE/NFS mount, `--leader-socket` | none | the clone is a real checkout on disk; `--leader-socket` is refused; no performance comparison with Grove was made | +| daemon auth: `GROVE_AUTH_TOKEN`, git credentials, `GROVE_TOKEN_ROTATION` | git's own credential helper, SSH keys, known_hosts, `GIT_SSH_COMMAND`; `GROVE_AUTH_TOKEN` as an https Authorization header | the token is sent only to https (or loopback http) remotes through git's environment config and never written to `.git/config`; rotation has no daemon and is reported as ignored; `codsh --rust login` is never used for git; a rejected credential is reported as `unavailable` (the daemon's `expired-static` and `carrier-stale` classes do not apply) | + +The target directory must be missing or empty; a non-empty directory, a file, +or a symlink is refused (exit 3) and left alone. git writes into a hidden +sibling staging directory that is renamed onto the target only after the clone +and checkout succeed, so a failure (exit 1, the URL scrubbed from the message), +a rejected credential (exit 4), or Ctrl-C (exit 130) leaves nothing behind and +an empty target stays empty; a staging directory left by a killed process is +removed by the next clone. Running the same clone again into a finished clone +with the same origin reports it and fetches nothing (a different `-b` is exit +3). The summary prints the checkout, history depth, blob mode, backend (git +version), and credential source, then `Next: cd DIR && codsh --rust`. +`--remote ssh://[user@]host[:port]/abs/base` (a codsh extension; the reference +clone has no remote form) runs the same clone on that host under the remote's +own gate and git credentials, with the SSH rules above; `GROVE_AUTH_TOKEN` is +not sent, a closed connection cancels the remote clone and removes its partial +directory, and the next step is `codsh --rust --remote /`. The +worktree gate follows the reference order too: `GROK_WORKTREE_TYPE`, then +`[cli] grove_worktree` / `nfs_worktree` / `worktree_type`, then `GROK_GROVE` or +`[cli] grove`. A Grove request is recorded on the worktree (`worktree show`) +and falls back to a plain git worktree with a full checkout, as the reference +does when Grove is unreachable. There is no remote-settings layer. Checked on +Linux against local bare repositories (file://, a local `git http-backend` +over http with a token, and a local OpenSSH server), not against GitHub, a real +Grove, macOS, or Windows. + +Local MCP servers run inside dsh's own MCP client; codsh does not start a +second one. `codsh --rust mcp list|add|remove|enable|disable|doctor` edits and +checks `[mcp_servers.]` in `$GROK_HOME/config.toml` (stdio `command`, +`args`, `env`, `cwd`, or streamable-HTTP `url` and `headers`; `${VAR}` and +`${VAR:-default}` expand). In a trusted folder `.grok/config.toml` and +`.mcp.json` add or replace servers; in an untrusted one they are listed as +untrusted and nothing starts. Claude (`~/.claude.json`) and Cursor +(`~/.cursor/mcp.json`) definitions are read from the isolated Home and can be +turned off with `[compat.claude] mcps = false` or +`GROK_CLAUDE_MCPS_ENABLED=0` (Cursor likewise). `disabled_mcp_servers` or +`enabled = false` keeps a server from starting. Each session mounts the +servers at start; a missing program, a crash before `initialize`, or a refused +`initialize` is reported by name on stderr (plain mode), in the status line +(terminal), and by `/mcps`, and the session still starts with the others. +Tools appear as dsh's `mcp____`, and Grok's `search_tool` +(keyword search returning `server__tool` names and input schemas) and +`use_tool` (`tool_name` = `server__tool`, `tool_input`) are available too. +Every call, direct or through `use_tool`, runs once through dsh's pipeline: +allow/ask/deny rules and remembered grants use `server__tool` +(`mcp__server` in a rule means every tool of that server), Hooks see +`tool_name = server__tool` and never the dispatcher, and Ctrl+C or ACP +`session/cancel` sends `notifications/cancelled` to the server. Text results +over `[mcp] max_output_bytes` (default 20000; `GROK_MAX_MCP_OUTPUT_BYTES` or +`MAX_MCP_OUTPUT_BYTES` override) are cut on a UTF-8 boundary with Grok's +`[MCP output truncated: ...]` notice, and the full text is written under +`$DSH_HOME/mcp/output/`. `startup_timeout_sec` (default 30 s, +`GROK_MCP_STARTUP_TIMEOUT_SECS` / `MCP_TIMEOUT`) and `tool_timeout_sec` / +`tool_timeouts` are enforced by a small stdio launcher in `codsh-rust`, which +also keeps the server's stderr for `/mcps` and `mcp doctor`; dsh's own 60 s +per-call limit still applies above that. dsh reconnects a server that exits +mid-call; the failed call returns an error. `/mcps` (alias `/mcp`) lists +servers, state, failures, and tools; `/mcps enable|disable ` saves the +change and `/mcps restart [name]` or `/mcps refresh` restarts every server by +resuming the same session in a fresh dsh (refused while a turn runs). An +editor's `mcpServers` on `session/new` or `session/resume` are mounted after +the configured ones. Known limits: dsh renders only the text part of a result +(`structuredContent` is returned by `use_tool` only when there is no text), +tool names longer than 64 characters are hashed by dsh, the direct +`mcp__*` tools stay visible next to `search_tool`/`use_tool`, +and managed allow/deny MCP policy (`allowedMcpServers`/`deniedMcpServers`) and +`disabled_mcp_tools` are not applied. Servers from enabled plugins join the +same list (see Plugins). Through `codsh --rust` only +the listed MCP variables, `BROWSER`, and `*_API_KEY` pass from the host +environment, so put other values in the server's `env` table. Checked on Linux +with a real stdio fixture server; not on macOS or Windows. + +Remote servers (`url`, type `http`, or `sse`; a URL ending in `/sse` with no +type is SSE) run through codsh's remote proxy, which dsh sees as a stdio +server: streamable HTTP (JSON or SSE replies, `Mcp-Session-Id`, the negotiated +`MCP-Protocol-Version`, a 404 re-initializes once) and the legacy HTTP+SSE +transport (message endpoint must be same-origin). Static `headers` and +`bearer_token_env_var` go to an owner-only +`/.remote.json`, never into the plan or argv; `${session_id}` in a +header value becomes the session id. Redirects are refused (a token must not +follow them); a request whose connection breaks mid-flight is reported, never +resent. A server that answers 401 is signed in with OAuth: +`codsh --rust mcp login ` (or `/mcps auth ` in a session, or +editor `x.ai/mcp/auth_trigger`) discovers the authorization server (RFC 9728 / +RFC 8414, OpenID fallback), registers a public client dynamically (RFC 7591) +unless `oauth_client_id` (plus optional `oauth_client_secret_env_var`, +`oauth_scopes`, or an `oauth` table with `callback_port`) is set, and runs authorization code + +PKCE S256 with the `resource` indicator (RFC 8707) through `$BROWSER` or the +system opener and a loopback `http://127.0.0.1:/callback`; the URL is +also printed. Tokens live in `$GROK_HOME/mcp_credentials.json` (0600, keyed by +server name and URL) and are refreshed once on expiry or a 401; +`mcp logout ` / `/mcps logout ` revoke (RFC 7009, when offered) +and forget them, and `mcp remove` forgets them too. `mcp login`/`logout` and +`/mcps auth|logout` are codsh extensions; the reference signs in from its +extensions modal and ACP `auth_trigger`. Tool results keep image blocks for +dsh (a model without image input gets dsh's placeholder; +`expose_image_base64 = true` also appends the base64 as text); audio becomes a +short note, embedded text resources become text. MCP elicitation +(`elicitation/create`, form and URL modes) is advertised only when someone can +answer: the TUI opens an MCP card (↑/↓ or Tab move, Enter/Space edit or +toggle, ←/→ pick an option or button, Enter on Accept validates and submits, +`d` declines, Esc parks the keyboard, Ctrl+C cancels, `o` opens a URL), an +editor gets `x.ai/mcp/elicit` (and `x.ai/mcp/elicit_complete`, and +`$/cancel_request` when the request is answered elsewhere or withdrawn); +headless `-p` does not advertise it. Answers are re-checked against the +requested schema; an invalid answer or an editor error declines. dsh's 60 s +per-call limit includes the time the user takes to answer. Editors also get +`x.ai/mcp/auth_status` and `x.ai/mcp/read_resource`. Not handled: the +`-32042` URL-elicitation-required error (its message reaches the model as a +tool error). Checked on Linux against a keyless loopback fixture (OAuth +authorization server included) and the MCP TypeScript SDK 1.30.0 example +server with its demo OAuth server; not against hosted providers, macOS, or +Windows. + +Streamed answers, provider +thoughts, empty replies, and failures are shown as dsh reports them; a protocol +mismatch or missing dsh is refused instead of faked as success. It reuses licensed +Grok Rust UI components, requires no official account, and does not start the +legacy Bundle, official agent core, update check, telemetry, or feedback upload. +Enter submits the draft through dsh when connected, or reports that execution is +unavailable without sending it. File read, write, and edit run through real dsh +tools. A shell command runs through dsh's bash tool: the card shows stdout, +stderr, and the exit code, including 0. A non-zero exit is not shown as +success. Ctrl+C cancels the running command and dsh reports it as aborted. +A denied command does not run. dsh's persistent terminal is not mounted in +the acp profile, and window resize is not a dsh tool, so an interactive +terminal session is unavailable. Background jobs use the same bash tool. `--sandbox ` (`GROK_SANDBOX`, or `[sandbox] profile` in the +isolated `$GROK_HOME/config.toml`) applies Seatbelt on macOS or Landlock on +Linux to this process before dsh starts. `off` is the default and adds no +confinement. `workspace` reads broadly and writes the workspace, `$GROK_HOME`, +and temp directories. `read-only` and `strict` narrow writes. `devbox` does not +write-protect global hook or config files; a custom profile that extends +`devbox` still kernel-enforces its `deny` list. Custom profiles live in +`$GROK_HOME/sandbox.toml` or `.grok/sandbox.toml` (`extends`, `read_only`, +`read_write`, `deny`). A symlink `$GROK_HOME`, a symlink in a `hooks-paths` +target, a missing hook target, a malformed profile, or a kernel that cannot +apply the policy refuses startup instead of continuing unconfined. When +`$GROK_HOME/sandbox.toml` and `.grok/sandbox.toml` define the same custom +profile differently, startup uses the user file, warns, and names both paths. +Identical definitions do not warn. A relative deny glob stays inside the +workspace: `**` matches path segments there and does not deny a sibling +directory or a same-prefix path. `**` is only a whole path segment (`**/`, +`a/**`); an attached form such as `**.pem` or `certs/**.pem` refuses startup. +Seatbelt checks resolved paths, so every deny path and the literal prefix of +every deny glob is resolved through its deepest existing ancestor (for example +`/tmp` to `/private/tmp`, or a symlinked directory to its target) and both +forms are denied. That also covers a denied file created after launch. The +workspace a relative glob is anchored at is literal even when its name +contains `[`, `*`, or `?`. A deny path under a dangling symlink, or one with a +control character, cannot be written as a matching kernel rule and refuses +startup. A deny glob is anchored at its literal prefix, so that prefix +directory and its existing ancestors up to the write root are pinned against +rename or unlink. The walk stops at the resolved write root: `/tmp` and +`/private/tmp` are the same root, so a workspace under `/tmp` does not pin +`/tmp` or `/private/tmp` themselves, and a workspace-anchored glob does not +pin a directory outside that workspace. A directory inside the glob tail, including one created +after launch, is pinned the same way by a directory regex: Seatbelt matches +the resolved path, so renaming that directory onto another write root would +carry a matched file out from under the regex. Seatbelt only sees the source +path, so a rename that stays inside the glob is denied as well. A sandboxed child cannot use launchd (`launchctl submit`, +`launchctl bootstrap gui/$UID`) to get an unconfined process to read a denied +file: this escape is kernel-blocked under the profile, matching the reference +nono profile's `mach-lookup` rules (a probe confirms it succeeds only when the +sandbox is off). +A `.` or `..` segment anywhere in a deny entry, including `a/./secret`, also +refuses startup. `[!a]` and `[^a]` both negate in the macOS +profile. A POSIX class, an empty `//` segment, a trailing slash, or a caret +that would be literal is refused instead of applied. `codsh --rust inspect` and `inspect --json` do not apply the sandbox. They +print the resolved profile and every config error, including a broken +`fail_closed` file, instead of stopping at the first one. A non-inspect launch +still refuses that file before dsh starts. `[sandbox] profile` is +read by the same config loader as inspect: a signed `requirements.toml` pin +beats `--sandbox`, `GROK_SANDBOX`, `GROK_CONFIG` / `GROK_CONFIG_PATH`, and +every file below it. A managed default does not; those sources override it. +An untrusted project file does not select the profile, and naming a custom +profile with `--sandbox`, `GROK_SANDBOX`, or a requirements pin does not trust +`.grok/sandbox.toml`. A definition that exists only in that untrusted file +refuses startup. A user `$GROK_HOME/sandbox.toml` definition stays usable and +still wins when both files define the name. The untrusted project file is not +applied. A body that parses is compared only so a disagreement can name both +paths; a malformed, unreadable, or symlinked untrusted project file does not +veto the user definition. A trusted project file that is malformed still +refuses startup. `[sandbox]` is a known +policy key in the user config and in a trusted project config, including when +signed `fail_closed` requirements are active. On macOS +every existing ancestor of a protected path up through the write root that +contains it, not only its immediate parent, cannot be renamed onto another +write root. Linux +Landlock cannot deny a path inside a write root, so a profile that needs that +protection refuses startup there instead of applying an allow-only policy. +The status +line names the active profile and its write roots. Protected config and hook +files stay unchanged; a permission-mode change is kept for the session only. +`restrict_network` (built-in `read-only` and `strict`, or a custom profile) +denies network for this process and its children with macOS Seatbelt +`(deny network*)`. A profile that leaves it off still allows network. dsh's +per-call file mode is not this control and is not a network sandbox. Linux +Landlock network blocking is a different mechanism and is not claimed from a +macOS run: a profile that asks for network isolation refuses startup there +instead of continuing with network open. Windows network confinement is not +implemented and refuses the same way. `[shell_environment_policy]` in +`sandbox.toml` (`inherit` `all`/`core`/`none`, `exclude`, `include_only`, +`set`, `ignore_default_excludes`) filters the environment of a shell child +this client starts, including `sh -c`. Names matching `*KEY*`, `*SECRET*`, +or `*TOKEN*` are dropped unless `ignore_default_excludes` is set. An unknown +`inherit` or a pattern that is not a `*`/`?` glob refuses startup. When that +policy is active, the filtered map is the environment of the dsh process +spawned afterwards, so dsh's bash tool sees it too: dsh builds that child +from its own environment and only adds keys. With no policy, dsh keeps the +launch allowlist. A second Seatbelt profile is still not applied inside this one. A process already under +Seatbelt cannot apply another Seatbelt policy, so dsh's own per-call bash +sandbox cannot run inside a codsh profile. While a profile is applied, codsh +starts dsh with its per-call file mode set to `danger-full-access` (written +to `$DSH_HOME/codsh-kernel-sandbox.yml`). Approvals are unchanged, and the +kernel policy confines dsh, its bash children, and child agents instead; a +write is then bounded by the profile's write roots rather than dsh's +workspace-write fence. Linux and Windows are not marked supported by a macOS +run. Allow/ask/deny rules, remembered project grants, and permission modes +(`ask`, `auto`, `always-approve`/`--yolo`, `dontAsk`, `acceptEdits`) are +enforced before a dsh tool runs. Explicit deny, hook blocks, and locked +always-approve survive `--always-approve` and old grants. Released dsh does +not run Grok hooks, so `codsh --rust` runs command hooks from +`$GROK_HOME/hooks/*.json`, trusted `/.grok/hooks/*.json`, and the +`hooks` table in `config.toml` at SessionStart, UserPromptSubmit, PreToolUse, +PostToolUse, Stop, and SessionEnd (Cursor camelCase aliases included). Exit 2 +or `{"decision":"deny"}` blocks the prompt or tool; any other non-zero exit, +timeout, or malformed output is a recorded failure and does not look like +success. Hook stdout and stderr are shown as hook output, not as the model +answer; a JSON `systemMessage` (as in Claude Code) is shown by itself, for any +event, instead of the raw output. Hook commands also get `CODSH_HOOK_HOST_PID`, +the dsh process running them. An allowing hook does not skip the permission check, and a hook +cannot widen a sandbox or permission deny. Untrusted project hooks stay +skipped. HTTP hooks are not run. A `updatedInput` rewrite is not applied; +that call is blocked instead of running the original arguments. Unsplittable shell +(`$(...)`, a parameter expansion such as `$x`, `${x}`, `$1`, `"$1"`, `$@`, or `$*`, control flow) is not glob-allowed as a unit; Read/Edit deny also +covers shell operands; wrappers such as `timeout`, `nice`, `ionice`, +`sudo`, `nohup`, `xargs`, and +`env FOO=1` peel to the inner command (only real duration/priority tokens are +consumed; `sudo -u` and `xargs -n` keep their option values), while `env -S` prompts. Read/Edit deny and ask follow in-path +symlink targets; an unresolved link prompts. Brace groups, quoted or +backslash-escaped command words, `eval`, and ANSI-C `bash -c $'…'` scripts +(including a backslash-newline) cannot hide a denied command. A leading word +that is not itself the denied command, such as `time /bin/rm`, `exec /bin/rm`, +or `builtin rm`, does not hide it either. A shell option that takes the next +word, such as `bash -o errexit -c`, is consumed before the script is read, so +the inner command is still denied. An unquoted `*`, `?`, or `[` in a command +word is not expanded: a pathname glob such as `./r*`, `./*m`, or `./r?` can +become `rm`, including behind `time`, `exec`, `builtin`, `command`, `sudo`, or +`bash -o errexit -c`, so always-approve does not run it. A path-qualified +executable such as `/bin/rm`, `./rm`, or `RM.EXE` is matched by its command +basename without regard to case, so `Bash(rm -rf *)` still denies it under +always-approve. `sort -o`, including attached `sort -oFILE` and a cluster +such as `sort -uoFILE`, +`--output`, and a unique prefix of `sort --compress-program`, are not +read-only. Frozen git inspection commands +(`cat-file`, `ls-tree`, `check-ignore`, `show-ref`, `for-each-ref`, `rev-list`, +`name-rev`, `count-objects`, `check-attr`) auto-allow; git writes do not, +including `git branch `, `-f`/`--force`, `-u`/`--set-upstream-to` +(including the attached form `git branch -uorigin/main` and a bare `-u` or +`-t` with no operand), `git branch --track` and a unique prefix such as +`--tr` (a write even with no operand), unique +prefixes of `git branch --delete`/`--move`/`--copy`/`--force`, +`git diff`/`log`/`show`/`blame`/`rev-list --output`, and `git cat-file --filters`. +Claude rules load from `~/.claude` and walk up to the repo root. Always-approve +skips remembered grants and non-shell `ask`. A missing or corrupt policy file +refuses mutating tools instead of dropping deny. Remembered file grants are +path-scoped; `a` is not all-edits-forever. The UI shows the pending operation +and the dsh-supplied diff, then `y` allows that call once, `a` remembers it for +this project only, and `n` rejects it with no write. `/revoke-approvals` forgets +this project's remembered grants. Remembered grants are never described as a +permanent global rule; a failed save still allows once. File search uses dsh `grep` and `glob` (packaged ripgrep, not a shell). A result is capped (`glob` 100 paths, `grep` 250 matches) and says how to read the rest; a larger `grep` keeps the complete list in the spill store when one is mounted. `read` pages with `offset` and `limit` and says the next offset. An empty search says `No matches found` or `No files found` and invents nothing. A binary file is `binary file` / `FS_NOT_TEXT`, not decoded text. A file that changes after it was read fails the edit with `FS_STALE_VERSION` and is left unchanged. Code navigation is dsh `lsp` (`goToDefinition`, `findReferences`, `goToImplementation`, `hover`). With no language server configured the call fails (`no LSP provider handles` the file; dsh code `LSP_UNAVAILABLE`) and returns no location. A Read/Edit deny covers a named search root, every grep/glob hit, and a shell search operand such as `rg`; a denied file is omitted from both the model result and the tool card, and switching to another tool does not reveal it. Missing files, tool +errors, cancelled or duplicate approval replies are shown as failures, never as +success. `Ctrl+C` clears a non-empty draft without cancelling work; an empty +draft cancels the running turn through dsh `session/cancel`. Esc never cancels a +turn or a pending approval — it dismisses selection and reminds you to use +`Ctrl+C`. Cancelled tools cannot run from a late allow or process teardown; unknown +external results are shown as cancelled, not success. After cancel, the prompt +accepts a new turn. Idle empty `Ctrl+C` still quits before any turn exists. +While a turn runs (Rust client), Enter queues the draft and the notice shows +`Queued N` with the next row; Enter on an empty prompt sends the top row now. +`Ctrl+Enter` or `Ctrl+I` sends the draft (or the selected row) now: the running +turn is cancelled through dsh without a `[cancelled]` marker and that row runs +next. Apple Terminal also takes `Ctrl+O`; VS Code-family terminals (vscode, +cursor, windsurf, zed) use `Ctrl+L` instead. `Ctrl+Enter`/`Ctrl+I` need a +terminal that reports them distinctly (kitty keyboard protocol); elsewhere +`Ctrl+O` is not bound. `Ctrl+;` or `Ctrl+'` (or ↑ on an empty prompt) opens +the queue pane: ↑/↓ select, `e` edits a row in place (Enter saves, an empty +save removes it, Esc cancels), Enter sends it now, `x`/Del/Backspace deletes, +`Shift+J`/`Shift+K` reorder, Esc closes. Queued rows run in order, one per +turn, after the turn ends or is cancelled with `Ctrl+C`; a pending approval, +compaction, or a row being edited keeps them waiting, and switching +`/minimal`/`/fullscreen` keeps them. Slash commands typed while busy queue as +their own rows. `[ui] follow_up_behavior = "steer"` sends plain text +follow-ups into the running dsh turn at its next model step instead; a steer +dsh did not use goes back to the queue. `[ui] combine_queued_prompts = true` +joins consecutive plain rows into one turn. `/queue` lists the queue. +`/btw ` (also typed mid-message) asks a side question from the +current session context through a private dsh control channel, with no tools; +the answer shows in a panel that Esc dismisses (minimal prints it to +scrollback), a late answer to a dismissed question is dropped, and neither +question nor answer enters the conversation. +`codsh --rust -p "task"` (or `--single`, `--prompt-file `, `--prompt-json `) runs one prompt through the same dsh ACP session and prints only the final answer on stdout. `--verbatim` sends that user content unchanged and does not expand custom slash commands. Rules from files, `--rules`, and `--system-prompt-override`, plus enabled first-turn memory, still apply: they lead as their own block ahead of your exact bytes (dsh joins adjacent text blocks, so the model sees the rules and notes, then your prompt). dsh receives codsh rules as prompt context, not as a separate system prompt. Permission policy stays on the tool channel. A plain turn has no time limit: it ends when dsh finishes or fails, or on a signal. Thoughts, tool cards, and errors stay off stdout; diagnostics go to stderr. `-c`/`--continue` or `-r`/`--resume ` with a plain prompt continues that session, and `--fork-session` copies it. `--max-turns ` stops before model step N+1 and says so on stderr. `--tools` and `--disallowed-tools` mask tools before the first model request; public ids such as `read_file` and `Bash` map to dsh names, `Agent` removes every registered subagent spawn tool (`subagent` and `subagent_fork`) and the `workflow` tool, and `--disallowed-tools Agent(type)` in any letter case removes those subagent types (see Subagents below); `--tools Agent(type)` and `Agent()` are refused before any provider call, and so is a typed entry in an inherited `CODSH_PLAIN_TOOLS`. Deny wins when both are set, and an unknown name is an error. A `CODSH_PLAIN_TOOLS` or `CODSH_PLAIN_MAX_TURNS` value inherited from a parent process follows the same rules in a plain prompt; interactive sessions ignore both. `--allow`/`--deny` still gate execution and do not remove the tool. `--tools`, `--disallowed-tools`, `--max-turns`, and `--verbatim` are headless flags: without a plain prompt they print a warning and are ignored. `--cwd `, `--sandbox `, `--no-memory`, `--no-subagents`, and `--disable-web-search` work for plain prompts and interactive sessions alike. `--cwd` is entered before config, trust, and the sandbox are read, so the sandbox write root is that directory. A relative `--prompt-file` is read after `--cwd` is entered, so it names a file inside that directory. A relative `--trust-folder` or sandbox report path still names a file beside where you ran the command. `--no-memory` keeps notes out of the first prompt. `--disable-web-search` turns web search and web fetch off for the process and removes `web_search` and `web_fetch` from the model's tool list. `-m` is `--model` and `-v` is `--version`. A second prompt source is rejected before a provider call. A positional prompt does not start plain mode, and piped stdin is not the prompt. `codsh --rust help` and `-h` print help; `completions bash|zsh|fish|powershell|elvish` prints a script for that shell. `--output-format` is `plain` (the default, final answer only), `json` (one object: `text`, `stopReason`, `sessionId`, `requestId`, and `thought` when dsh sent reasoning), `streaming-json` (one ACP-shaped object per line, last line `end`), or `streaming-messages-json` (`system`/`init`, then `assistant` and `user` messages, last line `result`). `--include-partial-messages` adds `stream_event` deltas and changes only `streaming-messages-json`; other formats print a warning and ignore it. Tool arguments, tool results, and reasoning are copied from the dsh update and are not rewritten. `usage`, `modelUsage`, and `num_turns` come from the dsh session log of the turns this prompt started, subagents included (see Usage and cost below). A provider call that reported no usage sets `usage_is_incomplete: true` instead of a zero; when nothing was recorded the terminal object says `usage_absent: true`. `cost_status` is `unknown` and no cost key is written, because dsh reports no cost. A partial `message_start` omits `usage`; the totals arrive on the terminal object. A truncation stop (`max_tokens`), a model error, and SIGINT/SIGTERM (130/143) end the process and do not report `end_turn` success for a turn that failed. A tool approval with no terminal is rejected inside dsh and exits 1; the file is unchanged. `json` and `streaming-json` print `{"type":"error",...}` and `streaming-messages-json` prints `result` with `is_error: true` and a subtype other than `success`. None of them say `end_turn`. The same prompt through `plain` and through `json` uses the same dsh session path, so the file side effects and stored session match. Agent selection, plan, `--experimental-memory`, `--memory-flush`, and `--json-schema` name the flag and remain later tickets. `-r`/`--resume` without a value is not the most-recent-session shortcut yet (ticket 159 owns it). Unknown options and missing values exit 2. SIGINT exits 130 and SIGTERM exits 143, also while dsh is still starting, and a dsh or provider error exits 1. + +Usage and cost. `/usage` (alias `/cost`) in the Rust client, `/session-info`, the status line command payload (`context_window.session_input_tokens`, `session_output_tokens`, `session_usage`, `cost.total_api_duration_ms`), headless output, and `codsh --rust usage [turn]` (JSON) all read one ledger folded from the dsh session log. The fold covers the whole log, so a resumed session is not counted twice. As in the reference, a forked session's totals include the history it inherited, while a headless `-p` result reports only the turns that prompt started. A model call is one dsh attempt (`step/start` or a retry to its settle); its tokens are the provider's reported usage (input with cache reads and writes, output with reasoning). Subagent sessions fold into the parent turn that started them and are also shown per model route. A call with no reported usage, an interrupted call, a running or missing subagent log all mark the ledger incomplete ("may under-count") instead of adding zeros. API time follows dsh's `llmMs` (attempt start to settle). Auxiliary calls outside the session log (session title, compaction summary, `/btw`, memory) are not counted. Cost: dsh reports no cost and the pinned dsh sources carry no price table, so cost always shows as not available (unknown); nothing is estimated and `$0` is never shown. +Subagents in `codsh --rust` are created and run by dsh. The model's `subagent` tool takes `subagent_type`: `general-purpose` (every tool the parent has), `explore` and `plan` (read, search, and shell, but no write or edit), a `[subagents.roles.]` role (`description`, `default_capability_mode` = `read-only`, `read-write`, `execute`, or `all`, `model`, and `prompt_file` under `$GROK_HOME`), or an agent file in `.grok/agents/` or `$GROK_HOME/agents/` (its `tools` and `model` front matter). The type's capability becomes a dsh tool allow-list, so a removed tool is missing from the child's tool list and refused if called anyway. A tool dsh cannot classify, such as an MCP tool, is kept only by `all`. A child inherits the parent's permission mode, rules, hooks, and sandbox. In the interactive terminal a child's approval request (a workflow researcher, a `subagent` child, a scheduled loop) joins the main session's approval queue behind the main session's own prompt: the status line names who asks (`workflow [] ·