# Device-Flow Setup

`covibe-local setup` can provision per-agent MCP tokens without the manual
create-token-then-export ritual. When no token is available it runs an
RFC-8628-style device flow: the CLI requests a grant, the developer approves it
once in the Co-Vibe console, and the server mints one typed token per requested
agent (`claude-code`, `codex`, `cursor`). Typed tokens let the server attribute
sessions and usage to the exact agent instead of trusting the self-reported
`agent_type` at session start.

## Flow

1. `covibe-local setup` finds no `--token`, no usable `COVIBE_MCP_TOKEN`, and
   at least one requested agent without a stored credential token for the base
   URL, so it POSTs `{machine_name, agents[]}` (the missing agents only) to
   `/api/setup/device`.
2. The server creates a `setup_device_grants` row and returns a short-lived
   user code (8 characters, shown as `XXXX-XXXX`), a one-time device code, a
   `verification_uri`, `expires_in` (600 seconds), and a poll `interval`.
3. The CLI prints `Open <verification_uri> and confirm code XXXX-XXXX`,
   best-effort opens the browser (`open` on macOS, `xdg-open` on Linux,
   `start` on Windows — never a hard failure), and polls
   `/api/setup/device/poll` with the device code at the server interval,
   backing off on 429 via the `retry-after` header.
4. The developer signs in to the console at `/setup/authorize?code=XXXX-XXXX`,
   reviews the machine name and requested agents, and approves or denies. The
   approval binds the developer, tenant, and workspace from the signed-in
   session. A signed-out visit round-trips through sign-in with a sanitized
   `returnTo`, so the code survives the hosted (WorkOS) sign-in redirect.
5. The next poll mints one typed token per requested agent, marks the grant
   consumed, and returns the tokens exactly once as
   `{"tokens": {"claude-code": "cv_...", ...}}`. A replayed poll gets
   `{"status": "consumed"}` and no tokens.
6. The CLI writes the tokens into the credentials file (merging with existing
   agent tokens) and continues with the normal config writes. When stored
   credentials already cover every requested agent, setup is fully zero-touch;
   `--dry-run` always skips the flow.

`--agents claude-code,codex` overrides agent auto-detection (Claude Code is
always included by default; Codex and Cursor are added when their home
directories are detected). The list scopes both token minting and the Codex
and Cursor config writes: an agent left out of `--agents` gets no config even
when its home directory is detected, and a requested agent whose home
directory is missing still gets its token but its config write is skipped
with an INFO line (Codex and Cursor cannot run without their home
directories). The repo-local Claude Code config is always written.

## Endpoints

- `POST /api/setup/device` (unauthenticated, rate-limited): create a grant.
- `POST /api/setup/device/poll` (unauthenticated, rate-limited): poll by
  device code; terminal states are `approved` (tokens, once), `denied`,
  `expired`, and `consumed`.
- `GET/POST /api/setup/device/grant` (cookie-authenticated): look up a grant
  by user code for the approval page and approve or deny it.

## Credentials File

Tokens land in `~/.covibe/credentials.json` (override with
`COVIBE_CREDENTIALS_PATH`; tests always set it to a temp path). The file is
written with mode 0600 and its directory with 0700:

```json
{
  "version": 1,
  "servers": {
    "http://localhost:3000": {
      "tokens": { "claude-code": "cv_...", "codex": "cv_..." }
    }
  }
}
```

The file is per-user and keyed by lowercase origin, so running setup in a
second repo against the same server is zero-touch. Long-lived processes (the
machine-level watch service) resolve the token from this file at runtime on
every tick — including after an auth failure — so token rotation never
requires a service reinstall and the service unit embeds no token.

## COVIBE_AGENT Markers And Token Resolution

Setup writes a non-secret `COVIBE_AGENT` marker into each agent config:
`claude-code` in `.mcp.json`, `codex` in the `~/.codex/config.toml` env, and
`cursor` in `.cursor/mcp.json`. Configs stay secret-free — the raw-token
validators still fail on any literal `cv_` value.

Everywhere a token is needed (the stdio MCP bridge, the Claude Code and Cursor
hooks, companion snapshot/telemetry/watch), resolution order is:

1. an explicit `--token` flag (companion CLI only)
2. the `COVIBE_MCP_TOKEN` environment variable (legacy shared-token setups
   keep working unchanged)
3. the credentials file, looked up by `COVIBE_BASE_URL` plus `COVIBE_AGENT`

Empty strings and unexpanded `${...}` placeholders — what agent runtimes pass
when the shell variable is unset — count as absent, so the credentials
fallback actually fires.

Hook commands carry no env of their own: when `COVIBE_BASE_URL` is unset, the
Claude Code and Cursor hooks read the Co-Vibe server env block in the
project's `.mcp.json` / `.cursor/mcp.json` to discover the origin before
defaulting to `http://localhost:3000`, so hosted (and non-:3000 local) setups
resolve credentials and post telemetry against the right server.

## Doctor

`covibe-local doctor` adds a `credentials` check. The expected agent list is
derived from what setup actually wired — `claude-code` when `.mcp.json`
carries the `COVIBE_AGENT` marker, `codex` when `~/.codex/config.toml` has the
Co-Vibe block, `cursor` when `.cursor/mcp.json` has the Co-Vibe server —
falling back to auto-detection only before any config exists, so a deliberate
`setup --agents claude-code` never fails on a detected-but-unwired agent. The
check passes when the file exists with 0600 permissions and a token per
expected agent, fails on loose permissions or a missing agent token
(downgraded to info while the legacy env token still covers every agent), and
is info when no credentials exist yet. The
`token` check warns on legacy shared `COVIBE_MCP_TOKEN` setups and suggests
rerunning setup. A config written before this feature lacks the
`COVIBE_AGENT` marker; the `MCP config` check downgrades that single defect
to a warn — such setups still work via the legacy env token — and rerunning
`covibe-local setup` migrates them in place. Any other config defect (stale
base URL, raw token, wrong cwd) stays a hard fail.

## Updates

The machine service keeps itself current. Each watch tick the companion reports
its installed version on the session heartbeat; when the server advertises a
newer hosted tarball (see `mcp-contract.md`), a `--service` run silently
self-updates between ticks. Auto-update is intentionally narrow:

- **Only the service updates.** Service mode is detected by the `--service`
  flag alone — foreground `watch`, pipes, CI, and test subprocesses never
  self-update. Setup adds `--service` to the units it installs.
- **Only npm-managed installs update.** The package must be `co-vibe` directly
  under a `node_modules/` directory with a matching `package.json` and no
  `.git`. Git checkouts get a `git pull` hint instead; npx caches are refused.
- **Disable it** with the environment variable `COVIBE_AUTO_UPDATE=off`, or the
  marker file `~/.covibe/auto-update-off`. `covibe-local setup --no-auto-update`
  writes that marker at setup time; `covibe-local update --off` / `--on` toggle
  it afterward.

`covibe-local update` is the manual path: a one-off session-less heartbeat
fetches the version handshake, then the updater runs immediately (it ignores
the once-per-hour throttle and the service-mode gate, but still enforces the
sha256 and npm-root gates). On a git checkout it prints the `git pull` hint and
exits cleanly.

The update mechanism downloads the tarball from the server's `/downloads`,
verifies its sha256 against the handshake, and `npm install`s the `.tgz` into
the install prefix with `--no-save --no-package-lock --ignore-scripts` (local
installs) or `npm install -g` (global installs) so the host project's
`package.json` and lockfile stay byte-identical and host lifecycle scripts
never run. Because the sha256 gate already proved the bytes are exactly what the
server intended, a non-zero `npm install` exit is treated as environmental
(EACCES/ENOSPC, a held npm lock, an engine mismatch) — the attempt is recorded
for WARN spacing but the version is **not** blacklisted, so a healed machine
retries it automatically. The still-running old code then boot-checks the new
install (`covibe-local --version`); on failure it auto-reverts to the previous
tarball, records the failed version so it is not retried, and keeps running the
old code. Genuine version faults (a version mismatch or a failed boot-check) are
the only failures that blacklist a release. On success it logs `INFO update: updated <old> → <new>` and exits 0 so
launchd/systemd respawn the new code on the next supervised restart.

**Rollout:** machines whose service was installed before this release re-run
`covibe-local setup` once to gain `--service`. The write-only unit-drift
self-repair also rewrites a stale unit file at the next login or boot, so old
units pick up `--service` with no manual step at the next restart.

## Automatic Repo Registration

There is no per-repo registration command. Agent activity — the Claude Code
and Cursor hooks, the stdio MCP bridge, and the companion's own sync commands
— registers the enclosing git repo root in `~/.covibe/repos.json` (override
with `COVIBE_REGISTRY_PATH`). Entries are keyed per server base URL, so a repo
used against two Co-Vibe servers gets one entry per server, and the machine
watcher only syncs repos registered for its own server. Registration is local
metadata only; nothing syncs at registration time, and the home directory
itself is never registered.

Setup resolves the new-repos policy up-front: on a TTY it asks once —
`Sync git activity from new repos on this machine to <server>? [Y/n]` — and
the `--log-new-repos` / `--no-log-new-repos` flags skip the prompt for
automation (non-TTY and `--dry-run` leave the stored policy untouched). Under
the opt-in policy, first activity registers a repo as *pending*: the agent
gets the one-line hint "new repo registered but not logged — run
`covibe-local include` inside it to opt in" (once per repo), the repo shows a
`pending` pill in Settings → Connected repos, and no data leaves the machine
until it is included.

## Excluding Personal Repos

Every repo a developer works in is logged to Co-Vibe unless excluded — setup
prints exactly this disclosure after wiring configs, and doctor repeats it as
the `exclusions` check. To keep a personal repo private:

```bash
npm exec -- covibe-local exclude            # exclude the current repo
npm exec -- covibe-local exclude /path/to/repo
npm exec -- covibe-local exclude --list     # show excluded repos and the policy
npm exec -- covibe-local exclude --remove   # log the current repo again
```

To flip the machine to opt-in instead — new repos are NOT logged until you opt
them in — use the `--new-repos` policy:

```bash
npm exec -- covibe-local exclude --new-repos           # stop logging new repos
npm exec -- covibe-local include                       # opt the current repo in
npm exec -- covibe-local exclude --new-repos --remove  # log new repos again
```

Exclusions are machine-level, stored canonicalized in
`~/.covibe/exclusions.json` (override with `COVIBE_EXCLUSIONS_PATH`), and cover
the repo plus its subdirectories. Exclusion is sticky: `covibe-local exclude`
also deletes the repo's registry entries (every server), and no session
activity can re-register an explicitly excluded repo — only
`covibe-local include` inside it lifts the exclusion, re-registers it for the
CLI's server, and lets the machine watcher resume on its next tick. For an
excluded repo, the Claude Code and
Cursor hooks exit before any tool call or state write, the stdio MCP bridge
returns a terminal error without contacting the API, `covibe-local snapshot`,
`covibe-local telemetry`, and the trailing setup snapshot send nothing, and
the machine-level `covibe-local watch` filters the repo out of its per-tick
sync set (snapshots, telemetry inbox flushes, heartbeats) instead of exiting;
only the single-repo debug form, `watch --repo <path>` pointed at an excluded
repo, warns and exits. The Codex and Cursor usage sweeps are scoped to the
repo of the watch process's working directory (the machine service runs from
`$HOME`, so its sweeps carry no repo attribution), so an excluded repo's
threads never sync from another repo's watcher either. The server never sees
the repo.

The repo an agent works in is resolved deterministically from the OS/runtime,
never from agent self-report: repo-local configs make the bridge's
`process.cwd()` correct for Claude Code and Cursor, and the global Codex
config forwards the shell-set `PWD` via `env_vars` so the bridge checks the
repo Codex was actually launched in (see `codex-integration.md`).

## Security Notes

- The server stores only the SHA-256 hash of the device code; the raw code is
  returned once at grant creation and never logged.
- Raw `cv_` tokens are delivered exactly once by the poll endpoint, never
  appear in work-event payloads or CLI logs, and are never written into agent
  configs.
- Grants expire after 600 seconds; approved-but-unconsumed grants expire one
  TTL after approval, and pending grants cannot be approved past expiry.
- Both unauthenticated endpoints and the user-code lookup are rate limited.

## Verification

```bash
npx vitest run tests/unit/credentials-file.test.ts tests/unit/device-flow.test.ts \
  tests/unit/setup-device-command.test.ts tests/unit/doctor-credentials.test.ts
```
