diff --git a/.prettierignore b/.prettierignore index b0c35e79..bf1b3dec 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,8 @@ vercel.json cache temp .temp + +# dsh writes its profile files itself (the Settings UI and `dsh plugin`), and +# its YAML dumper wraps prose differently from prettier; formatting them here +# would be undone on the next write. +home/base/tui/agents/dsh/web diff --git a/Justfile b/Justfile index 3f214486..89f1f0b0 100644 --- a/Justfile +++ b/Justfile @@ -254,6 +254,23 @@ kana mode="switch": k3s-test mode="switch": colmena apply {{mode}} --on '@k3s-test-*' --verbose --show-trace +# ================================================= +# +# AI agent commands +# +# ================================================= + +# Start the dsh web UI without opening a browser, proxied via mihomo's mixed port. +[group('agents')] +dsh-web: + #!/usr/bin/env nu + # dsh's web fetch refuses a DNS answer it does not call public, and a fake-ip + # resolver answers with 198.18.0.0/15. A proxied hop lets mihomo resolve the + # origin instead, which skips that check and leaves fake-ip acceleration + # intact. The policy reaches every Node fetch the harness makes, so mihomo + # becomes a dependency for LLM, web search and HTTP MCP traffic too. + with-env { HTTPS_PROXY: "http://127.0.0.1:7897", HTTP_PROXY: "http://127.0.0.1:7897" } { ^dsh web --no-open } + # ================================================= # # Other useful commands diff --git a/home/base/tui/agents/README.md b/home/base/tui/agents/README.md index 9df61712..c14bf353 100644 --- a/home/base/tui/agents/README.md +++ b/home/base/tui/agents/README.md @@ -13,6 +13,8 @@ Nix dependency; this module only wires it into place. - `packages.nix`: agent CLIs (`codex`, `opencode2`, `pi`) from the `llm-agents` flake input, plus `pkgs.mcp-grafana` for the Grafana MCP server. - `env.nix`: telemetry and auto-update opt-outs for the agents. +- `dsh/`: the dsh `web` profile that lives in `~/.dsh`. See [dsh/README.md](./dsh/README.md) for the + link granularity, the tracked members and which hosts import it. ## Deployed rule targets @@ -30,6 +32,3 @@ Out-of-store means an edit to `agents/AGENTS.md` takes effect on the next agent Home Manager switch. The trade-off is that the rules stay writable in the checkout; use a store symlink (`home.file..source = ../../../../agents/AGENTS.md`) if immutability matters more than fast iteration. - -The module is imported through `home/base/tui`, so it applies to every host that imports -`home/linux/gui.nix` or the macOS `home/darwin` stack. Core-only servers do not get it. diff --git a/home/base/tui/agents/dsh/README.md b/home/base/tui/agents/dsh/README.md new file mode 100644 index 00000000..514c5951 --- /dev/null +++ b/home/base/tui/agents/dsh/README.md @@ -0,0 +1,46 @@ +# dsh + +The dsh (DeepSeek Harness) `web` profile, linked into `~/.dsh/profiles/web` from the checked-in +files under [`web/`](./web). + +## Why the link targets the directory + +dsh persists Settings through `cordis.patch.yml` with an atomic write (a random-suffix sibling, then +a rename), and that rename replaces a symlinked target instead of writing through to its referent. A +file-level link — store or out-of-store — is destroyed by the first Settings save, while a directory +link keeps the write inside the checkout. Same reasoning as `home/base/tui/tuios`, one level up. + +| Profile member | Tracked | Role | +| --------------------- | ------- | ------------------------------------------ | +| `cordis.patch.yml` | yes | the user patch layer the Settings UI edits | +| `package.json` | yes | the profile's `dsh.profile.bundles` list | +| `pnpm-workspace.yaml` | yes | the profile's pnpm settings | +| `cordis.yml` | no | rewritten on every boot | +| `node_modules/` | no | written by `dsh plugin --profile web ...` | + +`web/.gitignore` owns the untracked rows. As with the rules links in the parent module, the +trade-off is a config that stays writable in the checkout: a Settings change and a commit are the +same edit, and the configuration is shared by every host that deploys the profile. + +## Which hosts deploy it + +A host only gets this link by importing the module. `home/linux/gui.nix` and `home/darwin` pull it +in through `home/base/tui`, so core-only servers miss it — `idols-ruby` installs dsh from +`environment.systemPackages` and imports `home/base/tui/agents/dsh` from its own home module. A new +host that ships dsh needs one of those two wirings. + +## Default model route + +`web/cordis.patch.yml` pins `agent-default-model` to the API-key route (`deepseek-official`). The +account route (`deepseek-account`) never falls back to an API key, so a host holding only +`DEEPSEEK_API_KEY` fails every freshly created session with `ACCOUNT_SIGN_IN_REQUIRED`. A host that +signs in instead can select an account model per session, or override the field in its own +`$DSH_HOME/cordis.patch.yml`, which the home layer applies last and which therefore wins. + +## Running it + +`just dsh-web` starts the harness without opening a browser and routes its outbound HTTP through +mihomo's mixed port. dsh's web fetch refuses a DNS answer it does not call public, and a fake-ip +resolver answers with `198.18.0.0/15`; a proxied hop lets mihomo resolve the origin instead, which +skips that check and leaves fake-ip acceleration intact. The policy reaches every Node fetch the +harness makes, so mihomo becomes a dependency for LLM, web search and HTTP MCP traffic too. diff --git a/home/base/tui/agents/dsh/default.nix b/home/base/tui/agents/dsh/default.nix new file mode 100644 index 00000000..1fe83d4e --- /dev/null +++ b/home/base/tui/agents/dsh/default.nix @@ -0,0 +1,21 @@ +{ + config, + ... +}: +let + # dsh persists Settings through the profile's cordis.patch.yml with an atomic + # write (a random-suffix sibling, then a rename), and that rename replaces a + # symlinked target instead of writing through to its referent. A file-level + # link, store or out-of-store, therefore survives only until the first save. + # Linking the whole profile directory keeps the write inside the checkout and + # leaves the directory symlink intact. Same reasoning as + # `home/base/tui/tuios`, one level up; the checkout path is hardcoded like the + # repo's other out-of-store links. + profileDir = "${config.home.homeDirectory}/nix-config/home/base/tui/agents/dsh/web"; +in +{ + # The `web` profile's configuration, owned by hand and by the Settings UI. + # Generated members of the profile (cordis.yml, node_modules, lock files) are + # gitignored inside that directory rather than tracked. + home.file.".dsh/profiles/web".source = config.lib.file.mkOutOfStoreSymlink profileDir; +} diff --git a/home/base/tui/agents/dsh/web/.gitignore b/home/base/tui/agents/dsh/web/.gitignore new file mode 100644 index 00000000..f50e4f3d --- /dev/null +++ b/home/base/tui/agents/dsh/web/.gitignore @@ -0,0 +1,10 @@ +# Regenerated by dsh on every boot (always the empty root entry list). +cordis.yml + +# Written by `dsh plugin --profile web ...`. +node_modules/ +pnpm-lock.yaml + +# Transient siblings of the atomic-write and file-lock protocols. +*.tmp +*.lock diff --git a/home/base/tui/agents/dsh/web/cordis.patch.yml b/home/base/tui/agents/dsh/web/cordis.patch.yml new file mode 100644 index 00000000..37e3a301 --- /dev/null +++ b/home/base/tui/agents/dsh/web/cordis.patch.yml @@ -0,0 +1,77 @@ +# Your patch layer for this dsh profile, applied after every bundle layer: +# a top-level YAML array of loader patch entries (id-targeted config +# overrides, disables, and insert lists; `!!js` expressions allowed). +- id: ui-settings-general + name: "@deepseek-ai/dsh-client-ui-settings-general" + config: + welcomeNoticeVersion: 2026-09-28.1 +- id: agent-default-model + name: "@deepseek-ai/dsh-agent-default-model" + config: + provider: deepseek-official + model: deepseek-flash + reasoningEffort: high +- id: ui-settings-account + name: "@deepseek-ai/dsh-client-ui-settings-account" + config: + version: 1 + step: done + purpose: development + process: standard + completion: completed + usage: detailed + developerTools: true +- id: ui-chat + name: "@deepseek-ai/dsh-client-ui-chat" + config: + transcriptView: standard + performanceUsage: detailed +- id: ui-settings + name: "@deepseek-ai/dsh-client-ui-settings" + config: + enabled: true +- id: locale + name: "@deepseek-ai/dsh-client-locale" + config: + preference: zh +- id: agent-preset-registry + name: "@deepseek-ai/dsh-agent-preset-registry" + config: + default: standard + selectedDefault: ptc +- id: llm-deepseek-account + name: "@deepseek-ai/dsh-llm-deepseek-account" + config: + models: + - id: deepseek-flash + name: DeepSeek-V41-Flash + inputModalities: + - text + - image + systemPromptUpdate: in-history + toolUpdate: addition-only + contextWindow: 1000000 + - id: deepseek-v4-pro + name: DeepSeek-V4-Pro + description: Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost. + contextWindow: 500000 + inputModalities: + - text +- id: agent-loop + name: "@deepseek-ai/dsh-agent-loop" + config: + agents: [] + maxParallelToolCalls: 8 +- id: subagent + name: "@deepseek-ai/dsh-subagent" + config: + maxActiveSubagents: 3 +- id: compaction-basic + name: "@deepseek-ai/dsh-compaction-basic" + config: + thresholdRatio: 0.35 + retainRatio: 0.16 + headroomTokens: 65536 + compactionRetries: 1 + maxOverflowRetries: 1 + auto: true diff --git a/home/base/tui/agents/dsh/web/package.json b/home/base/tui/agents/dsh/web/package.json new file mode 100644 index 00000000..d2f8e1e8 --- /dev/null +++ b/home/base/tui/agents/dsh/web/package.json @@ -0,0 +1,14 @@ +{ + "name": "dsh-profile-web", + "private": true, + "dependencies": {}, + "dsh": { + "profile": { + "bundles": [ + "@deepseek-ai/dsh-base", + "@deepseek-ai/dsh-web-app", + "@deepseek-ai/dsh-experimental-auto-review" + ] + } + } +} diff --git a/home/base/tui/agents/dsh/web/pnpm-workspace.yaml b/home/base/tui/agents/dsh/web/pnpm-workspace.yaml new file mode 100644 index 00000000..f24c22e1 --- /dev/null +++ b/home/base/tui/agents/dsh/web/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - . + +nodeLinker: hoisted +autoInstallPeers: false diff --git a/home/hosts/linux/idols-ruby.nix b/home/hosts/linux/idols-ruby.nix index 4577dfdf..7961b302 100644 --- a/home/hosts/linux/idols-ruby.nix +++ b/home/hosts/linux/idols-ruby.nix @@ -2,6 +2,11 @@ imports = [ ../../linux/core.nix ../../linux/gui/i3 + # Ruby installs the dsh CLI system-wide from `hosts/idols-ruby/packages.nix` + # while running the core home stack, so it misses the agents module that the + # GUI hosts pull in through `home/linux/gui.nix`. Import just the dsh + # profile, which keeps its Settings-owned configuration in this repository. + ../../base/tui/agents/dsh ]; # Headless X11/i3 session for computer use. diff --git a/outputs/x86_64-linux/tests/home-manager-dsh/expected.nix b/outputs/x86_64-linux/tests/home-manager-dsh/expected.nix new file mode 100644 index 00000000..b87854c3 --- /dev/null +++ b/outputs/x86_64-linux/tests/home-manager-dsh/expected.nix @@ -0,0 +1,7 @@ +{ + # The link targets the whole profile directory, not a file inside it: dsh's + # atomic Settings write replaces a symlinked file on the first save, while a + # directory link keeps the write inside the checkout. + keys = [ ".dsh/profiles/web" ]; + source = "/home/tester/nix-config/home/base/tui/agents/dsh/web"; +} diff --git a/outputs/x86_64-linux/tests/home-manager-dsh/expr.nix b/outputs/x86_64-linux/tests/home-manager-dsh/expr.nix new file mode 100644 index 00000000..9411e034 --- /dev/null +++ b/outputs/x86_64-linux/tests/home-manager-dsh/expr.nix @@ -0,0 +1,17 @@ +# The dsh module is evaluated on its own here. Reading `home.file` out of a real +# host would force the catppuccin theme derivations, which turns an eval test +# into a build, and the profile link is system-independent anyway. The stub +# supplies the only two config values the module reads. +let + module = import ../../../../home/base/tui/agents/dsh/default.nix; + evaluated = module { + config = { + home.homeDirectory = "/home/tester"; + lib.file.mkOutOfStoreSymlink = path: path; + }; + }; +in +{ + keys = builtins.attrNames evaluated.home.file; + source = toString evaluated.home.file.".dsh/profiles/web".source; +}