Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dist/
sparks.json
config/sparks-secrets.json
config/.secrets-key
config/session-sources.json
config/bench-history.json
config/bench-active.json
config/showcase-history.json
Expand All @@ -24,3 +25,4 @@ HANDOFF.md
HANDOFF-HERMES.md
handoff.md
test/
.context/compound-engineering/
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ It also supports **non-Spark units**: any Linux machine with an NVIDIA GPU (e.g.
- [Repository layout](#repository-layout)
- [REST API](#rest-api)
- [Configuration](#configuration)
- [Harness sessions](#harness-sessions)
- [Security](#security)
- [Scripts](#scripts)
- [How it works](#how-it-works)
Expand Down Expand Up @@ -85,6 +86,7 @@ Full history: [CHANGELOG.md](./CHANGELOG.md)
| **Secrets** | SSH passwords AES-256-GCM encrypted; never in `sparks.json` or API responses |
| **Docker-first** | Single privileged container for host metrics; prod and dev Compose files |
| **Hot config** | Add / edit / remove / reorder Sparks from the UI with no process restart |
| **Harness sessions** | OpenClaw / Hermes / OpenCode / oh-my-pi / DeepSeek Harness sessions on each LLM card; onboarding wizard for setup |

---

Expand Down Expand Up @@ -357,6 +359,9 @@ sparkDash/
| GET | `/api/sparks/:id/llm/daily` | Daily busy decode/prefill tok/s (`port`, `days`) |
| GET | `/api/settings` | Global settings |
| PUT | `/api/settings` | Update global settings |
| GET | `/api/session-sources` | Occupancy attaches (tokens redacted) |
| PATCH | `/api/session-sources` | Update occupancy attaches |
| POST | `/api/session-sources/test` | Check occupancy attach reachability |
| WS | `/ws` | Real-time metrics stream |

There is no authentication on the HTTP/WebSocket API. Run sparkDash only on a trusted network (or behind your own reverse proxy with auth).
Expand Down Expand Up @@ -444,6 +449,16 @@ Choice is stored in `localStorage`.

---

## Harness sessions

Active coding-agent sessions from OpenClaw, Hermes, OpenCode, oh-my-pi, and DeepSeek Harness show on each Spark’s LLM card. Sessions are matched to Sparks by the LLM host:port they hit.

Configure harnesses via the onboarding wizard: **Header → Manage harnesses** (or any LLM card → Settings → Add harness). The wizard walks you through picking harnesses, choosing local or remote mode, checking connectivity, and saving.

For full setup details — helper snippets, security rules, multiple attaches, origin matching, and LiteLLM notes — see [`docs/harness-sessions.md`](./docs/harness-sessions.md).

---

## Security

- **SSH passwords** are not stored in `sparks.json` and are never returned by the API.
Expand Down
124 changes: 124 additions & 0 deletions docs/harness-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Harness sessions

sparkDash shows active coding-agent sessions on each Spark's LLM card. Sessions are matched to Sparks by the LLM host:port they hit — so a session using a model on `mama:4000` shows on mama's port-4000 card.

## Supported harnesses

| Harness | Local mode | URL mode | State-dir mode |
|---------|-----------|----------|----------------|
| **OpenClaw** | `~/.openclaw` (or gateway on same host) | Gateway URL + token | Custom state dir |
| **Hermes Agent** | `~/.hermes` | Dashboard URL + token | Custom state dir |
| **OpenCode** | `~/.local/share/opencode` (reads `opencode.db`) | Helper URL + token | Custom state dir |
| **oh-my-pi (omp)** | `~/.omp` (reads JSONL + `models.yml`) | Helper URL + token | Custom state dir |
| **DeepSeek Harness (dsh)** | — | Helper URL + token (remote-only) | — |

## Configure harnesses

Dashboard-wide — the onboarding wizard walks you through it:

1. **Header → Manage harnesses** (or any LLM card → Settings → Add harness)
2. Pick the harnesses you use
3. For each, choose local or remote mode and fill in URL/state-dir/token as needed
4. **Check connection** to test connectivity
5. **Review and save**

Sessions land on the Spark whose `llmPorts` match the session's origin host:port.

## Session age filter

**Settings → Occupancy session age limit** (default: 12 hours). Sessions older than this are hidden from the dashboard. Set to `0` to show all sessions regardless of age.

## Local mode (same host as sparkDash)

When sparkDash runs on the same machine as the agent, use **Local** mode. No helper needed — the collector reads the state directory directly.

For Docker deployments with `HOST_ROOT_PATH=/host/root`, local mode reads through the host root mount.

## URL mode (remote machine)

When the agent runs on a different machine, run a helper on that machine and attach via URL.

### OpenCode helper

On the OpenCode machine (Node 22+, sparkDash checkout):

```bash
BIND="$(tailscale ip -4 2>/dev/null || ipconfig getifaddr en0 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}')"
TOKEN="$(openssl rand -hex 32)"
printf 'URL: http://%s:8788/occupancy\nToken: %s\n' "$BIND" "$TOKEN"
OPENCODE_OCCUPANCY_BIND="$BIND" OPENCODE_OCCUPANCY_TOKEN="$TOKEN" \
node scripts/opencode-occupancy-helper/index.js
```

Full env and macOS path notes: [`scripts/opencode-occupancy-helper/README.md`](../scripts/opencode-occupancy-helper/README.md)

### oh-my-pi (omp) helper

On the omp machine (Node 22+, sparkDash checkout):

```bash
BIND="$(tailscale ip -4 2>/dev/null || ipconfig getifaddr en0 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}')"
TOKEN="$(openssl rand -hex 32)"
printf 'URL: http://%s:8789/occupancy\nToken: %s\n' "$BIND" "$TOKEN"
OMP_OCCUPANCY_BIND="$BIND" OMP_OCCUPANCY_TOKEN="$TOKEN" \
node scripts/omp-occupancy-helper/index.js
```

Full env notes: [`scripts/omp-occupancy-helper/README.md`](../scripts/omp-occupancy-helper/README.md)

### DeepSeek Harness (dsh) helper

dsh is remote-only — it has no local mode. On the machine that hosts the dsh web API (Node 22.13+, sparkDash checkout):

```bash
BIND="$(tailscale ip -4 2>/dev/null || ipconfig getifaddr en0 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}')"
TOKEN="$(openssl rand -hex 32)"
printf 'URL: http://%s:8791/occupancy\nToken: %s\n' "$BIND" "$TOKEN"
DSH_OCCUPANCY_TOKEN="$TOKEN" DSH_OCCUPANCY_BIND="$BIND" DSH_WEB_URL="http://127.0.0.1:3080" \
node scripts/dsh-occupancy-helper/index.js
```

Full env notes: [`scripts/dsh-occupancy-helper/README.md`](../scripts/dsh-occupancy-helper/README.md)

### Security rules for helpers

- **Never bind `0.0.0.0`** — the helpers refuse it. Use a Tailscale or LAN IP.
- **Token required** for non-loopback binds. The helper refuses to start without one.
- Helpers serve **metadata only** — session handles, model origins, timestamps. No transcripts, no API keys, no credentials.
- Response is cached for 2 seconds to avoid re-reading files on every poll.

### Running multiple helpers on one machine

OpenCode, omp, and dsh helpers use different ports (8788, 8789, and 8791), so they can run side by side:

```bash
# Terminal 1 — OpenCode
OPENCODE_OCCUPANCY_TOKEN="token-oc" OPENCODE_OCCUPANCY_BIND="$(tailscale ip -4)" \
node scripts/opencode-occupancy-helper/index.js

# Terminal 2 — omp
OMP_OCCUPANCY_TOKEN="token-omp" OMP_OCCUPANCY_BIND="$(tailscale ip -4)" \
node scripts/omp-occupancy-helper/index.js
```

Then add two URL attaches in sparkDash (one OpenCode, one oh-my-pi), each pointing at the same IP on the respective port.

## Multiple attaches of the same kind

Click **Add** next to a source kind to attach more than one instance (e.g., OpenCode on both your Mac and your workstation). Each attach gets a unique ID (`opencode-2`, `opencode-3`, etc.) and can be checked and toggled independently.

## How origin matching works

Each session has an `originHost` and `originPort` derived from the provider's `baseUrl`:

- OpenClaw: provider `baseUrl` from the gateway payload
- Hermes: provider `baseUrl` from the dashboard API
- OpenCode: provider `baseURL` from `opencode.jsonc`/`opencode.json`
- omp: provider `baseUrl` from `~/.omp/agent/models.yml`
- dsh: provider host:port from `cordis.patch.yml` (read by the helper)

The projector matches sessions to Sparks by comparing `originHost:originPort` against each Spark's `llmPorts` and listen IPs. If a Spark has multiple LLM ports, sessions show on the port they actually hit.

## LiteLLM note

If you route models through LiteLLM (e.g., `kalliope` on port 4000), set the LLM API key in sparkDash so the `/v1/models` probe authenticates. LiteLLM without a database returns `400` on `/v1/models` without a key, which sparkDash interprets as "unreachable." With the key, all routed models appear on the single LiteLLM port — no need to expose the raw backend port separately.
Loading