@f12o/dsh-dots
Persistent named agents ("dots") for DeepSeek Harness, with a
root-level Dots panel and a controller that can bundle multiple DSH hosts into one dot list.
A dot is a durable home session plus its own workspace:
$DSH_HOME/dots/
registry.json # local dot records
hosts.json # optional remote host list (controller only)
dot-researcher-a1b2c3/
AGENTS.md # managed operating contract
MEMORY.md # durable semantic memory
report.md # optional output
.dsh-dot/manifest.json
Each dot gets:
- a private workspace and sandbox root;
- a home session that survives restarts;
- an operating contract loaded through the normal
AGENTS.md mechanism;
- bounded
MEMORY.md injection on every assignment;
- durable schedules that wake a cold session;
- an execution-time tool allowlist;
- a host attribution so one controller can manage dots on several DSH machines;
- a root-level sidebar panel with creation, status, assignment, memory, schedule, and archive actions.
Repository: https://github.com/f4ah6o/dsh-dots
This package is experimental and targets DSH 0.2.1-rc.2.
日本語クイックスタート
- 各ホスト(マシン)の Web プロファイルへこのバンドルを入れます。
- リモートにしたいホストだけ
peer.enabled: true と bearer token を設定します。
- 制御側ホストの
$DSH_HOME/dots/hosts.json に、各ホストの URL と token を書きます。
- 制御側の DSH で「mac-mini に researcher という dot を作って」と依頼します。
- 左サイドバーの Dots から、全ホストの dot と到達性を確認できます。新規作成・依頼・記憶の追記・
定期実行・アーカイブはパネルから行えます。UI は DSH の言語設定に従い、日本語と英語を切り替えます。
モデル向けには
dot_list / dot_assign / dot_schedule も同じ操作を提供します。
「複数の dsh を束ねる」は、制御側 1 台 + 各ホストに同じプラグイン、という構成です。各ホストが落ちても
他のホストの dot は動き続けます。制御側が落ちても、各ホストのスケジュールは動き続けます。
Install
Build the bundle, then install it into a profile:
git clone https://github.com/f4ah6o/dsh-dots.git
cd dsh-dots
pnpm install
pnpm build
dsh plugin --profile web add "$PWD"
The row is inserted by cordis.patch.yml as id: dots. Rebuild and restart the host after code changes.
The host needs the Web composition (the Session controller and ctx.webServer for peer mode).
For another machine, build a tarball once and install that:
pnpm pack
# copy f12o-dsh-dots-0.1.1.tgz to the peer, then:
dsh plugin --profile web add ./f12o-dsh-dots-0.1.1.tgz
Peer host configuration
On every host that the controller should reach, enable peer mode in
$DSH_HOME/profiles/web/cordis.patch.yml:
- id: dots
config:
peer:
enabled: true
name: mac-mini
tokenFile: /Users/you/.dsh/dots/peer-token
Create the token file with owner-only permissions:
mkdir -p ~/.dsh/dots
openssl rand -hex 32 > ~/.dsh/dots/peer-token
chmod 600 ~/.dsh/dots/peer-token
The peer route is POST /dots-peer/rpc. It is authenticated only by the bearer token; it is not
the browser /api auth. Bind the host Web server to a trusted network (Tailscale/WireGuard/LAN) and,
for anything beyond a trusted tailnet, terminate TLS in front of it. Do not expose the route to the
public internet with plain HTTP.
Controller host configuration
On the controller, create $DSH_HOME/dots/hosts.json (mode 0600):
{
"version": 1,
"hosts": [
{
"id": "mac-mini",
"label": "Mac mini",
"url": "http://100.64.0.7:8080",
"token": "paste-the-peer-token-here"
}
]
}
id: [A-Za-z0-9_.-]{1,64}; local is reserved.
url: no trailing slash; http or https; no embedded credentials.
token: at least 16 characters; use the peer's token file contents.
path: optional, defaults to /dots-peer/rpc.
timeoutMs: optional per-host deadline.
The controller also owns local dots, so local is always present in dot_list. A malformed
hosts.json fails plugin activation instead of silently dropping hosts.
Dots panel
The bundle ships a browser panel as well as Host tools. Installing it adds a
root-level Dots entry to the sidebar beside Workspaces and Automation tasks,
and a main panel that:
- shows every local and remote dot with host reachability and live status;
- creates a dot on a local or remote host, including its model and reasoning
effort;
- opens a local dot's Session, or a remote host's own DSH UI;
- queues assignments, appends durable memory, creates an every-N-seconds
schedule, and archives a dot;
- follows the DSH locale: English and Japanese copy are bundled, with the
shipped language selector switching both live.
The panel reads its data from POST /dots/api, an owner-authenticated route
using the same browser login-token cookie and Host/Origin fence as the rest of
the Web UI. The machine-to-machine peer route remains /dots-peer/rpc with a
bearer token; the browser never receives that token.
The client half is bundled into lib/client.js during pnpm build and is
served by the normal client-module graph. The UI state is page-local and polls
every 8 seconds; it is not part of the Session log.
Use
The tools are model-facing. Ask the assistant to create/inspect/assign, or call the tools directly.
| Tool | Meaning |
|---|
dot_create | Create a dot on host (omit for local) with name, purpose, preset, permission preset, allowlist, and optional provider/model/reasoning effort. |
dot_list | Merge local and remote dots, including per-host reachability and live status. |
dot_assign | Queue a task. Returns when the assignment enters the inbox; it is not completion. |
dot_update | Replace purpose/contract, append memory, or replace the allowlist. |
dot_schedule | Create a durable schedule on the dot's host; one timing selector is required. |
dot_archive | Delete active schedules, cancel the live turn, and remove the dot from the active list. Per-schedule or cancellation failures are returned as warnings. |
References accept an id, a name, or host:name / host:id. A bare name is resolved local-first; if
multiple remote hosts have that name, the call fails and lists the candidates. Remote lists are
fetched on every dot_list and on bare-name resolution; offline hosts are reported, not cached.
Examples:
Create a dot named "release-notes" on mac-mini. Its job is to keep the release notes aligned with
the approved changelog. Use the standard preset and allow only read, grep, web_search, and web_fetch.
Assign release-notes: compare the draft against the approved changelog and write report.md.
Schedule release-notes every weekday at 09:00 Asia/Tokyo with this prompt: check the changelog and
prepare a short delta report.
Dot settings
The panel and dot_create expose these per-dot settings:
| Setting | Meaning |
|---|
| Name | Stable dot identity and Workspace/Session title. |
| Purpose | The managed AGENTS.md contract: outcome, sources of truth, quality bar, escalation. |
| Host | Local or a configured remote DSH host; the dot's workspace, session, and schedules live there. |
| Agent preset | DSH preset composition (tools, persona, delegation). Defaults to standard. |
| Permission preset | DSH sandbox + approval bundle, for example workspace-write. |
| Allowed tools | Execution allowlist of exact tool names or trailing-* prefixes. Empty denies all. |
| Model / reasoning effort | Session-level provider/model/effort. Omit for the deployment default. |
| Assignment | One queued task; it enters the dot's inbox and is processed serially. |
| Memory note | A dated entry appended to MEMORY.md, injected into later assignments. |
| Schedule | Title, task prompt, and an every-N-seconds interval in the panel; dot_schedule adds cron/daily/weekly/one-shot. |
Host and deployment settings live outside the dot:
$DSH_HOME/dots/hosts.json: remote host ids, labels, URLs, tokens, paths, timeouts.
- Profile
peer config: peer enabled flag, bearer token/token file, route path, body limit.
- Plugin config:
root, defaultPreset, defaultPermissionPreset, maxDots, maxTaskBytes,
maxPurposeBytes, maxMemoryBytes, enforceToolAllowlist, hostsFile, requestTimeoutMs, peer.*.
Model selection
A model belongs to the home Session, not to an individual assignment. dot_create accepts
provider, model, and optional reasoning_effort; the Host validates them against its live model
catalog and applies sessionController.selectModel() after creating the Session. The choice persists
on that Session, so every later dot_assign and scheduled wake uses the same route. It is not
re-selected per assignment.
If no model is supplied, the Session uses the deployment default. Existing dots created before this
field existed have no stored model and keep using the deployment default. You can still change the
model from the dot Session's own composer model picker; the Dots panel's Model row records the
selection made at creation.
The UI catalog is host-specific: a local dot uses this host's providers, while a remote dot uses the
peer's catalog. If a catalog is empty or unreachable, the form offers only “Deployment default”.
How it works
Creation
- Validate name/purpose/allowlist and the local cap.
- Create
$DSH_HOME/dots/<id>/, AGENTS.md, MEMORY.md, and .dsh-dot/manifest.json.
- Register a Workspace for the directory (when available).
- Create a home Session with the configured preset and attach it to the Workspace.
- Rename the Session, apply the permission preset, select the requested model, and persist the dot record.
If Workspace registration fails, the dot still runs from its directory. If Session creation fails
after a Workspace was created, the partial directory is kept and the error names it; retry with a
new dot or clean up manually.
Assignment
dot_assign resolves the dot, resumes a cold session through the Session controller, reads the tail
of MEMORY.md (up to maxMemoryBytes), and queues a [DOT ASSIGNMENT] message through
agent.followup(). The source kind is dot. A running dot receives the assignment on a later turn;
one dot processes assignments serially. The controller records lastAssignedAt and
assignmentCount.
Scheduling
dot_schedule calls the Host Schedule service on the dot's host. The stored prompt is framed with
[DOT SCHEDULED TASK] and instructs the dot to read AGENTS.md and MEMORY.md first. The Host
Schedule service restores a cold Session when the target is due, so a schedule survives host
restarts. Recurring schedules keep only their latest missed occurrence, which is the underlying
Schedule contract.
Memory
AGENTS.md is managed by this package and is regenerated when dot_update changes the purpose or
allowlist. Put durable learned facts in MEMORY.md; the dot is instructed to append dated entries.
Giving feedback through the controller:
Tell release-notes to remember that the user prefers a one-paragraph summary with links.
The controller calls dot_update(add_memory: ...); the next assignment injects that memory even
after compaction or a cold resume.
Permissions and guardrails
- A dot's home session can carry any configured permission preset (sandbox mode + approval policy).
allowedTools is enforced by a tools/pre-execute listener: exact names or trailing * prefixes.
An empty array denies every tool. Tool schemas remain visible; only execution is denied.
- A dot can never call
dot_* tools, so dots cannot create or manage other dots.
bash in the allowlist still permits arbitrary commands inside the sandbox; the allowlist is not
a shell-level policy.
- When no approval answerer is available, the existing approval seam fails closed. Unattended dots
should use a permission preset appropriate for autonomous work, or keep work read/draft-only.
Multiple hosts
Each host has its own registry, dot directories, sessions, schedules, and credentials. The controller
does not copy remote state; it calls the peer RPC. A remote dot continues to run when the controller
is offline, as long as its own host is running. The controller sees it again on the next dot_list.
If a host is offline, listing reports it and operations that need that host fail with
dots/peer-offline; nothing is queued for later delivery.
Configuration
All fields are optional except where noted.
| Field | Default | Meaning |
|---|
root | $DSH_HOME/dots | Local dot root (registry, workspaces, host list). |
defaultPreset | standard | Agent preset for a new dot when dot_create omits one. |
defaultPermissionPreset | unset | Permission preset applied to new dots. |
maxDots | 32 | Maximum active local dots. Each host enforces its own cap. |
maxTaskBytes | 20000 | Maximum UTF-8 bytes in one assignment or memory note. |
maxPurposeBytes | 4000 | Maximum UTF-8 bytes in a purpose. |
maxMemoryBytes | 8192 | MEMORY.md UTF-8 bytes injected per assignment. |
enforceToolAllowlist | true | Enforce allowedTools at execution time. |
hostsFile | $DSH_HOME/dots/hosts.json | Controller remote host list. |
requestTimeoutMs | 15000 | Default per-remote-call deadline. |
peer.enabled | false | Register POST /dots-peer/rpc. |
peer.token | unset | Inline bearer token (prefer tokenFile). |
peer.tokenFile | unset | File containing the bearer token. |
peer.path | /dots-peer/rpc | Exact peer route path. |
peer.name | unset | Diagnostic name returned by ping. |
peer.maxBodyBytes | 1048576 | Maximum peer request body. |
Security
- The peer route runs in the DSH host process with the host user's permissions. Treat the token as a
host administrator credential.
- Use a unique token per host, store it in a
0600 file, and rotate it by restarting the peer.
- Prefer a tailnet/WireGuard network or TLS. The route has no rate limiting, CSRF protection, or
browser auth; it is a machine-to-machine API.
- Remote dots execute with the remote host's sandbox and permission presets; their
AGENTS.md and
MEMORY.md live on that host.
- Do not put secrets in
MEMORY.md, AGENTS.md, or assignments. Memory is model-visible.
Limitations
- One home session per dot; assignments are serial. Create more dots or use subagents inside a dot for
parallelism.
- Archiving hides the dot from the Dots panel but retains its Workspace and Session in the Workspaces
sidebar. Remove those separately if you want the sidebar clean.
- The panel's schedule form creates fixed every-N-seconds schedules. Use
dot_schedule or the dot
itself for cron, daily, weekly, or one-shot schedules.
- The panel polls every 8 seconds and has no live push feed; status changes can lag briefly.
- No automatic cloud VM provisioning. This is a local-foreground/host-machine model. A dot's workspace
is on its host.
- No Slack/Teams/SMS/email channels.
- A model changed later in the dot Session's composer is not written back to the dot record; the
panel's Model row shows the creation-time selection.
- No automatic token/cost quota. Schedules and autonomous work consume model tokens.
- The controller loads
hosts.json at activation; edit and restart to add a host.
- Remote dot names are resolved by listing every host; a large fleet makes bare-name resolution
proportional to fleet size. Use
host:name for deterministic routing.
- Archiving is not transactional across hosts: a schedule that cannot be deleted, or a turn that
cannot be cancelled, is reported in
warnings while the dot record is still archived. Inspect the
remote host if a warning appears.
Development
pnpm install
pnpm build
pnpm typecheck
pnpm test
npm pack --dry-run
The tests use a structural FakeHost; no live DSH process or model key is required. Integration
checks against live hosts should use a separate DSH_HOME and a non-default port.
License
MIT