dsh-remote 0.2.0: agent tools to drive slaves from the master

- remote_devices / remote_exec / remote_file / remote_task tools on the master
  (exec: bash -lc with timeout + output clipping; files: read/write/list;
  task: delegate a prompt to the device's own AI, wait for turn end, return
  its answer; optional permission preset, status/cancel)
- only the master may control devices (allowPeerControl to relay between slaves),
  agentTools.enabled=false opts a device out
- FORGE_CREDENTIALS opt-in pseudo-secret: ships the master's tea logins to a
  slave (tea config + git credential store + identity)
- /dsh-remote/debug/inspect + deploy/session-log.mjs for transcripts
- tests for exec/files/task/forge install
This commit is contained in:
lyuksovannyy committed 2026-10-11 10:08:04 +00:00
1 parent 8872e73e76
commit d1e134196d
7 files changed
+625 -8

No files matched your search

+40 -2
View File
@@ -44,6 +44,37 @@ The slave gets its own device key; the master stores only its hash. **Revoke** c
- **Open DSH ↗** opens a device's whole UI.
- **Toasts:** "<device>: '<title>' finished", with an *Open* action.
## Agent tools: the master's AI drives the slaves
On the master, sessions get four tools. Every paired, online device can be targeted by id or name; `self` means the master itself.
| Tool | What it does |
| --- | --- |
| `remote_devices` | Lists devices with online state, user, home and kernel. With `sessions: true` it also returns each device's workspaces and recent sessions. |
| `remote_exec` | Runs `bash -lc <command>` on the device as its DSH user and returns exit code, stdout and stderr. It is non-interactive (`GIT_TERMINAL_PROMPT=0`). Options: `cwd`, `timeoutMs` (up to 30 min), `stdin`. Use it for quick deterministic steps: clone/pull, build, run tests. |
| `remote_file` | `read`, `write` (utf8 or base64, with append) and `list` for files on the device. Up to 4 MiB per call. |
| `remote_task` | Delegates a job to the device's **own AI**. It creates a session there (attached to the `cwd` workspace), or continues `sessionId`, sends the prompt and waits for the turn to end, up to 2 h. It returns the final answer, a count of tools used, and a link to the session (`/r/<device>/#/session/…`). `permission: "danger-full-access"` runs the session without the workspace sandbox. `action: status` / `cancel` poll or stop a running task. |
Example, typed into any master session:
> On ai-pc, clone https://git.rizzy.dev/Python/ai-pc into ~/work/ai-pc (or git pull), run its unit tests and report the result.
The master's AI calls `remote_exec` (clone, venv, `pytest`) and reports "20 passed, 5 deselected, commit 32d4434". With `remote_task` the AI on ai-pc does the work itself, and its session stays browsable under Devices.
**Choosing a tool:** `remote_exec` is cheap and exact; you get the command output. `remote_task` uses the slave's model tokens and judgement, which suits open-ended work ("fix the failing test"). The full transcript stays on the slave.
**Who can control whom:** only the master controls devices. Slaves reject agent ops from anyone except their master, and the master rejects them from slaves. `agentTools.allowPeerControl: true` on the master lets slaves relay ops to each other. `agentTools.enabled: false` on a device refuses all agent control of that device.
### Git credentials on slaves (`FORGE_CREDENTIALS`)
Private repos need a login on the slave. `FORGE_CREDENTIALS` is a pseudo-secret that works like the other opt-in secrets: list it in the master's `offerSecrets`, enable it for the device on the Devices page, and add it to the slave's `sync.secrets`. On the next sync the slave gets the master's Gitea/Forgejo logins from `tea-cli`, written as:
- `~/.config/tea/config.yml` (0600), so the synced git-forge plugin's `forge_*` tools work there;
- `~/.git-credentials` (0600) plus `git config --global credential.helper store`, so plain `git clone`/`push` over https works;
- a default git identity, unless one is already set.
GitHub `gh` logins are not shipped.
## Configuration (`cordis.patch.yml`)
```yaml
@@ -57,10 +88,14 @@ The slave gets its own device key; the master stores only its hash. **Revoke** c
auto: true # slave: sync on link-up and when the master pushes
allow: [] # slave: only these bundles (exact or prefix*)
deny: [] # slave: never these bundles (wins over allow)
secrets: [] # slave: credential refs it accepts from the master
offerSecrets: [OMNI_API_KEY] # master: refs it may hand out (per-device opt-in on the Devices page)
secrets: [] # slave: credential refs it accepts from the master (+ FORGE_CREDENTIALS)
offerSecrets: [OMNI_API_KEY, FORGE_CREDENTIALS] # master: what it may hand out (per-device opt-in on the Devices page)
excludeBundles: [] # master: never sync these bundles
excludePatchIds: [] # master: never sync these cordis.patch.yml entry ids
agentTools:
tools: true # register remote_* tools (default: on for the master, off for slaves)
enabled: true # false = refuse agent control of THIS device
allowPeerControl: false # master: let slaves relay exec/file/task ops to other devices
```
Never synced: the master's `dsh-remote` and `dsh-lanmode` entries, and `connection`/`webserver` settings, since these are machine-local.
@@ -83,6 +118,7 @@ Local plugins are compared by content hash and re-shipped only when they change.
- The proxy strips the browser's DSH cookies. The **owning** device mints a short-lived cookie for its own loopback authority, signed with its own browser-session secret, and rewrites `Host`/`Origin`. A remote DSH therefore only trusts its own minted cookie, and credentials never leave the machine they belong to, except secrets you explicitly opted into.
- `/dsh-remote/enroll` is the only unauthenticated route. It accepts a single-use pairing code.
- Use TLS (`https://` master URL) whenever the link crosses an untrusted network.
- **Agent control is real remote code execution.** `remote_exec`, `remote_file` and `remote_task` run as the device's DSH user. Anyone who can prompt a master session can run code on every paired device. Revoke a device, or set `agentTools.enabled: false` on it, to opt it out.
## Public access via rizzy.dev
@@ -109,6 +145,7 @@ For example, add a second Traefik router with a higher priority, `Host(\`dsh.riz
| `POST /dsh-remote/sync` | slave: pull now · master: push to all |
| `GET /dsh-remote/sync/status` | manifest (master) / last result (slave) |
| `POST /dsh-remote/new-session` | `{device, workspaceId? , cwd?}` |
| `GET /dsh-remote/debug/inspect` | `?sessionId=…&device=…`: last events of a session on any device |
| `* /r/<device>/…` | that device's DSH |
## Development
@@ -119,6 +156,7 @@ node deploy/dshr-cli.mjs GET state # call the local API as the DSH owner
node deploy/e2e-ws.mjs ai-pc # Remote WebSocket through /r/ai-pc/
node deploy/e2e-events.mjs 30 # watch the SSE event stream
node deploy/e2e-ui.mjs <chrome> /tmp/out # headless UI check + screenshots
node deploy/session-log.mjs <sessionId> # compact transcript (DEVICE=ai-pc for a remote one)
deploy/push-slave.sh agent@192.168.0.110 # copy to a slave + restart its dsh
```