Rewrite the xterm-zerolag-input README (hero demo GIF, value-first
structure) and fix its drift against the source: 175 tests not 78,
CJK/emoji wide-char support documented instead of listed as a
limitation, setPrompt() documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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.
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>
Feature + layout changes:
- Phone toolbar: Enter replaced Shell below 430px; shell launching moved into
the Run dropdown. Documents the ordering (setRunMode -> run -> runShell) and
that runMode is a loose string server-side so new modes need no schema edit.
- Repo root layout: config/ holds knip.json, Prettier config is the package.json
"prettier" key, SECURITY.md is under .github/, and the list of files that must
stay at the root with the reason each one is pinned there.
- Pointer to docs/SPEEDRUN.md, which nothing linked to after the move.
Traps worth not rediscovering:
- sendEnterKey MUST use triggerDataEvent, not sendInput or a raw POST. Local
echo is on by default on touch devices, so typed text is buffered client-side
and a bare CR submits an empty line while the text stays stranded. Cost me two
wrong fixes before the real cause surfaced.
- styles.css nests skin overrides under html:not([data-skin="og"]), giving a
bare .btn-toolbar rule (0,2,1) which outranks .btn-toolbar.btn-x (0,2,0) in
mobile.css whatever the load order. Explains why mobile.css needs !important.
- Browser tests pass vacuously on mobile input: sendInput() bypasses the overlay,
and headless Chromium reports isTouchDevice() false even with hasTouch, so the
local-echo branch never runs. Assert on overlay state and the tmux pane.
- The working tree is shared with other agent sessions: check the branch before
every commit (a commit silently landed on feat/web-tabs today and the push to
master reported "Everything up-to-date"), push with HEAD:master rather than
checking master out, and never git add -A.
- COM step 5 no longer tells you to git add -A, which has swept another
session's WIP into a release before.
Verified: 33 relative links and 30 invariants anchors all resolve, and every
factual claim re-checked against the tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Continues trimming the repo root so the README is reached with less scrolling.
Root files: 19 -> 15 across both passes.
- knip.json -> config/knip.json, joining eslint.config.js and the vitest
configs. `npm run knip` now passes --config explicitly. Verified by A/B: the
run from the new location produces byte-identical findings and the same five
configuration hints as from the root, so knip resolves its globs relative to
cwd rather than the config file. Those hints are pre-existing, not caused by
the move.
- .prettierrc -> the "prettier" key in package.json, a config source Prettier
reads natively, so editor format-on-save keeps working with no --config flag
anywhere. Verified live: `npm run format:check` still passes across src/**,
which it could not if the config had been lost (Prettier's defaults are
double quotes at 80 columns and would flag nearly every file).
.prettierignore deliberately stays at the root: Prettier resolves it relative
to cwd, so moving it would require threading --ignore-path through every
script and would break editor integration.
Everything else in the root is load-bearing: .editorconfig (walks up from the
edited file), .nvmrc/.npmrc (read from the project root), tsconfig.json (bare
`tsc` discovers it), LICENSE (GitHub license detection), install.sh (its raw
URL is the published one-liner in the README and cannot move without breaking
every copy in the wild), plus the five documented .md files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trims the repo root listing so the README is reached with less scrolling.
Only these two were movable; the other five root .md files are load-bearing
and stay put:
- README.md / README.zh-CN.md — the landing page and the language-switcher
entry point
- CLAUDE.md — Claude Code loads project instructions from the ROOT path;
moving it silently breaks every future session in this repo
- AGENTS.md — the agent-convention file Codex reads from the root and injects
as context (see the comments in session-routes.ts)
- CHANGELOG.md — the changesets default writer emits it next to package.json,
so moving it breaks `npm run version-packages`
GitHub officially resolves .github/SECURITY.md, so the Security policy tab
keeps working. Inbound links updated in both READMEs, CLAUDE.md and
docs/versioning-policy.md. CHANGELOG.md also names SECURITY.md but is left
alone: it is a historical record, not a live reference.
The move broke a link the other direction too: SECURITY.md pointed at
docs/security-architecture.md, which from .github/ resolved to
.github/docs/... — repointed to ../docs/. All relative links in the six
touched files verified resolving (62 links, 0 broken).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md was 110KB (~27.5k tokens) loaded into every session, with 30 lines
carrying 49% of the bytes as single-paragraph walls (the Docker cases entry
alone was 9,388 chars). Extract the implementation detail verbatim into
docs/architecture-invariants.md (41 sections) and leave the rule plus a
pointer inline. Result: 59.5KB, ~14.9k tokens, 46% smaller.
Also:
- Add CLAUDE.md to .prettierignore. Prettier's markdown printer escapes
underscores in the glob-heavy paths used throughout, which had already
corrupted the Ultracode paragraph (agent-*.jsonl became agent-\_.jsonl,
collapsing backtick spans). npm run format:check is unaffected; its globs
are src/** only.
- Move version archaeology (PR numbers, ticket ids, commit shas, "was X now
Y" lineage) into the invariants doc, keeping the rules and their reasoning
inline.
- De-duplicate the Core Files table against Key Patterns.
- Document install.sh in Scripts, and why Prettier's scope is deliberately
narrow (14 hand-formatted public JS modules are guarded by
check:public-assets and check:frontend-syntax instead).
Two factual fixes found while verifying: displayKeys is a client-side merge
policy, not a wire filter, and showResponseViewer / showPlanUsageLimits /
language are declared in SettingsUpdateSchema and do persist server-side; and
the respawn route count is 7, not 18.
Verified: 30/30 cross-doc pointers resolve, 1,184 of 1,190 backticked
identifiers from the original survive (the 6 others are dropped archaeology
or the prettier-corrupted spellings), 59 table rows well-formed,
format:check and check:frontend-syntax clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds src/docker-hosts.ts + src/docker-export.ts to the Infra row and
corrects the app.js size note (4906 lines), merged with the 1.7.0
i18n.js additions to the same rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
README.md + README.zh-CN.md:
- New "Remote SSH Sessions" section (durable remote tmux, auto-reconnect,
discover/attach with detach-not-kill, shared sessions, injection-safe ssh)
- New "Session Manager & Command Palette" subsection (pinning survives kill,
name retention on resume, cross-device tab order sync)
- Multi-user quick start right after installation (users add + --multiuser),
and the zh-CN README gains the full Multi-User Mode section it was missing
- Security: document the configurable startup permission mode (skip/auto/
normal/allowedTools) and the multi-user auto downgrade
- Cron header button noted as opt-in (Header Displays); API section counts
refreshed (~190 handlers / 20 route modules) with pin, session-order and
unified endpoints; Development now recommends npm run test:ci
CLAUDE.md (/init audit): session-order.ts in the Session row, PR #157
session-manager polish appended to the unified-list pattern, opt-in Cron
button documented, route/SSE counts refreshed (20 modules, ~188 handlers,
~146 events)
install.sh: the post-install "How would you like to run Codeman?" menu (and
the CLI picker + yes/no prompts) read from stdin, which under curl | bash is
the pipe, so choices were impossible and the script silently fell through to
the default. New has_tty()/read_reply() helpers prompt via /dev/tty whenever
a real terminal exists (same approach the sudo path already used) and only
fall back to defaults when there is genuinely none, now with an info line
saying so. Verified both paths with a pty harness (script(1)) and setsid.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
Stamp the plan doc with shipped-by-phase status; add the multi-user Key Patterns
entry + State Files + case-spaces note to CLAUDE.md; add a multi-user section to
the security architecture (threat model: workspace separation, not a security
boundary) and a README opt-in section; add a minor changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CLAUDE.md (the 1.4.1 release commit only reformatted it): document the
seeded credential-isolation model (resolveDockerClaudeArtifacts /
resolveDockerCredentialArtifacts, buildSeamlessClaudeConfig), the
auto-built agent base image (ensureAgentBaseImage + docker:imageBuild*
SSE events), the C.UTF-8 image locale, w<n>-<case> tab naming, and the
opt-in File Viewer header button; bump the SSE registry count to ~138.
README.md: add Gemini to every CLI enumeration (tagline, install, WSL,
Multi-CLI, security, architecture diagram), split the Docker section's
hardening bullet into hardening + seamless-auth/credential-isolation,
note the base image now auto-builds on first use, add a File Viewer
bullet to More Features.
README.zh-CN.md: mirror all of the above, add the previously missing
"Isolated Docker Sessions" section and a Docker bullet in More
Features, fix the Node badge to 22+, and run prettier over the file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- CLAUDE.md: rewrite the Docker cases Key Pattern to the shipped 1.4.0 state
(removes the stale "not on master / Phases remaining" framing); add
docker-quickcreate/templates/GPU/elastic-disk/export-import, the
CODEMAN_DOCKER_BRIDGE_HOOKS listener, docker state files, env vars, route +
SSE counts, and the build-agent-image command.
- README.md: new "Isolated Docker Sessions" section + a More Features bullet.
- docs/security-architecture.md: new §10 "Docker container isolation" (hardening,
commit-safe creds, blast radius, untrusted-import safety, bridge-hooks) +
Quick-reference env vars.
docs/docker-cases.md (user guide) and docs/docker-cases-plan.md (design) were
shipped with the feature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55f5ada (COD-105) added Phase 2 of the remote-tmux arc: discover codeman-*
sessions on a host and attach to non-owned ones, with detach-not-kill on close.
- Data model: SessionRemote.owned/remoteSessionName + RemoteSessionInfo;
toSessionRemote (owned:true) vs toAttachedSessionRemote (owned:false).
- New Ownership section: discovery (listRemoteCodemanSessions, the literal-\t
parse quirk, never-throws/VITEST), attach-vs-launch selection
(buildRemoteSessionCommand), and the killSession detach-not-kill guarantee.
- API: GET /api/remote-hosts/:hostId/sessions + the attachRemoteSession
create path.
- CLAUDE.md Remote Key Pattern notes discover/attach + detach-not-kill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit f321e1200a9a7c1e58c69ba936b680200fd53275)
Release 1.3.5. Consumes the changeset from PR #155: re-issue the
codeman_session cookie on every authenticated request so the browser cookie
lifetime tracks the server-side sliding TTL, fixing the recurring native Basic
Auth dialog during active use.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deterministic claude --version probe seeds cliVersion so wheel-forwarding
to Claude's transcript engages (banner scrape was unreliable on 2.1.187+
and resumed sessions). Shift+wheel reads the dominant axis so a trackpad's
horizontal Shift-scroll reaches local scrollback. New per-device
"Wheel Scrolls Local History" opt-out. Wheel reports use a fire-and-forget
send path so they no longer flicker the pending-bytes indicator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the Cron Jobs modal skin-aware + consistent with App Settings:
skin-variable selects (appearance:none, --bg-input fill, custom chevron),
color-scheme:dark for native controls, themed date/time inputs, and
btn-toolbar-sized toolbar/footer buttons. Bumps aicodeman to 1.3.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>