Webapp for spawning and managing dockerized Claude Code agents.
  • TypeScript 75.7%
  • Svelte 19.9%
  • Shell 2.6%
  • CSS 1.2%
  • Dockerfile 0.3%
  • Other 0.3%
Find a file
Luca Zanussi bf3a3a8603
All checks were successful
ci / nexus (push) Successful in 9m5s
ci / images (push) Successful in 3m23s
docs: plan the MCP resource server (#154)
Probing @modelcontextprotocol/server@2.0.0 reversed five things the issue
specified. Two matter: createMcpHandler serves the 2026-07-28 revision with the
stateless 2025 fallback the issue described by hand, so taking the bare
transport would ship a 2025-only server; and no SDK helper checks the token
audience, so RFC 8707 binding is entirely ours.

Access tokens move from JWT to opaque. AccessToken.find hands the raw value to
the adapter keyed on jti and formats/jwt.js exports no getTokenId, so the
lookup can never resolve a JWT — which would leave the resource server
verifying signatures itself and unable to see a revocation before expiry.
2026-09-16 22:34:18 +00:00
.forgejo/workflows chore(worker): bump @playwright/cli to 0.1.19, pin the version in one place 2026-09-05 12:29:48 +00:00
docs/superpowers docs: plan the MCP resource server (#154) 2026-09-16 22:34:18 +00:00
nexus fix(oauth): prefix the consent route's message constants so the build accepts them 2026-09-16 19:11:45 +00:00
worker test(worker): give each statusline case its own curl-shim output paths 2026-09-05 21:54:46 +00:00
.gitattributes feat(agent-instructions): add bundled default CLAUDE.md + .gitattributes LF pin 2026-05-25 23:44:24 +02:00
.gitignore chore: ignore .agent/ browser-pass screenshots 2026-09-04 20:36:36 +00:00
AGENTS.md docs: record three traps from the OAuth/CIMD work 2026-09-16 21:55:54 +00:00
CLAUDE.md Initial commit: Agent Nexus — self-hosted Claude Code agent manager 2026-05-11 20:46:39 +02:00
DEPLOYMENT.md fix(auth): stop the refusal log being the flood, and close five test gaps 2026-09-15 18:08:44 +00:00
docker-bake.hcl chore(worker): bump @playwright/cli to 0.1.19, pin the version in one place 2026-09-05 12:29:48 +00:00
docker-compose.desktop.yml refactor(compose): rename override.yml to desktop.yml so Linux stops auto-loading it 2026-07-17 15:39:56 +00:00
docker-compose.yml refactor(settings): remove the global worker_image setting and WORKER_IMAGE env 2026-06-24 19:32:13 +00:00
fixup.mjs fix(ui): a session's port chip says when a port is waiting on you 2026-09-04 15:49:22 +00:00
README.md docs: record the envelope vault, and retire three facts it inverted 2026-09-11 12:05:39 +00:00

Agent Nexus

Agent Nexus

Self-hosted webapp for spawning and managing dockerized Claude Code agents.

Connects to one or more git providers (Forgejo today, GitHub planned), lists your repositories, and spawns Nexus Worker containers — each running Claude Code under tmux with cloud Remote Control so you can attach from the Claude mobile/web app or docker exec from the host.

Quick start

# 1. Build images
docker build -t agent-nexus-worker:latest ./worker
docker build -t agent-nexus:latest ./nexus

# 2. One-time: bootstrap the shared Claude session.
#    Remote Control refuses inference-only setup-tokens, so workers share the
#    full-scope OAuth state produced by `claude auth login`. The bootstrap
#    script runs it interactively and saves the result to a docker volume.
docker run --rm -it -v claude-session:/session agent-nexus-worker:latest \
  /bootstrap-claude-auth.sh

# 3. Start Nexus + the docker-socket proxy
docker compose up -d

Then open http://localhost:3001/ and:

  1. Set a master passphrase (first run only — it encrypts provider tokens at rest)
  2. Unlock with that passphrase
  3. If the bootstrap step ran, the UI advances past the Bootstrap Claude session screen automatically. If not, you'll see instructions to run step 2 above.
  4. Add a Forgejo connection (base URL + PAT with write:repository write:issue read:user read:organization)
  5. Create a workspace (Provider + Repo), then add one or more sessions from the workspace card
  6. Each session shows up at claude.ai/code and in the Claude Android/iOS app a few seconds later

API tokens

To read Nexus state from another machine — a status widget, a phone shortcut, a cron job — mint a scoped token under Settings → API tokens rather than handing the script your master passphrase. The passphrase unwraps the vault's data key; a token carries no key material at all and never unlocks the vault.

curl -H "Authorization: Bearer nxs_…" http://your-nexus:3001/api/quota

Two scopes, granted independently:

Scope Reaches
quota:read /api/quota
workspaces:read /api/workers, and a workspace's sessions and session health

Tokens are read-only: they are rejected on any method other than GET, and on every route not explicitly listed above — including container logs, agent instructions, artifact contents, and token management itself. A token never unlocks the vault, so it cannot spawn a workspace or read a provider credential even if a route would otherwise allow it.

An expiry is optional and defaults to never. The plaintext is shown once at creation and only a SHA-256 digest is stored, so a lost token is replaced rather than recovered. Revoking takes effect immediately.

The token is a bearer credential in cleartext. Over plain HTTP anything on the network path can read it — put Nexus behind TLS before using tokens across an untrusted network. There is no rate limiting on the API yet.

Architecture

 ┌────────────────────────────────┐
 │ browser / Claude mobile app    │
 └──────────────┬─────────────────┘
                │ HTTPS (nginx, optional)
 ┌──────────────▼─────────────────┐         ┌─────────────────────────────┐
 │agent-nexus (SvelteKit + SQLite)│ via TCP │ docker-socket-proxy          │
 │  - master-passphrase vault     │────────►│  endpoint allowlist:         │
 │  - provider abstraction        │ to:2375 │  containers, exec, volumes,  │
 │  - spawns workers              │         │  images. Everything else 403.│
 └──────────────┬─────────────────┘         └────────────┬─────────────────┘
                │ creates / inspects                     │ mounts /var/run/docker.sock:ro
                │                                        ▼
                │                          ┌─────────────────────────────┐
                │                          │ host Docker engine          │
                ▼                          └────────────┬─────────────────┘
 ┌───────────────────────────────────┐                  │ spawns
 │ agent-nexus-worker (one/workspace)│◄─────────────────┘
 │  - tmux PID 1, N session windows  │
 │  - claude --remote-control NAME   │       ┌─────────────────────────┐
 │    cwd = .nexus/worktrees/NAME    │──────►│ Anthropic cloud relay   │
 │  - /session ← claude-session vol  │       └────────────┬────────────┘
 │  - /workspace ← per-worker volume │                    │
 └───────────────────────────────────┘                    │
                                                          ▼
                                          your Claude mobile / claude.ai/code
  • One worker container = one tmux server = N sessions. Each session is a tmux window running its own claude --remote-control, rooted in a Nexus-created git worktree at /workspace/.nexus/worktrees/<name> on a branch named exactly <name>, so collaborating agents share one /workspace clone while their changes land on isolated branches. Nexus creates and removes those worktrees itself — it does not pass --worktree to the CLI.
  • Shared OAuth state: the claude-session docker volume holds credentials.json + account.json produced by claude auth login. Workers mount it read-write so refresh-token rotations persist across the fleet.
  • Docker socket is fronted by a proxy (tecnativa/docker-socket-proxy) on a default-deny internal network. Even if Nexus is compromised, the attacker cannot reach docker run --privileged or any non-allowlisted endpoint.
  • Per-spawn secrets (Forgejo token, authed clone URL) are injected via tmpfs after the worker starts. They do NOT appear in docker inspect or /proc/1/environ.
  • Worker discovery: containers carry nexus-managed=true. The UI never lists unrelated host containers.

Repo layout

agent-nexus/
├── nexus/                  # the webapp (SvelteKit + Vite + Kysely)
├── worker/                 # the Claude runtime image
├── docker-compose.yml      # nexus + docker-socket-proxy
├── DEPLOYMENT.md           # threat model, nginx, backups
└── .forgejo/workflows/ci.yml

See DEPLOYMENT.md for production deployment, nginx reverse-proxy config, the threat model after the socket-proxy hardening, and backup snippets.