Skip to content

LAN Manager

André Borchert edited this page Sep 29, 2026 · 9 revisions
TinyTitan

LAN Manager (dsh-lan-manager)

plugins/dsh-lan-manager is a DeepSeek Harness bundle that puts a small HTTP API in front of a running dsh instance — and finds the other instances running it. One machine on your network can then list what is open anywhere in the group, send prompts, and tidy up workspaces and sessions, without a window and a mouse.

It exists because a harness instance is otherwise only reachable through its browser UI on 127.0.0.1. When you run several — one per Mac, or a rack of Linux boxes in datacenters — there is no supported way to ask "what is running over there, and can I drive it?".

Caution

This plugin deliberately accepts requests from other machines. Read Security before enabling it, and never port-forward it to the public internet.

What it does

Capability Endpoint
List active workspaces — the ones the web page shows GET /dsh-lan/workspaces
List every visible session GET /dsh-lan/sessions
List one workspace's sessions GET /dsh-lan/workspaces/:id/sessions
Prompt one session POST /dsh-lan/prompt
Prompt every active session POST /dsh-lan/prompt-all
Read a session's messages back — the answers, not just the questions GET /dsh-lan/sessions/:id/messages
Archive a session POST /dsh-lan/sessions/:id/archive
Register an existing folder as a workspace POST /dsh-lan/workspaces
Delete a workspace POST /dsh-lan/workspaces/:id/delete
Who else is in the group, and what each one holds GET /dsh-lan/peers · GET /dsh-lan/peers/:id
The whole group in one answer: this Mac and every member GET /dsh-lan/inventory
Share address lists between members (mesh) carried in the peers array of GET /dsh-lan/inventory — there is no push endpoint, by design
Liveness, and where the request came from GET /dsh-lan/health

Active means what the page shows. A session is visible when it is not in the harness's archive set; a workspace is active when at least one visible session lives in it. Archiving hides a row and keeps its history — it never deletes.

Install

dsh plugin --profile web add ./plugins/dsh-lan-manager

The bundle patch mounts the row, so the next dsh web picks it up. No --patch flag is needed.

Reaching it from another machine

It cannot, on the pinned harness, and the plugin has no way around it. The API is registered on the harness's own web server, so it is reachable exactly where that server is — and @deepseek-ai/dsh-host-webserver accepts only two bind addresses:

$.host expected "127.0.0.1" | "0.0.0.0" but got "192.168.1.50" (at host)

127.0.0.1 is the default, and answers this machine alone. 0.0.0.0 — the value that would put the API on the network — is refused by the harness before it binds:

error: --host 0.0.0.0 is intentionally not supported yet for safety:
it would expose remote code execution to the network; use 127.0.0.1 instead

Naming a specific interface instead does not help: the webserver plugin's schema rejects it, and the profile fails to load. So today only the machine running the harness can call this API, even though every request is fenced and token-gated and the fleet-wide prompt is already built for the day that changes. The unblock is upstream's — a third ask alongside the two in docs/dsh-upstream-asks.md.

For now, a caller on the same machine:

curl -H "x-dsh-token: $DSH_LAN_KEY" http://127.0.0.1:3080/dsh-lan/health

The group

Every instance carries one group key — a string, default tinytitan-lan, and the first thing to change on a network you do not solely own. Instances with the same key are one fleet, and the key is also the door key each request presents.

Discovery runs by itself, every 60 seconds by default:

Source Finds Notes
Tailscale online tailnet peers with an IPv4 address Any continent. Mobile platforms (iOS, Android) and IPv6-only peers are skipped, as are peers that are offline
Bonjour (_dsh-lan._tcp) a third-party advertiser, and nothing else today Browse-only, and idle here. Nothing registers the service, and registering it would advertise this host's LAN address where nothing is listening, because the API answers on loopback alone. It does not cross a tailnet either — multicast stays on the link
peers: whatever you seed The escape hatch for a host on a non-default port or behind an ACL
Local /24 sweep hosts that advertise nothing Off by default: the only source that touches machines which never opted in

Each member keeps the others' active workspaces and sessions, and members trade address lists with each other, so one found machine is enough for the rest to learn the whole group. Two rules keep that safe:

  • Nothing is dialled before it is validated. Every candidate — including one another member told us about — must resolve to an address inside the same LAN/Tailscale allowlist the request fence uses, and must answer with our group key, or it is dropped.
  • A member's list is a hint, never authority. Gossiped addresses are candidates for the next cycle, validated then like any other. A poisoned list cannot become a connection.

Two things that will make a machine invisible, and the fix for each: a tailnet ACL that blocks the peer port, and a harness listening on a port other than 3080 — the Tailscale listing carries no port, so that host needs a peers: entry.

What discovery costs the harness

Discovery never blocks the window: every expensive part is asynchronous I/O, so nothing occupies the event loop. A name that has gone away is the one real cost — a stale Bonjour entry takes the full mDNS timeout — so answers and failures are cached for resolveTtlMs (default five minutes), resolveConcurrency (default 4) bounds how many slow lookups are in flight, and the discovery timer is jittered by ±10 % so a fleet installed together does not run every cycle in lockstep for ever.

Why a fleet control plane is worth having

A single prompt box is a convenience. Every prompt box at once, from one command, is an operational tool — and the thing it buys is time. These are the jobs it is for, cheapest first.

1. Stop the line

prompt-all reaches every active session in the group within seconds, so a runaway fleet costs seconds instead of an afternoon:

ttlanmanager prompt-all --text "STOP. Do not start new work. Finish nothing. \
Write a one-line status to STATUS.md in your workspace and wait for instructions."

The same call is the response to a billing spike, a shared API quota about to be exhausted, or a site losing power.

2. Fleet audit

Ask every agent what it is doing, before you touch anything, and then read the answers back:

curl -H "x-dsh-token: $DSH_LAN_KEY" -H 'content-type: application/json' \
  -d '{"prompt":"Report your current task, the files you have open, and anything uncommitted, in three lines."}' \
  http://127.0.0.1:3080/dsh-lan/prompt-all

curl -H "x-dsh-token: $DSH_LAN_KEY" \
  "http://127.0.0.1:3080/dsh-lan/sessions/<session-id>/messages?limit=4"

The answers arrive per session, so one machine that cannot be reached does not stop the audit. Reading goes through the live agent's session — one the UI has open or that is running — because that is where the harness derives history from; a session with no live agent answers 404 and says so, rather than reporting an empty conversation.

3. Incident broadcast

A vulnerability, a poisoned dependency, a bad instruction in a shared prompt — one call tells every instance at once, and session archive freezes the ones that touched it:

ttlanmanager prompt-all --text "SECURITY: stop using package X, remove it from \
your working set, and report anything that imported it."
ttlanmanager session archive --session s-abc123

4. Credential rotation

When a key leaks, the leak is not the only problem — it is every agent that still holds it in context. Tell the whole group to drop it, in one move:

ttlanmanager prompt-all --text "Rotate: the token in your context is revoked. \
Stop using it, and list every file where you wrote it."

5. Release freeze

Before cutting a tag, make sure no agent is mid-write:

ttlanmanager prompt-all --text "Release freeze: finish the current step only, \
checkpoint, and do not start anything new."
ttlanmanager list          # confirm every workspace is quiet

6. Onboard a machine

A new Mac joins and you point its work at a folder from wherever you are:

ttlanmanager workspace create --on new-mac --path /Users/me/Project --title Project

7. Decommission a machine

Before wiping a box, take its work with you — from another continent, over the tailnet:

ttlanmanager list                       # note its workspaces and sessions
ttlanmanager session archive --session <id>
ttlanmanager workspace delete --workspace <id>

8. Presence monitoring you did not have to build

/peers is a heartbeat for free: a machine that stops answering drops out of the group, and that is itself the alert — the Frankfurt box stopped reporting, the laptop's tailnet dropped, one datacenter lost its link.

ttlanmanager list --json | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["peers"]), "members")'

9. Work split and handoff

Target sessions individually to divide a job by machine, or hand a blocked task to a different host:

ttlanmanager prompt --session s-client --text "Take the client side only."
ttlanmanager prompt --session s-server --text "Take the server side only."

10. Campaign fan-out

The same prompt on every machine is a benchmark across hardware — the fleet is your test matrix, and prompt-all is the harness driver.

11. Policy and skill rollout

A new instruction file, a new skill, a changed house rule: broadcast it instead of editing N machines by hand.

12. Runaway containment

/inventory reports each session's turn count, so an agent that has been grinding for hours is visible before the invoice arrives; archive it and tell the rest to bound their work.

Not yet

  • A session with no live agent has no readable history. GET /dsh-lan/sessions/:id/messages projects the live agent's own derivation, so a session that has gone idle cannot be read; it answers 404 and says so rather than reporting an empty conversation. A cold read needs the session store.
  • Starting a session needs a profile that composes the harness session controller. Where it does — the web profile does — POST /dsh-lan/sessions works, and POST /dsh-lan/workspaces takes "startSession": true. Where it does not (a headless or SDK-only runtime) the endpoint answers 501 and says why rather than pretending.

The manager

sources/TinyTitanFleet builds ttlanmanager, the TinyTitan DSH LAN Manager. It reads the group from any one member and then talks to the member that owns the thing being acted on — a plugin never relays a prompt for another instance.

swift build -c release --product ttlanmanager

ttlanmanager top                    # the live dashboard
ttlanmanager list
ttlanmanager prompt --session ID --text "..."
ttlanmanager prompt-all --text "..." [--limit N]
ttlanmanager workspace create --on NAME --path DIR [--title TITLE]
ttlanmanager session archive --session ID
ttlanmanager workspace delete --workspace ID [--keep-sessions]

--peer is the member the group is read from (default 127.0.0.1:3080); --on names the member an action goes to; --json prints the raw answer. The key resolves from --key, then DSH_LAN_KEY, then DSH_LAN_TOKEN, then the default.

The dashboard

ttlanmanager top draws the group live and refreshes it on a timer. Each member is one row, and opening it shows its workspaces and the sessions inside them:

 ttlanmanager  ·  tinytitan-lan  ·  5 members  ·  3 ws  ·  4 sess  ·  refreshed 0s
  NAME                  TYPE        ADDRESS              DSH              WS   SESS  SEEN   IPV6
  ▾ mac-a               this Mac    127.0.0.1:3080       0.1.6-alpha.2     1     1  —      fd7a:115c:a1e0::11
      /Users/you/Proje…  workspace   w-ab                                     1
        Release v5.7      session     s-ab
  ▾ node-b              Tailscale   100.64.0.11:3080     unknown           1     2  4s     fd7a:115c:a1e0::12
      /Users/you/Downl…  workspace   w-n3                                     2
        story benchmark …  session   s-n3a
  ▾ node-c              Tailscale   100.64.0.12:3080     unknown           0     0  1m

Only this machine's harness version is known. A peer reports the plugin's version, not its harness's, so a remote row prints unknown; a manager that wants the harness version of every member needs the peers to publish it. What the column does establish is that every member is running the plugin at all — and therefore the one harness release it supports, since it refuses to start on any other.

Key Does
↑ ↓ / k j move through members, workspaces and sessions
→ / Enter open the selected member; ← closes it
p prompt the selected session — or, on a workspace row, every session in it
P prompt every session on the selected member
A prompt every session in the whole group
c register a folder as a workspace on the selected member
a / d archive the selected session / delete the selected workspace (both confirm first)
r / ? / q refresh now / help / quit

It scans in the background. A scanner on its own task polls the fleet every 30 seconds — half the plugin's discovery period, so the manager reads no faster than members are found while its polling adds at most half a cycle of latency. Each scan asks the seed, then asks every member directly rather than trusting one machine's cached view of another: a member's own answer is the truth, it names members the seed has not learned yet, and it is what makes a new machine appear within one interval. Members are matched by machine name, not by id, because the same Mac is mac-a:3080 to itself and 100.64.0.11:3080 to a peer — keying on the id would list it twice.

Nothing in the window waits on the network: an unreachable member costs a timeout in the background, and prompts and mutations run on their own task so the window keeps drawing while a fleet-wide prompt is delivered.

It resizes. Columns give ground in a fixed order as the window narrows — the IPv6 address first, then the age, then the harness version, then the type — and the two that carry the answer, the member and where it answers, are the last to go. A window too small to draw a table says so instead of drawing rubbish, and the selection keeps its place across a refresh.

Run it outside a terminal (--once, with --width/--height) and it prints a single frame instead — which is also how you render an inventory captured earlier, with --from FILE or --from - for stdin, and no fleet running at all.

Managing a fleet without the plugin mesh

If the network has neither Tailscale nor Bonjour, drive the addresses you know:

for host in 192.168.1.10 192.168.1.11; do
  printf '%s: ' "$host"
  curl -fsS --max-time 5 -H "x-dsh-token: $DSH_LAN_KEY" "http://$host:3080/dsh-lan/workspaces" \
    | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["workspaces"]), "workspaces", sum(w["sessionCount"] for w in d["workspaces"]), "sessions")' \
    || echo unreachable
done

Security

Three checks run in this order, and the first one happens before the request body is read:

  1. Source address. Allowed: loopback (127.0.0.0/8, ::1/128), the private ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and fc00::/7, link-local (169.254.0.0/16, fe80::/10), and 100.64.0.0/10 — the Tailscale range, so peers on a tailnet work without configuration, wherever they are in the world. Everything else is refused, including any address that cannot be parsed. The peer address comes from the socket; X-Forwarded-For is deliberately not trusted, because a forwarded header is attacker-controlled and trusting it would let any caller claim to be loopback.

  2. The group key, compared in constant time. It ships with a public default (tinytitan-lan) so a fresh install joins the group with no setup — which means by default it groups rather than protects. Change it on every machine when the network is not entirely yours:

    export DSH_LAN_KEY="$(openssl rand -hex 24)"

    Clients send it in x-dsh-token; DSH_LAN_TOKEN is accepted as the same variable under its older name.

  3. Origin, on requests that change something. A page from an unrelated site, loaded in an allowlisted browser, must not be usable to drive the API.

The harness web server itself carries no TLS and no authentication of its own. This plugin fences its own routes; it does not secure the UI beside them.

Configuration

Key Environment Default
basePath DSH_LAN_BASE_PATH /dsh-lan
groupKey / token DSH_LAN_KEY, DSH_LAN_TOKEN tinytitan-lan — change it
peers DSH_LAN_PEERS [] — seed addresses, host or host:port
discoveryIntervalSeconds DSH_LAN_DISCOVERY_SECONDS 60
discoverTailscale — true
discoverBonjour — true
discoverSubnet — false
peerPort — 3080
probeTimeoutMs DSH_LAN_PROBE_TIMEOUT 3000
allowAddresses DSH_LAN_ALLOW [] — extra hosts or CIDRs to admit
includeEmptyWorkspaces — false
maxBodyBytes — 262144

These are the operator-facing keys. A few more exist and are not repeated here, because the security section above is their description: ipv4Networks, ipv6Networks, originNetworks and enforceOrigin (the three checks), trustedOrigins and allowPrivateOrigins (Origin exceptions for a known client), logToHost, discoveryConcurrency, resolveConcurrency, resolveTtlMs, and the DSH_LAN_RESOLVE_CONCURRENCY environment fallback. Their defaults live in plugins/dsh-lan-manager/src/config.js — change them there rather than guessing from this table.

Notes on what it reads

Active workspaces are derived from the session projection, not from the harness workspace registry. The registry holds only the directories a user explicitly added, while the page groups sessions by the directory each one ran in; reading the registry would answer a different question than the one being asked. A workspace that exists only because sessions ran there is reported with "registered": false and a null id, and can still be addressed by path.

Prompting requires a live agent for the target session — one the UI has open or is running. A session with no live agent returns 404 rather than silently queuing work, and prompt-all reports that per session instead of failing the whole request.

Clone this wiki locally