415 lines
23 KiB
Markdown
415 lines
23 KiB
Markdown
# DeepSeek Harness project VMs
|
|
|
|
Official **`deepseek-ai/deepseek-harness` (`dsh`)**, not OpenCode. A reusable NixOS
|
|
module + flake function + shell launcher. No custom Python control plane, host
|
|
service, sudo launcher, or host rebuild is needed.
|
|
|
|
## Start a project
|
|
|
|
As your normal user, on **x86_64 Linux** with accessible `/dev/kvm` and enabled
|
|
unprivileged user namespaces (for Bubblewrap):
|
|
|
|
```sh
|
|
mkdir -p ~/projects/example && cd ~/projects/example
|
|
nix flake init -t path:/etc/nix#agent
|
|
# Edit project.nix (tools/env) and flake.nix (RAM/CPU/network).
|
|
git init
|
|
git add flake.nix project.nix README.md .gitignore
|
|
nix flake lock
|
|
git add flake.lock
|
|
nix develop # ordinary host project shell
|
|
nix run .#agent # boots VM; waits and prints a private browser URL
|
|
```
|
|
|
|
Run commands **from the project directory**, including in the second terminal:
|
|
|
|
```sh
|
|
nix run .#agent -- url # print the current token-bearing browser login URL
|
|
nix run .#agent -- ssh # root shell, starting in the SAME project cwd
|
|
nix run .#agent -- ssh 'id; pwd; rg --version'
|
|
nix run .#agent -- ssh 'journalctl -u agent -b --no-pager'
|
|
nix run .#agent -- stop # graceful poweroff; Ctrl-C in the launcher also stops it
|
|
```
|
|
|
|
Open the printed URL. In a fresh DSH profile, **Choose workspace → add/select the
|
|
project's original absolute path**, which exists in the guest. Then
|
|
**Settings → Models** to configure DeepSeek, if you haven't already. Existing
|
|
credentials/settings are reused. No API key is required merely to boot the UI.
|
|
No model inference runs locally: the VM's RAM/CPUs are for tools/builds, not model weights.
|
|
The VM is **headless by default**: use DSH's Web UI from your host browser; no
|
|
VM desktop or display forwarding is started. Browser automation uses headless
|
|
Firefox inside the VM, as described below.
|
|
|
|
The template input `path:/etc/nix/agent-vm` is a local bootstrap. For collaboration
|
|
or guest-side evaluation of the flake, replace it with your accessible Git remote,
|
|
e.g. `git+https://git.cyber.ayyalasomayajula.net/marsultor/nixconfig.git?dir=agent-vm&ref=main`
|
|
**after these files have actually been committed and published there**. No push is
|
|
performed by this setup. Input changes require `nix flake update agent-vm`.
|
|
|
|
## Exactly what is shared
|
|
|
|
All of these mounts are **read-write**, as requested:
|
|
|
|
| Host source, resolved when you launch | Guest path | Includes |
|
|
| --- | --- | --- |
|
|
| Current working directory, not the flake's store copy | Same absolute cwd; `/workspace` is also an alias | Live source, uncommitted work, `.git`, project `.dsh/skills` and `.agents/skills` |
|
|
| `$DSH_HOME`, otherwise `~/.dsh` | `/root/.dsh` | `.credentials.yaml`, `settings.yaml`, `.env`, profiles, plugins, skills, sessions and other DSH state |
|
|
| `${DSH_AGENTS_HOME:-~/.agents}/skills` | `/root/.agents/skills` | Shared cross-agent skills only, not the rest of `~/.agents` |
|
|
|
|
**Upstream's standard directory is `~/.dsh`, not `~/.config/dsh`.** If you have
|
|
chosen an XDG-style location, use the upstream override, consistently for all commands:
|
|
|
|
```sh
|
|
export DSH_HOME="$HOME/.config/dsh"
|
|
nix run .#agent
|
|
```
|
|
|
|
Missing directories are created. Existing DSH home must be user-owned and private:
|
|
`chmod 700 ~/.dsh` (or your actual `DSH_HOME`); existing credentials must be mode
|
|
600 as DSH requires. The launcher never reads the credentials into Nix, copies
|
|
them to the store, or replaces your settings/skills with generated content.
|
|
Before starting the Web UI, the guest installs **only `dsh-context`** into the
|
|
shared `web` profile if missing (details below). DSH/pnpm manages that profile's
|
|
manifest, lockfile and dependencies; existing settings and other profiles stay intact.
|
|
The one bundled `playwright-firefox` skill is seeded into the actual shared skills
|
|
directory only if its `SKILL.md` is absent. It is a normal writable file (0600),
|
|
not a store symlink; existing files and user edits are never overwritten.
|
|
|
|
Root inside the guest is **not host root**. Bubblewrap maps the launching user's
|
|
UID/GID to namespace `0:0`, so 9p ownership matches guest root and pnpm's atomic
|
|
saves can preserve it. All capabilities remain dropped. QEMU/9p still writes as
|
|
the launching host user, so newly created project files belong to you. The whole
|
|
DSH directory is mounted instead of individual credential files so atomic
|
|
rename-based saves work.
|
|
DSH retains each project's real cwd identity rather than confusing every project's
|
|
sessions with a single `/workspace` identity. Do not run host DSH and several
|
|
VMs concurrently against the same mutable profile: package-fallback links and
|
|
profile changes can race. Separate `DSH_HOME` values provide independent profiles.
|
|
|
|
The runner retains a private per-cwd directory under
|
|
`${XDG_STATE_HOME:-~/.local/state}/agent-vm/`: dedicated SSH keys, console log,
|
|
control socket, and a 4 GiB sparse npm/pnpm-cache disk. Guest root, guest Nix writes,
|
|
processes and other unshared state are otherwise ephemeral. This is **not** a
|
|
push/pull/snapshot workflow; edits immediately affect the mounted host files.
|
|
|
|
## Resources and Nix composition
|
|
|
|
The template uses one ordinary Nix attrset:
|
|
|
|
```nix
|
|
project = import ./project.nix { inherit pkgs; };
|
|
devShells.${system}.default = pkgs.mkShell {
|
|
inherit (project) packages env;
|
|
};
|
|
agent = agent-vm.lib.mkAgentVM {
|
|
inherit system project;
|
|
modules = [ {
|
|
microvm.mem = 8192; # MiB
|
|
microvm.vcpu = 6;
|
|
agentVM.packages = [ pkgs.strace ]; # additional guest-only tool
|
|
} ];
|
|
};
|
|
```
|
|
|
|
The function imports the microvm.nix and agent NixOS modules and merges your
|
|
modules with them. Normal `imports`, `mkDefault`, `mkForce`, and list merging work.
|
|
`apps.${system}.agent = agent.app` exposes `nix run .#agent`; `agent.nixos` exposes
|
|
the NixOS configuration for inspection. RAM/CPU/network changes apply on **stop
|
|
and restart**, not live. vCPUs are not a host CPU-time quota, and QEMU has some
|
|
memory overhead beyond guest RAM.
|
|
|
|
The standard guest toolkit includes Git/LFS, ripgrep, fd, Python, Node/npm/pnpm,
|
|
jq/yq, curl/wget, common archive tools, Make, pkg-config, ShellCheck, and ordinary
|
|
Unix inspection tools, plus the official **Playwright CLI and patched Firefox**.
|
|
Project versions/tools belong in `project.nix`.
|
|
|
|
This deliberately shares **packages and non-secret environment variables**, not
|
|
an arbitrary shell's internals. Existing `shellHook`, `inputsFrom`, cross-compilation
|
|
setup hooks, library search paths and background dev services are not magically
|
|
converted into NixOS configuration. Factor tools/env into the shared attrset;
|
|
configure required guest services through `modules`. Do not put credentials in
|
|
`project.env`. Build outputs/package definitions can use the same `pkgs` input.
|
|
|
|
## Firefox automation and subagent sessions
|
|
|
|
DSH discovers the bundled [playwright-firefox skill](agent-vm/skills/playwright-firefox/SKILL.md)
|
|
from `/root/.agents/skills/playwright-firefox/SKILL.md`, which is the live host
|
|
`${DSH_AGENTS_HOME:-~/.agents}/skills/playwright-firefox/SKILL.md`. No changes to
|
|
`settings.yaml`, credentials, plugins or external MCP configuration are needed.
|
|
Ask DSH/subagents to use **playwright-firefox** for browser tasks.
|
|
|
|
The `playwright-cli` executable uses the official CLI shipped in Playwright core
|
|
(`playwright-core cli`), with its matching Nixpkgs Firefox and rendering fonts.
|
|
It defaults to **Firefox, headless, nonpersistent profiles**. This avoids an
|
|
independently updated npm CLI demanding a different browser revision, or trying
|
|
to automate ordinary Firefox without Playwright's Juggler support. CLI, patched
|
|
browser and dependencies advance together with the consuming flake's Nixpkgs
|
|
input; no handwritten Playwright version/hash or runtime browser download.
|
|
|
|
Each subagent must use its own unique name and artifact directory. For example,
|
|
inside the guest, from the same project cwd throughout:
|
|
|
|
```sh
|
|
# Parent-assigned examples: give each task a different suffix.
|
|
PLAYWRIGHT_MCP_OUTPUT_DIR="$PWD/.playwright-cli/login-a1b2c3d4" \
|
|
playwright-cli -s=login-a1b2c3d4 open http://127.0.0.1:3000 --browser=firefox
|
|
PLAYWRIGHT_MCP_OUTPUT_DIR="$PWD/.playwright-cli/search-e5f6a7b8" \
|
|
playwright-cli -s=search-e5f6a7b8 open http://127.0.0.1:3000 --browser=firefox
|
|
playwright-cli -s=login-a1b2c3d4 snapshot
|
|
playwright-cli -s=search-e5f6a7b8 eval 'document.title'
|
|
playwright-cli list
|
|
playwright-cli -s=login-a1b2c3d4 close # leaves the other subagent alone
|
|
playwright-cli -s=search-e5f6a7b8 close
|
|
```
|
|
|
|
Cookies, DOM, tabs and storage are independent between named sessions. This is
|
|
**not a security boundary** between root-running subagents. The skill requires
|
|
explicit `-s=` on every browser command, unique per-task output paths, modest
|
|
parallelism (normally two browsers with 4 GiB RAM), and scoped cleanup. It forbids
|
|
`close-all`, `kill-all`, global process killing and accidental use of the default
|
|
session. Profiles are ephemeral unless persistence is explicitly requested;
|
|
screenshots/traces/auth-state exports can contain secrets. New templates ignore
|
|
`.playwright-cli/`; add that ignore rule to existing projects too.
|
|
|
|
The application server must run **inside the VM** for guest `127.0.0.1` URLs.
|
|
Headed operation is opt-in (`open --headed`) and needs a separately configured
|
|
guest display; the default VM provides none and never mounts your host desktop.
|
|
Do not import your normal browser profile or install another browser/CLI with npm.
|
|
|
|
After updating the helper input, stop/restart the project VM to get the new tools
|
|
and initial skill. Existing skill edits deliberately survive restarts and updates;
|
|
merge later changes from the repository's skill file yourself rather than silently
|
|
replacing local instructions. This does not restart an already-running VM.
|
|
|
|
## Networking: bind address versus VM interface
|
|
|
|
DSH intentionally rejects `dsh web --host 0.0.0.0`. We don't patch around its
|
|
browser protections: it listens on **guest `127.0.0.1:3080`**, and the shell launcher
|
|
publishes an **SSH local forward** on your chosen **host** IPv4 address. By default
|
|
it tries **3080 through 3100, in ascending order**, and keeps the first port SSH
|
|
successfully binds. There is no separate free-port probe that could race another
|
|
process. The printed login URL and `nix run .#agent -- url` use the chosen port,
|
|
recorded in the project's private runtime state. Each new launch starts at 3080
|
|
again; shutdown clears the selection. If the range is full, launch fails clearly
|
|
and cleans up its VM/tunnel instead of leaving an inaccessible instance running.
|
|
DSH's random launch-token → signed-cookie authentication and Host/Origin checks
|
|
remain in use. Only SSH is forwarded by QEMU, always on host loopback.
|
|
|
|
```nix
|
|
agentVM.network = {
|
|
hostAddress = "127.0.0.1"; # default: this computer only
|
|
sshPort = 2222;
|
|
webPort = 3080; # First candidate.
|
|
webPortEnd = 3100; # Last candidate, inclusive.
|
|
};
|
|
```
|
|
|
|
Set both values to the same port for a fixed listener. For compatibility, setting
|
|
only a nondefault `webPort` still means that single fixed port; give `webPortEnd`
|
|
explicitly to select a different range. Keep the SSH port outside the Web range.
|
|
|
|
- **One LAN/VPN interface:** set `hostAddress = "192.168.1.20"` (an IP actually
|
|
assigned to this host). Its exact authorities across the configured port range
|
|
are automatically trusted.
|
|
- **All IPv4 interfaces:** set `hostAddress = "0.0.0.0"` and
|
|
`trustedHosts = [ "192.168.1.20" "laptop.example" ];`. Port-less entries accept
|
|
that exact host on any port; an explicit `host:port` accepts only that port.
|
|
Use a real address, not `0.0.0.0`, in your browser. The printed local URL can
|
|
have its host replaced with one of those authorities. This includes public
|
|
interfaces too; it is not shorthand for “LAN only.”
|
|
- Multiple project VMs still need distinct **SSH** ports. Web ports are selected
|
|
automatically from the range; SSH port selection is unchanged.
|
|
- Binding a host IP controls the **incoming listener**, not outgoing routing or
|
|
which NIC reaches DeepSeek. Outgoing traffic follows host routes/VPN policy.
|
|
- No host firewall is changed. For LAN access, explicitly allow the selected Web
|
|
port (or intended Web range) on the intended host interface. Do not open the SSH
|
|
forward. Use a fixed port if a reverse proxy needs a stable upstream.
|
|
|
|
**Use a VPN or a TLS reverse proxy for off-host access.** The forward is encrypted
|
|
between host and guest, but browser → host remains HTTP. A token/cookie on an
|
|
untrusted network can be stolen. Static assets and third-party plugin routes
|
|
may not share DSH's RPC authentication. Never expose this developer-preview
|
|
service directly to the public Internet.
|
|
|
|
### Optional dedicated TAP interface
|
|
|
|
Default user networking needs no administrator setup. If you need your own guest
|
|
IP/interface rather than NAT, add:
|
|
|
|
```nix
|
|
agentVM.network = {
|
|
mode = "tap";
|
|
tapName = "agent0";
|
|
mac = "02:00:00:00:00:10";
|
|
guestAddress = "192.168.77.2";
|
|
prefixLength = 24;
|
|
gateway = "192.168.77.1";
|
|
dns = [ "YOUR_REACHABLE_DNS_IP" ];
|
|
hostAddress = "127.0.0.1"; # Web publication is still independently configurable.
|
|
};
|
|
```
|
|
|
|
An administrator must first provision `agent0`, owned by the launching user,
|
|
with `multi_queue` when using multiple vCPUs, and arrange the host's address,
|
|
routing/NAT/DNS/firewall. For example, the interface creation portion is
|
|
`sudo ip tuntap add agent0 mode tap user "$USER" multi_queue`. That command alone
|
|
**does not** establish routing. See microvm.nix's linked routed-network docs for
|
|
persistent NixOS setup. Do not substitute a physical Wi-Fi/Ethernet interface for
|
|
`tapName`, or run QEMU as root. TAP joins a less isolated network; the host's
|
|
loopback publication default does not hide guest SSH from other routed peers.
|
|
|
|
## What “latest” means
|
|
|
|
- **Nix:** moving `nixos-unstable` (latest tested rolling channel), microvm.nix's
|
|
default branch, and `inputs.*.follows` to keep the project's package set coherent.
|
|
`nix flake update` refreshes the resolved snapshots; restart to use them.
|
|
`flake.lock` is retained because that is how flakes work, not a manually selected
|
|
old package version. The helper's own lock and each consuming project's lock
|
|
are independent.
|
|
- **DSH:** `npm exec --yes --package=@deepseek-ai/dsh@latest -- dsh ...` runs
|
|
**inside the VM**. Each launch resolves the current npm `latest` dist-tag,
|
|
using a persistent download cache. Network/registry availability is required;
|
|
failed installs are visible in `journalctl -u agent`. There is no fixed DSH
|
|
version, handwritten dependency hash, host `npm -g`, or claim of reproducibility
|
|
for this mutable part. `latest` can itself be a release candidate. A two-line
|
|
Node shim supplies `--expose-internals`, required by the current Cordis HMR
|
|
dependency but missing from the published CLI's shebang.
|
|
- **Plugin:** the guest installs `dsh-context@latest` into the `web` profile once.
|
|
Normal restarts keep the installed version; updates are explicit and targeted
|
|
to `dsh-context` below. This is mutable DSH/pnpm state, not a Nix-pinned package.
|
|
No other third-party plugin is added or automatically upgraded.
|
|
- **Workstation:** the root `/etc/nix` flake now also follows `nixos-unstable` and
|
|
Home Manager `master`; CLI tools still use Nixpkgs `master`. Its existing daily
|
|
updater refreshes those inputs and stages the tested result for next boot.
|
|
`stateVersion = "26.05"` remains a compatibility setting, **not a package pin**.
|
|
The existing separate Neovim-dotfile revision and manually packaged Element
|
|
binary are unchanged; their documented update boundaries still apply.
|
|
|
|
## Included context plugin
|
|
|
|
[**dsh-context**](https://github.com/bowenliang123/dsh-context) is the only
|
|
third-party plugin added by this setup. It provides the **Context** tab and
|
|
**`/context`** command for context composition, token use and history. The guest's
|
|
`agent.service` runs `dsh plugin --profile web add dsh-context@latest` before its
|
|
first Web startup; no manual install or host package install is needed. Update
|
|
the helper input and stop/restart existing VMs to pick up this setup change.
|
|
|
|
The installed plugin and bundle registration live in the writable
|
|
`$DSH_HOME/profiles/web`, alongside the profile's pnpm lockfile. Already-installed
|
|
versions are left alone. An incomplete install (missing package or bundle
|
|
registration) is retried with the profile's existing dependency spec, if any.
|
|
Removing the required plugin causes it to be added again at the next service
|
|
start. Existing user-installed plugins are **not removed**, and no settings,
|
|
credentials, profile patches or other profiles are replaced. Use a separate
|
|
`DSH_HOME` for an independent profile; don't run concurrent writers on one profile.
|
|
|
|
First installation needs registry access. Failure blocks Web startup rather than
|
|
silently omitting the plugin; inspect `journalctl -u agent -b` inside the VM.
|
|
The DSH wrapper puts pnpm's store on `/var/cache/dsh/pnpm`: its SQLite index needs
|
|
the guest's local cache disk, not the shared 9p filesystem. Installed packages and
|
|
profile files remain shared. No build-script approval policy is relaxed.
|
|
To explicitly update **only** this plugin, inside the guest:
|
|
|
|
```sh
|
|
nix run .#agent -- ssh
|
|
systemctl stop agent
|
|
dsh plugin --profile web update dsh-context@latest
|
|
systemctl start agent
|
|
exit
|
|
nix run .#agent -- url # restart creates a fresh browser launch token
|
|
```
|
|
|
|
DSH and this plugin remain developer-preview code, not an audited combination or
|
|
a compatibility guarantee for tomorrow's `latest`. The Firefox CLI + skill needs
|
|
no additional plugin or external MCP server.
|
|
|
|
## Sandbox boundary and limitations
|
|
|
|
- Trust the project's flake and launcher before `nix run`: an arbitrary flake app
|
|
is host code and can choose not to launch this sandbox at all.
|
|
- Separate KVM guest kernel; agent is root only there. Rootless QEMU is additionally
|
|
confined by Bubblewrap: restricted filesystem, private process/IPC/UTS/user
|
|
namespaces, dropped capabilities, no inherited host environment.
|
|
- No host home mount, Nix daemon/socket, SSH-agent forwarding, Docker socket,
|
|
desktop session sockets, or full host Nix-store share **inside the guest**.
|
|
QEMU itself needs read-only access to `/nix/store` to run its host binaries.
|
|
- Writable **cwd + DSH home + shared skills are intentional holes in the boundary**.
|
|
A bad agent/plugin can delete those files, steal tokens, corrupt Git metadata,
|
|
and plant malicious skills/plugins for future host runs. Back them up and use
|
|
scoped/revocable API keys. Never execute the shared plugin state on the host
|
|
without trusting changes made in the VM.
|
|
- Symlinks outside the shares do not grant those extra host directories. This
|
|
also means external Git-worktree metadata, symlinked skills, local path flake
|
|
inputs and host-built `node_modules` may not work. Use self-contained checkouts
|
|
and Linux-compatible dependencies, not wider home mounts to make errors vanish.
|
|
- Outgoing networking is **not filtered**: API access, exfiltration, host/LAN
|
|
services and cloud metadata can be reachable. This is filesystem/process
|
|
isolation, not an egress-security appliance. Use a separate filtered network
|
|
or machine for hostile code and don't give it real shared credentials.
|
|
- 9p favors a small rootless setup over maximum filesystem throughput. Remote
|
|
host edits may require a DSH refresh/restart for watchers to notice them.
|
|
- Latest code, kernels, QEMU and mounts can contain vulnerabilities. This is
|
|
defense in depth, not a claim of an audited or escape-proof sandbox.
|
|
|
|
## Checks and sources
|
|
|
|
```sh
|
|
nix flake check path:/etc/nix/agent-vm --no-build
|
|
nix build path:/etc/nix/agent-vm#checks.x86_64-linux.config \
|
|
path:/etc/nix/agent-vm#checks.x86_64-linux.shell \
|
|
path:/etc/nix/agent-vm#checks.x86_64-linux.context \
|
|
path:/etc/nix/agent-vm#checks.x86_64-linux.playwright --no-link
|
|
# Actual offline microVM boot/mount/SSH/browser test, with a fake harness (no API calls):
|
|
nix build path:/etc/nix/agent-vm#checks.x86_64-linux.boot --no-link
|
|
# Real port contention, range exhaustion and remembered-URL checks:
|
|
nix build path:/etc/nix/agent-vm#checks.x86_64-linux.ports --no-link
|
|
```
|
|
|
|
The boot and port tests require KVM and nested user namespaces in the Nix build sandbox.
|
|
The context check uses a strict offline DSH fixture to check context-only install,
|
|
restart idempotence, preservation of existing profile data/version selections,
|
|
incomplete-install repair, failure/retry and malformed-profile rejection. The boot
|
|
check also verifies setup before Web startup and writable host-owned plugin state
|
|
that survives a service restart without reinstalling.
|
|
The Playwright check launches two real Firefox instances concurrently against a
|
|
local HTTP fixture, checks headless/default browser selection, separate cookies,
|
|
DOM and localStorage, screenshots, close-one/keep-one behavior and profile expiry.
|
|
The boot check runs it inside the guest too, and checks writable skill seeding,
|
|
host ownership and preservation of a user edit across a harness restart. No live
|
|
websites, real credentials or model calls are used.
|
|
A real `dsh@latest` Web startup additionally requires network access and is not
|
|
silently replaced by the fake harness in normal usage.
|
|
|
|
Validated here: template/host/helper flake evaluation, ShellCheck, the real
|
|
microVM offline boot/mount/ownership/isolation test, and a separate live official
|
|
DSH Web launch with disposable config. The live check returned **401** without
|
|
a cookie, **200** after the token exchange, and **403** for an untrusted Host;
|
|
DSH stayed running without restarts. That core-only check used no real API
|
|
credentials, model requests, TAP network provisioning or host activation.
|
|
The Firefox two-session regression also passed natively and in the real headless
|
|
microVM, including screenshot generation and writable, non-clobbering skill seeding.
|
|
A separate live VM check with a disposable DSH home installed only `dsh-context`,
|
|
verified its composed bundle and authenticated HTTP **200**, then confirmed a
|
|
service restart left the manifest/lockfile unchanged without reinstalling. No
|
|
real credentials or model calls were used; the test VM and profile were removed.
|
|
|
|
Research used the **new official wiki**, plus upstream sources:
|
|
|
|
- [NixOS wiki: Flakes](https://wiki.nixos.org/wiki/Flakes) — lockfiles, pure inputs,
|
|
dev shells, apps, and Git-tracked source.
|
|
- [NixOS wiki: Virtualization](https://wiki.nixos.org/wiki/Virtualization) — points
|
|
to [microvm.nix](https://microvm-nix.github.io/microvm.nix/declaring.html).
|
|
- [microvm interfaces](https://microvm-nix.github.io/microvm.nix/interfaces.html),
|
|
[shares](https://microvm-nix.github.io/microvm.nix/shares.html), and
|
|
[routed networking](https://microvm-nix.github.io/microvm.nix/routed-network.html).
|
|
- [DSH CLI reference](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.md),
|
|
[Web guide](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/index.md),
|
|
[skill paths](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/skill/skill-filesystem/README.md),
|
|
[browser auth](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/connection/README.md),
|
|
[safety notice](https://github.com/deepseek-ai/deepseek-harness/blob/master/SAFETY.md).
|
|
- [Official Playwright CLI](https://github.com/microsoft/playwright-cli), its
|
|
[session-management reference](https://github.com/microsoft/playwright-cli/blob/main/skills/playwright-cli/references/session-management.md),
|
|
and the CLI/core implementation shipped in the locked Nixpkgs Playwright package.
|
|
- [dsh-context](https://github.com/bowenliang123/dsh-context) — upstream install,
|
|
targeted update and context UI documentation.
|