Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTINUITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Key decisions:
- React UI is primary; server should serve React build when present.
- Agent lane follows vanilla JS behavior: show all open sessions (active + idle) and sort by state rank then CPU.
- Layout uses pinned positions per agent identity; only new agents are placed; existing tiles never move; placement uses max-height bounds to prevent overlap.
- Scale placement with spatial hash buckets (cell -> occupants) and persistent per-group spiral frontier; collisions checked by bounds against bucket occupants.
- Codex prompt-like titles containing temp paths/turn markers are ignored for lane labels; fallback to repo or codex#pid.
- Agent lane now shows only active/error sessions and uses stable sort (state rank then identity) to prevent reordering.
- Active lane items now glow via `is-active` class (lane item box-shadow) to make active codex sessions visibly highlighted.
Expand Down Expand Up @@ -133,6 +134,11 @@ State:
- Tests re-run: `npm run test:unit` (149 pass), `npm run test:integration` (54 pass), `npm run test:ui` (19 pass).
- Build run after pinned layout changes: `npm run build` (pass).
- Copied edited files to clipboard via pbcopy; temp bundle at `/tmp/consensus-edited-files.txt`.
- Implemented spatial hash buckets + persistent group spiral frontier; fixed cell packing and bounds-based collision checks (public/src/lib/layout.ts).
- Renderer now keeps a single RAF loop, exposes hit list, and uses correct view transform for hit tests (public/src/hooks/useCanvasRenderer.ts).
- CanvasScene no longer rebuilds hit list; uses renderer hit list and stable render loop (public/src/components/CanvasScene.tsx).
- Obstruction detection checks only front occluders with AABB guard (public/src/hooks/useCanvasRenderer.ts).
- Tests run after layout/renderer updates: `npm run test:unit` (152 pass), `npm run test:integration` (58 pass), `npm run test:ui` (19 pass).
- Now:
- Answer user question about hot reload configuration (Vite HMR + server watch + live reload SSE).
- Answer whether any additional setup is missing when using Vite.
Expand Down Expand Up @@ -161,6 +167,7 @@ State:
- Claude CLI hooks not firing for real sessions; user reports Claude does not show active when working.
- Need to validate live Claude session while recording via agent-browser and inspect hook delivery.
- Copy edited layout/test files to clipboard via pbcopy. (DONE)
- Implement spatial hash buckets + persistent group frontier with correct cell packing and bounds checks. (DONE)
- User requested a best-practice plan (no code edits) to address review comment: `npm run dev` no longer starts Vite, so TSX client fails unless dev:client or build is run.
- Q&A answered for dev workflow: always start Vite; use CONSENSUS_UI_PORT for port; on port conflict try next port.
- Review plan requested for new P2/P3 items: Codex in-flight timeout default in codexLogs; TOML [tui] notifications insertion in cli/setup.
Expand Down
33 changes: 16 additions & 17 deletions docs/api-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,31 @@ This document records how Consensus exposes HTTP/WS APIs today and how callers p

## Trust boundary
- The server binds to `CONSENSUS_HOST` (default `127.0.0.1`) and `CONSENSUS_PORT` (default `8787`). By default all API traffic stays on localhost and the UI/API share the same origin.
- There is no built-in token, cookie, or OAuth layer. The assumption is that if a client can reach the port, it is running on the same machine (or behind an OS-level firewall or SSH tunnel) and therefore already trusted.
- `docs/constitution.md` mandates that any future remote access requires authentication, so the current pattern is safe only because the default bind address never leaves the host.
- Remote bind is blocked unless `CONSENSUS_ALLOW_REMOTE=1` and `CONSENSUS_API_TOKEN` are both set. This prevents accidental exposure of unauthenticated endpoints.
- When `CONSENSUS_API_TOKEN` is set, all API and WebSocket clients must present `Authorization: Bearer <token>` (or `?token=<token>` for WebSocket/browser clients).

## Endpoint summary
| Route | Method | Purpose | Authentication | Notes |
|-------|--------|---------|----------------|-------|
| `/api/snapshot` | `GET` | Returns the last snapshot emitted by the scan loop. The React-less canvas UI polls this endpoint to rebuild the agent map. | None | The handler runs `scanCodexProcesses` and pushes a JSON payload (ts + agents). |
| `/api/snapshot` | `GET` | Returns the last snapshot emitted by the scan loop. | Required when token is set or host is non-loopback. | The handler runs `scanCodexProcesses` and pushes a JSON payload (ts + agents). |
| `/health` | `GET` | Basic JSON health check for monitoring. | None | Always responds `{ ok: true }`. |
| `/api/codex-event` | `POST` | Codex notify hook forwards Codex events into the server. | None | Payload validated against `CodexEventSchema`; rejects with `400` on schema mismatch. |
| `/api/claude-event` | `POST` | Claude Code hooks post lifecycle events. | None | Schema validated via `ClaudeEventSchema`; `dist/claudeHook.js` reads stdin and forwards to this endpoint. |
| `/__debug/activity` | `POST` | Toggles extra activity logging (guarded by localhost). | None | Accepts `enable` via query or JSON body. |
| `/__dev/reload` | `GET (SSE)` | Development reload stream for browser clients. | None | Only available when `CONSENSUS_LIVE_RELOAD=1`. |
| `/api/codex-event` | `POST` | Codex notify hook forwards Codex events into the server. | Required when token is set or host is non-loopback. | Payload validated against `CodexEventSchema`; rejects with `400` on schema mismatch. |
| `/api/claude-event` | `POST` | Claude Code hooks post lifecycle events. | Required when token is set or host is non-loopback. | Schema validated via `ClaudeEventSchema`; `dist/claudeHook.js` reads stdin and forwards to this endpoint. |
| `/__debug/activity` | `POST` | Toggles extra activity logging. | Required when token is set or host is non-loopback. | Accepts `enable` via query or JSON body. |
| `/__dev/reload` | `GET (SSE)` | Development reload stream for browser clients. | Required when token is set or host is non-loopback. | Only available when `CONSENSUS_LIVE_RELOAD=1`. |

The UI also opens a WebSocket (handled by `ws` in `src/server.ts`) but the WebSocket connection is only permitted from the same origin as the served static files.
The UI also opens a WebSocket (handled by `ws` in `src/server.ts`). When `CONSENSUS_API_TOKEN` is set, the WebSocket must include `?token=<token>` in the URL.

## Client expectations
- Codex notify hooks call `noun codex config set -g notify` with the Consensus endpoint (`/api/codex-event`) and expect no authentication steps beyond the default localhost requirement.
- Claude Code hooks run `dist/claudeHook.js` which POSTs a minimal JSON event directly to `/api/claude-event` from the hook process; it neither signs the request nor retries if the hook fails.
- The browser UI fetches `/api/snapshot` and opens the WebSocket without extra headers.
- Codex notify hooks call `noun codex config set -g notify` with the Consensus endpoint (`/api/codex-event`). If `CONSENSUS_API_TOKEN` is set, the hook forwards `Authorization: Bearer <token>`.
- Claude Code hooks run `dist/claudeHook.js` which POSTs a minimal JSON event directly to `/api/claude-event`. If `CONSENSUS_API_TOKEN` is set, the hook forwards `Authorization: Bearer <token>`.
- The browser UI opens the WebSocket, optionally with `?token=<token>` if required.

## Hardening guidance
1. Any time the server binds to a non-localhost address (custom `CONSENSUS_HOST`), add auth before enabling the port in `docs/constitution.md`'s sense. A simple opt-in token header (e.g., `CONSENSUS_API_TOKEN`) or Mutual TLS would be appropriate.
2. When introducing authentication, keep the current schema validation for Codex/Claude events so that invalid or replayed payloads are rejected even before checking credentials.
3. For debugging or automation endpoints (`/__debug/activity`, `/__dev/reload`), gate them behind the same token or a separate debug-only header to keep the trust boundary intact.
4. Document the chosen auth pattern once implemented (update this file). If more than localhost access is required, pair it with firewall rules or SSH tunnels that still keep secrets off disk.
1. Keep the remote bind guard (`CONSENSUS_ALLOW_REMOTE=1` + `CONSENSUS_API_TOKEN`) in place to prevent accidental exposure.
2. Pair remote exposure with a TLS-terminating reverse proxy and firewall rules.
3. Continue schema validation for Codex/Claude events so malformed payloads are rejected before processing.
4. Treat `CONSENSUS_API_TOKEN` as a secret; rotate if leaked.

## Observability
- Authentication and access attempts should be logged without secrets, matching the `IX.7` rule in `docs/constitution.md`. Currently, there are no tokens to log, so the focus is on request/response status.
- Future token-based auth can reuse `recordHttpMetrics` and `annotateSpan` already wired in `runHttpEffect` to trace auth failures alongside scan duration and errors.
- Authentication and access attempts are logged via `recordHttpMetrics` and `annotateSpan` without exposing tokens.
15 changes: 15 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ All configuration is via environment variables.
- `CONSENSUS_HOST`
- Default: `127.0.0.1`
- Bind address for the server.
- `CONSENSUS_ALLOW_REMOTE`
- Default: disabled
- Set to `1` to allow binding to non-loopback hosts. Requires `CONSENSUS_API_TOKEN`.
- `CONSENSUS_API_TOKEN`
- Default: unset
- If set, all API and WebSocket clients must send `Authorization: Bearer <token>` or `?token=<token>`.
- `CONSENSUS_PORT`
- Default: `8787`
- Port for the HTTP server.
Expand Down Expand Up @@ -104,6 +110,15 @@ All configuration is via environment variables.
- `CONSENSUS_REDACT_PII`
- Default: enabled
- Set to `0` to disable redaction.
- `CONSENSUS_REDACT_STRICT`
- Default: disabled
- Set to `1` to enable strict secret/token redaction patterns.
- `CONSENSUS_RATE_LIMIT_WINDOW_MS`
- Default: `10000`
- Sliding window size for API rate limiting (ms).
- `CONSENSUS_RATE_LIMIT_MAX`
- Default: `120`
- Max requests per window per IP+route before 429.
- `ACTIVITY_TEST_MODE`
- Default: disabled
- Set to `1` to enable test-only activity injection endpoints under `/__test`.
Expand Down
110 changes: 110 additions & 0 deletions docs/security-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Security Audit Report (Trail of Bits Style)

Date: 2026-02-04
Scope: Consensus CLI (server, CLI, hooks, UI, docs, build/test pipeline)

## Executive summary
- Status: No critical/high-risk findings remain **unresolved in code** after this audit.
- The primary risks were unauthenticated remote exposure, missing request throttling, and limited redaction. These are now mitigated with remote-bind guardrails, token auth, rate limits, and strict redaction mode.
- Compliance gaps remain in **process/controls** (policies, incident response, dependency scanning, and evidence of regular security testing). These require organizational action and CI changes.

## Methodology
- Manual code review of HTTP/WS entry points, hooks, redaction, logging, and configuration.
- Threat model update and trust boundary validation.
- Dependency and build pipeline review (package manifests and docs).

## System overview
- Node.js/TypeScript service serves a canvas UI, polls local process state, and ingests Codex/Claude/OpenCode activity.
- Primary data sources are local JSONL logs and event hooks.
- Service is expected to run on the same machine as the UI by default.

## Threat model (summary)
### Assets
- Activity metadata about local agents (process list, command summaries, file paths).
- Event streams from Codex/Claude/OpenCode hooks.
- Optional API token (if remote access is enabled).

### Trust boundaries
- Localhost boundary for HTTP/WS by default.
- Optional remote exposure guarded by `CONSENSUS_ALLOW_REMOTE=1` + `CONSENSUS_API_TOKEN`.

### Attackers
- Remote unauthenticated network actor if the service is exposed.
- Local unprivileged user on the same host.
- Malicious dependency or compromised supply chain.

### Entry points
- HTTP: `/api/codex-event`, `/api/claude-event`, `/api/snapshot`, `/health`, `/__debug/activity`, `/__dev/reload`.
- WebSocket: `/ws` (any path is accepted by `ws`).
- CLI and hook scripts (`codexNotify`, `claudeHook`).

## Findings and remediations

### Critical
- None remaining.

### High
1. **Unauthenticated remote exposure** (resolved)
- Risk: Remote bind could expose event ingestion and activity endpoints without auth.
- Fix: Enforced non-loopback bind guard (`CONSENSUS_ALLOW_REMOTE=1` + `CONSENSUS_API_TOKEN`) and token auth for HTTP/WS.
- Files: `src/server.ts`, `docs/api-authentication.md`, `docs/configuration.md`.

2. **Missing rate limiting on event endpoints** (resolved)
- Risk: Unbounded POSTs allow resource exhaustion and event spam.
- Fix: In-memory rate limit keyed by IP+path; enforced on event/debug/test routes.
- Files: `src/server.ts`, `src/server/activityTestRoutes.ts`.

### Medium
1. **Insufficient redaction for secrets in summaries** (partially resolved)
- Risk: Hook payloads and derived summaries could leak secrets.
- Fix: Added strict redaction mode with common token/private-key patterns.
- Remaining: Consider entropy-based redaction or configurable patterns for enterprise deployments.
- Files: `src/redact.ts`, `docs/configuration.md`.

2. **Security headers missing** (resolved)
- Risk: Default Express headers disclose framework; no baseline hardening headers.
- Fix: Disabled `x-powered-by` and added basic security headers.
- Files: `src/server.ts`.

### Low
1. **/health endpoint unauthenticated on remote bind** (resolved)
- Fix: `/health` now requires auth when token is set or host is non-loopback.
- Files: `src/server.ts`.

## Compliance gap analysis
This review focuses on technical controls in code. The following gaps require organizational/process work and evidence for HIPAA, PCI-DSS, and NIST 800-171.

### HIPAA Security Rule (45 CFR 164 Subpart C)
- **164.308(a)(1)(ii)(A) Risk analysis (R)**: No formal risk assessment artifact in repo.
- **164.308(a)(6)(ii) Incident response (R)**: No incident response policy or runbook.
- **164.312(b) Audit controls (R)**: Logs exist but no documented retention/access controls.
- **164.312(e)(2)(ii) Transmission security (A)**: TLS guidance for remote exposure not documented.

### PCI DSS v4.0.1
- **Req 6 Secure Development**: No dependency/secret/license scanning in CI.
- **Req 10 Logging and Monitoring**: No log retention or monitoring policy.
- **Req 11 Regular Security Testing**: No security test cadence or evidence.

### NIST 800-171 Rev 3
- **3.1 Access Control**: Token auth added, but no account lifecycle or privileged access controls.
- **3.3 Audit and Accountability**: No defined audit log retention or review process.
- **3.12 Security Assessment**: No documented security assessment plan.

## Remediation checklist (prioritized)
1. Add CI security scanning (dependency, secret, and license scanning).
2. Document TLS reverse-proxy guidance for remote exposure and enforce HTTPS in deployment guides.
3. Add incident response and logging retention policies in `SECURITY.md` or `docs/`.
4. Expand redaction controls (configurable patterns, entropy-based fallback).
5. Implement optional IP allowlist or mTLS for enterprise deployments.

## Long-term hardening recommendations
- Add structured audit logs with configurable retention and access controls.
- Provide a dedicated auth mechanism for multi-user deployments (OIDC or mTLS).
- Ship a minimal SBOM and pin high-risk dependencies.
- Introduce security regression tests for auth and rate limiting.

## Evidence and artifacts
- Threat model: `docs/threat-model.md`.
- Auth model: `docs/api-authentication.md`.
- Configuration: `docs/configuration.md`.
- Code changes: `src/server.ts`, `src/server/activityTestRoutes.ts`, `src/redact.ts`, `src/codexNotify.ts`, `src/claudeHook.ts`, `public/src/App.tsx`.
7 changes: 4 additions & 3 deletions docs/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

## Trust boundaries
- Local machine only by default.
- No external services required.
- Remote bind requires `CONSENSUS_ALLOW_REMOTE=1` and `CONSENSUS_API_TOKEN`.

## Secrets handling
- The app does not require secrets.
- Redaction protects common PII patterns.
- The app does not require secrets by default.
- Optional `CONSENSUS_API_TOKEN` gates remote access.
- Redaction protects common PII patterns; strict mode expands secret patterns.

## Data in transit
- HTTP over localhost unless explicitly exposed.
Expand Down
4 changes: 4 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Claude activity is hook-driven. Configure Claude Code hooks to call
`dist/claudeHook.js` and post to `http://127.0.0.1:<port>/api/claude-event`
from `~/.claude/settings.json`, `.claude/settings.json`, or
`.claude/settings.local.json`.
If `CONSENSUS_API_TOKEN` is set, the hook process must run with that env var
so it can include the `Authorization: Bearer` header automatically.

Minimal config (tool hooks require a matcher):
```json
Expand Down Expand Up @@ -60,3 +62,5 @@ TypeScript entrypoint so changes apply without a build:

## WebSocket disconnects
Check for network proxies, and reload. The client will auto-reconnect.
If `CONSENSUS_API_TOKEN` is set, include `?token=<token>` in the UI URL or
`ws` override so the browser can authenticate.
17 changes: 16 additions & 1 deletion public/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const query = new URLSearchParams(window.location.search);
const mockMode = query.get('mock') === '1';
const wsOverrideRaw = query.get('ws');
const wsOverrideDecoded = wsOverrideRaw ? decodeURIComponent(wsOverrideRaw) : null;
const wsToken = query.get('token');

let wsOverride: string | null = null;
if (wsOverrideDecoded) {
Expand All @@ -25,6 +26,19 @@ if (wsOverrideDecoded) {
}
}

const withToken = (url: string | null): string | null => {
if (!url || !wsToken) return url;
try {
const parsed = new URL(url);
if (!parsed.searchParams.has('token')) {
parsed.searchParams.set('token', wsToken);
}
return parsed.toString();
} catch {
return url;
}
};

if (mockMode) {
initMockBridge();
}
Expand All @@ -38,7 +52,8 @@ if (wsOverrideRaw || mockMode) {
}

const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = wsOverride || `${wsProtocol}://${window.location.host}/ws`;
const wsBaseUrl = wsOverride || `${wsProtocol}://${window.location.host}/ws`;
const wsUrl = withToken(wsBaseUrl);

function App() {
const { status, agents, meta } = useWebSocket(wsUrl, { mockMode });
Expand Down
Loading
Loading