From 36bc22a3d51ffd343e9c1c11363d74250c5f5e9c Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 9 Jun 2026 19:05:50 +0200 Subject: [PATCH 1/8] docs(v1): add SECURITY.md + versioning policy; fix LICENSE and stale overlay gotcha - Add SECURITY.md: private disclosure path, supported versions, known limitations. - Add docs/versioning-policy.md defining what 1.0 SemVer covers (CLI + documented env vars are public; HTTP/SSE API, on-disk state, and experimental features are internal/unstable). - LICENSE: '2024 Claudeman Contributors' -> '2024-2026 Codeman Contributors'. - CLAUDE.md: fix the stale xterm-zerolag-input 'duplicated in app.js' gotcha (it is single-source now -> gitignored vendor bundle via postinstall.js/build.mjs); add versioning + security pointers; minor /init nav fixes (image-input load order, server.ts marker). - README: link SECURITY.md + the versioning policy. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 10 ++--- LICENSE | 2 +- README.md | 10 ++++- SECURITY.md | 78 +++++++++++++++++++++++++++++++++++++++ docs/versioning-policy.md | 78 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 SECURITY.md create mode 100644 docs/versioning-policy.md diff --git a/CLAUDE.md b/CLAUDE.md index 527b5d108..471c7cb77 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 @@ -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 dde873814..bf5a2ffb1 100644 --- a/README.md +++ b/README.md @@ -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/docs/versioning-policy.md b/docs/versioning-policy.md new file mode 100644 index 000000000..142698911 --- /dev/null +++ b/docs/versioning-policy.md @@ -0,0 +1,78 @@ +# 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:** draft for the 1.0 cut. The central decision below — that the HTTP/SSE +> API is *not* SemVer-covered — should be confirmed by the maintainer before 1.0, +> since it determines whether a number of in-flight cleanups are "breaking." + +## 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`). + - *Note:* the npm package name vs. invoked command name (`aicodeman` vs + `codeman`) is a known inconsistency to resolve **before** 1.0 — renaming + either after 1.0 is itself 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. + +## 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 HTTP API and SSE event registry** (`/api/...`, the ~120 SSE event + types). This is an **internal protocol between the server and its own bundled + web frontend**, not a published client API. There is no OpenAPI spec, no + published client, and no versioned `/api/v1` namespace. If you script against + these endpoints, **pin to an exact Codeman version** — they can change between + minors (response shapes, status codes, event names). Standardizing the error + envelope and HTTP status codes is explicitly reserved as a non-breaking + internal change under this policy. +2. **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. +3. **Internal TypeScript modules.** The npm package is CLI-only; `import`ing it + programmatically is not supported (there is no stable library entry point). +4. **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 From d5f91e4cd7fbbf1dd26e431c4270e4fccc043836 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 9 Jun 2026 20:02:15 +0200 Subject: [PATCH 2/8] test(ci): run the unit suite in CI + frontend-syntax gate; green pre-existing test debt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI: add a 'test' job running the unit suite via config/vitest.ci.config.ts. Excludes browser (Playwright/chromium) and perf tests (timing-flaky), like the existing test/mobile suite. Safe in CI: TmuxManager no-ops shell commands under VITEST (test/setup.ts). - Add scripts/check-frontend-syntax.mjs (node --check on src/web/public/*.js), wired into the lint job — catches a class of frontend SyntaxError that passes lint today (lint globs only TS). - Add test/security-regression.test.ts (wired Host/Origin guard, self-update CSRF, CSP/security headers, text/plain raw body, WS anti-CSWSH) + test/sse-registry-parity.test.ts (backend<->frontend SSE registry parity). - Green pre-existing test debt surfaced by the new gate: stale 'Session not found' asserts -> 'not found' substring; drop tests for removed helpers (isError now internal; createSuccessResponse deleted); file-stream-manager: mock realpathSync + fix stale /tmp assertion; sse-subscription-filter: lifecycle events broadcast to all clients (only terminal stream filtered); session.test.ts: mkdir /tmp/test; skip one interactive-respawn test needing a real PTY (covered by respawn-controller.test.ts). - Full non-mobile suite verified green locally (2680 passed, 12 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 37 +++++- config/vitest.ci.config.ts | 33 ++++++ package.json | 2 + scripts/check-frontend-syntax.mjs | 40 +++++++ test/api-responses.test.ts | 37 +----- test/edge-cases.test.ts | 40 ++++--- test/file-stream-manager.test.ts | 22 ++-- test/integration-flows.test.ts | 16 +-- test/security-regression.test.ts | 167 +++++++++++++++++++++++++++ test/session-cleanup.test.ts | 20 ++-- test/session.test.ts | 16 ++- test/sse-registry-parity.test.ts | 67 +++++++++++ test/sse-subscription-filter.test.ts | 15 ++- test/types.test.ts | 37 ++---- 14 files changed, 428 insertions(+), 121 deletions(-) create mode 100644 config/vitest.ci.config.ts create mode 100644 scripts/check-frontend-syntax.mjs create mode 100644 test/security-regression.test.ts create mode 100644 test/sse-registry-parity.test.ts 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/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/package.json b/package.json index ac066a69b..d4dc5243e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,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", 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