Engineering

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 installs 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
View as .md