diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e718c423c..755396dff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: - name: Lint run: npm run lint + - name: Frontend JS syntax check + run: npm run check:frontend-syntax + - name: Format check run: npm run format:check @@ -60,6 +63,34 @@ jobs: cat /tmp/boot.log exit 1 -# Note: The test suite is intentionally excluded from CI. -# Tests spawn real tmux sessions and require a full system environment. -# Run tests locally with: npx vitest run test/.test.ts + test: + name: Unit & integration tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install tmux + run: | + if ! command -v tmux >/dev/null; then + sudo apt-get update -qq + sudo apt-get install -y tmux + fi + + - name: Run unit & integration tests + # Excludes the browser-driven mobile suite (test/mobile/**); see config/vitest.ci.config.ts. + # Safe in CI: TmuxManager no-ops all shell commands under VITEST (test/setup.ts). + run: npm run test:ci + +# Note: The browser-driven mobile suite (test/mobile/**) is excluded from CI — +# it needs a live server + chromium + environment-specific PNG baselines. +# Run it locally/manually. All other tests run via the `test` job above. diff --git a/CLAUDE.md b/CLAUDE.md index 527b5d108..a25de045d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ The production server caches static files for 1 year, `immutable` (`maxAge: '1y' ## COM Shorthand (Deployment) -Uses [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`) via `@changesets/cli`. +Uses [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`) via `@changesets/cli`. What SemVer actually covers (the CLI + documented env vars are public; the HTTP/SSE API, on-disk state, and experimental features are internal/unstable) is defined in `docs/versioning-policy.md`. Security reporting + known limitations live in `SECURITY.md`. When user says "COM": 1. **Determine bump type**: `COM` = patch (default), `COM minor` = minor, `COM major` = major @@ -66,7 +66,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph **TypeScript Strictness** (see `tsconfig.json`): `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `allowUnreachableCode: false`, `allowUnusedLabels: false`. -**Requirements**: Node.js 18+, Claude CLI, tmux +**Requirements**: Node.js 22+, Claude CLI, tmux **Git**: Main branch is `master`. SSH session chooser: `sc` (interactive), `sc 2` (quick attach), `sc -l` (list). @@ -102,7 +102,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph - **Effort is NOT an env var** — never carry effort as `CLAUDE_CODE_EFFORT_LEVEL`: the env var hard-locks effort and blocks in-session `/effort` switching (incl. ultracode). It flows as the dedicated `effort` payload field → `Session._effort` → `claude --effort ` for regular levels incl. `max` (the settings `effortLevel` key is `enum(["low","medium","high","xhigh"]).catch(undefined)` — `max` gets SILENTLY dropped there), or `claude --settings '{"ultracode":true}'` for ultracode (rejected by `--effort`). Both are soft defaults the user can override anytime. Legacy env-var entries are auto-migrated by the Session constructor and unset from tmux sessions in `applyEnvOverrides()`. See `buildEffortCliArgs()` in `session-cli-builder.ts`, tests in `test/effort-injection.test.ts` - **Dual-CLI prefix discipline** — Codeman supports both Claude Code and OpenCode (`claude-cli-resolver.ts` / `opencode-cli-resolver.ts`); env-var prefix is CLI-specific (`CLAUDE_CODE_*` vs `OPENCODE_*`) and the allowlist in `schemas.ts` enforces this. When adding settings, decide which CLI(s) it applies to and gate the env export accordingly — don't blindly forward both prefixes. See `docs/opencode-integration.md` for the OpenCode resolver design - **Zod `.optional()` rejects `null`** — accepts `undefined` only. When the frontend builds a request body with `JSON.stringify`, an explicit `null` field is preserved on the wire and fails validation with `INVALID_INPUT`. Convert `null` → `undefined` before stringifying (e.g. `field: value ?? undefined`), or declare the schema `.nullish()`. Real bugs caused: 0.6.4 (`durationMinutes` for ∞ respawn), and the same shape pattern hit `opusContext1mEnabled` in 0.6.3 -- **`xterm-zerolag-input` is duplicated** — the local-echo overlay lives in BOTH `packages/xterm-zerolag-input/src/` (published to npm as a standalone library for external consumers — see README "Published Packages") AND inline inside `src/web/public/app.js` (runtime copy the web UI actually loads, since the page ships as plain JS without a bundler). Any change to overlay behavior MUST be applied to both, or dev and prod diverge — and a public API break in the package warrants a separate version bump for `xterm-zerolag-input` in the changeset. Always test on mobile after touching it. See `docs/local-echo-overlay-plan.md`. +- **`xterm-zerolag-input` is single-source — edit the package, then rebuild the bundle** — the local-echo overlay source lives ONLY in `packages/xterm-zerolag-input/src/` (`zerolag-input-addon.ts`; also published to npm as a standalone library — see README "Published Packages"). It is bundled (esbuild → IIFE, with appended `window.LocalEchoOverlay` aliases) into the **gitignored** `src/web/public/vendor/xterm-zerolag-input.js` by `scripts/postinstall.js` (for dev/`tsx`) and into `dist/.../vendor/` by `scripts/build.mjs:50` (for prod). `app.js` only **consumes** it via `new LocalEchoOverlay(terminal)` — there is NO inline copy to keep in sync. So: change behavior in the package source, then re-run the bundle step (`npm install` reruns postinstall; `npm run build` for prod); **never hand-edit `app.js` for overlay behavior or commit the gitignored vendor bundle**. A public-API break in the package still warrants a separate `xterm-zerolag-input` version bump in the changeset. Always test on mobile after touching it. See `docs/local-echo-overlay-plan.md`. - **Default bind is loopback-only; non-loopback without a password starts but warns** — since COD-29 (PR #107) the web server defaults to `--host 127.0.0.1` (was `0.0.0.0`). As of **0.9.0** binding a non-loopback host (`--host`/`-H`/`CODEMAN_HOST`) without `CODEMAN_PASSWORD` **no longer refuses to start — it starts and prints a loud warning** listing the fixes (set `CODEMAN_PASSWORD`, bind loopback + tunnel/`tailscale serve`, or `--allow-unauthenticated-network` / `CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1` to acknowledge → terser note). Host classification is `isLoopbackBindHost()` in `network-auth-policy.ts`; the warn-vs-start logic is in `server.ts` `start()`; flags wired in `cli.ts`. ⚠️ Operational note: the production systemd unit runs `node dist/index.js web --https` with no `--host`, so it binds **localhost only** — reach it remotely via `tailscale serve`/tunnel to `127.0.0.1`, or add `Environment=CODEMAN_HOST=0.0.0.0` + `Environment=CODEMAN_PASSWORD=…` to `~/.config/systemd/user/codeman-web.service`. A loopback bind is reachable through a same-host tunnel (cloudflared/tailscale → `127.0.0.1`) but NOT by a browser hitting the box's LAN IP. Auth user defaults to `admin`. **Full model: `docs/security-architecture.md`.** - **Instance isolation / multi-instance attach danger** — data dir (`~/.codeman`) and tmux socket (`tmux -L codeman`) are PROCESS-WIDE and shared by every Codeman on the machine, derived from `CODEMAN_INSTANCE` via `src/config/instance.ts` (`getDataDir()`/`dataPath()`/`DEFAULT_TMUX_SOCKET`). ⚠️ A 2nd instance on the SAME socket **discovers and attaches PTYs to the first instance's live sessions** (`tmux -L codeman attach-session …`), resizing/mutating them — `$HOME` isolation is NOT enough (tmux is system-global). To run two instances, give each a distinct `CODEMAN_INSTANCE` (scopes BOTH dir+socket: `~/.codeman-` + `-L codeman-`), or set `CODEMAN_TMUX_SOCKET` + `CODEMAN_DATA_DIR` individually. **`CODEMAN_INSTANCE` defaults to empty = the production layout (`~/.codeman`, `-L codeman`, port 3000)**, so this branch is safe to ship to master without disturbing existing installs. To run THIS beta alongside prod, launch with `scripts/run-beta.sh` (`CODEMAN_INSTANCE=beta` + `CODEMAN_PORT=5000`) — it never collides with prod's data dir/socket/port. Any new `~/.codeman/...` path MUST go through `dataPath()`, never `join(homedir(), '.codeman', …)`. @@ -126,11 +126,11 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | | | **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts` | | | **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | | -| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (15 route modules + barrel), `src/web/route-helpers.ts`, `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts`, `src/web/self-update.ts` | | +| **Web** | `src/web/server.ts` ★, `src/web/sse-events.ts`, `src/web/routes/*.ts` (15 route modules + barrel; `session-routes.ts` ★), `src/web/route-helpers.ts`, `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts`, `src/web/self-update.ts` | | | **Frontend** | `src/web/public/app.js` (~3.4K lines, core) + 5 infra modules (`constants.js`, `mobile-handlers.js`, `voice-input.js`, `notification-manager.js`, `keyboard-accessory.js`) + 7 domain modules (`terminal-ui.js`, `respawn-ui.js`, `ralph-panel.js`, `orchestrator-panel.js`, `settings-ui.js`, `panels-ui.js`, `session-ui.js`) + 5 feature modules (`ralph-wizard.js`, `api-client.js`, `subagent-windows.js`, `input-cjk.js`, `image-input.js`) + `sw.js` | | | **Types** | `src/types/index.ts` (barrel) → 15 domain files; also `src/types.ts` root re-export | See `@fileoverview` in index.ts | -★ = Large file (>50KB). All files have `@fileoverview` JSDoc — read that before diving in. Discovery aid: `grep -l '@fileoverview' src/web/routes/*.ts` lists all route modules; same grep works for `src/types/`, `src/web/public/*.js`. +★ = Large, central file (>50KB) — read its `@fileoverview` first. All files have `@fileoverview` JSDoc — read that before diving in. Discovery aid: `grep -l '@fileoverview' src/web/routes/*.ts` lists all route modules; same grep works for `src/types/`, `src/web/public/*.js`. **Local packages**: `packages/xterm-zerolag-input/` — local echo overlay for xterm.js; copy embedded in `app.js`. `packages/gesture-control/` (`codeman-gesture-control`) — hand-tracking overlay source; built to `src/web/public/gesture/gesture-codeman.js` via `npm run build:gesture` (see Frontend → Gesture control). @@ -165,7 +165,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph ### Frontend -Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. Load order: `constants.js`(1) → `mobile-handlers.js`(2) → `voice-input.js`(3) → `notification-manager.js`(4) → `keyboard-accessory.js`(5) → `input-cjk.js`(5.5) → `app.js`(6) → `terminal-ui.js`(7) → `respawn-ui.js`(8) → `ralph-panel.js`(9) → `orchestrator-panel.js`(9.5) → `settings-ui.js`(10) → `panels-ui.js`(11) → `session-ui.js`(12) → `ralph-wizard.js`(13) → `api-client.js`(14) → `subagent-windows.js`(15). `input-cjk.js` handles CJK IME composition via an always-visible textarea below the terminal (`window.cjkActive` blocks xterm's onData). +Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. Load order: `constants.js`(1) → `mobile-handlers.js`(2) → `voice-input.js`(3) → `notification-manager.js`(4) → `keyboard-accessory.js`(5) → `input-cjk.js`(5.5) → `app.js`(6) → `terminal-ui.js`(7) → `respawn-ui.js`(8) → `ralph-panel.js`(9) → `orchestrator-panel.js`(9.5) → `settings-ui.js`(10) → `panels-ui.js`(11) → `session-ui.js`(12) → `ralph-wizard.js`(13) → `api-client.js`(14) → `subagent-windows.js`(15) → `image-input.js`(16). `input-cjk.js` handles CJK IME composition via an always-visible textarea below the terminal (`window.cjkActive` blocks xterm's onData). **Z-index layers**: subagent windows (1000), plan agents (1100), log viewers (2000), image popups (3000), local echo overlay (7). diff --git a/LICENSE b/LICENSE index bb7a89c93..69cc33af7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Claudeman Contributors +Copyright (c) 2024-2026 Codeman Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9a91164bf..76e04f36d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

License: MIT - Node.js 18+ + Node.js 22+ TypeScript 5.9 Fastify Tests @@ -427,7 +427,7 @@ When someone authenticates via QR, the desktop shows a notification toast with t ## Security -Codeman launches sessions with `--dangerously-skip-permissions`, so the web UI is by design a remote-code-execution surface for whoever can reach it — the whole security model exists to control *who* that is. Recent hardening (v0.9.0 + v0.9.5) closes the browser-driven attack paths that bite self-hosted dev tools. Full model: [`docs/security-architecture.md`](docs/security-architecture.md). +Codeman launches sessions with `--dangerously-skip-permissions`, so the web UI is by design a remote-code-execution surface for whoever can reach it — the whole security model exists to control *who* that is. Recent hardening (v0.9.0 + v0.9.5) closes the browser-driven attack paths that bite self-hosted dev tools. Full model: [`docs/security-architecture.md`](docs/security-architecture.md). **Found a vulnerability?** See [`SECURITY.md`](SECURITY.md) for private disclosure and the list of known limitations. ### Network & access @@ -651,6 +651,14 @@ npm install xterm-zerolag-input --- +## Versioning + +Codeman follows [SemVer](https://semver.org/). What the version number actually +commits to — and what counts as internal (the HTTP/SSE API, on-disk state, +experimental features) — is spelled out in +[`docs/versioning-policy.md`](docs/versioning-policy.md). If you script against +the HTTP API, pin to an exact version. + ## License MIT — see [LICENSE](LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..077461639 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,78 @@ +# Security Policy + +Codeman launches AI coding sessions with `--dangerously-skip-permissions`, so the +web UI is **by design a remote-code-execution surface for whoever can reach it**. +The entire security model exists to control *who* that is. Please read this before +exposing an instance beyond `localhost`. The full model lives in +[`docs/security-architecture.md`](docs/security-architecture.md). + +## Supported versions + +Security fixes land on the latest published `codeman@X.Y.Z` release and `master`. +Older versions are not patched — upgrade to the latest release (App Settings → +Updates for git-clone installs, or `npm i -g aicodeman@latest`). + +| Version | Supported | +| ------- | --------- | +| latest `0.9.x` / `master` | ✅ | +| anything older | ❌ (upgrade) | + +## Reporting a vulnerability + +**Please do not open a public issue for security problems.** + +Report privately via **GitHub's private vulnerability reporting**: +the repository's **Security** tab → **Report a vulnerability** +(). This opens a private +advisory thread with the maintainer. + +> Maintainer note: enable *Settings → Code security and analysis → Private +> vulnerability reporting* so this channel is live. + +When reporting, please include: affected version/commit, the deployment shape +(loopback-only, `CODEMAN_PASSWORD` set, tunnel/`tailscale serve`, custom +reverse proxy), reproduction steps, and impact. We aim to acknowledge within a +few days. Coordinated disclosure is appreciated — we'll agree a disclosure +timeline with you once impact is confirmed. + +### In scope +- Authentication / session-cookie bypass when `CODEMAN_PASSWORD` is set +- DNS-rebinding, CSRF/CSWSH, or Origin/Host-guard bypass reaching state-changing routes +- Remote code execution reachable **without** local OS access (e.g. via a browser, a tunnel, or a foreign origin) +- Path traversal / arbitrary file read or write through the HTTP API +- Supply-chain integrity of the in-app self-updater + +### Out of scope (by design — see Known limitations) +- Anything requiring an already-trusted **same-machine, same-uid** process. Codeman trusts the local OS user it runs as; a peer process of that user is already inside the boundary. +- Running an authless instance bound to a non-loopback host after dismissing the startup warning (you explicitly acknowledged it). +- The default loopback + no-password posture itself (it is reachable only from the same machine). + +## Trust model (summary) + +- **Loopback by default.** Binds `127.0.0.1`; the no-password default is safe out of the box. Binding a non-loopback host without `CODEMAN_PASSWORD` *starts but prints a loud warning* with concrete fixes. +- **Always-on Host + Origin guards.** Block DNS-rebinding and cross-site state-changing requests even on the no-auth loopback install (a missing Origin is allowed so CLI/hooks work). +- **Optional auth.** HTTP Basic via `CODEMAN_USERNAME`/`CODEMAN_PASSWORD`; success issues an opaque server-side 256-bit cookie. Per-IP rate limiting on failures. +- **Hardened file serving, tmux launch, transport headers, and multi-instance isolation** — see the full architecture doc. + +## Known limitations and accepted risk + +A 1.0 release is an implicit statement that the documented model *is* the model, so +these residuals are stated explicitly. Most sit **inside the same-uid OS trust +boundary** or behind the always-on Origin guard; they matter mainly for +shared-host, multi-user, or tunneled deployments. + +- **Self-update trusts an unsigned release tag.** The in-app updater does `git checkout && npm install` (lifecycle scripts run) of a tag matched only by name shape, from whatever `origin` points to — no signature/commit verification. Treat the updater as trusting your `origin` remote and your release pipeline. (Hardening tracked for 1.0.) +- **CSP ships `'unsafe-inline'`.** Inline handlers mean the Content-Security-Policy is defense-in-depth only; all AI-/file-derived sinks are escaped, but a future missed escape would be executable. +- **`workingDir` is unconstrained.** A session may be created with any absolute working directory (e.g. `/`), which becomes the file-route boundary for that session. Scope it to trusted paths on shared hosts. +- **Hook-event auth exemption is loopback-IP-based.** `POST /api/hook-event` is exempt from auth for loopback callers; because tunnels (cloudflared / `tailscale serve`) terminate at `127.0.0.1`, a loopback-terminating tunnel inherits the exemption. Set `CODEMAN_PASSWORD` and prefer a tunnel that preserves the client identity if this matters. +- **Session cookie is not bound to client IP/UA on reuse, and refreshes without an absolute cap.** A stolen cookie replays until its idle TTL elapses. +- **Multi-instance tmux socket is process-wide.** Two Codeman instances on the same `CODEMAN_INSTANCE` share a tmux socket and can attach each other's live sessions — isolate with distinct `CODEMAN_INSTANCE` values. +- **The live log-tail route reads `/var/log` and `~/logs`** in addition to the session working directory (read-only) — a deliberate choice for tailing system/app logs. On a password-protected remote deployment an authenticated user can therefore read those roots outside their session. See `docs/security-architecture.md` §5. + +Recent hardening (this release): web-push subscription endpoints are restricted +to https public hosts (SSRF guard — rejects internal/metadata IPs, validated at +subscribe and send time), and tmux session names discovered on the shared socket +are validated against the safe-name pattern before reaching any shell call site. + +For the detailed rationale, defenses, and recommended secure setups, see +[`docs/security-architecture.md`](docs/security-architecture.md). diff --git a/config/vitest.ci.config.ts b/config/vitest.ci.config.ts new file mode 100644 index 000000000..24e060527 --- /dev/null +++ b/config/vitest.ci.config.ts @@ -0,0 +1,33 @@ +import { resolve } from 'node:path'; +import { defineConfig, configDefaults } from 'vitest/config'; + +const root = resolve(import.meta.dirname, '..'); + +/** + * CI test config — same as vitest.config.ts but EXCLUDES the browser-driven + * mobile suite (test/mobile/**). Those are Playwright visual-regression tests + * that need a live server + chromium + environment-specific PNG baselines, so + * they are run/maintained separately and are not part of the CI gate. + * + * Keep the rest in sync with config/vitest.config.ts. + */ +export default defineConfig({ + test: { + root, + globals: true, + environment: 'node', + include: ['test/**/*.test.ts'], + exclude: [ + ...configDefaults.exclude, + 'test/mobile/**', // browser/visual (Playwright + chromium) + 'test/perf-*.test.ts', // timing-sensitive perf benchmarks (flaky in CI) + 'test/inline-rename.test.ts', // browser (Playwright) + 'test/opencode-resize.test.ts', // browser (Playwright) + 'test/webgl-fallback.test.ts', // browser (Playwright) + ], + setupFiles: ['./test/setup.ts'], + fileParallelism: false, + testTimeout: 30000, + teardownTimeout: 60000, + }, +}); diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 000000000..7866a8980 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,90 @@ +# HTTP API Reference + +Codeman's HTTP API is a **stable contract** as of 1.0 — see +[`versioning-policy.md`](versioning-policy.md) for the SemVer guarantee. This page +defines the response envelope, status codes, error codes, versioning, and the SSE +event channel. + +## Versioning + +- The stable, public surface is served under **`/api/v1/...`**. Pin external + clients to this prefix. +- The unversioned **`/api/...`** paths are a permanent alias of the current + version (what the bundled web UI uses). They are kept working, but new external + integrations should use `/api/v1`. +- Breaking changes to the contract ship under a new prefix (`/api/v2`); `/api/v1` + keeps its semantics. Additive changes (new endpoints, new optional fields, new + error codes) are non-breaking and may appear in a minor release. +- The implementation rewrites `/api/v1/*` → `/api/*` at the server level + (`rewriteApiV1Url` in `src/web/server.ts`). + +## Response envelope + +Every JSON response uses one uniform envelope, applied centrally by a +`preSerialization` hook (`src/web/server.ts`) — handlers return bare data and the +hook wraps it: + +**Success** — HTTP `2xx`: + +```json +{ "success": true, "data": } +``` + +`data` is the endpoint's payload (object, array, or value). Endpoints with no +payload return `{ "success": true, "data": {} }`. + +**Error** — HTTP `4xx`/`5xx`: + +```json +{ "success": false, "error": "human-readable message", "errorCode": "NOT_FOUND" } +``` + +`ApiResponse` in `src/types/api.ts` is the canonical type. + +> Non-JSON endpoints are exempt from the envelope: `GET /api/sessions/:id/file-raw`, +> `GET /api/sessions/:id/tail-file` (SSE), `GET /api/download`, +> `GET /api/screenshots/:name`, `GET /q/:code` (QR redirect), and the +> `GET /ws/sessions/:id/terminal` WebSocket upgrade. + +## Error codes → HTTP status + +The single source of truth is `ErrorStatus` / `httpStatusForErrorCode()` in +`src/types/api.ts`. Clients should branch on `errorCode` (stable) and may rely on +the HTTP status. + +| `errorCode` | HTTP | Meaning | +|-------------|------|---------| +| `INVALID_INPUT` | 400 | Malformed request / failed validation | +| `UNAUTHORIZED` | 401 | Authentication required or failed | +| `NOT_FOUND` | 404 | Resource does not exist | +| `SESSION_BUSY` | 409 | Session is busy | +| `CONFLICT` | 409 | Conflicts with current state (e.g. already running) | +| `ALREADY_EXISTS` | 409 | Resource already exists | +| `OPERATION_FAILED` | 422 | Well-formed but could not be completed | +| `RATE_LIMITED` | 429 | Too many requests | +| `INTERNAL_ERROR` | 500 | Unexpected server error | + +Adding a new error code is non-breaking; removing or renaming one is a major change. + +## Authentication + +Optional HTTP Basic (`CODEMAN_USERNAME`/`CODEMAN_PASSWORD`) → opaque +`codeman_session` cookie. When enabled, unauthenticated requests get +`401 UNAUTHORIZED`; rate-limited requests get `429 RATE_LIMITED`. See +[`security-architecture.md`](security-architecture.md). + +## SSE event channel + +`GET /api/events` is a Server-Sent Events stream (`text/event-stream`); each +message is `event: ` + `data: `. The event-name registry +(`src/web/sse-events.ts`, mirrored in `src/web/public/constants.js`) is part of +the stable contract — event names are not renamed without a major bump. An +optional `?sessions=` filter suppresses only the high-volume terminal +stream; lifecycle/metadata events are delivered to all clients regardless. + +## Consuming from JavaScript + +The bundled frontend reads responses through `_apiJson()` +(`src/web/public/api-client.js`), which unwraps `{success:true,data}` → `data` and +returns `null` on a non-2xx / `{success:false}` response. External clients should +do the same: check the HTTP status (or `body.success`), then read `body.data`. diff --git a/docs/security-architecture.md b/docs/security-architecture.md index 3b3b20bfd..a2a3300be 100644 --- a/docs/security-architecture.md +++ b/docs/security-architecture.md @@ -306,6 +306,20 @@ injected from API JSON (`innerHTML`), not via `file-raw`, so they are unaffected is **defense‑in‑depth, not the primary boundary** — the realpath containment is the control. +### SSE log‑tail route — intentional extra read roots + +The live file‑tail SSE route (`FileStreamManager`, used to stream a growing log +into the UI) does **not** use `validateSessionFilePath`; it has its own validator +with a deliberately **wider** allowlist: the session `workingDir` **plus two +read‑only log roots — `/var/log` and `~/logs`** — so operators can tail +system/app logs. `/tmp` is intentionally excluded (world‑writable). Like the +other routes it `realpath`s the target and re‑checks right before spawning `tail` +(TOCTOU guard), and it is read‑only. This is the one place the per‑session +boundary is intentionally relaxed; on a password‑protected remote deployment an +authenticated user can therefore read `/var/log` and `~/logs` outside their +session dir. (Security review M5: this divergence is by design and is now +documented here rather than silently diverging from the per‑session claim above.) + ### Known limitation — `workingDir` scope The file‑route boundary is the session's `workingDir`, and `POST /api/sessions` diff --git a/docs/versioning-policy.md b/docs/versioning-policy.md new file mode 100644 index 000000000..01a72d495 --- /dev/null +++ b/docs/versioning-policy.md @@ -0,0 +1,79 @@ +# Versioning & Stability Policy + +Codeman follows [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`), +managed via `@changesets/cli` (see the COM workflow in `CLAUDE.md`). + +This document defines **what the version number actually promises** — i.e. which +surfaces are covered by SemVer and which are explicitly not. It exists because +"1.0" is a commitment to stability, and an undocumented public surface invites +incompatible client assumptions we would then be pressured to keep. + +> **Status:** finalized for the 1.0 cut. The HTTP/SSE API **is** part of the stable +> surface — served under `/api/v1` with a uniform response envelope and +> conventional HTTP status codes. See [`api-reference.md`](api-reference.md). + +## What SemVer covers (the public, stable surface) + +A **MAJOR** bump is required to break any of these after 1.0: + +1. **The CLI.** Command names, documented flags, and their behavior for + `codeman ` (published to npm as `aicodeman`; invoked as `codeman`). + This is the package's actual public entry point (`bin`). + - The package is published to npm as `aicodeman` and installs **both** the + `aicodeman` and `codeman` commands (`bin` aliases); `codeman` is the + canonical command used throughout the docs. Renaming either after 1.0 is a + breaking change. +2. **The published `xterm-zerolag-input` library**, but on **its own version + line** — it is versioned and released independently of the Codeman app. Its + 1.0 status is a separate decision; the Codeman app reaching 1.0 does *not* + imply `xterm-zerolag-input` is 1.0. +3. **Documented environment variables** that configure deployment: + `CODEMAN_PASSWORD`, `CODEMAN_USERNAME`, `CODEMAN_HOST`, `CODEMAN_PORT`, + `CODEMAN_INSTANCE`, `CODEMAN_ALLOWED_HOSTS`, `CODEMAN_DATA_DIR`, + `CODEMAN_TMUX_SOCKET`, and the `--host` / `--port` / `--https` CLI flags. + Removing or changing the meaning of one of these is breaking. +4. **The HTTP API and SSE event channel**, served under **`/api/v1`** with the + uniform `{success:true,data}` / `{success:false,error,errorCode}` envelope and + conventional HTTP status codes. Endpoint paths, the response envelope, error + `errorCode` values, and SSE event names are stable — see + [`api-reference.md`](api-reference.md). *Additive* changes (new endpoints, new + optional fields, new error codes, new SSE events) are non-breaking; breaking + changes ship under a new prefix (`/api/v2`). The unversioned `/api/...` alias + is kept working for the bundled UI. + +## What SemVer does NOT cover (internal surfaces — may change in any release) + +These may change in a **MINOR** (or even PATCH) release without a MAJOR bump: + +1. **The `~/.codeman/` state file formats** (`state.json`, `settings.json`, + `mux-sessions.json`, etc.). We make a **best-effort** to migrate existing data + forward (and have done so across renames), but the on-disk schema is not a + stable contract — do not write tooling that depends on its exact shape. +2. **Internal TypeScript modules.** The npm package is CLI-only; `import`ing it + programmatically is not supported (there is no stable library entry point). +3. **Experimental / opt-in features**, regardless of the app's version: + Gesture Control (beta), Agent Teams + (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`), and anything labeled experimental + in the UI or docs. These may change or be removed at any time. + +## Deprecation policy + +When we need to change a covered surface: + +- Prefer **additive** changes (new flag/env var/command) over breaking ones. +- A covered surface slated for removal is **deprecated first** — it keeps working + for at least one MINOR release with a runtime warning and a `CHANGELOG.md` note + pointing to the replacement — then removed in the next MAJOR. +- Back-compat migration shims (e.g. the historical Claudeman→Codeman data/socket + migration) are kept until a MAJOR boundary, then may be dropped. + +## Pre-1.0 (`0.x`) caveat + +Until 1.0 ships, **any release may contain breaking changes** per SemVer's `0.x` +allowance. The commitments above take effect at `1.0.0`. + +## See also + +- `CLAUDE.md` — the COM release workflow (changesets, version bump, deploy) +- `SECURITY.md` — security reporting and the supported-version policy +- `docs/security-architecture.md` — the full trust model diff --git a/package.json b/package.json index ac066a69b..aff35ad10 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { - "aicodeman": "./dist/index.js" + "aicodeman": "./dist/index.js", + "codeman": "./dist/index.js" }, "scripts": { "postinstall": "node scripts/postinstall.js", @@ -19,6 +20,8 @@ "test": "vitest run --config config/vitest.config.ts", "test:watch": "vitest --config config/vitest.config.ts", "test:coverage": "vitest run --config config/vitest.config.ts --coverage", + "test:ci": "vitest run --config config/vitest.ci.config.ts", + "check:frontend-syntax": "node scripts/check-frontend-syntax.mjs", "typecheck": "tsc --noEmit", "lint": "eslint --config config/eslint.config.js 'src/**/*.ts'", "lint:fix": "eslint --config config/eslint.config.js 'src/**/*.ts' --fix", @@ -117,7 +120,7 @@ } }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "repository": { "type": "git", diff --git a/scripts/check-frontend-syntax.mjs b/scripts/check-frontend-syntax.mjs new file mode 100644 index 000000000..40c9ba001 --- /dev/null +++ b/scripts/check-frontend-syntax.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +/** + * Frontend JS syntax check. + * + * CI's `npm run lint` only lints TypeScript under src/, and `tsc` excludes the + * frontend — so a plain SyntaxError in a shipped `src/web/public` script (loaded + * as a bare