dragoon-server 0.1.0

Public-relay server for the dragoon remote-executor: axum + rusqlite + ed25519 task signing + per-user message inbox.
Documentation
# dragoon

[![crates.io: dragoon-proto](https://img.shields.io/crates/v/dragoon-proto.svg?label=dragoon-proto)](https://crates.io/crates/dragoon-proto)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

> **Disconnected Rapid Guidance Once-deployed Operational Network** — a
> public-relay command executor for long-running experiments, named
> after the Gundam SEED 浮游炮 system since the architecture is
> isomorphic: a controller pilots remote autonomous nodes that execute
> tasks under cryptographically authenticated commands.

The model is **ctl → server → worker(s)** over HTTPS: every component
connects out, so no NAT/firewall holes are needed on the worker boxes.

Three binaries, one Cargo workspace:

| Binary       | Role |
|--------------|------|
| `dragoon-server`  | Public-relay HTTP server (axum + rusqlite). The only host that needs an inbound port. |
| `dragoon-worker`  | Execution agent. Polls the server, runs shell payloads, streams logs/artifacts back. |
| `dragoon-ctl`     | Controller CLI. Submits commands, tails logs, drains the message inbox. |

The original Python implementation lives in git history (last commit:
`8cce0de` deletes it). Wire format, SQLite schema, canonical strings and
fingerprints are byte-for-byte unchanged — fixture parity tests in
`crates/dragoon-proto/tests/` lock that down.

See [`docs/plans/2026-04-27-remote-executor-design.md`](docs/plans/2026-04-27-remote-executor-design.md)
for the full design (auth model, protocol, state machine).

## Build

```bash
cargo build --release --workspace
# produces target/release/{dragoon-server, dragoon-worker, dragoon-ctl}
```

Tests: `cargo test --workspace` (~120 unit + integration tests, including
4 byte-for-byte parity fixtures and the auth_routes axum integration tests).

Lint: `cargo clippy --workspace --all-targets -- -D warnings` is clean.

## Quick start (loopback, no TLS)

```bash
DATA=/tmp/re-demo
mkdir -p "$DATA" && ssh-keygen -t ed25519 -f "$DATA/id" -N "" -q
cat > "$DATA/server.toml" <<EOF
[server]
data_dir = "$DATA/srv"
bind_host = "127.0.0.1"
bind_port = 8765
public_url = "http://127.0.0.1:8765"
EOF

# admin pipeline
echo hunter2 | ./target/release/dragoon-server user create --name alice --data-dir "$DATA/srv"
TOTP_SECRET=...                                 # printed by the previous step
./target/release/dragoon-server user pubkey add --user alice --pub "$DATA/id.pub" --data-dir "$DATA/srv"
CODE=$(./target/release/dragoon-server token issue --worker lab-w1 --data-dir "$DATA/srv" | awk '{print $NF}')

# server up
./target/release/dragoon-server run --config "$DATA/server.toml" &

# worker register + run (on the same or a different box)
./target/release/dragoon-worker init --server http://127.0.0.1:8765 --code "$CODE" \
    --name lab-w1 --workdir "$DATA/wd" --state-file "$DATA/state.json"
./target/release/dragoon-worker run --name lab-w1 --state-file "$DATA/state.json" \
    --workdir "$DATA/wd" --server http://127.0.0.1:8765 &

# ctl: log in, submit, watch
TOTP=$(oathtool --base32 --totp "$TOTP_SECRET")
printf 'hunter2\n%s\n' "$TOTP" | ./target/release/dragoon-ctl login \
    --server http://127.0.0.1:8765 --username alice --ssh-key "$DATA/id"
./target/release/dragoon-ctl worker list
TID=$(./target/release/dragoon-ctl submit lab-w1 -c "echo hello-from-rust && hostname")
./target/release/dragoon-ctl tail "$TID"
./target/release/dragoon-ctl events
```

## Cross-compile a worker for another arch

The `xtask` cargo subcommand detects the target's arch over SSH and
builds a static musl binary against the right Rust target triple, then
scps it over and installs.

```bash
cargo xtask deploy-worker --host me@some-arm-box
# detects Linux aarch64 -> aarch64-unknown-linux-musl
# cargo build --release -p dragoon-worker --target aarch64-unknown-linux-musl
# scp -> /tmp/dragoon-worker.new.<pid> -> install -m 0755 to /usr/local/bin/dragoon-worker
```

Build all supported triples for an offline distribution:

```bash
cargo xtask release-all-triples
ls dist/   # dragoon-worker-x86_64-unknown-linux-musl
           # dragoon-worker-aarch64-unknown-linux-musl
           # dragoon-worker-armv7-unknown-linux-musleabihf
```

If a target's musl linker isn't on `PATH`, `cargo xtask doctor --triple T`
tells you what to install (`apt install musl-tools`,
`aarch64-linux-musl-cross`, etc.) — or fall back to `cargo install cross`
and use `cross build` directly.

macOS targets (`x86_64-apple-darwin`, `aarch64-apple-darwin`) only build
on a mac host — Linux→Mac cross-compilation is intentionally out of scope.

## Workspace layout

```
crates/
    dragoon-proto/   shared protocol primitives (constants, wire format,
                canonical strings, signers, models). Pure-Rust, no
                tokio/reqwest/sqlite. The byte-parity fixture tests
                live here.
    dragoon-server/  axum app + rusqlite (bundled) + argon2id + totp-rs +
                ed25519/RSA key handling. Routes:
                  /v1/auth/{challenge,login,logout}
                  /v1/workers, /v1/workers/{name}/fetch
                  /v1/tasks, /v1/tasks/{id}/{cancel,log,artifact}
                  /v1/messages, /v1/messages/ack
                  /v1/worker/{init,poll,log,finish,blob}
    dragoon-worker/  reqwest + nix (setsid + killpg) + globset + walkdir.
                Spawns bash -c in its own process group; threaded
                stdout/stderr readers; SIGTERM→grace→SIGKILL on cancel.
    dragoon-ctl/     reqwest signed-request client (KeyFileSigner or
                AgentSigner over SSH_AUTH_SOCK) + clap CLI.
    dragoon-testkit/ dev-only fixture loaders + harness.
xtask/          cross-compile orchestrator (see above).
fixtures/       JSON fixtures committed to repo; the source of truth
                for canonical_request, canonical_task, fingerprints
                and ed25519 signatures.
.claude/skills/ Project-level Claude Code skills (deploy-server,
                deploy-worker, send-command).
```

## Authentication summary

### ctl → server

Every business request must carry **all** of:

- `Authorization: Bearer <session_token>`
- `X-RE-Timestamp` (±60 s window)
- `X-RE-Nonce` (5-min one-shot)
- `X-RE-Key-Fingerprint` (must match the session-bound fp)
- `X-RE-Signature` (SSH-wire ed25519 / rsa-sha2-512 signature over the
  `RE-V1` canonical request bytes)

`KeyFileSigner` (loaded private key) and `AgentSigner` (talks the SSH
agent protocol over `SSH_AUTH_SOCK`) both produce the wire format the
server's `verify_ssh_wire_signature` accepts.

### server → worker

Every assigned task is signed by the per-server ed25519 task-signing
key. Worker pins the public key on `init` and rejects any
out-of-signature, mismatched-name, or out-of-order-seq task with
`error="bad_task_signature: <reason>"` *before* spawning a subprocess.

## License

Dual-licensed under either of:

- [Apache License, Version 2.0]LICENSE-APACHE
- [MIT license]LICENSE-MIT

at your option.