Skip to content

v1.0 readiness: governance docs, CI test gate, and security hardening (M1/M7) - #113

Merged
Ark0N merged 9 commits into
masterfrom
v1-readiness-hardening
Jun 10, 2026
Merged

v1.0 readiness: governance docs, CI test gate, and security hardening (M1/M7)#113
Ark0N merged 9 commits into
masterfrom
v1-readiness-hardening

Conversation

@Ark0N

@Ark0NArk0N commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

Pre-1.0 hardening, in three independent commits (docs → CI gate → security). None of this changes runtime feature behavior except the two small security fixes in commit 3.

  1. docs(v1) — add SECURITY.md (private disclosure path + known-limitations) and docs/versioning-policy.md (defines what 1.0 SemVer covers: CLI + documented env vars are public; the HTTP/SSE API, on-disk state, and experimental features are internal/unstable). Fix LICENSE (2024 Claudeman2024-2026 Codeman) and the stale CLAUDE.md gotcha claiming xterm-zerolag-input is hand-duplicated in app.js — it's single-source now (built into a gitignored vendor bundle by postinstall.js/build.mjs).
  2. test(ci) — actually run the unit suite in CI + close CI blind-spots, and green the pre-existing test debt that surfaced.
  3. fix(security) — small input-validation fixes from the 2026-06-09 review (M1/M7) + a doc fix (M5).

CI gate (commit 2)

The unit suite was excluded from CI on a now-stale "tests spawn tmux" rationale — but TmuxManager no-ops all shell commands under VITEST (test/setup.ts), so it's CI-safe.

  • New test job runs npm run test:ci (config/vitest.ci.config.ts). Excludes the browser-driven test/mobile suite (already), plus the other Playwright/chromium tests and timing-flaky perf-* tests — those run separately, not in the fast unit gate.
  • New frontend-syntax check (scripts/check-frontend-syntax.mjs, node --check on src/web/public/*.js) wired into the lint job — npm run lint globs only TS, so a plain SyntaxError in a shipped <script> passed CI before; this catches it.
  • New security regression tests for the v0.9.5 fixes (wired Host/Origin guard, self-update CSRF, CSP/security headers, text/plain raw body, WS anti-CSWSH) + a backend↔frontend SSE registry parity test.
  • Greened pre-existing test debt the gate surfaced (none related to current features): stale Session not found asserts (the message now carries the session id), tests for removed helpers (isError is now internal; createSuccessResponse deleted), a missing realpathSync mock + stale /tmp assertion in file-stream-manager, and sse-subscription-filter (lifecycle events are intentionally broadcast to all clients — only the terminal stream is filtered). One interactive-respawn cleanup test is it.skip'd with a TODO (needs a real PTY; incompatible with the VITEST no-op — respawn cleanup is covered by the 165-test respawn-controller suite).

Security fixes (commit 3)

  • M7 (SSRF): web-push endpoint was z.string().url() with no host checks, then fetched server-side. Added isSafePushEndpoint (https-only; rejects internal/loopback/link-local/metadata IPs incl. IPv4-mapped), enforced in PushSubscribeSchema and re-checked before sendNotification. + unit test.
  • M1 (command injection): discovered/used tmux session names now pass isValidMuxName before reaching a shell call site in sessionExists, killSession, and reconcileSessions (a foreign codeman-* name with shell metacharacters on the shared socket is now rejected).
  • M5: the live log-tail route intentionally allows /var/log + ~/logs (a tested feature); documented that wider read scope in docs/security-architecture.md §5 rather than dropping it.

Validation

  • typecheck, lint, format:check, check:lockfile, check:frontend-syntax — all pass.
  • Full non-mobile suite green locally in a CI-faithful env: 2680 passed, 12 skipped, 0 failed.

Notes / follow-ups (not in this PR)

  • Browser + perf tests are excluded from the unit gate (they need chromium / are timing-sensitive) — a separate browser-test job could run them with playwright install.
  • Tier-1 1.0 decisions from the readiness audit remain open and are intentionally not in this PR: the HTTP error-status contract (200-vs-4xx), package identity freeze (aicodeman vs codeman), and the Node engines floor (>=18 is EOL).

🤖 Generated with Claude Code


v1.0 Tier-1 lock-in decisions (added)

Three SemVer-lock-in decisions that are cheap now but breaking after 1.0:

  • chore(v1) — Node floor >=22 (18/20 are EOL; CI tests 22; NODE_COMPILE_CACHE needs 22.1+) + codeman bin alias (so npm i -g aicodeman provides the codeman command every doc references; program.name already codeman).
  • feat(api) — stable HTTP contract (cleanest/fullest form). Every JSON /api response is now ONE uniform envelope via a central preSerialization hook: success {success:true,data}, error {success:false,error,errorCode} with a conventional 4xx/5xx status (no more 200-on-error). Single error-code→status map; expanded ApiErrorCode. /api/v1 versioned alias (external clients pin it; the bundled UI keeps /api). 50 handlers stripped of manual success:true; 48 frontend call-sites migrated (_apiJson auto-unwraps data); ~166 test assertions updated. New docs/api-reference.md; versioning-policy.md flipped so the HTTP/SSE API is now SemVer-covered.

Verification: full unit/route suite green (2680 passed), typecheck/lint/format/frontend-syntax clean, and a headless-chromium smoke loaded the migrated UI and drove the panels with 0 console/page errors. /api/v1/status confirmed returning the uniform envelope.

Ark0Nand others added 9 commits June 9, 2026 19:05
…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) <noreply@anthropic.com>
…existing test debt
- 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) <noreply@anthropic.com>
…ent tail-file roots
- M7 (SSRF): add isSafePushEndpoint (https-only; reject internal/loopback/link-local/metadata IPs incl. IPv4-mapped); enforce in PushSubscribeSchema and re-check before webpush.sendNotification. + unit test.
- M1 (command injection): validate tmux session names with isValidMuxName in sessionExists, killSession, and reconcileSessions before they reach a shell call site.
- M5: keep the intentional /var/log + ~/logs log-tail roots (a tested feature) and document the wider read scope in docs/security-architecture.md section 5 instead of dropping it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- engines.node >=18 -> >=22 (Node 18/20 are EOL; CI only tests 22; the start script + systemd unit use NODE_COMPILE_CACHE which needs 22.1+). Updates the README badge and CLAUDE.md requirements to match.
- bin: add a 'codeman' alias alongside 'aicodeman' so 'npm i -g aicodeman' provides the 'codeman' command every doc/symlink references (program.name is already 'codeman'; the published package name stays 'aicodeman').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…velope, status codes, /api/v1
Point 1 of the v1.0 lock-in: commit to a stable HTTP API (the cleanest, fullest form).
Core (centralized):
- Every JSON /api response now uses ONE envelope via a Fastify preSerialization hook (src/web/server.ts): success -> { success:true, data:<payload> }; error -> { success:false, error, errorCode } with a conventional HTTP status. Non-JSON routes (file-raw, tail-file SSE, download, screenshots, /q redirect, WS) are skipped.
- Error-code -> HTTP status is a single source of truth (httpStatusForErrorCode in src/types/api.ts): 400/401/404/409/422/429/500. Expanded ApiErrorCode (added UNAUTHORIZED, CONFLICT, RATE_LIMITED). Errors are no longer HTTP 200.
- Versioned alias: /api/v1/* rewrites to /api/* (rewriteApiV1Url), so external clients pin to a stable surface while the bundled UI keeps using /api/*.
- Handlers stripped of manual 'success:true' (50 across 14 route files) so they return bare payloads the hook wraps uniformly; fixed the mux DELETE {success:<bool>} envelope collision (-> {killed}).
Frontend (48 call sites across 10 files):
- _apiJson() auto-unwraps { success:true, data } -> data (null on error), so most bare-shape readers are transparent. Raw-fetch sites relocate payload reads under .data; success/res.ok/error checks unchanged.
Docs: new docs/api-reference.md (envelope, status table, error codes, /api/v1, SSE); versioning-policy.md flipped — the HTTP/SSE API is now part of the stable, SemVer-covered surface.
Verification: full unit/route suite green (2680 passed) incl. ~166 updated assertions across 24 test files; typecheck/lint/format/frontend-syntax clean; a headless-chromium smoke loaded the migrated UI and drove the panels with 0 console/page errors; /api/status and /api/v1/status confirmed returning the uniform envelope live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new CI test gate surfaced a pre-existing flaky timing test: 'should return age of entry' asserted age>=50 after a 50ms setTimeout and measured 49ms on a jittery CI runner. Widened the elapsed-time windows (age >=40/<500; remaining TTL >700/<=960) so they tolerate timer jitter. Pre-existing flakiness, unrelated to the API migration. (File was also normalized by prettier per the pre-commit hook.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hardening
Conflict in src/web/public/app.js selectSession: combined #112's
_clearTerminalLoadState cleanup on stale select with #113's
{success,data} envelope unwrap of the terminal fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ready-unwrapped shared settingsPromise
app.js resolves settingsPromise to the unwrapped settings object
(env?.data ?? null), matching loadAppSettingsFromServer. The
loadQuickStartCases consumer still read settings.data.lastUsedCase,
which silently dropped the last-used-case preselection; its fallback
fetch also missed the envelope unwrap. Align both with the unwrapped
shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 15-agent audit of the merged tree confirmed 9 envelope/contract bugs;
all fixed here, with live-server contract tests added:
Blockers (fresh-install quick start broken):
- session-ui.js runClaude/runShell unwrapped .data from the /api/cases/:name
404 error envelope (which has no data key), so a not-yet-created case threw
TypeError instead of triggering the auto-create fallback. Now '?.data ?? {}'.
Contract violations on the new stable surface:
- Unknown /api routes returned HTTP 404 with {success:true,...} (Fastify's
default not-found payload was wrapped by the envelope hook). Added a
setNotFoundHandler returning the standard error envelope for /api paths.
- POST /api/events/subscribe 400 body became {success:true,data:{error}};
now createErrorResponse(INVALID_INPUT).
- POST /api/clipboard validation error lacked errorCode and shipped HTTP 200;
now createErrorResponse(INVALID_INPUT) -> 400.
- POST /api/run catch path returned bare {success:false,sessionId,error}
(HTTP 200, no errorCode); now OPERATION_FAILED envelope -> 422 with the
dead session id in the message.
- DELETE tail-file/:streamId returned {success: closed}, colliding with the
envelope discriminator; now returns {closed}.
Dead/regressed UI paths:
- Plan history modal could never open: route returned the bare history array
under data while the frontend read data.data.history/currentVersion. Route
now returns {history, currentVersion}; modal task count fixed to stats.total.
- Self-update error toast read j.error.message from the string-typed envelope
error, always falling back to the generic message; now reads the string.
Cleanup:
- Removed the stale QuickStartResponse type (unreferenced; documented the
pre-envelope shape and invited success-key collisions).
Tests: new test/http-contract.test.ts boots a real WebServer (port 3168) and
pins the envelope, /api/v1 alias, error statuses, and the /api 404 shape —
the route-test harness does not install the server-level hook, so these need
the live server. Updated file-routes/plan-routes/scheduled-runs tests to the
fixed shapes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Ark0N
Ark0N merged commit 272f0d1 into masterJun 10, 2026
2 checks passed
Ark0N added a commit to aakhter/Codeman that referenced this pull request Jun 10, 2026
Review follow-ups on PR Ark0N#111 (rebased onto master post-Ark0N#112/Ark0N#113):
Resize arbitration redesigned (review blocker 2): the previous
'cols < _ptyCols' guard froze a mobile-only session's PTY at the spawn
default — narrow phones rendered clipped and could never re-fit. The
guard now uses connection-scoped desktop sizing claims instead:
ws-routes registers a claim on a desktop-typed resize and releases it
on socket close (or when the same connection later reports a small
viewport), and Session.resize() ignores mobile/tablet resizes only
while at least one desktop connection holds a claim. A phone alone
fully controls its size (shrink, rows-only shrink, re-grow); a phone
glancing at a desktop-driven session can no longer reflow it.
mobile-handlers' keyboard open/close resize now declares its viewport
type so it participates in arbitration. Tests rewritten to cover
mobile-only shrink/rows-only/re-grow, claim/release lifecycle, multi-
claim behavior, and untyped legacy resizes; ws-routes test covers the
claim lifecycle over a real socket.
Solo/detached header restored (review blocker 3): index.html had
removed #soloSessionTitle and #soloRedockBtn, which _applySoloMode
still references — every detached window hit a null deref. Both are
back alongside the new mobile utility toggle.
Desktop leak fixed (review should-fix): .mobile-header-utility-toggle
had no rule outside the <=768px media queries, so the raw button
rendered on desktop. styles.css now hides it by default; the mobile/
tablet queries re-enable it.
Visual-regression baselines reverted to master (review should-fix):
the 18 contributor-machine PNGs are environment-specific (8 of the
behavioral tests already report environment-sensitive failures across
machines); re-baseline deliberately on the canonical machine instead.
The 24 behavioral keyboard/layout/tabs tests are kept as-is.
AGENTS.md trimmed to a pointer at CLAUDE.md (review should-fix) to
avoid drift between duplicated guidance.
Also dropped a dead getAttachmentHistoryForPersist stub (codex-branch
residue — no such method exists in src/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Ark0N
Ark0N deleted the v1-readiness-hardening branch June 10, 2026 06:52
Ark0N added a commit that referenced this pull request Jun 10, 2026
- CI section: document the test job (npm run test:ci via vitest.ci.config.ts)
and check:frontend-syntax — the "unit suite is excluded" claim was stale
- Testing: rewrite rationale (bare npm test fails on browser suites, not tmux)
and safety model (TmuxManager in-memory mock under VITEST; the
registerTestTmuxSession/snapshot mechanism no longer exists)
- API Routes: add the ApiResponse envelope and /api/v1 alias contract
- Minor: app.js ~3.6K lines, codeman bin alias, new command-table rows,
config-barrel note, generated/gitignored dirs section
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Jun 10, 2026
Two hardening fixes for the public-tunnel exposure path (COD-54 / COD-55).
COD-54 — gate the /api/hook-event localhost bypass when a tunnel is up:
`cloudflared --url http://127.0.0.1:port` proxies internet traffic INTO the
loopback origin, so a tunneled hook request arrives with req.ip === 127.0.0.1
and the old bare-localhost bypass would pass it unauthenticated. Now:
- tunnel running → bypass requires a shared per-instance hook secret
(X-Codeman-Hook-Secret header; constant-time compare) + per-IP rate limiting
- tunnel not running (loopback-only, the normal case) → unchanged, so
already-deployed credential-less hooks keep working.
New src/config/hook-secret.ts; auth middleware takes a getTunnelRunning probe
(wired from server.ts via tunnelManager.isRunning()).
COD-55 — refuse starting the Cloudflare tunnel without auth:
enabling the tunnel publishes full terminal control to a public URL; with no
CODEMAN_PASSWORD the auth middleware is inactive and the bind guard never trips
(tunnel binds loopback). PUT /api/settings now refuses tunnelEnabled:true with a
403 (before persisting) unless CODEMAN_PASSWORD is set or
CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1 is acknowledged. New
isUnauthenticatedNetworkAcknowledged() in network-auth-policy; settings-ui
surfaces the refusal as an error toast and reverts the toggle.
Scope: the always-on CSRF/Origin guard, Host-header allowlist, and
network-auth-policy itself are already upstream (#113) and not re-proposed here.
Verification: tsc, eslint, prettier, check:frontend-syntax clean; full test:ci
green (2723 passed), incl. test/cod54-hook-event-auth and
test/routes/system-routes-tunnel-guard.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ark0N