Skip to content

codeman@1.29.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 22:08
· 6 commits to master since this release

Minor Changes

  • Custom model endpoints, HTTP API first (#393). Any run mode that has a mechanism for it can be pointed at a custom OpenAI-compatible endpoint (a local llama.cpp, llama-swap, Ollama or vLLM, or a cloud gateway) instead of its native backend, per session. Endpoints are stored in ~/.codeman/custom-model-hosts.json (GET/POST/PUT/DELETE /api/model-endpoints, admin-only in multi-user mode), their model lists are discovered from the endpoint's own /v1/models, and POST /api/sessions/:id/custom-model applies one to a session by restarting its CLI in place. The mechanism is per-CLI registry data (capabilities.customModelInjection): env vars for Claude, Gemini, Grok and DeepSeek, OPENCODE_CONFIG_CONTENT for opencode, an isolated config dir for Codex, Pi and OMP, unsupported for Antigravity. Verified live against a llama-swap server for claude, opencode, pi, grok and omp; gemini and deepseek reach the server and fail for reasons not yet understood, and codex only speaks the Responses API, so a plain chat-completions server cannot serve it. Those three are documented as gaps rather than shipped as working. The toolbar picker is a follow-up; until it lands the feature is HTTP-API only (docs/custom-model-endpoints.md), and the customModelEndpointsEnabled setting is declared but read by nothing yet. Merged with maintainer follow-ups: clearing a selection now actually clears it (the injected vars are delivered by tmux setenv, which respawn-pane inherits, so the relaunched CLI came back still pointed at the endpoint; retired keys are now setenv -u'd before the respawn), applying a model to a local claude session no longer kills the pane (the relaunch pins --resume <id> with the --session-id fallback, since Claude Code refuses a session id that already has a transcript), pi, omp and grok now select the generated model through a registry-declared launchModel (custom/<id>, -m codeman-custom) instead of writing a config the CLI then ignored, remote and Docker sessions are refused with a clear 400 until those paths are plumbed, the selection survives a Codeman restart, discovery goes through the egress-guarded webviewFetch(), key-bearing files are written 0600 and the per-session config dir is removed with the session, and the design plan moved from the repo root to docs/custom-model-endpoints-plan.md. Along the way the multi-user clamp learned about GOOGLE_GEMINI_BASE_URL, GROK_BASE_URL, CODEX_HOME, PI_CONFIG_DIR and OPENCODE_CONFIG_CONTENT, which were already reachable through envOverrides and now count as privileged keys.

    Single-page apps work as web tabs, and a frame that reloads comes back (#402). A history-routed dashboard (React Router, Vue Router, a Vite dev server) read /webview/<cap>/ as its location.pathname and rendered its own "page not found" the moment its script ran. The proxy's runtime shim now masks the prefix off the document URL before any page script runs, while every URL the page emits still goes through the rewrite layers (now including Worker, SharedWorker, sendBeacon and window.open). A navigation the page starts itself afterwards (a dev server's full reload, a root-absolute location.href) used to land on Codeman's root with no capability; it is now recognised by shape, answered with a static recovery page that posts the lost path to the owning tab, and the frame is remounted inside the prefix at that path, bounded to five recoveries a minute per frame. Merged with maintainer follow-ups: the recovery path is sanitised properly (a leading backslash, or a tab/newline the URL parser deletes before parsing, resolved /\evil.com to a foreign origin in a direct-mode tab); a reload on the dashboard's landing page is recovered too, on password-protected and passwordless installs alike (it used to render Codeman's own shell inside the web tab); and the recovery page is written down as the third unauthenticated 200 in the security table and docs/security-architecture.md, with the route-enumeration property it implies stated rather than left to be discovered.

    Shift arrows for Codex on the phone keyboard bar (#408). Two keys, ⇧← and ⇧→, send the Shift-modified arrows Codex binds to editing the last queued message and walking the prompt stack (verified against Codex 0.154.0's /keymap). Merged with a maintainer follow-up: the keys are shown only on Codex sessions (a codex-enabled class on the bar, the same shape as the Read My Mind key), because tapping one in any other session did nothing except hand that session to plain PTY echo for the rest of the prompt.

    Remote (SSH) cases can finally show you their files (#421, fixes #415). File previews, downloads, text reads and the out-of-workspace attachment path resolved every path against the Codeman host's own filesystem, so in a remote case every click ended in "File not found" while the file plainly existed on the other machine. A single new ssh read layer (src/remote-files.ts, built on the same buildSshConnectionArgs() the launch uses) probes realpath and stat for the file and the workspace root in one round trip, then streams the body with cat (or a tail/head slice for a Range), so the 200/206/416 contract holds and nothing is buffered on the server. Symlinks are resolved on the host that can resolve them, containment is checked against the resolved remote root, the size cap applies to the remote size before a byte is requested, an unreachable host is a 502 rather than a 404, and there is deliberately no local fallback: a same-named file on the Codeman host is never served under a remote name. Writes, Office previews and generated thumbnails answer 400 for a remote case instead of a misleading 404. Merged with maintainer follow-ups: the readlink -f fallback resolved only the directory chain, so on a host without it a symlink's final component was returned unresolved and ws/notes.txt -> ~/.ssh/id_rsa passed containment while cat served the key; it now follows the last component with plain readlink for a bounded number of hops and fails closed (404) on a loop or the cap; PUT /api/sessions/:id/file-content answers 400 for a remote case as the PR already claimed (it still validated against the local filesystem, so a same-named local directory took the write); ssh children are bounded by a small semaphore (CODEMAN_MAX_REMOTE_FILE_SSH, default 4) covering the attachment-history fan-out, which now probes the whole history in one batched call, and the fire-and-forget magic-link registrations an injected agent could use to fork hundreds of ssh processes; probe records are NUL-delimited and index-keyed so a newline in a filename cannot shift one path's result onto the next; and a 502 body never carries the ssh command line.

    Docker Compose: bind-mount ownership, override files, a codeman runtime account, and no more stale volumes (#377). A missing bind source (first run, cleared appdata, restored backup) is created root-owned by the daemon, and the unprivileged server crash-looped on EACCES when Compose was run directly; the image now starts through an entrypoint that corrects a root-owned bind mount and drops to PUID:PGID with setpriv, and the compose file adds back only the capabilities that needs. Start-Codeman.sh honours docker-compose.override.yml (naming a Compose file with -f silently disables Compose's own discovery of it), pre-creates the cases directory like it already did for appdata, and detects when the checkout's HEAD or lockfile moved under the codeman-node-modules/codeman-dist volumes and refreshes them, which used to leave a docker compose build serving stale compiled routes. The default runtime account is named codeman (it was opencode), the four global agent CLIs live in their own /opt/codeman-cli prefix so the runtime account can update them in place without owning /usr/local/bin, and CODEMAN_ALLOWED_HOSTS is documented and forwarded. Merged with maintainer follow-ups: cap_add gains KILL (with init: true tini runs as root while the server runs as PUID, and without CAP_KILL its SIGTERM forward failed and the server was SIGKILLed on every compose down/restart); the CLI prefix is appended to PATH rather than prepended and the root entrypoint pins its own PATH, since a PUID-writable directory ahead of /usr/bin let the runtime account plant a setpriv that ran as root on the next start; the entrypoint decides with a real writability probe as the runtime identity instead of an owner comparison, so ACLs, group-writable trees and NFS/CIFS mounts work and only a genuinely unwritable directory is refused, by name; the cases directory is created with the runtime owner after PUID/PGID are known; the build-source marker is written only when a refresh actually happened, an empty Compose project name falls back to down --volumes, the build runs before the down so the stack is offline only for the recreate, docker-compose.override.* stays out of the image, and test/docker-entrypoint.test.ts pins cap_add against what the entrypoint needs. ⚠️ Compose users: run Start-Codeman.sh once for this release rather than a plain docker compose up, so the rebuilt image, the refreshed volumes and the new entrypoint arrive together.

    Selected text is visible again on the light skins (#423, part of #360). Every skin palette named its selection layer selection, the key xterm renamed to selectionBackground in v5, so all seven skins had been painting xterm's default white at 30% instead of the colour next to it in the palette. Dark skins hid it; on the four light skins a selection was white on near-white. The key is renamed and test/skin-themes.test.ts pins it. CI additionally exercises install.sh's dsh identity probe with timeout missing under bash 3.2 (#422), the guard #382's fix shipped without.

    Eight fixes salvaged from #375 (dignfei; landed with the author's commits preserved, the rest of that PR is covered below). Shift+drag starts a text selection in a pane whose mouse reports go to the CLI, and right-click copies the selection. Ctrl- and Alt-modified navigation keys typed through the CJK composer reach the CLI as the modified sequences instead of plain arrows. A browser whose reliable-input sequence counter fell behind the server's watermark (a restored tab, a cleared localStorage) now recovers: the duplicate ACK carries dup: true plus the watermark, the client lifts its counter and re-sends, so a session that had silently stopped accepting typed prompts accepts them again. An SSE reconnect that lands on the session you are already looking at keeps its terminal buffer and resyncs instead of resetting the whole terminal. The hidden offline overlay and the file-preview overlay only apply backdrop-filter while shown, which removes a stale compositing layer that swallowed clicks. One adopted Docker container can back several cases at different in-container directories, and the adopt panel gains a "copy an existing case" picker. Of the PR's 27 commits, 14 had already shipped through #357, the selection theme key rename shipped as #423, and foreign tmux adoption plus SSH password auth stay with the author.

    Thanks

    • @opticon454 for custom model endpoints (#393), including the part nobody enjoys: working out each CLI's real endpoint mechanism against real binaries and writing down which ones do not work yet instead of claiming they do; and for the Docker Compose deployment fixes (#377), rebased and reworked through three review rounds.
    • @shenlvkang-collab for making single-page apps route inside web tabs and recovering a frame that reloads (#402), the best-engineered PR of this batch, and for the Codex Shift arrows on the phone keyboard bar (#408), verified against Codex's own keymap.
    • @dignfei for the eight fixes salvaged from #375 (terminal selection and copy, CJK navigation keys, input recovery, SSE reconnect, overlay compositing, multi-case adopted containers), landed under their own name.
    • @Randalix for reporting #415 and then fixing it themselves with the whole missing ssh read side for remote cases (#421), with a real-shell test for the probe script and a full route suite.

Patch Changes

  • 349a89e: 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.
    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, while every URL the page emits still goes through
    the existing rewrite layers (plus Worker, sendBeacon and window.open, which the masked
    Referer can no longer rescue). A navigation the page starts itself afterwards — a dev
    server's full-reload HMR, a root-absolute location.href — lands on Codeman's root with no
    capability; it is recognised by shape (an iframe navigation asking for HTML for a path Codeman
    does not serve), answered with a static page that tells the owning tab which path was lost,
    and the tab remounts the frame inside the prefix at that path. That answer is served before
    the credential checks, so it never counts as a failed login.

  • 013a5d9: File previews, downloads and text reads now work in a remote (SSH) case.

    A remote case's working directory is an absolute path on the remote host, but the
    file routes resolved it with local fs — so a clicked path (or the File Viewer) always
    failed as "File not found" even though the file existed and the session was clearly
    working in that directory. GET /api/sessions/:id/file-raw, file-content,
    file-preview and file-thumbnail now resolve and read through the same
    buildSshConnectionArgs() connection the launch uses (src/remote-files.ts, one
    realpath+stat probe per request returning both the file and the workspace root).

    Clicked paths that point OUTSIDE the case directory (a remote /tmp scratchpad capture,
    a screenshot elsewhere in the remote home) go through the attachment routes, which had
    the same local-fs assumption: registration, the by-id raw stream, the metadata poll
    and the attachment history list now resolve over ssh as well, so the click-path works
    whether the file sits inside or outside the case. Which host a record is read from
    follows the SESSION, never the path string — the same absolute path means a different
    file on each host, and a remote session never falls back to a local file.

    The guards are unchanged in strength: the workspace boundary is still enforced (now
    resolved on the host that can actually resolve it), the sensitive-path blocklist and
    the size cap (CODEMAN_MAX_DOWNLOAD_BYTES) still apply before any bytes are read, and
    Range requests keep working, so remote <video>/<audio> seeking behaves like a
    local file. An unreachable host is reported as 502 with the remote reason instead of
    a misleading 404. Nothing is ever copied to the Codeman host.

    Still not available for remote cases, and now said explicitly instead of 404-ing:
    editing a file (edit=1 / PUT answer 400, the viewer hides its Edit affordance),
    office-document previews and generated thumbnails (both need the bytes on the server's
    disk), the file tree / path picker, and tail-file. Docker cases are unaffected (their
    workspace is bind-mounted at the same absolute path).

  • b357fe8: Add Shift+Left and Shift+Right buttons to the default and extended mobile agent keyboard bars, shown only on Codex sessions, enabling Codex queued-message editing and prompt-stack navigation. Flush locally buffered drafts before navigation and keep terminal focus after taps.

  • 9acc5aa: Fix an invisible terminal text selection on the light skins (#360). Every xterm palette declared its selection colour under the key selection, which xterm.js renamed to selectionBackground in v5. An ITheme is a plain object, so the unknown key was dropped without an error and every skin fell back to xterm's own default of rgba(255,255,255,0.3): unnoticeable on the dark skins, which wanted roughly that anyway, and effectively invisible on Paper Gray, Solarized Light, Catppuccin Latte and Rosé Pine Dawn, where white at 30% over a near-white background moves a channel by about 3/255. Selecting text on those skins now highlights it, with desktop drag-select and the mobile long-press both fixed by the same rename.