mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
164 lines
7.8 KiB
Markdown
164 lines
7.8 KiB
Markdown
# Cloud agent sessions (Codespaces)
|
||
|
||
Run long-lived, human-steered agent sessions (Claude Code / OpenCode) on a
|
||
GitHub Codespace instead of your laptop: start a task, close the lid, steer it
|
||
from anywhere with a terminal, resume tomorrow.
|
||
|
||
This is a separate devcontainer config from the laptop one in
|
||
`.devcontainer/` — it ships both agent CLIs, `tmux` for session persistence,
|
||
Playwright system deps, and Docker-in-Docker (for testcontainers and
|
||
`pnpm --filter n8n-containers services`), on the same Postgres sidecar setup.
|
||
|
||
## One-time setup (~5 min)
|
||
|
||
1. Add your key at [github.com/settings/codespaces](https://github.com/settings/codespaces)
|
||
→ **New secret** → name `ANTHROPIC_API_KEY`, repository access `n8n-io/n8n`.
|
||
(Alternative for Max subscriptions: `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token`.)
|
||
2. Give the GitHub CLI the codespace scope:
|
||
|
||
```bash
|
||
gh auth refresh -h github.com -s codespace
|
||
```
|
||
|
||
## Daily flow
|
||
|
||
```bash
|
||
pnpm session # attach the default agent session (creates everything on first run)
|
||
pnpm session fix-flaky # a second, parallel agent in its own git worktree
|
||
pnpm session ls # what's running
|
||
pnpm session tunnel # forward n8n ports (default 5678, 8080) to localhost; Ctrl-C to stop
|
||
pnpm session stop # end of day: billing stops, disk survives
|
||
pnpm session rm # delete the codespace
|
||
```
|
||
|
||
- **Detach** with `Ctrl-b d` — the agent keeps working without you.
|
||
- **Scroll** with the mouse wheel (tmux mouse mode is on). To use the
|
||
terminal's own text selection, hold **Shift** and drag.
|
||
- **Reattach** by running the same `pnpm session <name>` from any machine.
|
||
- Each named session gets its own worktree (`/workspaces/wt-<name>`, branch
|
||
`session/<name>`), so parallel agents never touch each other's tree. Builds
|
||
in fresh worktrees are cache-hits via a shared turbo cache.
|
||
- First codespace creation takes ~20 min uncached (image + full build). After
|
||
that, sessions attach instantly; new worktrees cost a `pnpm install` (~1–2 min).
|
||
|
||
## Agent broker (call the codespace over HTTP)
|
||
|
||
`agent-broker.mjs` runs one Claude turn per HTTP request, so an external caller
|
||
(an n8n workflow, a script) can drive a session on the codespace. Each turn is a
|
||
headless `claude -p`. State persists on disk, so `--resume` continues a
|
||
conversation across turns and across a stop/start.
|
||
|
||
The broker starts on every container start (`postStartCommand`). It needs the
|
||
`AGENT_BROKER_TOKEN` secret and refuses to start without it. Add the token at
|
||
[github.com/settings/codespaces](https://github.com/settings/codespaces) the
|
||
same way as `ANTHROPIC_API_KEY`. Logs are at `/tmp/agent-broker.log`; the tmux
|
||
session is `broker`.
|
||
|
||
The port is private by default. Make it reachable, then health-check it:
|
||
|
||
```bash
|
||
gh codespace ports visibility 8787:public -c <codespace-name>
|
||
curl https://<codespace-name>-8787.app.github.dev/healthz # {"ok":true}
|
||
```
|
||
|
||
A public port has no auth in front of it, so the bearer token is the only
|
||
guard. Keep it secret.
|
||
|
||
Send a turn (omit `sessionId` to start one; pass it back to continue):
|
||
|
||
```bash
|
||
curl -s https://<codespace-name>-8787.app.github.dev/turns \
|
||
-H "Authorization: Bearer $AGENT_BROKER_TOKEN" -H 'content-type: application/json' \
|
||
-d '{"message":"what branch is this?"}'
|
||
# reply carries .result.session_id — send it back as "sessionId" next turn
|
||
```
|
||
|
||
Turns take minutes. Pass a `callbackUrl` to get `202 {turnId}` at once and the
|
||
result by POST when the turn ends. See the file header for the full contract.
|
||
|
||
## Flaky tools (MCP)
|
||
|
||
Claude sessions get the `flaky` MCP server automatically: Currents
|
||
flaky/quarantine data, the `qa_*` BigQuery dataset, Sentry RCA, live Linear,
|
||
and repo investigation. Login registers it from the repo-level
|
||
`FLAKY_MCP_TOKEN` / `FLAKY_MCP_URL` secrets. Forks have no secrets and skip
|
||
it. Tell the agent to call `get_flaky_context` first — it returns the rules
|
||
the tools assume.
|
||
|
||
## Viewing the dev UI locally
|
||
|
||
Two terminal windows:
|
||
|
||
```bash
|
||
pnpm session # window 1: attach the agent session
|
||
pnpm session tunnel # window 2: forward 5678 + 8080 to localhost
|
||
```
|
||
|
||
Attaching lands you in the agent (Claude), not a shell — open a shell in a
|
||
new tmux window with `Ctrl-b c` (or ask the agent) and run `pnpm dev` there.
|
||
Then open http://localhost:8080.
|
||
|
||
The tunnel prints nothing while forwarding — that's normal. `Connection
|
||
refused` means nothing is listening on that port in the codespace yet; it
|
||
starts serving as soon as `pnpm dev` is up, no restart needed. Pass ports to
|
||
override the defaults (`pnpm session tunnel 5678 8080 5679`), but always
|
||
forward the pair together with matching numbers: the Vite dev UI points its
|
||
API base at `localhost:5678` (the `N8N_PORT` default), so an asymmetric or
|
||
partial mapping breaks it.
|
||
|
||
## What survives what
|
||
|
||
| Event | Running processes | Disk (checkout, worktrees, chat history) |
|
||
|---|---|---|
|
||
| Detach / close laptop / network drop | ✅ keep running | ✅ |
|
||
| Stop, or idle timeout (default 30 min, max 4 h) | ❌ killed | ✅ |
|
||
| Delete (`pnpm session rm`) | ❌ | ❌ (push your branches first) |
|
||
|
||
After a stop, `pnpm session <name>` restarts the codespace (~30–60 s); run
|
||
`claude --resume` (or `--continue`) inside to restore the conversation from disk.
|
||
|
||
## Gotchas (learned the hard way)
|
||
|
||
- **Codespaces clones the repo to `/workspaces/n8n`**, not `/workspaces` —
|
||
`workspaceFolder` here differs from the laptop config on purpose.
|
||
- **A failing `onCreateCommand` is fatal**: Codespaces discards the container
|
||
and drops you into a minimal recovery container (no node, no CLIs). If your
|
||
codespace has nothing installed, check
|
||
`/workspaces/.codespaces/.persistedshare/creation.log`.
|
||
- **`gh codespace ssh` needs sshd inside the container** — provided by the
|
||
`sshd` devcontainer feature, don't remove it.
|
||
- **User secrets aren't visible in ssh shells by default**: the codespace
|
||
agent injects them into VS Code sessions only; they're delivered
|
||
base64-encoded to `/workspaces/.codespaces/shared/.env-secrets`. The image
|
||
sources `/usr/local/lib/codespaces-env.sh` in login shells (profile.d), in
|
||
interactive shells (bashrc), and in the `pnpm session` prelude. If Claude
|
||
Code shows `Missing environment variables: FLAKY_MCP_TOKEN`, the shell that
|
||
started Claude did not source the file. Run
|
||
`. /usr/local/lib/codespaces-env.sh` and start Claude again.
|
||
- **You cannot paste images into a remote Claude session.** Image paste reads
|
||
the clipboard of the machine where `claude` runs — the codespace, not your
|
||
laptop. Drag the file into the VS Code explorer (or
|
||
`gh codespace cp shot.png remote:/workspaces/n8n/`) and give Claude the
|
||
path. The file stays on disk and survives detach and `--resume`.
|
||
- **`git push` / `gh` return 401 in tmux and long sessions** — same root
|
||
cause as the secrets gotcha, plus rotation: Codespaces refreshes the
|
||
on-disk `GITHUB_TOKEN` every few minutes, so a login-time snapshot goes
|
||
stale. The image fixes both: a credential helper reads the current token
|
||
on each `git push` (`gitcredential-refresh.sh`), and a shim at
|
||
`/usr/local/bin/gh` does the same for `gh`. The token is scoped to
|
||
`n8n-io/n8n`: fork-based flows do not work, push branches directly. Do
|
||
not add SSH keys as a workaround — they have no per-repo granularity.
|
||
- Codespaces created by org members on this repo are **org-owned and
|
||
org-billed** (organization ownership + a monthly Codespaces budget are
|
||
enabled for n8n-io). Codespaces created before that change, or by
|
||
non-members, bill to the personal account (free tier: 120 core-hours/month).
|
||
If creation unexpectedly falls back to personal billing, the org budget is
|
||
exhausted for the period.
|
||
|
||
## Costs
|
||
|
||
Compute bills only while the codespace runs: ~$0.36/hr (4-core) / ~$0.72/hr
|
||
(8-core), ~nothing stopped. Usage draws from a shared monthly org budget, so
|
||
`pnpm session stop` when you leave; the idle timeout is the backstop, not the
|
||
plan.
|