mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
19ffe9b7a859b0fb76440881724ca652cacf6a50
330
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
492f8d8ddf |
Merge pull request #436 from irisitymichaelgrundberg/fix/replay-output-that-arrived-after-the-capture
fix(terminal): keep the output a pane capture could not contain |
||
|
|
75a028e825 |
fix(terminal): re-take the sticky-scroll baseline after a replay
A capture load now replays its queued tail, and that replay runs through `batchTerminalWrite`, which samples `_wasAtBottomBeforeWrite` before it queues. It runs inside `chunkedTerminalWrite`, before that promise resolves, with the terminal freshly reset and rewritten — so the sample is always true. The caller then restored the reader's position and the next `flushPendingWrites` scrolled straight back to the bottom off the latched flag, undoing it. The only thing in the way was `_hasRecentUserScrollUp()`, a 1500ms window a server-triggered refresh is usually past. `_syncStickyScrollBaseline()` re-takes the flag from wherever the viewport now sits, and the two paths that restore a position call it right after doing so: `_onSessionNeedsRefresh` and `_maybeRefetchFullHistory`. Those are the paths #259 and #205 exist for, and they are also where a non-empty queue is most likely, since a needsRefresh fires when output is flooding. Re-taking rather than suppressing the sampling: suppressing leaves whatever stale value the flag held from before the load, which on the full-history re-pull has no reason to be false. `selectSession` and `_onSessionClearTerminal` deliberately end at the bottom, so the sampled true is already the truth there and they do not call it. `_bufferLoadFinishOpts` gains the coverage the CI gate can see: both mux sources flush, `history` does not, and a payload naming no source does not. Its only coverage was the browser suite, which CI does not run. The JSDoc and the changeset now record the one duplicate window this cutoff cannot close. The server appends output to the byte buffer in the same tick it emits, but broadcasts on a batch timer — 8ms over WebSocket, 16 to 50ms over SSE — so a batch pending when `capture-pane` ran leaves the server after the reply and is replayed although the capture holds it. It is one batch interval wide against a recovery window spanning the whole chunked write, and closing it means flushing that batch server side before the capture. The second browser test asserts its session was created, so a failed create fails it instead of passing with zero hits. docs/architecture-invariants.md no longer claims the replay leaves the queued-event discard window alone. That clause now describes what decides how a load ends, the baseline rule, the batch window, and the three covering tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bb8ada7e5f |
fix(reboot-restore): the merge-time items from the #442 review
Seven things, none of which changes what the feature does. 1. The rebuilt Session dropped `nameSource`, so the constructor re-inferred it from the name: a session the user renamed by hand to something shaped like `w<n>-<case>` came back as `placeholder`, and with auto-naming on the next prompt overwrote their name. The route persists right after, so the loss went to disk. `restoreMuxSessions()` already passes it. 2. The already-live sets were snapshotted once before a loop that awaits a real `startInteractive()` per entry, so by the tenth entry the snapshot was tens of seconds old and a conversation resumed by hand from the Resume list in that window was invisible to it: two panes on one transcript, the exact thing the check exists to prevent. Both sets are now read per iteration, and the late case is spent rather than re-offered for the same reason the batch case is. 3. Auto-resume no longer re-arms the pre-reboot `autoResumeAt` on this path. The stamp predates the reboot and the pane is new, so honouring it meant one click had every restored session type `continue` into itself about a minute later, unattended, against the route header's own promise that a restored session comes back idle and disarmed. The setting stays ENABLED, so it re-arms on the next real limit message. A Codeman restart still re-arms from the stamp, because the limit footer will not reprint on its own; the new option exists only to tell the two paths apart. 4. `discardPartiallyBuiltSession()` now also calls `recordSessionStopped()` and `ralphTracker.fullReset()`, the two teardown steps `_doCleanupSession` performs that it was missing. Cosmetic, but a run left open reads as still going in the away digest. 5. A restored claude session gets `seedAgentSessionPreamble()` like both create paths, so the agent skill's bootstrap stays a two-line loader. 6. The heuristic's container comment was wrong in one direction and quiet about the real gap: after a genuine host reboot a containerized Codeman sees the host's short uptime and the banner does appear. What it cannot see is a container-only restart, which is where this would help most. 7. The banner is hidden in a solo window, which shows one session and has no tab strip to put restored ones in. Also reverts 17 of the 18 hunks in docs/api-reference.md, which were Prettier reformatting of prose the PR does not otherwise touch (docs/ is outside the format glob), keeping only the Reboot restore section and repairing the two continuation lines that reformat de-indented; renumbers reboot-restore-ui.js to @loadorder 11.65, since 11.7 is admin-ui.js, which loads after it; and gives the feature its CLAUDE.md entry plus a route test for the multi-user workspace-forbidden branch, the only new rule that had nothing behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
71ed7b127c |
fix(sessions): make the discard a real inverse of the construction
Third review of the reboot-restore branch. The narrow discard the previous commit introduced avoided everything cleanupSession() did wrongly, and in dropping so much of it also dropped four things it had to keep. The worst broke the retry the whole design rests on. setupSessionListeners() returns early while sessionListenerRefs still holds the session id, and the discard never cleared that entry. So the advertised flow — a rebuild fails because the agent binary is missing, the user fixes their PATH and clicks again — reused the same id, wired no listeners at all, and produced a tab that never showed output, never updated its status and never persisted. That is worse than the leak the discard was added to prevent. Three more registrations leaked with it: a RunSummaryTracker and its interval, an image watcher on the workspace, and the Ralph fix-plan watcher. The discard now undoes each registration setupSessionListeners() makes, in its order, and the per-session custom-model config directory, which holds the endpoint's API key literally and which nothing else would ever remove. The image-watcher flag was restored after the code that reads it, so a session came back reporting the feature as on with nothing watching. It moves to the before-spawn phase, and that phase now runs before the listeners rather than after them. The generation counter that lets a mid-restore dismiss win was global while clear() is ownership-scoped, so one user's dismiss discarded another user's unspent entries, permanently, because nothing rebuilds an in-memory plan. It is now per owner. Bumping only the owners of entries the dismiss removed was not enough either: take() has already emptied the plan by then, so a dismiss landing mid-restore saw nothing of that owner's to remove and invalidated nothing. The owners that matter are those with a restore in flight, filtered by what the dismissing user may access, and that is what clear() now bumps. Plan expiry bumps too, so a restore straddling the 24-hour boundary cannot hand entries back and give an expired plan another full day. Tests. discardPartiallyBuiltSession had no test at all: the only implementation any test ran was the mock's one-line stub, which is why every defect above was invisible. test/discard-partially-built-session.ts drives the real WebServer, and the retry assertion fails if the listener refs are left behind — verified by reverting the fix. The dismiss-race test drove the registry by hand, so deleting the route's generation argument left it green; it now goes through the route, and two further tests cover the multi-user cases. The mock context has now gone stale twice, because route tests pass it as `ctx as never` and tsconfig.json includes only src, so nothing ever compares it to the ports. A type-level guard is therefore inert — I wrote one and confirmed it never fires. test/mocks/mock-route-context-completeness.ts compares the mock's keys against WebServer.createRouteContext() at runtime instead, and names what is missing. Also: the API reference now says workspace-forbidden is judged against the owner's grant, the banner's module header no longer claims Restore always dismisses it, and the detail span gets the same min-width: 0 the phone rule already needed. Refs #411 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fbede5cd2a |
fix(sessions): act on the dual review of the reboot-restore route
Fifteen findings from two independent reviews of #442, three of them blocking. Every one is addressed here. The three blockers all sat in the restore route. A rebuild that threw after addSession left a registered session with no pane behind it, visible on the board, holding a layout slot and written to state.json, with its plan entry already spent; the catch now cleans the session up and puts the entry back. The loop checked neither the global nor the per-user session cap, so one click could take a board past a documented limit; capacity is now re-checked per iteration, because the loop is itself creating the sessions it counts. Worst of the three, a rebuilt session carried none of the state its constructor has no parameter for and then persisted itself over the record that held it, zeroing token and cost totals and dropping the pin. The pin matters most: pruning keeps a record only while it is pinned, so discarding it handed the record to the next stale sweep. A new reapplyPersistedSessionState() on the session port restores the pin, the token totals, auto-compact, auto-clear, auto-resume, nice priority, the flicker filter and the custom-model selection, and it runs before both startInteractive and the first persist. The rest, in the order they bite a user. Every rebuild failure was reported as workspace-missing, so the banner told users their repo was gone when the agent had simply failed to start; there are now distinct reasons, and the toast names each one. The client read restored and skipped off the outer response object rather than through the uniform envelope, so every count came back zero and neither toast ever fired. A board left open across the reboot never learned an offer existed, because the banner was seeded only on the page-load path; it now re-reads on every SSE init. The workspace check was existence-only, skipping the multi-user confinement that the create route applies, so a withdrawn grant would not be noticed. The banner had no phone breakpoint while its text was nowrap and its buttons could not shrink. Smaller: a missing workspace is now re-offered rather than dropped, while an already-open conversation is dropped rather than re-offered forever; a throw anywhere in the route returns the unspent entries instead of discarding the plan; the single flight is keyed by owner, since take() already stops two callers receiving one entry; the env clamp's header no longer claims a protection it cannot provide on this path today, and names the check that does bite; the three endpoints are documented in docs/api-reference.md; and the module header now says that os.uptime() reads the host's clock, so the feature is effectively off inside a container. The review also explained why the tests missed all of this: they proved the construction claim through their own copy of the construction rather than through the route, and the route tests used workspaces that did not exist, so no Session was ever built. test/routes/reboot-restore-rebuild-failure.ts mocks the Session module to drive the route's real path, and covers the cleanup, the reason reported, the re-application ordering, the broadcast and the caps. The mock route context gains the port method and the mux call the route needs. Refs #411 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bd286bf502 |
docs(wiki): catch the manual up to 1.29.0 and add the three run modes it never had
The wiki was written for seven run modes and never received Grok Build, DeepSeek Harness or OMP. They now appear everywhere the others do: the modes table and per-CLI notes, install commands, environment prefixes, the Quick Start table, the requirements rows, the vocabulary, and every "seven modes" count. The 1.27 to 1.29.0 changes land on the pages that own them: attaching a case to an existing container, multi-case adoption and the copy-a-case picker (Docker Cases); file reads over ssh in remote cases and what stays unavailable (Remote SSH Sessions, Working With Files, Security); single-page app routing, frame recovery, localhost links as tabs and the egress guard (Web Tabs); DeepSeek as the one non-Claude mode with real stop/blocked signals and Approvals items, Codex's own work detection, last-response, the model-endpoint routes and refreshed counts (HTTP API, Driving From An Agent, Hooks, Notifications, Keeping Agents Running, Core Concepts); Shift+drag, right-click copy, Auto Copy, the Ctrl+Z guard, font weight, the vertical rail and its activity sort (Keyboard Shortcuts, Input And Voice, The Dashboard, Settings Reference); the 600px phone cutoff, Codex shift arrows and iPhone Duo (Mobile Guide); the Docker Compose route and its update rule (Installation, Running As A Service); four new symptom entries and a "which CLIs" question (Troubleshooting, FAQ). Custom model endpoints are deliberately left to #430, which adds that page and edits Agent CLIs, Settings Reference and the sidebar; these edits stay out of the regions #430, #428 and #376 touch, and all three still merge cleanly on top. Both READMEs: the web-tab menu entry is labelled "Add URL" in the UI, not "Add dashboard". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
5b920cb43d |
feat(sessions): land auto-naming opt-in, in the prefix form, from the first user prompt only
Finishes #376. The contributed keystroke tracker sat on the raw byte stream and named tabs wrong five ways (every prompt, every write path, a bare Esc eating the next prompt's first character, pasted newlines as Enter, any CSI clearing the draft) and replaced the whole name, which dropped the case from the tab and reset the w<n> counter. This lands the feature with each of those closed: - First prompt means the first: applyAutoName() flips a placeholder to `auto` whether or not the string changed. nameSource is now the tri-state placeholder | auto | manual; the name setter is the only manual path. - Only user-originated input counts: write()/writeViaMux() take SessionWriteOptions.fromUser, set by the browser WS path and POST /input only, so Ralph, respawn, cron, approvals and the trust-dialog keys can never name a tab. A startMode 'shell' CLI never feeds the tracker (a capability, not an id check); the send-key route feeds trackUserInput() because its line feed bypasses the session. - Prefix form `w3-case: title`: parseSessionPrefix() already renders it as the title with the prefix in the tooltip and the next-session counter still matches it. Composed within MAX_SESSION_NAME_LENGTH. - Tracker rules per key: bare Esc resolves at chunk end; mouse/focus reports, Tab, cursor keys, Shift+Tab are no-ops; Up/Down and Ctrl+P/N/R taint the draft so Enter submits nothing rather than a fragment; bracketed-paste newlines and Ctrl+J / Shift+Enter join with one space; the draft keeps its head past 8192 code points; an escape past 64 bytes is abandoned. - Title: slash commands by shape (a path is a prompt), `!` escapes refused, first sentence only past 8 code points ("e.g." is not a title), 72 code points on a word boundary. - Synced `autoNameSessions` setting, default OFF (the prompt reaches mux-sessions.json, session:updated and /api/search), App Settings -> Appearance -> Tabs, read fresh per prompt after the eligibility check. Tests: test/session-auto-name.test.ts (tracker, title, composition, ownership, emit gating), the wiring test (once, prefix, setting off, manual protected), test/routes/session-name-routes.test.ts (PUT /name flips to manual and persists). Verified live on an isolated instance: API and browser-typed prompts name the tab, a second prompt does not, shells and renamed tabs are untouched, nameSource survives a restart. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c4322513d9 | Merge pull request #376 from shenlvkang-collab/feat/auto-session-names-upstream | ||
|
|
70fc6b32d5 |
docs: record the dup/last input ACK, Shift+drag and right-click copy, and multi-case adopted containers
Three behaviours landed from #375 without their doc entries: the duplicate input ACK now carries `dup:true` and the server's watermark (`docs/reliable-input-delivery.md` still described a bare ACK), Shift+drag and right-click copy in the terminal (the shortcut list did not know them), and one adopted container backing several cases at different in-container directories (the Docker cases paragraph still implied one case per container for adopted containers too). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
942bf37e48 |
fix(custom-model): unset injected env on clear, resume on restart, select the model for pi/omp/grok
Custom Model Endpoint Profiles (#393) let a session point its CLI at a custom OpenAI-compatible endpoint by injecting env vars or a config file and restarting the CLI in place. Review of the apply path found four things, two of them destructive. This lands all four plus the smaller items from the same review. 1. Clearing a selection did not clear it. The injected vars reach the CLI via `tmux setenv`, which persists at the tmux-session level and is inherited by `respawn-pane` (measured: `setenv FOO bar` survived two successive `respawn-pane -k`), so deleting the keys from the session's envOverrides relaunched the CLI still pointed at the old endpoint, and for the configDir kinds at a HOME/CODEX_HOME/GROK_HOME that had just been deleted. `Session.setCustomModel()` now reports the removed keys, queues them (`_pendingEnvUnsets`), and `RespawnPaneOptions.unsetEnvKeys` carries them into `applyEnvOverrides()`, which `setenv -u`s them before re-applying the live overrides, on the same path that already unsets the legacy CLAUDE_CODE_EFFORT_LEVEL. Verified on a private tmux socket that `setenv -u HOME` hands the next respawn the global HOME back. 2. Applying a model to a local claude session killed the pane. The relaunch was `claude --session-id <id>` and Claude refuses an id that already has a transcript, and unlike the dead-pane respawn this one kills a working pane first. `restartCli()` now pins the live conversation id as the resume id for that respawn when the CLI's launch declares a `fallback` chain, which renders the same `--resume <id> || --session-id <id>` shape the docker and remote pane commands use. Gated on the registry shape, not the CLI id: an entry whose resume id is minted by the CLI itself never declares that chain. 3. pi, omp and grok wrote their config file and then launched without the `--model` that selects it, so the file was ignored. The registry entry now declares `customModelInjection.launchModel` (`custom/{modelId}` for pi and omp, grok's `[model.codeman-custom]` block name), the builder renders it, and `_withCustomModelLaunchModel()` applies it onto the respawn options through `legacyConfigField`, leaving the stored <Mode>Config untouched so a clear falls back to the user's own model. A model id the CLI's `model` token pattern cannot carry is refused with a 400 rather than silently dropped by the argv engine. 4. Remote (SSH) and Docker sessions reported `restarted: true` and changed nothing: their `restartCli()` reattaches the durable tmux rather than relaunching the agent, and the env lands on the local pane. Both are refused with a 400 until those paths are plumbed. Smaller items from the same review: - The selection survives a Codeman restart as the disk-only `__customModel` bookkeeping (endpoint, model, injected key NAMES, config dir, launch model; never the values, which carry the API key). Recovery re-derives the values from the endpoint store through the same apply path the route uses and keeps the bookkeeping even when the endpoint is gone, so a later clear still has keys to unset. - Discovery goes through `webviewFetch()`, so the RESOLVED address is judged by the same egress guard the web-tab proxy uses, and `baseUrl` reuses `webviewUrlSchema` (http(s) only, no embedded credentials, link-local and cloud-metadata addresses refused). undici's `fetch failed` wrapper is unwrapped so the user sees the ECONNREFUSED underneath. - `custom-model-hosts.json` is written 0600 via tmp+rename, the per-session config dir 0700/0600 (pi and omp embed the key literally), and that dir is removed with the session. - `PR.md` is gone from the repo root and the design doc moved to `docs/custom-model-endpoints-plan.md` with the LAN address and the personal name scrubbed; every reference follows. The guide's `authStyle` text matches the shipped schema (`bearer | api-key`, default `bearer`) and says that `customModelEndpointsEnabled` is read by nothing until the picker lands. - `config/tsconfig.scripts.json` typechecks `scripts/test-local-llm-harnesses.ts` (four real type errors fixed). It is not yet wired into `npm run typecheck` because that line differs on master; adding `&& tsc -p config/tsconfig.scripts.json` there is the one-line follow-up. Tests: `test/session-custom-model-restart.test.ts` drives a real Session and fails on the unfixed code for items 1 to 3; the route suite covers item 4 and the pattern refusal; `test/tmux-manager.test.ts` pins that the unsets run before the overrides and that a shell-metachar key never reaches tmux. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
1e42cb4e2d |
Merge pull request #393 from opticon454/feature-custom-llm-server-support
feat: Custom Model Endpoint Profiles (local or cloud, all harnesses) |
||
|
|
e49c48145b |
fix(files): fail closed on remote symlinks, guard PUT for remote cases, bound ssh fan-out
Follow-up to #421 (remote-case file reads over ssh), addressing the review. Symlink escape on a host without `readlink -f` (blocker). The probe's portable fallback canonicalized only the directory chain and returned the final component unresolved, so on macOS < 12.3 `ws/notes.txt -> ~/.ssh/id_rsa` came back as `.../ws/notes.txt` (with the target's size), passed every containment and blocklist check that runs on `realPath`, and `cat` followed the link. The fallback now walks the directory chain with `cd -P`/`pwd -P` and follows the LAST component with plain `readlink` for a bounded number of hops, and anything it cannot fully resolve (a loop, a readlink failure, the hop cap) is reported with an `x` marker that parses as null, i.e. 404. It never returns the unresolved string. Measured on a real /bin/sh with `readlink -f` shadowed: the pre-fix script reports `/ws/notes.txt`, the fixed one `/secret/id_rsa`; both branches (native and fallback) now agree. `PUT /api/sessions/:id/file-content` never had the remote guard the PR described. It sits ahead of `validateSessionFilePath`, which resolves against the LOCAL filesystem, because with a same-named directory on the Codeman host (an sshfs mount of the remote tree, the documented stop-gap) the write landed on the local twin while the viewer believed it edited the remote file. ssh fan-out is bounded. `src/remote-ssh-limiter.ts` is a document-conversion-limiter-shaped semaphore (default 4, env `CODEMAN_MAX_REMOTE_FILE_SSH`) around every probe and buffered read; the attachment-history list resolves its whole history in ONE batched probe (`probeRemoteAttachmentHistory`, threaded into `registerExternalAttachment({remoteProbes})` so the guards run unchanged) instead of one handshake per entry; and probes chunk at 40 paths because the whole script is one argv string. Terminal output in a remote session is written on the remote host, so a prompt-injected agent printing hundreds of `codeman://attach` links forked one ssh per link, each holding a 20 s timeout, and a 100-entry history re-listed on every attachment:detected tripped OpenSSH's default MaxStartups. Streams are deliberately not counted (one per browser request, held for a whole playback, and gated behind a counted probe anyway). Smaller items from the same review: probe records are NUL-terminated and index-keyed after a leading NUL (a newline in a filename can no longer shift the alignment, and the banner is fenced off without last-N-lines guessing); size comes from `stat -c %s || stat -f %z`; the three IO functions refuse under VITEST instead of opening a connection; an unreachable host now reads as unknown (missing: false) for detected AND external history entries, where external used to fold its 502 into missing; a client that aborted during the guard probe has its body's ssh child reaped (`reply.raw.destroyed` is checked before the close listener is attached); `describeExecError` never returns Node's `Command failed: <ssh line>` message, which carried the identity path and the probe script into a 502 body; and the docs note that `isSensitivePath`'s three home-anchored entries resolve against the Codeman host's home, not the remote one. Tests: the probe script runs on a real /bin/sh with a `readlink` shim that rejects `-f` (the escape, a relative chain through a symlinked directory, a loop, a newline filename, banner chatter that itself looks like a record), the limiter's cap and FIFO order, and route tests for the PUT guard (local twin untouched, no connection), the single batched history probe, the unreachable-host alignment and the aborted-client reap. All four route tests fail against the pre-fix file-routes.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
792a251e35 |
Merge pull request #421 from Randalix/fix/remote-file-access
fix(files): read remote-case previews, downloads and attachments over ssh |
||
|
|
6dc27ae727 |
docs(webview): record the lost-frame page as the third unauthenticated 200, and the inline-style limit
The lost-frame recovery page is answered ahead of the credential checks in both auth hooks, which makes it the third unauthenticated 200 beside the two hook routes, and the only one decided by request headers alone. CLAUDE.md's security table listed exactly two, and docs/web-tabs.md is not where anyone auditing that looks, so it now has a row in the table and a fourth property in docs/security-architecture.md section 10b, including the `/` carve-out and its credential-free condition. Both state the property that comes with it: a non-browser client can set those headers, so an unauthenticated caller can tell a registered route (401) from a non-route (200) and enumerate the route table, accepted because the routes are public in docs/api-reference.md. docs/web-tabs.md gains the landing-page case in layer 6 and a Known limits entry: masking trades away the Referer safety net, only HTML is rewritten server-side, and a root-absolute url() inside an inline <style> block has the masked document as its Referer, so it 404s where the Referer fallback used to rescue it. External stylesheets are unaffected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b0dddc9c57 |
Merge pull request #402 from shenlvkang-collab/pr/webview-route-masking
fix(webview): let a proxied single-page app route on its own path, and recover a frame that reloads |
||
|
|
2bda191471 |
docs(docker): describe the root start and drop, and keep the override file out of the image
docker/README.md and docs/docker-compose.md now say that the container starts as root, corrects a daemon-created bind source and drops to PUID:PGID with setpriv, which capabilities that needs, and that a compose file written elsewhere must carry them. The README's PowerShell example runs Compose from inside docker/ so the override file is discovered, instead of the `-f docker/docker-compose.yaml` form its own Local customisation section warns silently drops it, and the reverse-proxy section no longer asks for an override file now that docker-compose.yaml forwards CODEMAN_ALLOWED_HOSTS itself. .dockerignore excludes docker-compose.override.* everywhere: it is the documented home for host-specific settings and rode `COPY . .` into the image, the same shape as the docker/.env exclusion above it (verified with a scratch build context: the override files and docker/.env are absent, .env.example and the compose file present). CLAUDE.md's Compose paragraph carries the corrected cap list, the writability probe, and the two traps behind it (KILL is for tini, the CLI prefix is appended to PATH). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
a29e1f61ef |
Merge pull request #377 from opticon454/bugfix-docker-user-perms
fix(docker): bind-mount ownership, Compose override discovery, and the default runtime account |
||
|
|
63aafdf274 |
fix(files): serve remote-case attachments, the path a click takes outside the case
A clicked path that points OUTSIDE the case directory goes through the attachment routes (the frontend's `_isExternalPreviewPath` sends every absolute path not under `workingDir` to `POST /attachments`), and those had the same local-`fs` assumption as file-raw: `realpathSync`/`fs.stat` on a path that only exists on the remote host, so the file never opened — the case the #415 report was actually about. - `registerExternalAttachment()` accepts `remote` and resolves through `remoteProbePaths` (canonical path, size/mtime, kind, plus the workspace root for the confinement check). Everything around it — blocklist, extension allowlist, workspace confinement, registry/dedupe — is now shared by both branches, so the remote path cannot drift from the local one. - The by-id routes (`raw`, `preview`, `thumbnail`), the metadata poll and the attachment history list resolve over ssh too. `raw` streams with the same Range contract as file-raw; `preview` (office) and `thumbnail` answer 400 for a remote record; an unreachable host answers 502, a vanished file 404. - Which host a record is read from follows the SESSION, never the path string: the same absolute path is a different file on each host, and a remote session never falls back to a local file with that name. - Codex generated artifacts keep force-workspace confinement for a remote case: the well-known artifact directories are anchored at THIS host's home, so only a file inside the remote workspace is trusted. Still local-only by design: writes, office conversion, thumbnails, the file tree/picker and tail-file. |
||
|
|
0c00a40530 |
Merge pull request #407 from Ark0N/feat/iphone-duo
iPhone Duo support: fold-aware dialogs, and a fold is no longer mistaken for the keyboard |
||
|
|
21dcec5d24 |
test(mobile): follow the 600px phone cut on the Duo branch
Rebased over #390, which moved the phone tier's cutoff from 430px to 600px. The palette's compound fold rule now lives in the 600-768px band mobile.css pads, the cascade samples the palette inside that band, and the closed iPhone Duo (466pt) is a phone rather than a small tablet while the open one (626pt) stays a tablet. Comments in both stylesheets, the device registry, CLAUDE.md and architecture-invariants say 600. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
707ea345eb |
fix(statusline): GET /api/settings never writes, and a save sends the collection switch only on a flip
Two follow-ups to #361's sticky telemetry switch. GET /api/settings reconciled an absent showPlanUsageLimits by persisting true, but readJsonConfig() answers {} for ANY read failure (a parse error, EACCES, EMFILE, a read landing inside PUT's non-atomic write), not only ENOENT, and every page load calls this route, so one unlucky read replaced the whole settings file with a one-key file. The route is a plain read again and the default moved into the reader: readPlanUsageTelemetryEnabled() treats an absent key as ON, the same way readWorkspaceHooksEnabled() does, which is what the desktop chip already shows for an install that never touched the setting. saveAppSettings() sent showPlanUsageLimits on every save. The chip defaults OFF on handhelds, so a phone saving its font size persisted false and switched collection off for every desktop, whose chip then went stale with no error anywhere. The key is now stripped like the other per-device display keys and re-added only when the save FLIPS the chip relative to what the device had (planUsageCollectionFlip), so an explicit toggle on any device still writes it in either direction. Tests pin both: the GET route with a mocked filesystem (absent, missing, EACCES, garbage, explicit), the reader default, and the flip helper plus its wiring in saveAppSettings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b2b2c767ea |
Merge pull request #361 from timkjr/fix/statusline-injection-opt-out
fix(statusline): inject plan-usage telemetry via ephemeral CLI flag, never disk |
||
|
|
2f9fc72252 |
docs(mobile): record the fold cascade traps and the keyboard-free baseline
CLAUDE.md's folding-devices rule gains the two new invariants (a shape change with the keyboard up baselines to window.innerHeight; a base gutter overridden by a later @media block needs its own zero-base fold restatement, and a compound rule written against a mobile.css shorthand is scoped to that band) plus the architecture-invariants pointer it lacked; the new Folding devices section there carries the mechanisms and the measurements. The device count is 138 since the two Duo profiles landed (68 Playwright + 70 custom). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
a5cf1f6005 |
docs(cli-registry): name the real tests and fields the catalogue docs point at
Three instructions a future contributor would follow literally were stale after the last review round: the "Adding a CLI" checklist sent the agent-image reason to AGENT_IMAGE_SPECIAL_CASES, a constant that no longer exists (it is discovery.install.agentImageLayer on the entry in stock.ts), the trust-boundary paragraph credited the embedded-commands pin to the invariants test when it is test/cli-catalog-sync.test.ts, and install.sh claimed "the parity test" pinned the DeepSeek Harness banner when no test did. That pin now exists: the invariants test asserts the script's grep literal and the registry's discovery.identity.regex agree on "DeepSeek Harness", and the comment names it. docs/docker-cases.md separated the two reasons a CLI stays out of the shared npm layer (no npmPackage at all versus an agentImageLayer entry), which it had folded into one, and architecture-invariants no longer lists the agent image's CLI set by hand. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
e6e5a62d9b |
Merge pull request #380 from opticon454/feature/cli-catalog-consumers
feat(cli-registry): drive install.sh and the Docker agent image from the CLI catalogue |
||
|
|
c9c8ffddde |
test(mobile): read PHONE_MAX as an exclusive bound everywhere, drop the stale 430px baselines
Follow-up to #390. PHONE_MAX had become 599, an inclusive bound, while three of its four consumers still read it as exclusive (width < PHONE_MAX for phone); the one site that switched to <= disagreed with getDeviceType(). It is 600 again with < at every site. The breakpoint table in docs/mobile-testing-report.md says 600, and the three 430px visual baselines are removed: they depict the tablet tier now, and the visual suite recreates a missing baseline on its next run on the machine that owns them. device-matrix.test.ts is also run through Prettier, which the commit hook demanded and the format gate (src/ only) never did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c2d019d956 |
chore: move the maintainer PR bot out of this repository
`scripts/pr-bot/` was maintainer tooling, not part of the server, the CLI or the npm package: a Telegram bot that reviews open pull requests in Codeman sessions and reports to the maintainer. It now lives in its own private repository and keeps running unchanged, as a client of Codeman's HTTP API like any other. It moved because it grew a second watcher, for GitHub Discussions, and shipping that here would mean publishing the briefs it hands its review sessions, the judgement calls in them and its safety model. None of that helps anyone installing Codeman, and all of it is easier to change when it is not a public interface. The move cost nothing structurally: the whole tree depended on one external package plus Node builtins. What this removes from the repo, and nothing else: the sources, their three test files, `config/tsconfig.pr-bot.json`, `docs/pr-bot.md`, the `pr-bot` npm script, the bot's globs in the typecheck/lint/format scripts, and its knip entry. CLAUDE.md keeps a short pointer in place of the section, because the bot still constrains work in here: it takes the `prbot-<n>` and `dscbot-<n>` session names on the local Codeman, holds clones under `~/.codeman/pr-bot/`, and fetches pull-request heads into `refs/pr-bot/*` of this checkout, which it must never check out or reset. The CHANGELOG entries from 1.25.0 and earlier still describe it. That is history rather than drift, and is left alone. Verified after the removal: typecheck, lint and format:check clean, and the suite passes 6843 tests across 357 files, which is the previous run minus exactly the 70 tests that moved out with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
013a5d9cc8 |
fix(files): read remote-case file previews and downloads over ssh
A remote case's workingDir is an absolute path on the remote host, but the file read routes resolved it with local `fs`: `validateSessionFilePath`'s realpathSync fails for a path that does not exist on the Codeman host, so every preview of an agent-written file answered "File not found" (#415). Add src/remote-files.ts as the single remote-read layer, built on the same buildSshConnectionArgs() the launch uses: - remoteProbePaths(): ONE round trip returning realpath + stat for the requested path AND the workspace root, so containment is checked against a remotely canonicalized root (a symlinked remotePath is ordinary). - remoteCreateReadStream(): streams the body (cat, or tail -c +N | head -c L for a Range) with nothing buffered in memory, and reaps the ssh child when the response ends so an aborted download cannot orphan it. - remoteReadFile(): bounded read for file-content. file-raw, file-content, file-preview and file-thumbnail now share one local/ remote target resolution. Guards keep their local strength: lexical pre-check, remote realpath, workspace containment, sensitive-path blocklist, and the size cap applied to the remote size before any bytes are read. An unreachable host answers 502 with the remote reason instead of a misleading 404. Nothing is ever copied to the Codeman host and there is NO local fallback (an sshfs mount of the same tree must not shadow the remote bytes). Deliberately unchanged: writes (edit=1 / PUT now answer 400 explicitly while the viewer hides its Edit affordance), office previews, thumbnails, file tree, picker, external attachment registration and tail-file stay local-only. |
||
|
|
fc098aaab2 |
docs(plugin): say to pick one install route, since plugin and user-level skill list twice
Measured with both installed: a fresh Claude Code lists `codeman` (the user-level or per-case copy) and `codeman:codeman` (the plugin). Neither shadows the other and both work, so this is noise rather than breakage, but the README, the wiki and the plugin README now say to choose one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
f6c08118dc |
feat(skill): ship the codeman agent skill as a Claude Code plugin from the repo's own marketplace
`.claude-plugin/marketplace.json` at the repo root makes `/plugin marketplace add Ark0N/Codeman` work, and the one plugin it lists is the repo itself (`source: "./"`), whose one component is `skills/codeman/`. So `/plugin install codeman@codeman` is a third install route next to `npx skills add` and `codeman skill install`, and the skill shows up in the plugin directories that index Claude Code marketplaces. Both manifests carry package.json's version: `scripts/sync-plugin-version.mjs` rewrites them inside `version-packages`, right after `changeset version`, and `test/plugin-manifest.test.ts` pins the equality, the skill's frontmatter name (without it the installed skill would be named after a versioned cache dir), and that no other plugin component (`commands/`, `agents/`, `hooks/`, `.mcp.json`, `settings.json`) appears at the repo root, since an install would silently ship it. Verified with `claude plugin validate` (one expected warning: CLAUDE.md at a plugin root is not plugin context) and a local marketplace add, install, details, uninstall cycle against a clean checkout of this commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
48f30f3055 |
style: drop em-dashes from the text added in c2114615
House style, and these land in the changelog. Only the sentences added in the previous commit are touched; the em-dashes in contributor text and in the pre-existing COD-54/COD-115 comments are left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c211461500 |
fix: merge-time follow-ups for #409, #404 and #399
#409 (Claude truecolor). The changeset becomes the changelog, and its premise does not hold on tmux 3.2 or newer. Measured here on tmux 3.4: `default-terminal` sits at its compiled default of `tmux-256color`, a live claude pane reports `TERM=tmux-256color`, and supports-color reads that as 256 colors, where rgb(55,55,55) lands on ESC[48;5;237m — visible, just not the color the theme named. The invisible block the PR describes needs TERM to resolve to a 16-color entry: tmux older than 3.2, or a ~/.tmux.conf setting `default-terminal screen`, which Codeman's own tmux server does read (it passes no -f). Both the changeset and the invariants paragraph now say that, so the next report here gets paired with the reporter's tmux -V instead of being read as universal. The change itself stands on the simpler argument: claude was one of two entries not asking for truecolor while twelve do. Also reorders buildClaudeEnv(). It applied the registry's unset/exports AFTER the whole env was built, so a clis.json entry naming CODEMAN_HOOK_SECRET_FILE or PATH would strip it on the direct-PTY path while the tmux pane kept it — buildEnvExports() emits `...cliEnv` ahead of `export CODEMAN_MUX=1` and cannot. The block now runs first and Codeman's own keys are assigned on top, matching the pane. #404 (Ctrl+Z trap). Adds the missing changeset, and records what the trap does not cover: an agent CLI already holds its tty with ISIG off (verified on three live panes: `susp = ^Z -isig -icanon`), so this is defence for the startup window rather than a fix for the steady state, and two input paths still reach the PTY unfiltered — the mobile accessory bar's one-shot Ctrl and the CJK textarea. #399 (path picker sort). The server sorts by name and cuts at 500, so the client sorting those 500 by date gives "the newest of the first 500 by name", which is wrong in exactly the >500-entry folder the date sort exists for. The status line now says "(first 500 by name)" so the cut is legible, with the reasoning parked on _sortEntries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e0ebbbdc91 |
Merge pull request #409 from irisitymichaelgrundberg/fix/claude-truecolor-in-panes
fix(terminal): let Claude use truecolor so its themed backgrounds render |
||
|
|
8c237223b0 |
Merge pull request #399 from shenlvkang-collab/pr/path-picker-sort-jump
feat(files): let the path picker jump to a typed path and sort by name or date |
||
|
|
dae2ac580f |
fix(terminal): read the colour env from the registry on every local spawn path
buildClaudeEnv(), the direct-PTY fallback taken when mux creation fails, now
reads getCli('claude').env and applies its unset and exports lists. It used to
delete COLORTERM and CLAUDECODE from a hand-maintained list of its own, which
left it contradicting the registry entry that the tmux pane and the attach
client both read. An engine value needing a mux name has nothing to resolve
against on this path, so it is skipped rather than guessed.
Claude no longer unsets NO_COLOR. The invisible-background bug does not need
it, and unsetting it overrides a preference the user set deliberately, so a
user who exports NO_COLOR globally keeps monochrome panes. The other seven
truecolor CLIs still unset it; that inconsistency is intentional and the
comment on the entry says so.
The invariants doc gains a Terminal colour env paragraph under Session launch
modes, where a reader looking up Claude will find it — the previous sentence
sat under a heading that lists only the non-Claude CLIs. It now says the lists
are the stock catalog and a clis.json override replaces them wholesale, and
that the declarations reach the tmux pane, its attach client and the direct
PTY but not a remote pane, whose command carries no env exports at all. Docker
hands COLORTERM=truecolor to every mode, including the two the registry says
must unset it.
The changeset named six peer CLIs and there are seven: deepseek also exports
truecolor. A test beside the existing OpenCode assertion pins the new
behaviour, so a future registry edit cannot make the backgrounds vanish again
in silence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7767b16d4f |
fix(terminal): let Claude use truecolor so its themed backgrounds render
Claude draws the user's own messages as a block of background color, and inside a Codeman pane that block was invisible. tmux hands each pane TERM=screen, which supports-color reads as 16 colors, and Claude's registry entry deleted COLORTERM on top of that. Claude therefore quantized every RGB color its theme asked for down to the basic palette, where rgb(55, 55, 55) and every other dark background becomes ESC[40m, the terminal's own black. Changing the color in a custom Claude theme moved nothing on screen. Claude now exports COLORTERM=truecolor and unsets NO_COLOR, matching codex, gemini, antigravity, pi, grok and omp. CLAUDECODE stays unset, because Claude reads it as a signal that it is running nested inside itself. Both the tmux session and the attach client read this one registry entry, so they cannot disagree. PR #3 introduced the unset in February, citing xterm.js#484 for the claim that xterm.js mishandles truecolor. xterm.js closed that issue in April 2019, Codeman now depends on @xterm/xterm 6, and TmuxManager already sets terminal-overrides ",*:Tc" on its own tmux server, so 24-bit color reaches the browser today for every CLI that asks for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a0628a40e8 |
fix(cli-registry): address maintainer review on #380
Rebased onto current master (the one real conflict was the import line
in docker-hosts.ts Ark0N flagged; kept both), then addressed every
point from the review:
**1. Rebase.** Done — this branch now sits on current upstream/master.
**2. Agent-image special cases are data now, not an id-keyed table
outside stock.ts.** `AGENT_IMAGE_SPECIAL_CASE_IDS`/`AGENT_IMAGE_SPECIAL_CASES`
are gone. `CliDiscovery.install.agentImageLayer?: { kind: 'dedicated';
reason: string }` is a field on the registry entry itself (pi,
deepseek), `reason` is required by schema.ts, both producers
(docker-hosts.ts and cli-catalog.mjs) filter on its presence instead
of an id, and the coverage test reads it from the generated catalogue.
Also added the npm-package-name validation to the TS producer, which
only the .mjs one had — same SAFE_PACKAGE regex, duplicated
(necessarily, one side can't import the other) and now pinned
byte-identical by a new parity test.
**3. Changeset said five, it's eight.** (Not nine — see the DeepSeek
point below, which changes the true count.) Reworded to state it
structurally rather than pin a number that will go stale again.
Then the four behavior-changing findings:
- **DeepSeek was offered as a normal install option but can't actually
drive a pane.** `npm install -g @deepseek-ai/dsh` installs the
launcher only; DeepSeek ships no profile that can run standalone.
The generator now emits an empty install command for any
`launcherProfile` entry, so install.sh's menu (which requires a
non-empty command) skips it and falls through to its docs URL hint
instead — matching what the old hand-written code did before this
PR replaced it.
- **wget-only hosts lost every automatic install, including the npm
ones that never needed curl.** The menu-building loop now filters
PER ENTRY (only a command starting with `curl ` is held back) rather
than wiping the whole menu when DOWNLOADER != curl.
- **The DISPLAY/TRUSTED split and the catalogue refresh didn't hold up
under review** (refresh's only real write was the label; it ran
before the Node existence check; its own eval-detection test was
tripped by the word "eval'd" in a comment). Dropped entirely per
your own recommendation — embedded catalogue only, no network
fetch, no second array. install-sh-invariants.test.ts now asserts
the refresh/DISPLAY machinery does not exist rather than testing its
internals.
The three take-or-leave items, applied:
- `dsh_banner_probe`'s bash 3.2 empty-array bug: `${runner[@]}` →
`${runner[@]+"${runner[@]}"}`. Verified live in a real `bash:3.2.57`
container with `timeout` removed from PATH — crashed before, clean
now, full `detect_all_clis` path exercised end to end.
- `docker-agent-image-coverage.test.ts` now anchors on each layer's
`<binary> --version` proof line instead of `Dockerfile.includes(binary)`,
which stayed true if a layer were deleted but its comment survived.
- Doc drift: docs/docker-cases.md (four → five, and now describes the
data field), docker/agent.Dockerfile's "other four CLIs" comment (no
longer a magic number — CLI_NPM_PACKAGES is generated and can grow),
CLAUDE.md's install.sh size (104KB → ~112KB) and its stale mention of
the now-dropped refresh.
Verified: tsc clean, prettier clean, the full targeted suite (142
tests across the 8 affected files) green, and the full `npm test` gate
diffed BY TEST NAME against a clean upstream/master baseline run on
this same machine — identical 201-name failure set both sides (168
tests / 67 files, all pre-existing Windows-environment noise: symlinks,
PTY spawning, POSIX permission bits — none of it touching anything
this PR changes), zero new failures either side of the diff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
|
||
|
|
c5c015d648 |
docs(cli-registry): document the catalogue's consumers and the trust boundary
Adds a "Consumers outside the server" section covering the two generated artifacts, why each exists (neither install.sh nor a .mjs can import TypeScript), what is deliberately NOT exported and why, the three-rule install command trust boundary, and the bash 3.2 constraint with the offset/length window shape it forces. The adding-a-CLI checklist gains the regenerate step, since forgetting it is how the installer would keep detecting the old set while the server offers the new one — the drift this change removes, one level out. docs/docker-cases.md gains how CLI_NPM_PACKAGES is derived, why it reads the stock catalogue and not the merged registry, and a table of the four documented Dockerfile special cases with their reasons. CLAUDE.md gains a command row and names the generated block, the bash 3.2 rule and the trust boundary in its install.sh paragraph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12 |
||
|
|
61779745aa |
test(custom-model): make the harness smoke test dynamic, verify all 9 CLIs end-to-end
Rewrites scripts/test-local-llm-harnesses.mjs -> .ts to read the live CLI
registry (enabledClis()) and call the real production
buildCustomModelInjection()/applyConfigDirInjection() instead of keeping a
second hand-maintained copy of every CLI's env/config shape. A future
registry change (new CLI, edited env var, fixed config template) is now
picked up automatically with zero edits to this script; only the one-shot
invocation flags (info the registry genuinely doesn't model) stay in a
small hand-maintained ONE_SHOT table, and a registry CLI with no entry
there reports UNKNOWN rather than being silently skipped.
Extracted src/custom-model-injection-apply.ts (applyConfigDirInjection/
removeConfigDir) so the production route and this script share one
implementation instead of two.
Full end-to-end run against a real llama-swap server, inside a
codeman/agent:llm-test Docker image with all 9 CLI binaries:
- claude, opencode, pi, grok, omp: PASS, real "hello world" replies
- codex: confirmed FAIL for a real protocol reason, not a bug — it only
speaks the Responses API since Feb 2026, which llama.cpp/llama-swap
don't implement
- gemini: confirmed FAIL, unresolved after real investigation — an
undocumented GATEWAY AuthType gemini-cli selects once
GOOGLE_GEMINI_BASE_URL is set rejects every auth-key format/override
tried
- deepseek: reaches the server (env vars are read) but gets a consistent
HTTP_404; root cause not identified, documented as best-effort/unknown
- antigravity: SKIP, no known mechanism (unchanged)
Two real bugs found and fixed along the way (grok, pi/omp registry
entries in stock.ts): grok's original recipe (env vars) was flat-out
wrong, not just unverified — the real mechanism is a config.toml
[model.<name>] block redirected via GROK_HOME. pi/omp's PI_CONFIG_DIR
does nothing for either (grepped pi's entire bundled source — the string
appears nowhere); the real redirect is the child process's own HOME, and
both need `models` as an array of {id} objects, not an object keyed by
id (silently loaded zero models otherwise).
deployment_plan.md, PR.md, docs/custom-model-endpoints.md, and CLAUDE.md
updated with the final confidence table reflecting all of the above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HqNWfmtBU2KN29SvSVWB3
|
||
|
|
41416566aa |
feat(custom-model): Custom Model Endpoint Profiles (local or cloud, all harnesses)
Point any Codeman-supported harness (Claude, opencode, Codex, Gemini, Pi, Grok, DeepSeek, OMP) at a custom OpenAI-compatible endpoint instead of its native cloud backend, for a given session. Covers local hardware (llama.cpp, Ollama, vLLM, DGX Spark, Strix Halo) and cloud (Azure AI Foundry, OpenRouter). Off by default (customModelEndpointsEnabled, synced, default OFF). - Registry: capabilities.customModelInjection per CLI entry (env / configContentEnv / configDir / unsupported kinds) - Pure injection builder (custom-model-injection.ts) turning an endpoint + model id into the real env vars / config content per CLI - Endpoint store + CRUD routes (custom-model-hosts.ts, custom-model-routes.ts), discovery via GET /v1/models, SSRF-guarded - Session integration: Session.setCustomModel()/restartCli() (POST /api/sessions/:id/custom-model), reusing the existing respawn-pane -k primitive to restart the CLI process with new env - Multi-user hardening: every new redirect-capable env var added to its CLI's privilegedEnvKeys, closing a pre-existing gap where several were already reachable via the generic envOverrides field's prefix allowlist - Standalone scripts/test-local-llm-harnesses.mjs: spawns real CLI binaries against a real endpoint outside the web UI, independent of tmux/sessions - Mock-server contract tests (test/fixtures/mock-openai-server.ts) replaying every CLI's injected values through a real HTTP shape Real end-to-end validation against a live llama-swap server (inside a codeman/agent:llm-test Docker image with all 9 CLI binaries) found and fixed three real bugs before they shipped: - Codex's config.toml schema was wrong ([model].default table instead of a top-level model string + [model_providers.custom]); fixing it then surfaced a genuine, documented protocol incompatibility (Codex only speaks the Responses API since Feb 2026, which llama.cpp/llama-swap don't implement) - Claude Code's async session-title-generation call validates ANTHROPIC_DEFAULT_HAIKU_MODEL against its own internal model list and hangs the whole -p invocation on an unrecognized name; documented for chunk 6, worked around in the standalone script only (--bare is NOT safe for a real interactive session, which needs hooks) - The discovery route's authStyle: 'both' option (send both Authorization and api-key headers) reliably hung a real server; removed the option entirely rather than just changing the default Status: draft. Chunk 6 (frontend toolbar/settings UI) not yet built — see PR.md and deployment_plan.md for the full chunk breakdown and confidence table. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HqNWfmtBU2KN29SvSVWB3 |
||
|
|
ae32daf135 |
fix(docker): address maintainer review on #377
Two real bugs the review caught, both verified live against a real build on the Unraid host: 1. entrypoint.sh's chown fired on ANY ownership mismatch, not just a directory the daemon itself created root-owned. A host tree legitimately owned by some other account - an existing CODEMAN_CASES_PATH the README already allows pointing at a normal projects directory, or appdata under a different PUID/PGID convention than the one in use - got silently recursively re-owned with one log line to explain it. Now gated on the target actually being root-owned; anything else is a clean refusal naming the directory, its owner, and PUID/PGID. Start-Codeman.sh also now pre-creates CODEMAN_CASES_PATH the same way it already did CODEMAN_APPDATA_PATH, so Compose never has to materialise a missing bind source as root in the first place - the in-container chown becomes a safety net, not the primary mechanism. 2. The CLI-update chown (chown -R .../node_modules /usr/local/bin) handed the runtime account write access to entrypoint.sh itself (root-owned, executed as root on every container start with CHOWN/DAC_OVERRIDE/SETUID/SETGID) and the node binary - owning the DIRECTORY is enough to rename it aside and drop a replacement, which would let a compromised session arrange for its own script to run as root at the next restart. The four CLIs now install into a dedicated /opt/codeman-cli prefix (NPM_CONFIG_PREFIX); only that directory is chowned, /usr/local stays root-owned throughout. Smaller fixes from the same review: - Start-Codeman.sh's volume-refresh label filter wasn't project-scoped: a second Compose stack on the same host sharing the `codeman-dist` volume KEY could have had ITS volume deleted. Added a com.docker.compose.project filter, resolved from this stack's own `compose config --format json`. - Override-file precedence was backwards (checked .yaml before .yml; Compose actually prefers .yml) - swapped, plus a warning when both exist. - entrypoint.sh's setpriv now also passes --bounding-set -all, so CapBnd actually clears post-drop rather than just CapPrm/CapEff. - A comment on git_head_commit() noting it returns nothing for a worktree checkout (.git as a file), consistent with the script's existing -d .git convention elsewhere. - Doc drift: CLAUDE.md's Docker Compose section still described the old pre-created-and-chowned-by-hand model and didn't mention the root-then-drop entrypoint; the state-files list was missing docker-build-source.json; docs/docker-compose.md and docker/.env.example still had the pre-rename `Coding/codeman` path in one place each. Verified end to end against a real build on the Unraid host: a root-owned bind source is corrected as before; a directory owned by neither root nor PUID:PGID is refused rather than silently rewritten; a correctly-owned directory is left alone entirely; the four CLIs resolve via PATH from /opt/codeman-cli while /usr/local/bin, /usr/local/lib/node_modules and entrypoint.sh itself stay root-owned; CapBnd is fully cleared post-drop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru |
||
|
|
8fe3f34fc5 |
fix(docker): detect and refresh stale build-artefact volumes
codeman-node-modules and codeman-dist (docker-compose.yaml) are seeded from the image only while empty, so a rebuilt image's fresh dist/ node_modules sat unused behind old volume content until something cleared it. The in-app self-updater never hit this (it rebuilds INSIDE the running container, into the very volume already in use), but a `docker compose build` triggered from outside it — Start-Codeman.sh, after a manual `git pull` — did: the container came back up looking unchanged, serving stale compiled routes against current source. Start-Codeman.sh now compares the checkout's HEAD commit and package-lock.json hash against a recorded marker (docker-build-source.json) and clears just the affected volume(s) before its own --build when either moved. The in-place self-update path writes that same marker after a successful build, so the two mechanisms agree on what the volumes currently reflect — without it, the next plain Start-Codeman.sh run would see the HEAD self-update just checked out, not recognise it as already accounted for, and wipe the volumes self-update just correctly rebuilt right back to the older baked image. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru |
||
|
|
65ddedd1d4 |
fix: act on the 1.27.0 pre-release review
A Fable 5.1 reviewer read the whole release diff against 1.26.2 and returned
SHIP WITH FIXES. These are its findings, verified before acting on each.
**The changelog advertised a feature the code refuses (major).** The #401
changeset and docs/web-tabs.md both listed `*.localhost` in the loopback set.
The follow-up in
|
||
|
|
02b0e27898 |
fix: merge-time follow-ups for #400, #401, #362 and #388
Each item is from the pre-merge review of the PR it names, applied on master rather than by pushing to a contributor branch. #400 (response viewer, shenlvkang-collab) - The brief view opened at `scrollTop = 0`, right when it was a single card holding the last row. Now that it renders the whole turn, the top is the turn's first narration line and the answer can be screens below it, while loadFullContext already scrolls to the bottom of the same turn. A multi-row turn now opens at its newest text; a single card still opens at the top. #401 (loopback links as web tabs, shenlvkang-collab) - Drop `*.localhost` from the auto-route set. Every other member is an address literal that can only mean this box; a `*.localhost` DNS name is not one, and a resolver with a search domain retries `evil.localhost` as `evil.localhost.<search domain>`. The link source is agent-written terminal output, so that set is the whole confinement on a tap that makes Codeman fetch a URL server-side and persist it. The page-side test stays broader (`isOnBoxHostname`), where a false positive only declines to proxy. - A link to the origin root navigated nothing: the path was flattened to '', which openWebview reads as "no deep link", leaving an open frame where it was. - `this.webviews` being set does not mean it is loaded. initWebviews() assigns a truthy empty map and only then awaits the list, so a tap during page load found nothing to reuse and POSTed a duplicate record. Join the in-flight refresh instead. - One dashboard per dev server rather than per host spelling, which is what the method's own comment already promised. - Toast on the auto-create: it writes webviews.json, broadcasts over SSE and adds a Run-dropdown row on every signed-in device, with a new tab as its only previous signal. #362 (remote omp continuation, timkjr) - Accept the allowlisted `mode === 'omp'` arm as-is; a blanket registry render would hand deepseek a locally-resolved --profile and bypass claude's own overlay. A registry-declared switch is the follow-up if a third mode needs it. - Revert the whole-file Prettier reformat of docs/remote-sessions.md (docs/ is hand-formatted and outside `npm run format`), keeping only the two new sections. - Correct three stale passages: architecture-invariants' `exec claude --dangerously-skip-permissions`, the `exec <cli>` paragraph (claude and omp now have their own arms, and the claude pane's PID is the login shell), and omp-integration's `-c 'omp'`. RemoteCommandMode gains deepseek and omp. - Add the missing `_maybeCaptureOmpSessionId` remote-guard test; the sibling guard in `_pinOmpRespawnId` had one and this path runs earlier, on the first idle turn. #388 (keyCode 229 recovery, aakhter) - Gate notifyCanonicalData on shouldSuppressTerminalQueryResponse and isTerminalFocusOrMouseReport. onData also carries the DA/DSR/CPR/OSC replies xterm answers during Ink redraws and its SGR mouse and focus reports; any of those landing between the keydown and the candidate's resolution was read as "xterm spoke for this keystroke", standing the recovery down and leaving the character dropped, worst on a busy agent pane. Reached through window.CodemanTerminalInput: the predicates live in a module IIFE that closes long before this call site, so bare references would throw into the surrounding try/catch and stop the notify from ever running. Every fix has a test that fails without it (verified by reverting each). Full gate green on the combined tree: 358 files, 6849 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a28b04c368 |
Merge pull request #362 from timkjr/feat/omp-remote-continuation
fix(omp,remote): thread remote-omp resume/continue through respawn and reattach |
||
|
|
e35b68e253 |
Merge pull request #401 from shenlvkang-collab/pr/loopback-links-webtab
feat(webview): open localhost links through a proxied web tab from another device |
||
|
|
349a89ec3b |
fix(webview): let a proxied single-page app route on its own path, and recover a frame that reloads
A dashboard served through a web tab saw `/webview/<cap>/` as its
`location.pathname`, and no app has a route for that: a React Router, Vue
Router or Vite dev-server page painted its HTML and CSS and then replaced
them with its own "page not found" the moment its script ran (reproduced
with a minimal history-routed page).
The proxy's runtime shim now rewrites the history entry to the path the
page would see on its own origin, before any page script runs. The base
element still resolves relative URLs inside the prefix and every root-
absolute sink is rewritten back into it, so only what the page READS
changes. With the document URL masked the Referer-keyed 404 rescue can no
longer help a request the shim misses, so the remaining URL-taking entry
points (`Worker`, `SharedWorker`, `navigator.sendBeacon`, `window.open`)
are covered by the shim as well.
A navigation the page starts itself afterwards — `location.reload()`
(a dev server's full-reload HMR), a root-absolute `location.href` — lands
on Codeman's root with no capability anywhere: no prefix in the path, no
cookie in an opaque-origin frame, a Referer naming the masked page. It is
recognised by shape (a top-level iframe navigation asking for HTML, for a
path Codeman does not serve) and answered with a static page whose only
script posts `{type:'codeman:webview-lost', path}` to the parent; the tab
that owns the frame (matched by `event.source`, never by the payload)
remounts it inside the prefix at that path, bounded per frame. The
unauthenticated form is answered in the auth middleware before the
credential checks, so a dev server that reloads on every save cannot
rate-limit its own user out of Codeman; the authenticated form (Basic
auth, trusted mode) is answered by the 404 handler.
Verified end to end against a history-routed page: boots on `/`, its
API call succeeds, a reload inside the frame comes back routed on the
path it had pushed, `location.href = '/about'` comes back on `/about`,
and a deep link opens on its path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
d9eeb039db |
feat(webview): open localhost links through a proxied web tab from another device
An agent prints `http://localhost:5173/` (a dev server, a preview it just served) and the user taps it on a phone. That address only exists on the Codeman box, so the link was a guaranteed connection error from any other device — while the web-tab proxy fetches from the server, where it works. A loopback link (`localhost`, `*.localhost`, 127/8, 0.0.0.0, ::1) activated in the terminal or clicked in the Response Viewer now opens as a proxied web tab whenever the Codeman page itself is not on that box. A saved proxied dashboard on the same origin is reused, with the link's own path, query and fragment opened inside it (a mounted frame is navigated, not torn down, so its state survives); otherwise one is saved under its host:port, sandboxed like any other web tab, so it is in the Run dropdown next time. Only loopback is routed this way. A LAN or tailnet address may well be reachable from the device (a VPN, the same Wi-Fi) and a direct open is the cheaper, richer path, so those keep opening in a new browser tab; on the box itself every link opens directly. The terminal link provider and the viewer's click handler consult one hook and fall through to their existing behaviour when it declines. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou |
||
|
|
bd61735393 |
fix(web): show the whole last turn in the Claude response viewer's brief view
The eye button rendered `data.text`, which is one row: the last assistant
row of the transcript. A Claude answer is a median of 3 model messages
(p90 11) split around tool calls, so the brief view usually showed the tail
of an answer ("Done.", "Let me look.") and the substance appeared only after
More. The full view was fine, which is why the brief one read as broken by
comparison.
The brief view now asks `?context=turn`. The reader answers with the
assistant messages of the last ANSWERED turn (`selectLastAnsweredTurn`: the
highest `turn` that has an assistant row, so a prompt queued after the
answer does not blank the view) and the frontend renders them exactly as
the full view renders that turn: one badge, then continuation segments,
gated on the numeric `turn` as before.
`data.text` is unchanged in every context — still the last assistant row,
never `messages.at(-1)` — because agent pollers hash it. Readers that emit
no turns (Codex, the pane parser, DeepSeek, an older server) return `text`
only for `context=turn`, and the brief view keeps its single card for them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou
|
||
|
|
58b4cb06d8 |
feat(files): let the path picker jump to a typed path and sort by name or date
The picker's current-folder line was a read-only breadcrumb, so reaching a deep folder meant tapping through every level, and the listing was fixed to name order, so the file an agent had just written was somewhere in a 500-entry list. The current folder is now an editable field: Enter or Go jumps there, a full file path lands in its folder with that file selected, and a path that does not resolve keeps the listing you had and says so, instead of the reset to the root that a stale initialPath gets. A Sort control orders the listing by name or modified time in either direction, folders always first, and the choice is remembered per device like the hidden toggle. Each entry shows a compact modified time (time of day today, month-day this year, else the date). GET /api/filesystem/browse stamps every entry with mtimeMs to make that possible; the stat that already fetched a file's size now serves both, so it is still one stat per entry. Entries without an mtime (an older server, the in-container listing) sort after dated ones and then by name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou |