Imported from ChenyuHeee/dsh-remote (
AGENTS.md). Install upstream withnpx skills add ChenyuHeee/dsh-remote. Copyright stays with the author.
AGENTS.md — deployment playbook for AI agents
This repository is a deployment kit: it installs a DeepSeek Harness (DSH) web server on a user's Linux box and publishes it on the public internet behind HTTPS + password authentication. When a user asks you to "set up DSH remote" or "deploy this repo to my server", follow this playbook.
TL;DR procedure
- Gather the four required inputs from the user (ask, don't assume):
DSH_DOMAIN— a domain/subdomain whose A record points at the server (DNS must resolve before the nginx step; remind the user to add it early)SSH_HOST—user@hostreachable from your machine (root/sudo needed); leave empty only when you are executing on the server itselfAUTHELIA_USER— portal login name (defaultadmin)AUTHELIA_PASSWORD— optional; leave empty to auto-generate a strong one (recommended). Never print generated passwords into chat/logs unnecessarily.
cp deploy.conf.example deploy.confand fill it in.- Run
./deploy.sh. It renders templates, uploads the kit, and executes the server scripts in order:setup.sh → install-dsh.sh → deploy-authelia.sh → deploy-nginx.sh → check.sh. - Verify
ALL CHECKS PASSEDin the output, then give the user: the URL, the username, where the initial password is stored (server/opt/dsh/authelia/config/INITIAL_PASSWORD.txt, root-only), and the change-password command.
File map
| Path | Purpose |
|---|---|
deploy.conf.example |
The only file a deployer edits. Copy to deploy.conf (gitignored). |
deploy.sh |
Orchestrator: render → upload → run server scripts. |
deploy/nginx/dsh.site.conf.template |
nginx SNI vhost: ACME + Authelia portal + auth gate + DSH proxy. |
deploy/authelia/configuration.yml.template |
Authelia: Argon2id auth, session cookies, login throttling + auto-releasing IP bans. |
deploy/authelia/users_database.yml.template |
Portal user (hash filled server-side). |
deploy/authelia/docker-compose.yml |
Authelia container, published on 127.0.0.1:9091 only. |
deploy/systemd/dsh.service.template |
DSH service: loopback bind, --trusted-host, hardening, MemoryMax=768M. |
deploy/scripts/*.sh |
Server-side steps (see below). |
docs/ |
Architecture, security model, prerequisites, operations runbook. |
Server scripts (run as root, all idempotent)
setup.sh— swapfile, Node 22 LTS, pnpm (corepack), nginx+certbot if missing,dshsystem user,/opt/dshtree, kit install. Never touches existing nginx sites or services.install-dsh.sh [version]— pins@deepseek-ai/dsh@<version>under/opt/dsh, approves native builds, installs & restarts the systemd unit.deploy-authelia.sh— generates secrets once, hashes the password,docker compose up -d, health check.deploy-nginx.sh— HTTP-only vhost →certbot certonly --webroot→ full HTTPS vhost → reload; handles the nginx http2 syntax split automatically; installs the junk-traffic 444 blocklist and the fail2bannginx-dshjail (scanner IPs banned at the firewall for 1h, auto-release).change-authelia-password.sh [user]— interactive password change.set-deepseek-key.sh [sk-...]— writes/opt/dsh/dsh.env(root-only 600).backup.sh [--install-cron]— nightly tar, keeps 7.check.sh— the verification checklist (must exit 0).
Verification checklist (all must pass before you report success)
check.shprintsALL CHECKS PASSED.- Anonymous
https://$DSH_DOMAIN/→302to/authelia/?rd=.... - Portal HTML has
<base href="https://$DSH_DOMAIN/authelia/"and its JS asset loads anonymously with HTTP 200 — this is a regression we actually shipped once (blank login page);check.shcovers it. - Login flow (JSON, not form-encoded):
GET /authelia/api/state?targetURL=..., thenPOST /authelia/api/firstfactorwith{"username","password","targetURL","workflow":"fast"}→200 OK; thenGET /with the session cookie →200and__DSH_BOOT__present. - Direct
http://127.0.0.1:3080/api/...with a foreignHostheader →403.
Hard-won gotchas (do not re-learn these)
- DSH refuses
--host 0.0.0.0by design (RCE surface). Always bind127.0.0.1and put nginx in front; declare the public authority with--trusted-host $DSH_DOMAIN, and preserve the publicHostheader in the proxy (proxy_set_header Host $host) or every browser call gets 403. - nginx http2 syntax split: ≤1.24 uses
listen 443 ssl http2;; ≥1.25 rejects that and wantshttp2 on;.deploy-nginx.shdetects and rewrites. - Authelia 4.39 single-domain portal: set
server.address: 'tcp://0.0.0.0:9091/authelia'(the oldserver.pathkey is deprecated and errors). Without the path in the address the portal's<base href>points at the root, its assets get caught by the auth gate, and the login page renders blank. - Authelia API headers: auth endpoints need
X-Original-Method(not the oldX-Forwarded-Method);POST /api/firstfactortakes a JSON body; the API lives under upstream/api/*, so the nginx location/authelia/api/must strip the prefix (proxy_pass http://127.0.0.1:9091/api/;). - Argon2id params must match between hash and config (
iterations 3,memory 65536KiB,parallelism 4, salt 16, key 32). The CLI flag--randomrandomizes the password, not the salt — never pass it with--password, or the stored hash won't match the saved password. Authelia 4.39 also requiresstorage.encryption_key. - npm OOMs on ≤2 GiB boxes — always install DSH with pnpm (corepack).
pnpm 10+ blocks native build scripts by default: approve
node-pty,koffi,@deepseek-ai/dsh-subprocess-local,protobufjs,@google/genaiviapnpm-workspace.yaml(onlyBuiltDependencies) andpnpm rebuild, or the bash/PTY tools silently break. - Docker published ports bypass ufw — publish Authelia on
127.0.0.1:9091only, never0.0.0.0. Alsocap_drop: ALLbreaks the image entrypoint (it needs SETUID/SETGID for its su-exec user switch). - certbot: run
certonly --webroot(never--nginx, which rewrites user config); install the renewaldeployhook that reloads nginx; DNS must resolve before issuance — tell the user to add the A record early and re-rundeploy-nginx.shif it fails. - DSH web profile is loopback-only by default (
127.0.0.1:3080) and the/apitrust fence answers 403 to any non-loopback authority not listed intrustedHosts— this is the DNS-rebinding defense; do not "fix" it by binding wider. - Use
Restart=always, neverRestart=on-failurefor the dsh unit: the harness exits cleanly (code 0) on signals/graceful shutdowns, which on-failure does NOT restart — the site then sits in 502 until someone notices. Also,MemoryMax/TasksMaxbound the WHOLE cgroup (DSH plus the agent's test runners); keep TasksMax ≥4096 and raise MemoryMax for vitest-class workloads. - systemd
RestrictRealtime=andLockPersonality=silently implyNoNewPrivileges=yes(both are enforced via seccomp filters that require NNP). If the deployment grants the agent sudo ("full-root mode"), these two options make everysudofail with "The 'no new privileges' flag is set" even whenNoNewPrivileges=is explicitlynoand the sudoers entry is correct. Removing them is a prerequisite — as is setting the deployment sandbox default todanger-full-access(profile patch onsandbox-policy, pluspermission.defaultPreset: danger-full-access), because the bash/terminal tools otherwise run under the Landlock launcher, which sets no_new_privs on every spawned shell and blocks setuid.
Safety rules (hard)
- Never edit or remove anything under
/etc/nginx/sites-enabled/except adding thedshvhost. The server usually hosts other sites. - Never write secrets into the repo:
deploy.conf,build/,secrets/are gitignored. Passwords/API keys exist only server-side (root 600) or in the deployer's localdeploy.conf. - Never bind DSH or Authelia to a wildcard interface.
- After deploying, tell the user to change the initial password and (if a key was shared in chat) rotate provider API keys.
- If any step fails, fix the cause and re-run the failing script — every
script is idempotent; a fresh full
./deploy.shmust also be safe.
After deployment
- API keys:
bash /opt/dsh/scripts/set-deepseek-key.sh sk-...then pick DeepSeek + model in the GUI (Settings → Models). - GitHub access for the agent (gh/git):
bash /opt/dsh/scripts/set-github-token.sh ghp_...— gh auth is per-machine, so the server needs its own PAT; the agent runs as thedshuser with git identity pre-set in/opt/dsh/.gitconfig. - Upgrade:
bash /opt/dsh/scripts/install-dsh.sh <version>. - Backups:
bash /opt/dsh/scripts/backup.sh --install-cron. - Full runbook:
docs/OPERATIONS.md.