semantic-craft/dsh-grokbot-dispatch

dsh-grokbot-dispatch

DSH Grok Bot dispatch plugin (public). Private deploy config is not in this repo.

未分類deepseek-harness

dsh-grokbot-dispatch

A DeepSeek Harness (DSH >=0.2.0-rc.2) official Cordis plugin for two-way task dispatch between DSH and other agents (Grok Bot, ZCode via zcode-dispatch, or anything that speaks the small HTTP API below): inbound jobs become visible DSH desktop sessions, outbound /dispatch-* commands send tasks out, and finished jobs report back.

Prerequisites

  • DSH >=0.2.0-rc.2 (desktop app for visible sessions; dsh on PATH or dshBin for the headless runner), Node >=20.
  • Project folders under projectsRoot (default ~/Projects/<project>); names matching denyProjects (default finance) are refused.
  • A network path from the callers to host:port (LAN, VPN, Tailscale, SSH tunnel…). The listener is plain HTTP: protected by a source-IP allowlist plus a shared Bearer key, no TLS. Keep it on a trusted network or put a TLS proxy/tunnel in front.
  • Not required: Yobiru / tailmsg, Tailscale, macOS / launchd. Those are optional (see the table).

Feature × dependency

FeatureDefault (transport: "direct")Optional transport: "yobiru"
Inbound: caller → POST /v1/jobs → DSH sessionkey file + host + allowedIpssame
Result pull: GET /v1/jobs/<id>✅✅
Result push on completioncallback_peer / callback_url → HTTP POST to that peer (/v1/callbacks)tailmsg request respond / send --reply-to
Outbound /dispatch-grok, /dispatch-zcode, POST /v1/outboundpeers.<name> {url, keyFile} → peer's POST /v1/jobsgrokbotPeer / zcodePeer Yobiru addresses + tailmsg CLI
Host / allowlist auto-discoveryoptional: tailscale status --json if Tailscale is installedsame
Auto-start fallback hostoptional (e.g. macOS launchd; private deploy config is not in this repo — see your own deploy docs/mesh)same

Minimal working config (direct)

umask 077; mkdir -p ~/.config/dsh-grokbot-dispatch
openssl rand -hex 32 > ~/.config/dsh-grokbot-dispatch/key     # callers send this as "Authorization: Bearer <key>"
# key of each peer you dispatch to (the peer's own inbound key), e.g. zcode:
cp /path/from/zcode-host/key ~/.config/dsh-grokbot-dispatch/zcode.key; chmod 600 ~/.config/dsh-grokbot-dispatch/*.key

~/.config/dsh-grokbot-dispatch/config.json:

{
  "host": "192.168.1.20",
  "port": 7790,
  "allowedIps": ["192.168.1.30"],
  "selfName": "dsh",
  "peers": {
    "zcode": { "url": "http://192.168.1.30:7791", "keyFile": "~/.config/dsh-grokbot-dispatch/zcode.key" }
  }
}

Then cd /tmp && dsh plugin --profile desktop add "link:$PWD/plugin" (from a checkout). On start the plugin logs a compat check (key file present and chmod 600, host/allowedIps non-empty, projectsRoot exists, dsh found, transport prerequisites) and GET /health returns the same as {ok, transport, checks:{errors, warnings}}; nothing fails silently.

A peer that should push results back to you needs you in its peers under your selfName (or send callback_url pointing at one of its configured peers). callback_url that does not start with a configured peer url is refused (no arbitrary outbound POSTs).

transport: "yobiru" is optional, for meshes already running Yobiru/tailmsg: add "transport": "yobiru" plus grokbotPeer / zcodePeer (Yobiru addresses) and optionally "yobiru": {"python", "script", "home", "path"} (defaults: python3 from PATH, ~/.tailmsg/tailmsg.py, ~/.tailmsg, inherited PATH). All tailmsg calls live in plugin/transport.js.

Architecture & Official DSH Plugin Surface

  • Official DSH Bundle & Metadata (plugin/package.json):
    • dsh.manifestVersion: 1, dsh.bundle.patch: "./cordis.patch.yml", dsh.harnessRange: ">=0.2.0-rc.2".
    • Exports . (./index.js), ./package.json, and ./locale/*.json (plugin/locale/en.json, plugin/locale/zh-CN.json) so DSH's readPluginMeta (@deepseek-ai/dsh-app-boot) displays localized titles and descriptions in the Desktop Plugin Inventory.
  • Primary host = the DSH desktop app:
    • Linked into ~/.dsh/profiles/desktop/package.json (dependencies + dsh.profile.bundles).
    • Opens an HTTP listener inside ctx.effect bound only to host (config, or this host's tailnet IP if Tailscale is installed; host, default port 7790) and writes ~/.local/state/dsh-grokbot-dispatch/primary.pid.
  • Fallback host (headless runner):
    • Optional. Example (macOS launchd; private deploy templates live outside this public repo): a standalone profile grokbot-dispatch with DISPATCH_STANDBY=1 DISPATCH_RUNNER=headless. While the desktop app holds 7790 it waits quietly; when the app is closed it takes the port and runs jobs with dsh --profile headless --json; once the app returns and it is idle it yields the port.
  • Visible sessions (app runner):
    • Each inbound job creates a visible session in the DSH desktop app's left panel under its project with [Job: <16-hex>] embedded in its title ([Grok Bot] [Job: <id>] <task…>, [Yobiru] [Job: <id>] <task…> or [Agenvo] [Job: <id>] <task…>): workspaceController.create({path}) → sessionController.create({workspaceId}) → permissionPresets.set(agents.get(id).session, mode) → sessionController.rename({sessionId, title}) → sessionController.follow({address:{kind:'session',sessionId}}, signal) → sessionController.prompt({requestId: job.id, sessionId, mode:'queue', content:[{type:'text', text}]}, signal).
  • Outbound slash commands (plugin/commands.js):
    • Registered via DSH's official ctx.commands.register (@deepseek-ai/dsh-commands):
      • /dispatch-grok <project> <task…> — validates <projectsRoot>/<project> (denyProjects refused), sends [Project: <project>] <task…> to peer grok-bot (direct: POST <url>/v1/jobs; yobiru: tailmsg.py request send <grokbotPeer> --as dsh --wake now), and records the sessionId <-> jobId mapping.
      • /dispatch-zcode <project> <task…> — dispatches to peer zcode the same way and records the mapping.
      • /dispatch-status [job-id] — inspects active jobs and recent sessionId <-> jobId mappings.
  • Persistent sessionId <-> jobId mapping & automatic reporting (plugin/job-map.js, plugin/hooks/):
    • JobMapStore persists byJob and bySession mappings in ~/.local/state/dsh-grokbot-dispatch/job-map.json (mode 0600, atomic rename).
    • If a session is created or continued with [Job: <16-hex>] or [Yobiru-ID: <16-hex>] in its title or prompt, resolveForSession automatically binds sessionId <-> jobId.
    • When a turn finishes (runAppJob completion, Cordis session/event turn/end, agent/turn-stopping, or the Claude-Code hook bridge plugin/hooks/hooks.json → plugin/hooks/stop-report.mjs), reportJob idempotently pushes the structured dsh-job-report-v1 payload through the transport (direct: POST JSON to callback_url or <peers[callback_peer].url>/v1/callbacks; yobiru: tailmsg.py request respond <id> <payload> --state <completed|failed> + ack <id>, or tailmsg.py send <callback_peer> <payload> --as dsh --reply-to <jobId> --wake now), guarded across processes by an exclusive <jobId>.report.lock reservation (released on send failure for retry; taken over after 10 min if abandoned) and an in-process Map.
    • Reporting routing policy: Only tasks dispatched by Grok Bot (source: "grok-bot" / "grok") fall back to grokbotPeer; source: "yobiru" requests are answered only via request respond to their requester. Tasks originating from zcode, dsh, or local/manual sessions never fall back or report to Grok: if an explicit callback_peer pointing to their non-Grok origin (e.g. ZCode) is provided, the report is sent to that origin; otherwise no Yobiru callback is sent and the result is only persisted locally on disk.

Native bridge (Agenvo dsh connector)

Inside the desktop Host only, the plugin also serves the Host's own Typert endpoints on a same-user Unix socket, ~/.local/state/dsh-grokbot-dispatch/native.sock (directory 0700, socket 0600; override with DSH_NATIVE_SOCKET or config nativeSocket; disable with nativeBridge: false or DSH_NATIVE_BRIDGE=0): GET /health, POST /api/<namespace>/<method> with {args}, and POST /stream/<namespace>/<method> (NDJSON). Calls go through ctx.typertGateway with one owner Peer that never answers forwarded waterfalls (on DSH without connection.createPeer, the Gateway's operator Peer). The Agenvo agenvo-dsh connector attaches here (plugin/native-bridge.js).

Configuration (no hosts, IPs or user paths in code)

Every setting resolves as env var > ~/.config/dsh-grokbot-dispatch/config.json > tailscale discovery (if installed) > default (plugin/config.js, plugin/transport.js):

SettingEnvconfig.jsonDiscovery when unset
bind hostDISPATCH_HOSThostthis node's tailnet IPv4 (tailscale status --json); never 0.0.0.0 — if nothing resolves the listener stays down
portDISPATCH_PORTport7790
Grok Bot peerGROKBOT_PEERgrokbotPeernone — outbound /dispatch-grok and Grok reporting stay disabled until set
ZCode peerZCODE_PEERzcodePeerthis node's MagicDNS name + /zcode
allowed source IPsDISPATCH_ALLOWED_IPS (comma list)allowedIpstailnet IPs of the grokbotPeer node (follows a box re-login IP change)
transportDISPATCH_TRANSPORTtransportdirect
direct peers—peers {name: {url, keyFile}}none
our name for callbacks—selfNamedsh
explicit callback URL—callbackUrlnone (send callback_peer: selfName)
dsh binaryDSH_BINdshBindsh on PATH
projects root / deniedDISPATCH_PROJECTS_ROOTprojectsRoot / denyProjects (regex)~/Projects / finance
Yobiru CLI (yobiru only)TAILMSG_PYTHON / TAILMSG_SCRIPT / TAILMSG_HOMEyobiru.python / .script / .home / .pathpython3 / ~/.tailmsg/tailmsg.py / ~/.tailmsg / inherited PATH

Use DSH_DISPATCH_CONFIG=<file> to point at another file, DSH_DISPATCH_DISCOVERY=0 to disable discovery, and TAILSCALE_BIN if the CLI is not on PATH.

Non-public Host Interfaces & Upgrade Notes

  • Official interfaces used: package.json (dsh.manifestVersion, dsh.bundle.patch, dsh.harnessRange, exports["./locale/*.json"]), ctx.effect, ctx.commands.register, and Claude-Code hook format (plugin/hooks/hooks.json).
  • Internal Host services used (no public SDK alternative in DSH 0.2.0-rc.2 for in-process visible session creation):
    • ctx.get('workspaceController'), ctx.get('sessionController'), ctx.get('permissionPresets'), ctx.get('agents'), and Cordis events session/event / agent/turn-stopping.
    • If a future DSH release changes these service signatures, pickRunner / getServices degrades or can be forced to the CLI fallback via DISPATCH_RUNNER=headless.

API

RouteBody / result
GET /health{ok, running, runner, app_available, transport, checks:{errors,warnings}, uptime_s} (IP check only)
POST /v1/jobs{project, text, timeout_seconds?, permission_mode?, source?: "grok-bot"|"yobiru"|"zcode"|"dsh"|"agenvo", job_id?, yobiru_id?, callback_peer?, callback_url?, report_via_yobiru?, runner?: "app"|"auto"|"headless"} → 202 {id, runner:"app"|"headless", state:"running", ...}
source: "agenvo" (Agenvo connector): title [Agenvo] …, runner defaults to and must be app; results stay local (caller polls GET /v1/jobs/<id>), so callback_peer/callback_url/yobiru_id/report_via_yobiru are rejected with 400 and nothing is ever reported to Grok/Yobiru.
runner: "app" is strict: without the desktop Host services the request fails with 503 and no job or headless process is created. It overrides DISPATCH_RUNNER (the standby host runs headless). Omitted = receiver default.
GET /v1/jobs/<id>{state: running/completed/failed, runner, session_id, title, tools, result, error, reported?, report_method?, ...}
POST /v1/callbacksa peer pushes dsh-job-report-v1 / {job_id, state, result, error} for a job we sent it → stored on the outbound job-map entry (see /dispatch-status <id>)
POST /v1/outbound{target: <peer name>, project, task, session_id?} → 202 {ok, jobId, target, peer, sessionId, ...}
GET /v1/job-map{entries: [...]}

Shell client (any machine with curl): DSH_DISPATCH_URL=http://host:7790 DSH_DISPATCH_KEY=... client/dsh-dispatch.sh send myproj "[Project: myproj] ...", then client/dsh-dispatch.sh wait <id>.

Install / operate

See Minimal working config above. Private deployment config (Yobiru transport, launchd fallback host, self-check) is not in this repository — see your own deploy docs/mesh.

Reliability, logs, self-check

  • Auto-start / restart (optional launchd example): the agent has RunAtLoad and KeepAlive (ThrottleInterval 30 s).
  • Sleep / tailnet loss: supervise retries bind errors every 30 s and TCP-probes <host>:<port> every 30 s. On the primary (desktop) host it never calls process.exit and resets downSince so it keeps rebinding across prolonged sleep.
  • Logs (~/.local/state/dsh-grokbot-dispatch/logs/): dispatch.log (rotated at 2 MB × 5) and launchd.log.
  • Self-check: GET /health (compat check); optional local self-check scripts (not shipped in this public repo) can print launchd state, /health, recent log events, and recent jobs.
  • Local literal-IP guard: bin/check (no network, no GitHub Actions) fails on hard-coded IPv4 addresses in the tailnet range. Docs, test//tests/ fixtures, and exact path|address|reason entries in .ip-literals-allow are exempt.