Commit Graph
1040 Commits
Author SHA1 Message Date
Codeman maintainer af9db455ff chore: version packages
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:50:41 +02:00
Codeman maintainer a406aef2fa chore: version packages
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:01:18 +02:00
Codeman maintainer b388b84cc2 merge master into claude-response-viewer-normalization
Only CLAUDE.md conflicted: master restructured it into the short-rule +
docs/architecture-invariants.md pointer layout while this PR was open.
The response-viewer detail now lives in architecture-invariants, so the
Claude turn-grouping and restored-placeholder rebind notes moved there.
Changeset rewritten to record the measured effect on real transcripts.
2026-07-28 11:04:41 +02:00
Codeman maintainer 3cff98fe56 fix(security): scope the filesystem path picker per user in multi-user mode
Both picker endpoints are a second file-serving surface, and they
inherited neither the attachment guard's confinement nor its ownership
scoping. Two separate holes:

1. `sessionId` contributes that session's workingDir as a browse root,
   but it was resolved straight off ctx.sessions/ctx.store with no owner
   check, unlike the nine other session-scoped handlers in this file. A
   non-admin could pin ANOTHER user's working directory as a root just
   by passing their session id, then list and preview underneath it. Now
   runs canAccessOwned and reports 404, which also avoids confirming
   that a session id exists.

2. `Home` and `CASES_DIR` were unconditional roots for every caller.
   Per-user spaces live at <USER_SPACES_DIR>/<username>, which is INSIDE
   homedir(), so the Home root alone exposed every other user's
   workspace. A multi-user non-admin now gets only their own
   userSpacePath plus anything explicitly listed in
   CODEMAN_FILE_PICKER_ROOTS. /mnt/d is dropped as well: a broad host
   mount should be an explicit operator decision in a multi-user
   deployment, and operators who want it can name it in that env var.

Admins and single-user mode keep the host-wide roots, so behavior is
unchanged unless CODEMAN_MULTIUSER is on (opt-in, off by default).

All three discriminating tests were verified to fail against the
previous code: browse and preview both returned 200 instead of 404, and
the roots came back as [Home, Codeman Cases, ...] instead of [My Space].
Full suite green, 3784 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 10:49:49 +02:00
Codeman maintainer 390516ca3f merge master into mobile-path-picker-preview
Only CLAUDE.md conflicted: master restructured it into the short-rule +
docs/architecture-invariants.md pointer layout while this PR was open.
Route counts reconciled against master's numbering (files 14 -> 16 for
the two new filesystem endpoints, total ~197 -> ~199) and the path
picker's detail moved into architecture-invariants under its own
section.
2026-07-28 01:13:32 +02:00
Codeman maintainer 84f47e8ee0 fix(skins): keep tinted badges readable on light skins, pin OG modals
The light skins themed the app chrome, but a class of status badges and
accent-tinted pills still hardcode pale light-on-dark ink (#cdddff,
#9dc0ff, #ffc107, #fff) over a low-alpha tint. Measured on a rendered
page, that lands at 1.0 to 1.9:1 under all four light skins: the search
filter chips (Sessions / Events / Files) render as empty blue pills.
Re-point the ink at each skin's own dark tokens and keep the tint as the
category signal, which moves the same components to 3.2 to 14:1.

Also pin --floating-bg on the OG skin. The new :root default is slate
rgba(31,38,48,.96), which suits the Daylight palettes (their glass
header is already rgba(31,38,48,.85)) but repaints OG's modals, command
palette and floating windows away from the neutral near-black that skin
is built on.

Verified against a live instance across all seven skins, plus a real
shell session for terminal ANSI output. Full suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:56:15 +02:00
Codeman maintainer 541d9c8131 merge master into light-skins 2026-07-28 00:09:34 +02:00
Codeman maintainer 346bc8b173 fix(terminal): link whole URLs and paths instead of truncating them
Three separate truncations, each cutting a clickable link short so it opened the
wrong target (or nothing at all).

1. A single `&` ended the match. It is a query-parameter separator, so every real
   query string was cut: a WordPress edit link resolved to `?post=1479` and opened
   the post list instead of the editor, and Claude Code's own `/login` URL was not
   usable at all. `&` is now part of a URL; `&&` stays a boundary, since that is
   the shell operator and never appears inside one. A lone trailing `&` is still
   trimmed as punctuation.

2. Links longer than the terminal is wide were cut at the row boundary. xterm
   calls the link provider once per visible ROW and translateToString returns only
   that row, despite a comment here claiming it handled wrapping. The provider now
   stitches the continuation rows back into one logical line and maps match offsets
   back to (x, y), so a link can span rows.

   Two kinds of continuation exist and handling only the first is not enough. A
   SOFT wrap is the emulator running out of columns, which flags the next row
   `isWrapped`. A HARD wrap is the program wrapping its own output and emitting a
   real newline, which flags nothing: Ink does this, which is why the /login URL
   was cut at the window edge and why the clickable part grew when the window was
   widened. A row that fills the full width is now treated as continuing into the
   next, that being the only trace a hard wrap leaves behind. Bounded to 12 rows so
   a screenful of wide output cannot make every hover re-scan the viewport.

3. Image and PDF paths were not matched at all. `.claude-images/paste-*.png`, what
   Codeman writes for a pasted screenshot, rendered as plain text. Those extensions
   are now linked and open the file preview, which renders images inline, rather
   than the log viewer, which would show binary noise.

Verified in a real terminal: a 450-char /login URL hard-wrapped across 5 rows with
zero isWrapped flags in the buffer (so a genuine hard wrap, not the soft case)
links intact, as do soft-wrapped URLs and a wrapped attachment path. Regression
cases added to link-provider-regex.test.ts, which extracts the patterns from the
shipped source so they cannot drift. Its existing ReDoS guard still passes, which
matters because this changes a pattern that once froze the tab on hover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:25:29 +02:00
Codeman maintainer b34fcaf928 feat(web-tabs): open dashboard URLs as tabs beside agent sessions
Adds a "Web / URL" section to the Run dropdown. A saved URL renders as a tab in
the same strip as Claude/Codex/Gemini sessions, with the same Alt+1..9 numbering,
so Codeman is one mission control instead of Codeman plus a pile of browser tabs.

A webview is NOT a sixth SessionMode: no PTY, no tmux, no respawn, no idle
detection. It is a separate resource sharing only the tab strip and the main
content area, the same call that keeps Docker and remote-SSH as case overlays.

Dashboards are proxied through Codeman's own origin, because a direct iframe
fails three ways at once in the shipped deployment: prod serves HTTPS behind
tailscale serve, so http:// targets are hard-blocked as mixed content (with no
override at all on iOS Safari); Grafana/Portainer-class dashboards send
X-Frame-Options: DENY; and our own default-src 'self' CSP blocks cross-origin
frames. Proxying dissolves all three and leaves the production CSP byte-for-byte
unchanged, since /webview/... is already covered by 'self'. A useful side effect:
the fetch happens server-side, so a tailnet-only dashboard is reachable from a
phone that is not on the tailnet.

The proxy is not an API surface. It authenticates on a 192-bit capability in the
path (memory-only, rolling TTL, bound to the minting user, revoked on edit or
delete) and is correspondingly exempt from the cookie and Origin checks, because
a sandboxed iframe is opaque-origin: it sends no SameSite=lax cookie and its
writes arrive with Origin: null. The Host allowlist is never bypassed. A second
Referer-keyed form of the exemption exists for root-absolute assets and is fenced
to safe methods on non-/api, non-/ws, non-/q paths.

Iframes omit allow-same-origin unless a URL is explicitly marked trusted, since a
proxied page is served from Codeman's own origin and could otherwise read this
document and drive the agent-spawning API. Authorization and codeman_session are
stripped upstream in BOTH modes, so CODEMAN_PASSWORD cannot leak into a dashboard.

Two things only a real browser reveals, both presenting as the dashboard's own
"Failed to fetch" while the page itself renders fine:

- Runtime-built root-absolute URLs (fetch('/api/data')) escape <base href> and
  land on Codeman's root. Widening the Referer fallback into /api would trade
  security for it, so an injected shim patches fetch/XHR/WebSocket/EventSource
  inside the frame instead, removing the class rather than the guard.
- An opaque-origin document CORS-checks every request, including to the host it
  was served from. Script/css/img loads are not CORS-checked, which is why the
  page renders while its API calls die. The proxy now emits CORS headers and
  answers preflights itself. registerSecurityHeaders answered every OPTIONS with
  a bare 204 before routing, carrying no ACAO for Origin: null, so that
  short-circuit now exempts a valid capability.

Neither is reproducible with curl, which does not enforce CORS.

Also fixes a pre-existing bug found on the way: .toolbar has backdrop-filter,
making it a stacking context that trapped .run-mode-menu's z-index:1000, so
.welcome-overlay painted over the whole Run menu. With no session open, every
item in it (Claude Code included) was unclickable.

Verified end to end against a real tailnet dashboard: live data, WebSocket push,
no failed requests, and switching tabs does not reload the frame. 98 new tests
cover the pure rewrite helpers, the CORS helper, the shim's rewrite logic, route
CRUD, and every edge of the auth exemption.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 17:06:36 +02:00
Codeman maintainer eb8724bbf2 feat(mobile): replace the phone Shell button with Enter, move Shell into Run
On phones the toolbar slot held "Shell", which starts a rarely-needed session
type. Sending Enter is a constant need on a touch keyboard, so the slot now
holds a dark blue Enter button and shell launching moves into the expandable
Run dropdown (Terminal / Shell, label "Run SH"). Desktop and tablet are
unchanged: the green Run Shell button stays exactly where it was.

Enter goes through xterm's own input path:

  coreService.triggerDataEvent('\r', true)

NOT through sendInput() or a direct POST to /input. localEchoEnabled defaults
to MobileDetection.isTouchDevice(), so on a phone the characters you type are
buffered client-side in the LocalEchoOverlay and have never reached the PTY.
The onData Enter branch in terminal-ui.js is what flushes that buffer before
sending \r. A bare \r submits an empty line and leaves the typed text stranded
on screen, which presents as "the Enter button does nothing". Replaying the
keypress reuses the overlay flush, the flushed-offset cleanup and the 80ms
text-before-CR ordering instead of reimplementing them.

Verified with local echo forced on: before the fix the overlay still held
"echo OLD_WAY" after Enter; after it, pendingText is empty and the command
executes in the pane.

The !important on the Enter button's colors is required, not habit: styles.css
nests its skin overrides inside `html:not([data-skin="og"]) { … }`, so a plain
.btn-toolbar there resolves to (0,2,1) and outranks .btn-toolbar.btn-enter at
(0,2,0). Without it the button renders in generic toolbar grey.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 03:01:14 +02:00
codeman-local f812f65a33 feat(files): preview picker documents and images 2026-07-25 15:29:00 +08:00
codeman-local 2667150f33 feat(mobile): add filesystem path picker 2026-07-25 15:28:19 +08:00
codeman-local a842b091bf fix(ui): theme stateful light surfaces 2026-07-25 15:28:04 +08:00
codeman-local dae82388ed feat(ui): add light skin themes 2026-07-25 15:28:04 +08:00
codeman-local bca56b4273 fix(web): normalize Claude response viewer turns 2026-07-25 15:26:34 +08:00
Codeman maintainer 211f3c07dd feat(ui): clean default header (usage chips, file viewer on; token chip, lifecycle log off)
The default desktop header right cluster is now: WS, CPU, MEM, File Viewer
folder button, 5H/7D plan-usage chips, gear. The token-count chip and the
lifecycle-log document button default OFF (both still honor stored prefs),
and the File Viewer button defaults ON (phones keep hiding it via mobile.css).
Templates ship the hidden/shown state so nothing flashes before settings load.
Capture scripts seed showTokenCount:false so screenshots match regardless of
server defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:50:43 +02:00
Codeman maintainer 0ee268ba82 fix(ui): stop the centered voice button overlapping the case picker
.toolbar-center is absolutely centered (left: 50%), so on viewports below
~1500px, or with long case names widening the left toolbar group, the voice
button rendered on top of the case picker's chevron and the + button. Below
1500px it now falls back into normal flex flow where overlap is impossible;
wide viewports keep the centered layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 23:36:38 +02:00
Codeman maintainer 1be98ff8a3 fix(mobile): collapse header brand to a C home button on phones
On <430px screens the full Codeman wordmark wasted header space; the brand
now renders a single C (same tap target, still app.goHome()). Desktop and
tablet keep the full wordmark. The compact letter lives in a separate
aria-hidden span so i18n custom branding keeps rewriting only the wordmark.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:47:09 +02:00
Codeman maintainer 4f8471189e chore: version packages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 09:43:22 +02:00
Codeman maintainer 56db02412b Merge master into feat/custom-name-i18n; keep windowTitle stable on solo renders
Resolves the CLAUDE.md paragraph conflict with #162, skips the
windowTitle recompute for solo-session renders so a detached window
cannot reset the push-notification hostTitle prefix to the default,
and prettier-formats test/mobile/devices.ts (came in unformatted via
the #162 merge; CI format:check only covers src/**).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 09:17:04 +02:00
Ark0N 689d9fc5e5 Merge pull request #164 from shenlvkang-collab/fix/run-session-tab-dedup
fix(ui): show new run tabs immediately
2026-07-23 09:14:22 +02:00
Ark0N 50547a4e89 Merge pull request #163 from shenlvkang-collab/fix/unicode-working-directories
fix(paths): accept Unicode working directories
2026-07-23 09:13:24 +02:00
Ark0N 3c2a5bfef3 Merge pull request #162 from shenlvkang-collab/fix/foldable-mobile-settings
fix(mobile): preserve settings across foldable postures
2026-07-22 19:05:02 +02:00
codeman-local 8d9fc4195b feat(ui): add custom branding and Chinese localization 2026-07-21 02:49:25 +08:00
codeman-local 5abcae16b4 fix(ui): show new run tabs immediately 2026-07-21 00:20:34 +08:00
codeman-local 66ad681666 fix(paths): accept Unicode working directories 2026-07-21 00:04:40 +08:00
Codeman maintainer 6c8d4ca72f chore: version packages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:35:40 +02:00
codeman-local 51cb3a7205 fix(mobile): preserve settings across foldable postures 2026-07-20 21:22:28 +08:00
Codeman maintainer 64559b60d1 feat(ui): hide the Cron toolbar button by default (opt-in via Header Displays)
The Cron button now follows the same opt-in pattern as the Session Manager /
Away Digest / File Viewer buttons: the template ships the btn-cron--hidden
marker class and applyHeaderVisibilitySettings() removes it only when the
per-device showCronButton setting (App Settings -> Display -> Header Displays)
is enabled. Defaults flipped to false in the mobile defaults block and both
?? fallbacks. Cron jobs remain fully functional; only the launcher is opt-in.

Verified in a live browser: fresh profile hides the button + unchecked toggle,
enabling shows it immediately and persists across reload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:43:26 +02:00
Codeman maintainer 683544a22e Merge master into PR #157 (session manager polish)
Resolutions (sse-events.ts / constants.js / app.js): unions of the docker/
multi-user event registrations from master with the session-order/pin events
from this branch.

Additions on top of the merge:
- POST /api/sessions/:id/pin now falls back to the persisted store record when
  no live session exists: COD-142 deliberately preserves pinned records after
  kill (and cleanupStaleSessions skips them), so without this a pinned-then-
  killed session could never be unpinned. Owner-scoped in multi-user mode.
- SessionOrderUpdateSchema bounds (id <= 100 chars, <= 500 entries) so a buggy
  client can't persist megabytes into state.json; empty strings still flow to
  normalizeSessionOrder which drops them.
- Route tests for the persisted-record pin fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:31:44 +02:00
Codeman maintainer 25c67f9415 Merge master into PR #156 (remote tmux durability)
Resolutions:
- session.ts: keep the extracted _buildRespawnPaneOptions() helper (COD-108)
  and add master's docker/owner fields to it
- tmux-manager.ts: docker branch first, then remote via buildRemoteSessionCommand
  (now an options object threading claudeMode/allowedTools into
  buildRemoteLaunchCommand, preserving the 6.3 multi-user permission downgrade)
- case-routes.ts: keep master's adminOnly helper; gate the new COD-105 discovery
  endpoint admin-only in multi-user mode (hosts are machine-level infra)
- settings-ui.js: union of remoteAutoReconnect + master's header-button defaults
- session-routes.ts: union of imports; session gets remote + owner

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:27:02 +02:00
Codeman maintainer 524a096e14 Merge feat/docker-session-mode into master (docker deep-review fixes)
Brings the docker session-mode deep-review work (intended for the skipped
1.4.2) onto the 1.5.x line: deterministic-conversation-id resume across
container stop/recreate, config-drift detection + POST /api/docker-cases/:name/recreate,
docker model-picker support, import-manifest hardening, remote-daemon (context/
daemonHost) correctness, comma-in-path rejection, and the zh-CN README re-translation.

Conflicts resolved to preserve BOTH the multi-user security scoping already on
master (ownership checks, workingDir confinement, permission downgrade) AND the
docker features. Version kept at master's 1.5.0 (the 1.4.2 bump is superseded;
a fresh changeset bumps to 1.5.1). tsc, eslint, and test:ci all green (3548 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 13:45:37 +02:00
Codeman maintainer ccb3afc9ee fix(multiuser): close cross-user web-layer scoping holes found in review
The opt-in multi-user feature's only enforcement is web-layer scoping
(all sessions share one OS account). An adversarial review found 8 critical
+ 7 high cross-user holes that defeated it, plus mediums; all fixed here.
Single-user (flag-off) behavior stays byte-identical apart from documented
consistency deltas.

Ownership / confinement:
- DELETE /api/sessions (bulk) + /:id now owner-scope / findSessionOrFail
- quick-start, cron (create+fire), scheduled runs confine workingDir to the
  owner's space; case link/docker-link/docker-import confine the host path
- resolveCasePath no longer resolves linked cases for non-admins; foreign
  remote/docker cases are skipped (fall through to the caller's own local case)
- history, subagents/workflows, mux-sessions, orchestrator, cron run-history,
  away-digest, and remote/docker host reads are owner- or admin-scoped

Permission policy (section 6.3):
- non-granted users are downgraded at every spawn site incl. legacy
  /api/scheduled, PlanOrchestrator one-shots, remote launch, and the cron-fire
  gemini/codex bypass switches; resolveClaudeModeForUsername now fails closed

Auth / store:
- verify-first login throttle (a correct password is never locked out),
  /ws terminal subject to the change-password lockbox, cookie fast-path
  re-validates identity live, role/grant changes revoke sessions, admin delete
  runs the last-admin guard before any teardown
- users.json: distinguish missing (ENOENT) from corrupt/unreadable so a bad
  read can't overwrite all accounts; unique per-process temp write path

Event streams:
- debounced session:updated + batched task:updated, clipboard, and push
  notifications route by owner (fail closed); getLightState hides machine-wide
  globalStats from non-admins

Tests: two suites updated to assert the fixed (secure) behavior. tsc, eslint,
and test:ci all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:33:12 +02:00
Codeman maintainer c3b0dc345b fix(multiuser): wrap modal tabs so the injected Users tab is clickable
A Playwright browser pass found the injected 9th App Settings tab (Users)
overflowed the non-wrapping .modal-tabs flex row and landed under the modal
backdrop (elementFromPoint returned .modal-backdrop, not the button), so a real
mouse click was intercepted. flex-wrap:wrap lets the tabs wrap to a second row;
the built-in 8-tab modals still fit on one row (no visual change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:38:06 +02:00
Codeman maintainer ac6fe6ef79 feat(multiuser): phase 5b, frontend (identity boot + admin panel)
- public/admin-ui.js (new, self-contained): on boot fetches GET /api/me and
  stores window.__codemanUser; installs a fetch interceptor that opens a
  change-password modal on any 403 PASSWORD_CHANGE_REQUIRED (and on boot when
  mustChangePassword is set); for a multi-user admin, injects a "Users" tab into
  the existing App Settings modal (create/reset/disable/enable/promote/demote/
  grant-bypass/delete with typed confirm + one-time-password reveal). No header
  button, so the mobile-header policy stays green; nothing renders in single-user
  mode.
- me-routes: GET /api/me returns a `multiUser` flag so the UI distinguishes a
  single-user admin (no admin UI) from a multi-user admin.
- index.html: load admin-ui.js after settings-ui.js, before session-ui.js.

Tests: test/admin-ui.test.ts (JSDOM: identity boot, Users-tab injection gating by
role/mode, forced change-password modal, script-order wiring). Backend verified
end-to-end by test/admin-routes.test.ts against a live server. A full Playwright
pass is recommended before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 04:31:30 +02:00
Codeman maintainer dafe3de185 feat(multiuser): phase 5a, admin user-management API
- routes/admin-routes.ts: GET/POST /api/admin/users, PATCH/DELETE
  /api/admin/users/:username, reset-password, logout. Multi-user only (404
  otherwise), requireAdmin, last-admin invariants, one-time-password on create /
  reset (returned once + mustChangePassword), disable/reset/delete revoke cookie
  sessions, delete kills the user's live sessions first (normal teardown) and can
  delete their space (guarded). Per-user stats (live/active sessions, case count).
- web/admin-audit.ts: append-only ~/.codeman/admin-audit.jsonl (timestamp, acting
  admin, action, target, IP) for every user-management action.
- SSE admin:usersChanged + auth:passwordChangeRequired (sse-events.ts + constants.js).

fix(user-store): serialize users.json read-modify-write

touchLastLogin fires on every Basic auth (fire-and-forget) and was racing route
writes (create/update), clobbering records — a real corruption bug surfaced by
the admin tests. All mutators now run under a single write lock, and
touchLastLogin is throttled to once/minute per user to bound disk churn.

Tests: test/admin-routes.test.ts (8, live server) + user-store lock verified by
the existing user-store suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 04:26:44 +02:00
Codeman maintainer 2a06f7a5a8 feat(multiuser): phase 4, event fan-out + stream scoping
Scopes real-time streams and the init snapshot so a multi-user client only
receives what it owns. No-op in single-user mode (identity-less clients).

- WS terminal (ws-routes): owner gate after the session lookup. A non-admin may
  only attach to their own session (close 4003); the global auth hook already
  ran on the upgrade and decorated req.authUser, so an unauthenticated upgrade
  never reaches the handler.
- SSE (sse-stream-manager): per-client identity stored at addClient; broadcast()
  and the terminal-batch flush both enforce a routing hint via canDeliver().
  WebServer.broadcast auto-derives the hint (deriveSseHint): session-scoped event
  families resolve the owner from the payload's session id (fail closed when the
  owner can't be resolved), machine-level families (docker/tunnel/update/system/
  cron) + host-plan telemetry are admin-only, everything else stays global. Raw
  terminal bytes resolve the owner once and are withheld from non-owners.
- getLightState is filtered per connection AFTER the shared cache (sessions,
  respawnStatus, subagents, workflowRuns by owner; scheduledRuns + planUsage
  admin-only); applied to both the SSE init snapshot and GET /api/status.
- file-routes: getKnownSessionWorkingDir + getSessionAttachmentHistory (the
  preview/thumbnail/history helpers that bypass findSessionOrFail) now owner-check
  the session, closing a cross-user file-read path.
- GET /api/search: harvestSources is owner-scoped.

Deferred to a follow-up (documented in docs/multi-user-plan.md): away-digest +
subagent/workflow REST list scoping, push-subscription identity + routing,
per-user screenshot subdirs. The live-event versions of these are already routed
by the SSE hint; only the on-demand REST aggregates remain global for admins-only
follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 04:17:17 +02:00
Codeman maintainer 453605a58f feat(multiuser): phase 3, ownership threading + scoping
Threads per-user ownership through sessions, cases, cron, and the permission
policy. All scoping is a no-op in single-user mode (isMultiUserMode() guards).

Sessions
- Session.owner stamped at every create path from req.authUser / job.owner:
  POST /api/sessions, /api/run, /api/quick-start, ralph start, cron launch,
  plan generation. Round-trips through recovery (MuxSession.owner mirror, read
  muxSession.owner ?? savedState?.owner) and the mux layer.
- findSessionOrFail(ctx, id, req) now does a NOT_FOUND owner check (never 403, so
  other users' session existence is not leaked); wired at ~50 call sites.
- List endpoints filtered by owner: GET /api/sessions, /api/sessions/unified
  (live+persisted+lifecycle scoped, host-wide transcripts admin-only), cron jobs.

Permission policy (section 6.3)
- resolveClaudeModeForUsername wraps getClaudeModeConfig at every spawn site so a
  non-granted user is forced to --permission-mode auto (bypass -> auto), including
  recovery (or a reboot would un-downgrade). buildPromptArgs now respects the
  session's claudeMode, closing the one-shot (runPrompt) bypass hole.
- Shell mode and cron launchCommand require canBypassPermissions: 403 at
  POST /api/sessions, /api/quick-start create, cron job create, AND cron fire time
  (re-checked against the owner's current grant).

Cases
- resolveCasesDir(user): per-user ~/codeman-users/<name>/cases in multi-user, the
  shared ~/codeman-cases otherwise. All case CRUD + ralph + plan + quick-start
  resolve through it. resolveCasePath is owner-aware.
- GET /api/cases scoped per user (own folders; legacy linked cases admin-only;
  remote/docker cases owner-filtered). RemoteCase/DockerCase gain owner, stamped
  at link/quickcreate/import.
- Remote + Docker host CRUD is admin-only.
- Non-admin workingDir confinement (the linchpin): realpath must resolve inside the
  user's space, enforced at POST /api/sessions and /api/run BEFORE any disk write.

Limits
- sessionCapacityState / sessionCapacityMessage centralize the global + per-user
  cap (CODEMAN_MAX_SESSIONS_PER_USER, default global/2), replacing the 6 copy-pasted
  MAX_CONCURRENT_SESSIONS checks.

Tests: test/ownership-scoping.test.ts (case isolation, host-CRUD gate, workingDir +
shell gates, and the scoping helpers). Deferred to phase 4: WS owner gate, SSE
fan-out filtering, file-route preview/thumbnail helper scoping, push routing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 04:02:46 +02:00
Codeman maintainer 4d8857f72a feat(multiuser): phase 2, multi-user auth pipeline
Adds a parallel multi-user auth branch (the single-user Basic-auth path is
left byte-identical). Off unless CODEMAN_MULTIUSER/--multiuser.

- middleware/auth.ts: mode-selecting registerAuthMiddleware. New async
  multi-user hook verifies username:password against the user store (scrypt),
  mints identity-carrying cookies, decorates req.authUser, enforces a per-IP
  AND per-username failure bucket, and the mustChangePassword lockbox. The
  hook-secret loopback bypass is now a single shared helper used by both
  branches. FastifyRequest.authUser module augmentation.
- ports/auth-port.ts: AuthSessionRecord gains username/role/mustChangePassword.
- user-store.ts: verifyPassword (timing-equalized against user enumeration).
- route-helpers.ts: getAuthUser (synthetic admin fallback), canAccessOwned,
  requireAdmin, revokeUserSessions; findSessionOrFail gains an optional req for
  a NOT_FOUND owner check (dormant until phase 3 wires callers).
- routes/me-routes.ts: GET /api/me (synthetic admin in single-user) and
  POST /api/me/password (verify current, min 8, clear mustChangePassword,
  revoke other sessions).
- QR: QrTokenRecord + AuthSessionRecord carry a username; tunnel-manager
  mintUserToken / consumeTokenWithIdentity / getQrSvgForCode; /q/:code binds
  the cookie to the token's user (rejects identity-less tokens in multi-user);
  GET /api/tunnel/qr mints a per-user token. Single-user keeps the rotating token.
- server.ts: bootstrap the initial admin from CODEMAN_USERNAME/PASSWORD on first
  boot (refuse to start with no users); multi-user with >= 1 user satisfies the
  non-loopback auth requirement and the tunnel-enable guard; userFailures bucket
  disposal.
- types/api.ts: FORBIDDEN, PASSWORD_CHANGE_REQUIRED, USER_EXISTS, USER_NOT_FOUND,
  LAST_ADMIN error codes (message + status wired).
- Session.owner field + getter/setter, SessionState.owner, MuxSession.owner,
  CreateSessionOptions.owner (foundation for phase 3 ownership threading).

Tests: test/multiuser-auth.test.ts (10, live server on 3170/3171). Existing auth
suite (auth-security, qr-auth, cod54-hook-event, network-auth-policy) unchanged
and green; full test:ci sweep passes (3519 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 03:26:21 +02:00
Codeman maintainer f496e35d71 feat(multiuser): phase 1, user store, mode plumbing, CLI
Opt-in multi-user foundation (off by default; no behavior change without
CODEMAN_MULTIUSER/--multiuser):

- src/config/multiuser.ts: isMultiUserMode(), getUserSpacesDir()/userCasesDir(),
  maxUsers(), maxSessionsPerUser() (per-user fairness cap = global/2).
- src/types/user.ts: UserRecord/PasswordHash/AuthUser/PublicUser/UserRole.
- src/user-store.ts: ~/.codeman/users.json (atomic tmp+rename, mode 0600, short
  TTL cache). scrypt hashing with per-record params + timingSafeEqual verify plus
  rehash detection; createUser/setPassword/updateUser/deleteUser with last-admin
  invariants; guarded deleteUserSpace (symlink + realpath confinement, section 8);
  pure section-6.3 resolvers (resolveClaudeModeForUser downgrades bypass to auto
  for non-granted users; canRunPrivilegedCommands); bootstrapInitialAdmin.
- src/cli.ts: "codeman users add|passwd|list|rm" (hidden prompt or
  --password-stdin) operating directly on users.json; a --multiuser flag on the
  web command.

Tests: test/user-store.test.ts (29 tests: hashing/verify/rehash, username
validation, atomic 0600 write, last-admin invariants, 6.3 resolvers,
delete-space guards, bootstrap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 02:58:51 +02:00
Codeman maintainer 91070f5dda feat(claude): add 'auto' startup permission mode
Adds Anthropic's classifier-guarded low-prompt mode (--permission-mode
auto) as a fourth ClaudeMode alongside skip-permissions/normal/allowedTools.
Wired through both spawn paths (buildPermissionArgs for direct PTY,
buildClaudePermissionFlags for tmux), the getClaudeModeConfig validator,
and the App Settings Startup Mode picker. Exports buildSpawnCommand for
test coverage.

This is the prerequisite for multi-user mode section 6.3, which downgrades
non-granted users' sessions to 'auto'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 02:48:14 +02:00
Codeman maintainer a21400614a chore: version packages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 02:36:02 +02:00
Codeman maintainer ca731c67b3 feat(docker): harden session mode + File Viewer button (v1.4.1)
Docker cases: seamless Claude auth (seed ~/.claude.json instead of the
corruption-prone single-file mount), full credential-store isolation for
claude + codex/gemini/gcloud/opencode (share only transcripts/rollouts,
seed the rest), auto-build the base image on first use, C.UTF-8 locale
(fixes box-drawing), collapsed/shortened Create-Case UI + short "(docker)"
case-menu tags, and w<n>-<case> tab naming for docker/remote sessions.
Also: opt-in File Viewer header button; fix a TZ-boundary flaky test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 01:36:21 +02:00
Codeman maintainer 3e1272a675 feat(docker): resource templates, GPU, elastic disk, bridge-hooks listener
- One-click "Run in Docker" gains an expandable settings panel with a Template
  picker (Small 2G/1 · Medium 4G/2 default · Large 8G/4 · GPU 8G/4/all) plus
  memory/cpu/gpu/network/image/mount-creds overrides. Any tweak creates a dedicated
  per-case host; the plain checkbox keeps using the shared `default` host.
- GPU passthrough: `gpus` on DockerHost/SessionDocker -> `--gpus <value>` in create
  args (needs the NVIDIA container toolkit). Elastic disk: no `--storage-opt` cap,
  so container storage grows as data flows in.
- CODEMAN_DOCKER_BRIDGE_HOOKS=1: opt-in second listener on the docker bridge gateway
  (auto-detected 172.17.0.1, override CODEMAN_DOCKER_BRIDGE_HOST) that serves ONLY
  the hook endpoints and delegates into the secret-gated pipeline, so in-container
  hooks fire on a loopback-only server. Non-hook paths -> 403; host-internal, not LAN.

Verified live: Large template applies real 8GB/4CPU limits; a secret-authenticated
hook POST from inside a container now reaches the handler (was connection-refused);
non-hook paths return 403; template UI + GPU field verified via Playwright.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 21:35:38 +02:00
Codeman maintainer db6cd838b1 feat(docker): one-click "Run in Docker" case creation + Export button
- New POST /api/cases/docker-quickcreate: creates a normal case (folder in
  CASES_DIR, scaffolded CLAUDE.md + hooks) AND links it to a hardened container
  with default settings, auto-provisioning a shared `default` docker host — the
  user never touches host/image/network fields.
- Create New tab gains a "Run in isolated Docker container" checkbox; on submit it
  calls docker-quickcreate then auto-starts a claude session inside the container.
- Case Manage list gains an Export (full-image) button per docker case.
- SSE listeners for docker:exportComplete/exportFailed toast + refresh the exports
  list.

Verified end-to-end on the live instance: one-click create put the case in
~/codeman-cases/<name>, auto-created the default host, launched claude in the
container; export button produces a bundle; checkbox + button render (Playwright).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 19:28:13 +02:00
Codeman maintainer a36c1f62db fix(docker): set CLAUDE_CODE_TMPDIR + document hook reachability limit
Found in live testing: claude refuses its default /tmp/claude-<uid> temp dir when
that path pre-exists root-owned (happens when the workspace bind-mount traverses
it, e.g. a workspace under /tmp/claude-<uid>). Set CLAUDE_CODE_TMPDIR to a
nonexistent HOME subpath the running uid creates+owns, so docker claude sessions
are robust to any workspace location.

Also document the hook-reachability constraint: in-container hooks POST to
host.docker.internal (the bridge gateway), so they only fire when Codeman is
reachable from the container (bind 0.0.0.0 + password); on a loopback-only bind
they don't fire and idle detection falls back to output-based (which works).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 18:19:50 +02:00
Codeman maintainer 8b2c857c3f feat(settings): wire session, away-digest, and cron button visibility toggles
Per-device App Settings > Header Displays toggles that show/hide the session
manager and away-digest header buttons (default OFF) and the cron footer
button (default ON). Adds the load/save/apply/default/displayKeys wiring in
settings-ui.js plus the marker CSS in styles.css. Client-only display keys,
stripped from the settings PUT so they never reach the strict server schema
(mirrors the showAttachmentsButton pattern); session/away stay hidden on
phones via the existing mobile.css rules. The button markup and checkbox
rows landed earlier in 5728b86.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:59:23 +02:00
Codeman maintainer 5728b86a68 feat(docker): frontend Docker tab, run wiring, and export/import UI
- index.html: Create Case "Docker" tab (name/workspace/host/image/network +
  advanced memory/cpus/mountCredentials/resumeOnStart), and a Docker-exports
  section in the Manage tab
- session-ui.js: linkDockerCase (POST docker-host, PUT on conflict, then
  docker-link; omitted optionals as undefined not null), case-picker label
  "name @ container" + search fields, switchCaseModalTab/submitCaseModal docker
  branch, and export/import UI (refresh/export/import/delete). Docker cases route
  through /api/quick-start like remote (runClaude/runShell/runOpenCode/Codex/Gemini)
- verified in a real browser (Playwright): Docker tab renders, linking through the
  UI creates the case and it appears in the picker as "uitest @ codeman-case-uitest"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:53:14 +02:00
Codeman maintainer 39ef17b6af feat(docker): export/import (move a container to another machine) + boot reaper
- src/docker-export.ts: full-image export (pause-consistent commit + save|stream +
  workspace tar + manifest -> one .codeman-container.tgz) and workspace-only; import
  validates manifest + per-member sha256, traversal-guards the workspace tar, docker
  load + quarantine re-tag (never overwrites a local tag). Bounded by
  runWithConversionLimit; free-space precheck; docker rmi in finally; sealed
  containers refuse full-image export.
- routes: POST /api/docker-cases/:name/export (background + SSE), GET/DELETE
  /api/docker-exports, GET download, POST /api/docker-cases/import (-> new host+case)
- instance-scoped boot reaper (docker-hosts.reapOrphanedDockerContainers) wired after
  restoreMuxSessions; never touches another instance's containers
- SSE docker:exportComplete/exportFailed/importComplete (both registries)
- fix: stream pipeline in saveImageToTar so the bundle isn't truncated

VERIFIED end-to-end on real docker: full export -> 326MB valid bundle -> delete
case -> import -> new container runs from the quarantined image with the workspace
file AND the in-image change both restored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:38:01 +02:00
Codeman maintainer e9f9497259 feat(docker): allowlist container-to-host gateway aliases in host guard
An in-container hook curl carries Host: host.docker.internal:<port> (the derived
CODEMAN_API_URL), so the always-on host guard must allow host.docker.internal /
host.containers.internal or every in-container hook is blocked 403. Exact-match
only; not a browser DNS-rebinding surface (resolves to the host only from inside
a container netns). Verified end-to-end: quick-start launches claude/shell in a
real container with the workspace bind-mounted and hooks scaffolded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 15:32:45 +02:00