Deployment

Deployment

This is the deployment posture for r1, covering today (main) plus the

r1d daemon posture introduced by specs/r1d-server.md, the web

hosting model from specs/web-chat-ui.md, and the desktop sidecar

fallback** from specs/desktop-cortex-augmentation.md.

Build and verification gate

go build ./cmd/r1
go test ./...
go vet ./...

These three commands are the CI gate. They must be green on every PR.

CI also runs:

version. Findings surface as ::warning:: annotations and are

advisory (a 30-PR cleanup campaign closed 600+ findings; new

findings are welcomed as separate cleanup PRs).

vulnerabilities trigger a Go-version upgrade PR rather than a code

change.

Once spec 5 lands, an additional gate is added:

on any os.Chdir, os.Getwd, or filepath.Abs("") call without a

// LINT-ALLOW chdir-*: reason annotation. This is the **mandatory

gate before multi-session is enabled** in r1d.

Deployment surfaces

Today, on main:

SurfaceBest fitNotes
CLI installindividual operators and developerscanonical r1 binary; ~30 subcommands
Container / release artifactspackaged distributionrelease automation and signed artifacts via goreleaser
Pack registry HTTP servicedeterministic skill distributionr1 skills pack serve
IDE pluginsVS Code + JetBrainscode in-tree; marketplace publishing pending
Tauri 2 desktop shellper-machine GUIR1D-1..R1D-12 phases shipped
Per-machine dashboardlocal observabilityr1-server on port 3948; live event stream + 3D ledger visualizer
Mission API HTTP serverprogrammatic accessstoke-server, r1 serve

After spec 5 lands:

SurfaceBest fitNotes
r1 serve daemonper-user singleton, hosts N concurrent sessionsWatchman pattern; spawn-on-demand; multi-session goroutines; cmd.Dir per session
Web app at /browser usersserved by the daemon from internal/server/static/dist/ (embedded via //go:embed static); CSP locked to loopback
Tauri 2 desktop with sidecar fallbackoffline desktop usersdiscover-or-spawn: probes ~/.r1/daemon.json, falls back to bundled r1 via ShellExt::sidecar
MCP endpointexternal agent integrationevery UI action has an MCP equivalent; internal/mcp/r1_server.go consolidated catalog

Install paths (today)

# 1. Homebrew (macOS + Linux) — published by goreleaser on each tag.
brew install RelayOne/r1-agent/r1

# 2. One-line installer — detects platform, verifies cosign signature
#    (keyless OIDC via sigstore) when cosign is on PATH, falls back to
#    building from source if no prebuilt binary exists for your target.
curl -fsSL https://raw.githubusercontent.com/RelayOne/r1-agent/main/install.sh | bash

# 3. Docker (linux/amd64 + linux/arm64; distroless, multi-stage).
docker pull ghcr.io/RelayOne/r1:latest

# 4. From source (Go 1.26+; CGO enabled for SQLite).
go build ./cmd/r1
sudo mv r1 /usr/local/bin/

# Verify a signed release tarball.
cosign verify-blob \
  --certificate-identity-regexp 'https://github\.com/(RelayOne/r1|ericmacdougall/Stoke)/\.github/workflows/release\.yml@refs/tags/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --signature r1_<ver>_<os>_<arch>.tar.gz.sig \
  r1_<ver>_<os>_<arch>.tar.gz

r1d daemon — install and discovery

The daemon is per-user singleton on-demand: no launchd / systemd /

SCM unit needed at first run. r1 chat calls connect() on the IPC

endpoint; if it fails, forks r1 serve itself. The first r1 chat

becomes the implicit daemon launcher.

For users who want always-on operation:

# macOS — writes ~/Library/LaunchAgents/dev.relayone.r1.plist
r1 serve --install

# Linux (systemd-user) — writes ~/.config/systemd/user/r1.service
r1 serve --install
# For headless boxes (SSH-only, no graphical login):
loginctl enable-linger $USER

# Windows (Service Control Manager) — registers service "r1.daemon"
r1 serve --install

# Inverse — stop and remove the unit:
r1 serve --uninstall

# Status:
r1 serve --status

Behind the scenes: r1 serve --install uses

github.com/kardianos/service to write a platform-appropriate service

unit. The service runs as the current user; auto-start is per-user, not

system-wide.

Discovery

On startup, the daemon writes ~/.r1/daemon.json (mode 0600):

{
  "pid": 12345,
  "sock": "/run/user/1000/r1/r1.sock",
  "port": 54123,
  "token": "<32 random hex bytes>",
  "version": "<git sha>",
  "protocol_version": 1
}

Clients discover via:

(no auth; loopback-only).

sessions, uptime_s}` (no auth).

The token rotates on every daemon start. Old clients see 401 and

re-discover via the file. Daemon refuses to start if ~/.r1/,

~/.r1/daemon.json, ~/.r1/daemon.lock, or the socket file have wider

permissions than expected after the chmod (fail-closed).

Single-instance enforcement

gofrs/flock advisory lock on ~/.r1/daemon.lock. Plus the

bind-is-exclusive property of the socket path / port. If a second

r1 serve runs:

$ r1 serve
daemon already running, pid=12345, sock=/run/user/1000/r1/r1.sock
use 'r1 ctl' to talk to it.

Exit code 1.

Listeners and ports

The daemon binds three listeners:

SurfaceEndpointAuth
CLI (r1 chat, r1 ctl)$XDGRUNTIMEDIR/r1/r1.sock (Linux/macOS) / \\.\pipe\r1-<USER> (Windows)Peer-cred check (SOPEERCRED / LOCALPEERCRED); no token
Web / Desktop / MCPws://127.0.0.1:<port> + http://127.0.0.1:<port>256-bit Bearer (HTTP) or Sec-WebSocket-Protocol: r1.bearer, <token> (WS); Origin pin + Host pin

current SID.

written to discovery file after all listeners accept connections

(CLI clients retry with 2s backoff).

Origin / Host pinning (CSWSH + DNS-rebind defense)

http://127.0.0.1:<port>, http://localhost:<port>, or

tauri://localhost. Configurable via ~/.r1/daemon.toml.

new WebSocket(url) from a malicious page without the subprotocol

fails the handshake.

Multi-instance management

The daemon hosts N concurrent sessions as goroutines, each with a

SessionRoot string field. The cmd.Dir discipline is non-negotiable:

# Create a session
curl -X POST -H 'Authorization: Bearer <token>' \
  http://127.0.0.1:<port>/v1/sessions \
  -d '{"workdir":"/home/eric/repos/foo","model":"claude-sonnet-4-6"}'
# {"session_id":"sess_xxx","workdir":"/home/eric/repos/foo","started_at":"..."}

# List sessions
curl -H 'Authorization: Bearer <token>' \
  http://127.0.0.1:<port>/v1/sessions
# {"sessions":[{"id":"sess_xxx","workdir":"/...","state":"running",...}]}

# Pause / resume / kill
curl -X POST -H 'Authorization: Bearer <token>' \
  http://127.0.0.1:<port>/v1/sessions/sess_xxx/pause
curl -X DELETE -H 'Authorization: Bearer <token>' \
  http://127.0.0.1:<port>/v1/sessions/sess_xxx

CLI equivalent (no token needed thanks to peer-cred):

r1 ctl sessions list
r1 ctl sessions get sess_xxx
r1 ctl sessions start /home/eric/repos/foo
r1 ctl sessions kill sess_xxx

Limits:

of scope for this iteration).

process limits (FD count, RAM). The bench/r1dservebenchtest.go

soak test asserts 50 sessions × 100 messages stable over an hour.

Hot upgrade

Restart-required, transparent. **No tableflip, no FD-pass, no plugin

tricks.**

r1 update                      # downloads new binary to ~/.r1/bin/r1 atomic-rename
r1 serve restart               # daemon receives daemon.shutdown {grace_s: 30}
                               #   broadcasts session.paused to every active subscriber
                               #   fsyncs every journal
                               #   exits 0
                               # new binary spawned (init via on-demand path or service)
                               # new daemon scans ~/.r1/sessions-index.json
                               # re-opens each journal.ndjson
                               # rebuilds *Session (workspace + Lobe state from journal)
                               # broadcasts daemon.reloaded {at, version: <new-sha>}

r1 doctor detects "installed binary newer than running daemon" and

prompts r1 serve restart.

Clients reconnect with Last-Event-ID (SSE) or since_seq (JSON-RPC) →

server replays from the journal. Protocol version handshake: WS

subprotocol negotiates r1.proto.v1; if a client requests r1.proto.v2

and server only knows v1, server closes with code 1002 and a

migration_hint close reason.

Journal storage paths

Per-session journal:

Daemon-level files (under ~/.r1/, mode 0700):

journalpath, lastseq}}`. Updated atomically (tmp+rename + fsync

parent dir) on Create / Kill / flush.

MemoryCuratorLobe auto-writes (`{ts, entryid, category, contentsha,

sourcemsgid}`).

sessionID}`.

retention overrides, etc.).

Backwards compatibility

Spec 5 consolidates r1 daemon and r1 agent-serve into r1 serve

without breaking either:

r1 daemon enqueue/status/workers/pause/resume/wal/tasks become

aliases for r1 ctl <verb>. Both keep working with a one-line

deprecation hint to stderr.

r1 serve --enable-agent-routes --addr.

Lane events are additive; they share the event field convention.

lane during the lanes-protocol migration window. After one minor

release, surfaces SHOULD prefer lane.delta.

Web UI hosting

Built artifacts emit to internal/server/static/dist/ and ship via the

existing embed.FS in internal/server/embed.go. Build:

cd web
npm ci
npm run build           # tsc --noEmit && vite build
                        # output: ../internal/server/static/dist/
cd ..
go build ./cmd/r1       # produces a binary that includes the SPA

CI gate runs cd web && npm ci && npm run build && npm run test before

the existing go build ./cmd/r1 && go test ./... && go vet ./...

triple.

Dev workflow:

cd web
npm run dev             # Vite on :5173; SPA connects to daemon at :7777
                        # cross-origin during dev; daemon Origin allowlist
                        # must include http://127.0.0.1:5173

CSP enforced in index.html:

default-src 'self';
connect-src 'self' ws://127.0.0.1:* http://127.0.0.1:*;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
script-src 'self';
worker-src 'self' blob:;
frame-ancestors 'none';

Test gates: Vitest + jsdom (npm run test), Playwright multi-browser

e2e (npm run test:e2e), @axe-core/playwright accessibility scan

(zero serious/critical violations on every route). Storybook MCP runs

component stories.

Desktop sidecar fallback

The Tauri 2 desktop bundles the r1 binary as bundle.externalBin:

Discovery flow on app launch (tauri::Builder::setup):

1. readdaemonjson() reads ~/.r1/daemon.json. If present and fresh,

tries probe_external() (1s timeout TCP connect to

ws://127.0.0.1:<port>).

2. On NotFound | Refused, spawn_sidecar() runs the bundled r1 serve

--port=0 --emit-port-stdout via ShellExt::sidecar. Reads the

chosen port from the child's stdout NDJSON

daemon.listening event.

3. Stores DaemonHandle{mode, url, token, child} in

tauri::State<Mutex<Option<DaemonHandle>>>.

4. On window close / app.exit: if mode == Sidecar, send

daemon.shutdown over WS, wait 5s, then child.kill().

The discovery banner in the UI (<DaemonStatus>) shows:

Wizard offers r1 serve --install the first time a sidecar is spawned

("Run as a system service so the app starts faster next time").

CSP delta in tauri.conf.json adds connect-src ws://127.0.0.1:*

(loopback only; explicitly NOT a ws: wildcard).

Daemon vs UI auto-start

Two independent concerns:

launchd / systemd-user / Windows SCM. The daemon runs even when no UI

is attached.

tauri-plugin-autostart. Login Items (macOS) / Run registry key

(Windows) / ~/.config/autostart/r1-desktop.desktop (Linux).

You can have either, both, or neither. The desktop's "Reconnect daemon"

button re-runs discoverorspawn if the user installs r1 serve

mid-session.

MCP endpoint exposure

The MCP endpoint is exposed in two ways:

(alias for stoke-mcp).

consolidated tool catalog (r1.session.*, r1.lanes.*,

r1.cortex.*, r1.mission.*, r1.worktree.*, r1.bus.tail,

r1.verify.*, r1.tui.*). MCP tool calls flow through the same

JSON-RPC 2.0 dispatcher as session control verbs.

The legacy stoke* aliases (buildfromsow, getmissionstatus,

getmissionlogs, cancelmission, listmissions) are preserved

verbatim until v2.0.0 per canonicalStokeServerToolName.

External MCP servers (GitHub, Linear, Slack, Postgres, custom) are

configured in stoke.policy.yaml:

mcp_servers:
  - name: linear
    transport: stdio
    command: linear-mcp-server
    auth_env: LINEAR_API_KEY
    trust: untrusted
    max_concurrent: 4
  - name: github
    transport: http
    url: https://api.github.com/mcp
    auth_env: GITHUB_TOKEN
    trust: trusted
    timeout: 30s

HTTP/HTTPS enforcement: non-localhost URLs must be https:// unless the

URL is http://localhost:* or http://127.0.0.1:*. Trust gating:

untrusted workers can only invoke tools from untrusted servers.

Operational runtime inputs

Deployment depends on the existing r1 runtime basics:

CLI, OpenRouter API key, direct Anthropic API key, or none for

lint-only fallback).

~/.r1/).

For the cortex / lanes scope, additional inputs:

fallback via internal/provider/).

journal, a WAL, a few subprocesses, a WS subscription).

What operators should verify post-deploy

Today, on main:

behave correctly.

being promoted.

experience.

After the cortex / lanes / multi-surface scope lands:

filepath.Abs("") calls).

token visibly rotates across restarts.

route.

is green under -race -count=10.

(cmd/r1/serveintegrationtest.go::TestKillAndResume) replays

journals correctly and reconnecting clients see daemon.reloaded

followed by deltas with monotonic seq.

Lobes hitting the same warmed breakpoint).

matching MCP tool).

Status

Done

Tauri R1D-1..R1D-12.

In Progress

cases.

Scoped

(spec 8).

Scoping

Potential — On Horizon

Pages in this directory