feat(dsh): ensure the managed web service on every run - #562
Conversation
Bare `dsh` refuses to start (--profile required), so v0.19.0's bare `deva.sh dsh` was dead on arrival. Every run now ensures `dsh web` in the container (the official recommendation), daemonized and idempotent so exec into a running container never double-binds. dsh binds loopback only and hard-rejects 0.0.0.0, which docker -p cannot reach: bridge runs get a socat sidecar bridging a publishable port to the loopback bind, published to the host loopback on the first free port from 3080 (DEVA_DSH_WEB_PORT overrides). Host-net runs skip the sidecar but probe a per-container free port so concurrent containers never adopt each other's server; the ensure check verifies the server is OUR pid namespace's process, not just an open port. Every run also seeds the workspace dir into dsh's registry (storages/workspace.json, domain v2, byte-compatible) so sessions from any profile land pre-grouped in the web UI; the seed is idempotent and backs off from state it does not own. Bare `deva.sh dsh` follows the service log; args after -- run in the foreground with the service ensured behind them. DEVA_DSH_WEB=0 and DEVA_DSH_WORKSPACE_AUTO=0 opt out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Publish + sidecar env on bridge, per-container port under host-net (no publish), wrapper passthrough of user args, and web ensured in every mode. Existing asserts updated for the dsh-web wrapper argv0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document the managed dsh web boot (ports, host-net, shared-storage caveat, workspace seed) and add a three-layer Testing Auth guide: hermetic wiring tests, dry-run against real auth, live smoke with the per-agent first-run table. Default homes list catches up with pi, dsh, and cursor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR makes deva.sh dsh usable out-of-the-box by ensuring dsh web is running in the container on every invocation, publishing it safely to the host loopback (with a socat sidecar when bridged), and auto-seeding the current workspace into dsh’s registry so sessions are grouped in the web UI.
Changes:
- Add a managed “ensure web + workspace seed + passthrough/log-follow” boot wrapper for dsh runs, including host-net vs bridge behavior and loopback publishing.
- Expand the dsh auth wiring test script with assertions covering web ensure/publish/sidecar and passthrough behavior.
- Update user docs to reflect the new default
deva.sh dshbehavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/test-dsh-auth.sh | Adds dry-run assertions for web ensure, publish/sidecar wiring, host-net behavior, and passthrough invocation shape. |
| README.md | Documents that deva.sh dsh boots the dsh web UI by default. |
| docs/quick-start.md | Updates quick start command list to mention dsh web UI + workspace auto-add. |
| docs/authentication.md | Adds detailed documentation of dsh web ensuring, sidecar publish behavior, host-net behavior, and workspace auto-add semantics. |
| agents/dsh.sh | Implements setup_dsh_web boot wrapper (ensure web + optional socat + workspace registry seed) and adjusts command invocation to go through the wrapper. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| dsh_web_port="${DEVA_DSH_WEB_PORT_CONTAINER:-3080}" | ||
| web_log="${DSH_HOME:-$HOME/.dsh}/web.log" | ||
| mkdir -p "$(dirname "$web_log")" | ||
| if _dsh_web_ours "$dsh_web_port"; then | ||
| : # already serving | ||
| elif _dsh_port_open "$dsh_web_port"; then | ||
| echo "deva: 127.0.0.1:${dsh_web_port} is bound outside this container (another host-net dsh container?) -- not starting dsh web; recreate this container to allocate a fresh port" >&2 | ||
| else |
| deva.sh opencode | ||
| deva.sh pi | ||
| deva.sh dsh | ||
| deva.sh dsh # boots the dsh web UI on http://127.0.0.1:3080 |
| deva.sh opencode | ||
| deva.sh pi | ||
| deva.sh dsh | ||
| deva.sh dsh # web UI on http://127.0.0.1:3080; repo auto-added as workspace |
| if [ -n "${DEVA_DSH_WEB_URL:-}" ]; then | ||
| echo "deva: dsh web UI: ${DEVA_DSH_WEB_URL}" >&2 | ||
| else | ||
| echo "deva: dsh web running on container loopback only (container predates the web publish; recreate it for host access)" >&2 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b5a53d7c32
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| fi | ||
| fi | ||
| if [ -n "${DEVA_DSH_PROXY_PORT:-}" ] && ! _dsh_port_open "$DEVA_DSH_PROXY_PORT"; then | ||
| setsid nohup socat "TCP-LISTEN:${DEVA_DSH_PROXY_PORT},fork,reuseaddr" "TCP:127.0.0.1:${dsh_web_port}" </dev/null >>"$web_log" 2>&1 & |
There was a problem hiding this comment.
Preserve isolation when publishing the dsh relay
On the default bridge network, TCP-LISTEN without a bind option listens on every container interface, not only on traffic forwarded by Docker's host-loopback publish. Any peer container that discovers this container's bridge IP can therefore connect directly to port 3081 and send a loopback Host header accepted by dsh's trust fence, exposing the dashboard and the target container's mounted workspace across the repository's container isolation boundary. Add authentication or network filtering that prevents peer containers from reaching this relay.
AGENTS.md reference: AGENTS.md:L3-L5
Useful? React with 👍 / 👎.
| # Bare `deva.sh dsh` follows the service log; args after -- run | ||
| # that dsh invocation in the foreground with the service ensured | ||
| # behind it. DEVA_DSH_WEB=0 skips the service entirely. | ||
| setup_dsh_web "${remaining_args[@]+"${remaining_args[@]}"}" |
There was a problem hiding this comment.
Forward the documented dsh opt-out variables
When a user runs DEVA_DSH_WEB=0 deva.sh dsh or sets DEVA_DSH_WORKSPACE_AUTO=0 in the host environment, neither value is added to DOCKER_ARGS or the persistent container's docker exec environment; only the wrapper inside the container reads them, so both resolve to their default of 1. Consequently the documented opt-outs still launch the service and mutate the workspace registry unless users happen to persist the variables through another configuration mechanism.
Useful? React with 👍 / 👎.
Why
Bare
dshrefuses to start (--profile <name> is required), so v0.19.0's baredeva.sh dshwas dead on arrival. The official recommendation is running the web service; deva now ensures it on every run.What
deva.sh dshrun ensuresdsh webin its container: daemonized ($DSH_HOME/web.log), idempotent, survives the launch session. Bare runs follow the service log; args after--run in the foreground with the service ensured behind them.DEVA_DSH_WEB=0opts out.0.0.0.0(docker-pcan't reach a container-loopback bind): bridge runs get a socat sidecar publishing to the host loopback on the first free port from 3080 (DEVA_DSH_WEB_PORToverrides the probe start). The mapping travels as container env so a reused container announces the port it actually published.storages/workspace.json, domain v2, byte-compatible) so sessions land pre-grouped in the web UI. Idempotent, backs off from state it doesn't own (foreign schema, pending mutation, uninitialized registry with session history).DEVA_DSH_WORKSPACE_AUTO=0opts out.Verify
bash scripts/test-dsh-auth.sh— 10 new asserts pin publish + sidecar env, host-net no-publish + per-container port, passthrough wiring, web ensured in every mode; all green.🤖 Generated with Claude Code