feat(server): optional sign-in through an identity-aware proxy (CODEG_AUTH=proxy-jwt) by MagTer · Pull Request #985 · spacering-net/codeg · GitHub
Skip to content

feat(server): optional sign-in through an identity-aware proxy (CODEG_AUTH=proxy-jwt) - #985

Merged
xintaofei merged 13 commits into
spacering-net:mainfrom
MagTer:feat/proxy-jwt-auth
Oct 20, 2026
Merged

xintaofei merged 13 commits into
spacering-net:mainfrom
MagTer:feat/proxy-jwt-auth

Conversation

@MagTer

@MagTer MagTer commented Oct 19, 2026

Copy link
Copy Markdown

Part 2 of #981. Builds on #984 (it reuses that PR's 401 handling in the client); please review that one first — this branch contains its commit.

What it does

With CODEG_AUTH=proxy-jwt, codeg-server also admits a request whose proxy assertion header holds a JWT that verifies against the proxy's published keys, with the expected iss and aud and an unexpired exp. The browser then needs no Codeg token: the proxy's login is the login. The token keeps working for clients that do not pass through the proxy (codeg-cli --url, scripts). Default behaviour is unchanged; the desktop app's web service stays token-only (build_router keeps its signature and stays token-only; build_router_with_proxy_auth is used by codeg-server alone).

Configuration (env, standalone server), documented in docs/proxy-auth.md:

Variable
CODEG_AUTH proxy-jwt turns it on; unset/token = today
CODEG_PROXY_AUTH_HEADER e.g. Cf-Access-Jwt-Assertion
CODEG_PROXY_AUTH_JWKS_URL e.g. https://<team>.cloudflareaccess.com/cdn-cgi/access/certs (https; http only for loopback)
CODEG_PROXY_AUTH_ISSUER exact iss
CODEG_PROXY_AUTH_AUDIENCE must be in aud

Naming is a proposal — happy to rename, or to add a settings entry / desktop exposure if you prefer (#981 left both open). The configuration page on docs.codeg.app would need these variables; I did not find its source in this repo.

Design

  • web/proxy_auth.rs — config parsing, JWKS parsing, JWS verification, key cache. Fails closed everywhere: incomplete/misspelled config, or proxy variables without the mode, stops startup with exit code 2 naming the variable (checked next to the upload-quota validation); a missing/malformed/expired/wrong-iss/wrong-aud assertion, an unknown key, or an algorithm other than RS256/ES256 is a 401; no keys fetched yet → every request without the token is refused.
  • The key decides the algorithm; the header's alg only has to agree. That rules out alg: none, HS256-with-the-public-key and other confusion. crit is rejected (RFC 7515 §4.1.11). 60 s clock leeway on exp/nbf. Assertions over 16 KB are refused before parsing.
  • Signature checks use ring, which is already in the lock tree through rustls, so this adds no new crate (Cargo.lock only gains the edge). I chose that over jsonwebtoken to avoid a new dependency; switching is easy if you prefer it.
  • Key cache: fetched at startup (not fatal if it fails — logged, requests refused until a fetch works), kept for an hour, refreshed early when an assertion names an unseen kid (rotation), at most once per 30 s so unknown kids cannot drive a fetch per request; a failed refresh keeps the previous keys. No redirects followed; 256 KB cap; 10 s timeout.
  • web/auth.rs: require_auth = token, or (proxy mode only) a verified assertion. Token mode ignores assertions entirely (tested). Rejections are logged by reason only, never the assertion.
  • Client: when the server admits the browser without a token (/api/health → 200), the app records that (codeg_proxy_auth in localStorage), skips the token prompt (root and /login), opens the WebSocket without the token sub-protocol (the proxy's header rides on the upgrade), and treats any 401 as the proxy's (Reload — there is no token to re-enter). "Go to login" clears the mark.

Tests

  • proxy_auth.rs: 31 unit tests — RS256/ES256 accepted; expired, not-yet-valid, wrong issuer, wrong audience (string, array, missing), tampered payload, foreign key, unknown kid, alg none/HS256/RS512/PS256, header/key algorithm mismatch, crit, malformed structure, non-object claims; JWKS parsing incl. Cloudflare's public_cert(s) and keys to skip; every config error; the cache (fetch once, refresh on rotation, rate limit, unreachable JWKS, keep keys on failed refresh) against a local key server.
  • Mutation check: each of 20 checks in the module was broken one at a time (signature, exp, leeway, nbf, iss, aud, aud-array, key-decides-alg, crit, length, use, JWK alg, EC point length, https rule, rate limit, cache, keep-on-failure, refresh-on-unknown-kid, mode-less settings, blank values); every mutant made at least one test fail.
  • proxy_auth.rs embeds a throwaway 2048-bit RSA test key (PKCS#8, labelled TEST-ONLY), because ring cannot generate RSA keys; a secret scanner may flag it.
  • auth.rs: proxy mode admits a valid assertion without the token, still admits the token, refuses missing/foreign-audience/forged assertions and a wrong token with Codeg's challenge; token mode ignores a valid assertion; an empty token never matches an empty bearer.
  • Frontend: 13 new cases (web-auth, web-transport, root and login pages), all seen failing against part 1's code. Two existing tests adjusted, with reasons in the diff: the login header test answers the new mount-time probe first; the version-notice mock gains hasCodegCredentials.
  • The module parses the real JWKS of a Cloudflare Access team (two RS256 keys plus public_cert/public_certs).
  • Run locally: pnpm lint ., tsc --noEmit, pnpm test, pnpm build; cargo test + clippy -D warnings for the server and cli cells. Not run locally: the desktop cells (no webkit dev libraries here); the new code is not cfg-gated, and tests/ still calls the unchanged build_router.
  • End to end (Playwright/Chromium, built codeg-server, local key server, fake proxy that signs ES256 assertions into every request including WebSocket upgrades): a half configuration exits 2 naming the missing variable; straight at the server, no credentials → 401, token → 200, valid assertion → 200, foreign key / other audience / other issuer / expired / alg: none → 401; through the proxy the workspace opens with no token prompt and no token stored, the event socket stays up, /login forwards to the workspace, an expired proxy session shows Sign-in expired, Reload reaches the proxy's login, and signing in again restores the workspace; no assertion appears in the server log.

Against real Cloudflare Access (this branch, release build from source, CODEG_AUTH=proxy-jwt with the team's JWKS, issuer and the application's AUD tag, behind a live Access application): the server loaded the team's two keys at startup, and a private browser window with no stored token opened the workspace without the token prompt. On loopback, the same server refused forged assertions with 401 and logged the reason: alg: none naming a real kid ("algorithm not accepted"), a real kid with a bad signature ("bad signature"), an unknown kid, and a malformed header. The token still admitted. Session expiry through real Access is described in the part 1 PR.

Part 2 of #981.

🤖 Generated with Claude Code

Magnus Ternström and others added 2 commits October 18, 2026 22:19
…necting forever

Behind an identity-aware proxy (Cloudflare Access in the reporter's setup),
an expired proxy session made the API fetch receive a 302 to the proxy's
login page on another origin. fetch cannot follow that, so the health probe
saw a network error and the reconnect dialog never resolved; only a manual
reload got out (spacering-net#981).

API calls, health probes and uploads now send X-Requested-With:
XMLHttpRequest, which Cloudflare Access answers with a 401 instead of the
redirect (measured against a live Access application: 302 without the
header, 401 with it).

A 401 alone would then bounce the user to the token login and wipe a token
that is still valid. Codeg's own 401 now carries WWW-Authenticate:
Bearer realm="codeg", and the client reads that challenge to tell the two
apart: Codeg's 401 keeps today's "Session expired" / "Go to login", any
other 401 shows "Sign-in expired" with Reload (the proxy then runs its own
login flow; the service worker passes navigations through, so a reload
reaches it) and keeps the token. "Go to login" stays in that dialog as the
way out if the proxy keeps refusing. The root page no longer clears the
token on a proxy's 401, and the login page no longer calls the token
invalid for one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_AUTH=proxy-jwt)

Behind a proxy that already signs users in (Cloudflare Access in spacering-net#981),
Codeg's token is a second login on top of the proxy's, kept and restored
by hand. With CODEG_AUTH=proxy-jwt, codeg-server also admits a request
whose assertion header holds a JWT that verifies against the proxy's
published keys with the expected iss and aud and an unexpired exp. The
token keeps working for clients that bypass the proxy (codeg-cli --url).
Token-only stays the default, and the desktop web service is unchanged.

It fails closed throughout: a half or misspelled configuration, or proxy
settings without the mode, stops startup with exit code 2 naming the
variable; a missing, malformed, expired, foreign or unknown-key assertion
is a 401; no keys fetched means no admission without the token. The key
decides the algorithm (RS256 / ES256), so alg none and HS256 confusion
never reach a verifier. Keys are cached for an hour, refreshed early on an
unseen kid at most every 30 s, and kept when a refresh fails. Signatures
use ring, already in the lock tree through rustls, so no crate is added.

The client records when the server admits it without a token, skips the
token prompt, opens the event socket without the token sub-protocol, and
answers any 401 with the Reload from the previous commit, since there is
no token to re-enter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sent

The proxy's assertion rides on its session cookie, and the browser
attaches that cookie to requests other sites' pages make as well:
Cloudflare sets its application cookie SameSite=None unless the operator
changes it. With CODEG_AUTH=proxy-jwt, any page the signed-in user opened
could therefore open /ws/events or /ws/browser-tunnel (a TCP tunnel from
the server's side), or post a multipart upload, as that user; neither
needs a CORS preflight.

A request admitted by an assertion now has to come from Codeg's own page:
Sec-Fetch-Site same-origin or none, or, in a browser without Fetch
Metadata, an Origin that names the Host or X-Forwarded-Host it was sent
to. Anything else gets a 403 and a warning with the reason. The token
path is unchanged: another site's page cannot send the token.
refresh() held the rate-limit mutex across the JWKS fetch, whose timeout
is 10 s. Once the keys were an hour old, every request waited behind each
refresh attempt, and while the key endpoint could not be reached each
attempt waited out the whole timeout.

Keys that hold the assertion's key id now answer at once, and a single
refresh runs beside the request. An unknown key id (a rotation) still
waits for its fetch, as before. The max age becomes a field so tests can
make the keys stale.
The 256 KB cap was checked against Content-Length, and otherwise only
after .bytes() had read the whole body, so a response without a length
was buffered in full until it ended or the fetch timed out. The body is
now read by chunk and the fetch fails as soon as it passes the cap.
Plain http is accepted only for a key server on this machine, but the
JWKS client still took HTTP(S)_PROXY from the environment, so with a
proxy set and no NO_PROXY entry the request went to a proxy that would
dial its own loopback.
…me straight to the workspace

Proxy sign-in was detected only on / and /login. A browser that opened
/workspace directly (a bookmark, a restored tab, or after the root page
could not reach the server) held neither the token nor the proxy mark:
its HTTP calls went through on the proxy's assertion, but the event
socket was never opened and nothing reported it, so the workspace stayed
"connected" without live updates.

When a consumer needs the socket and no credential is held, the
transport now asks the server. Admitted: remember the mark and connect,
and subscribe() still resolves only after __ready__. Refused: nothing
changes, the token flow keeps surfacing its own 401s, and the login page
(which subscribes too) has nothing to reconnect. Unreachable: asked again
on the reconnect backoff, quietly for the same reason.
…sage

Signed in through the proxy there is no token, and on the login page the
token being typed is not stored yet. Without that clause the message is
true wherever it shows.
Codeg admits every request the proxy vouches for with a valid assertion
for this application, service tokens the policy allows included, so the
policy is the user list.
probeProxyAuthentication() had no timeout. The root and login pages wait
on it, and subscribe() now does too when no credential is held, so a
server that took the request and never answered held them forever. It
now counts as unreachable after 8 s, the transport's health probe
timeout. The admission test also checks that subscribe() waits for the
discovered socket's __ready__.
@xintaofei

Copy link
Copy Markdown
Collaborator

@MagTer thank you for this, and for the care that went into it. The fail-closed configuration, the key deciding the algorithm, the mutation pass over the verifier and the runs against real Access all made the review much easier. I went through it for production readiness and found one security issue that comes with cookie-based sign-in, plus a few smaller ones. Since maintainer edits are allowed on this branch, I pushed the fixes as separate commits on top of yours instead of sending you a list. Please look them over, and push back on anything you see differently.

Summary

  1. Requests sent by other sites' pages were admitted. This is the security issue, fixed in 8409e21.
  2. Keys past their age made requests wait for the JWKS fetch (87a5505).
  3. A JWKS body sent without Content-Length was read in full before the size cap was checked (7e3d446).
  4. A loopback JWKS went through HTTP(S)_PROXY (45c7b68).
  5. Opening /workspace directly, with neither a token nor the proxy mark, never opened the event socket (96daf78, 96b81e4).
  6. Wording and docs (1298d67, d847fc9).

1. Requests from other sites

With CODEG_AUTH=proxy-jwt the credential is no longer something only Codeg's page can send. The assertion rides on the proxy's session cookie, and Cloudflare sets that application cookie SameSite=None by default (Authorization cookie), so the browser attaches it to requests other sites' pages make as well. Nothing on the server checked where a request came from, and two kinds of request need no CORS preflight: WebSocket handshakes and multipart POSTs. Any page the signed-in user opened could therefore open /ws/events (the live event stream) or /ws/browser-tunnel (a TCP tunnel from the server's side), or post to the upload endpoints, as that user. In token mode none of this works, because another site's page cannot send the token.

I reproduced it with the PR head before my commits (c51de28), Chrome 155, and a small stand-in for Access: a SameSite=None; Secure session cookie, and an ES256 assertion injected into every request that carries a valid session, WebSocket upgrades included. A page on another origin opened /ws/events and received the __ready__ frame. Its no-cors multipart POST to /api/upload_attachment returned 200 and wrote the file into uploads/.

The fix (auth.rs): a request admitted by an assertion now also goes through a check on where the browser says it came from. Sec-Fetch-Site decides when present, and only same-origin and none pass. Otherwise an Origin, when present, has to match the Host the request was sent to, or the X-Forwarded-Host of a proxy that rewrites Host, by host and port. A request with neither header passes, since browsers send one of them on every WebSocket handshake and cross-origin POST. A failed check gets a 403 and a warning that names the reason. The token path is unchanged. After the fix, the same page gets a 403 for both requests, and the workspace's own socket still upgrades.

One thing worth knowing: in Chrome 155, WebSocket handshakes carry no Fetch Metadata. I checked same-origin and cross-site handshakes with a server that logs headers: both carried Origin and Host, and no Sec-Fetch-*. For the event socket opened through the proxy, the Origin check therefore decides. cloudflared keeps the browser's Host by default, and moves it to X-Forwarded-Host when httpHostHeader is set (origin_proxy.go), so your setup should pass either way. A proxy that rewrites Host without setting X-Forwarded-Host would have its socket refused, and the docs now say so (proxy-auth.md). Could you run your live Access check once more on the updated branch, mainly to confirm that the event socket still connects through the real tunnel?

The stand-in run, before and after
before (c51de289c):
HTTP 200 POST /api/upload_attachment origin=http://127.0.0.1:18744 sfs=cross-site
WS HTTP/1.1 101 Switching Protocols /ws/events origin=http://127.0.0.1:18744 cookie=true
the page received {"channel":"__ready__","payload":null}; uploads/anon/evil.txt was written

after (96b81e41e):
HTTP 403 POST /api/upload_attachment origin=http://127.0.0.1:18744 sfs=cross-site
WS HTTP/1.1 403 Forbidden /ws/events origin=http://127.0.0.1:18744 cookie=true
WARN [AUTH] proxy-jwt: refused a request sent by another site's page (Origin: http://127.0.0.1:18744; sent to: localhost:18741)

localhost:18741 is the stand-in proxy and 127.0.0.1:18744 the other site's page; cookie=true means the browser sent the session cookie with the cross-site request.

2. Keys past their age

refresh() holds the rate-limit mutex across the fetch, whose timeout is 10 s. Once the keys were an hour old, every request went through refresh(), and the requests that arrived while a fetch was out waited for it. With the key endpoint unreachable, that meant waits of up to 10 s every 30 s. Now keys that hold the assertion's kid answer at once, and a single refresh runs beside the request (proxy_auth.rs). An unknown kid still takes the waiting path, as before, within the 30 s rate limit.

3. Size cap and 4. loopback JWKS

fetch() checked the 256 KB cap against Content-Length, and otherwise only after .bytes() had read the whole body. It now reads by chunk and stops at the cap (proxy_auth.rs). Plain http is accepted only for a loopback key server, so its client now skips HTTP(S)_PROXY: with a proxy set and no NO_PROXY entry, the request went to a proxy that would dial its own loopback.

5. Opening /workspace directly

Proxy sign-in was discovered only on / and /login. A fresh browser that opened /workspace itself, from a bookmark, a restored tab, or after the root page could not reach the server, had neither the token nor the mark. Its HTTP calls went through the proxy, but the event socket never opened, and the UI stayed "connected" without live updates. The transport now asks the server when something needs the socket and no credential is held (web-transport.ts). If the server says no, nothing changes, because the login page subscribes too and must not get a dialog. If it cannot be reached, it is asked again on the reconnect backoff. Because subscribe() now waits on that probe, probeProxyAuthentication() also gives up after 8 s (96b81e4).

6. Wording and docs

"your access token is kept" is not true without a token, nor on the login page, where the token being typed has not been stored yet, so 1298d67 drops that clause in all ten locales. d847fc9 adds the "Who gets in" section discussed in #981, and 8409e21 adds the cross-site section and suggests setting the application's cookie to SameSite=Lax.

Tests and checks

Every new test was run against the code before its fix and failed there. Afterwards each fix was broken on purpose, ten on the Rust side and seven in the frontend, and each time at least one test failed. Run locally on the final head:

desktop  cargo check · clippy --all-targets --features test-utils -D warnings · test --features test-utils (6015 passed)
server   cargo check · clippy -D warnings · test --bin codeg-server --lib (5610 passed)
mcp      cargo check · clippy -D warnings
cli      cargo clippy -D warnings · test --lib cli:: (56 passed)
web      eslint . · tsc --noEmit · vitest run (10136 passed) · next build

One full vitest run hit the sidebar-tag-filter timing flake you mentioned in #984; it passed alone three times, and the next full run was clean.

The stand-in run also covered opening /workspace directly with no token, a session expiry followed by Sign-in expired, Reload and back in, the same with Host rewritten and the original in X-Forwarded-Host, and plain token mode.

Two things I left as they are, for the record. Without Sec-Fetch-Site, the Origin check compares host and port but not the scheme. And a browser that holds only a stale mark, after the server goes back to token mode, gets the proxy dialog again on Reload; "Go to login" recovers from that.

I've approved the CI runs for both PRs.

The waits for a background refresh to start or land only have to catch a
hang, so they get 30 s instead of 5 s. And the single-flight test no
longer assumes the first refresh is over 600 ms after it started: it
waits for the refresh flag to clear before the request that should start
the next one. A slow machine can then only hide an extra fetch, never
fail a correct run.
@xintaofei

Copy link
Copy Markdown
Collaborator

One more commit since the comment above, test-only: 4876460 keeps the key-refresh tests from depending on timing, so a stalled CI machine cannot fail a correct run. The CI run for the new head is approved.

@MagTer

MagTer commented Oct 19, 2026

Copy link
Copy Markdown
Author

Thanks for the review and the fixes. I went through the nine commits before building, and found nothing I see differently.

I ran the live Access check again on 4876460bc: the release codeg-server built from that head, its out/ as the static dir, and the rest of the binaries from the signed v0.38.0. It ran on the same host and through the same tunnel and Access application as before, with CODEG_AUTH=proxy-jwt, the Cf-Access-Jwt-Assertion header, and the team's JWKS, issuer and the application's AUD.

Startup: proxy-jwt on with the configured header, issuer and audience, then loaded 2 verification key(s).

Loopback, POST /api/health:

Request Status Logged reason
No credential 401 –
Garbage assertion 401 malformed assertion: header
alg: none with a real kid 401 algorithm not accepted
Real kid, bad signature 401 bad signature
Unknown kid 401 unknown key id
The token 200 –

Through the real tunnel, in a fresh private window with no stored token:

  • The root page opened without the token prompt.
  • Opening /workspace directly, as the first page in a fresh private window, also came in without the prompt (your fix 5).
  • A session streamed agent output live, so the event socket connected through the tunnel. Nothing was refused as cross-site: the log has no refused a request sent by another site's page line from any of these requests.

The cross-site check, live: while signed in to Access, I opened the devtools console on another Access-protected app and ran new WebSocket("wss://<codeg host>/ws/events"). That app is on a sibling subdomain of the same registrable domain, under the same Access team. The handshake failed in the browser, and Codeg logged:

WARN [AUTH] proxy-jwt: refused a request sent by another site's page (Origin: https://<sibling host>; sent to: <codeg host>)

That line is written only after the assertion has verified, so the browser sent the Access cookie with the cross-origin handshake and the new check returned the 403. The browser does not expose a failed handshake's status to the page, so 403 here is read from the log line and the code path, not seen in devtools.

The same line confirms two things you expected. The handshake carried no Sec-Fetch-Site, because the reason names Origin, not Fetch Metadata. And cloudflared passed the browser's Host through unchanged without adding X-Forwarded-Host, because "sent to" lists exactly one host.

One point for the docs: our application's cookie was already SameSite=Lax, and the cookie still went along. SameSite is decided by site, the registrable domain, not by origin, so a page on a sibling subdomain is same-site and Lax does not hold back its requests. Lax is worth setting against unrelated sites, but anyone who serves other apps under the same domain still depends on the server-side check. The cross-site section could say that, so nobody reads Lax as making the check optional.

Afterwards the host went back to the signed v0.38.0 in token mode.

Browsers apply SameSite by site, the scheme and registrable domain, not
by origin, so a page on another subdomain of the same domain sends the
proxy's cookie even when it is Lax or Strict. The cross-origin check is
what refuses that page, and the docs no longer read as if Lax made it
optional. The section also says "another origin" where it meant that,
since a same-site sibling is refused too.
…in refusal

A page on a sibling subdomain is same-site, and the check refuses it as
well, so the warning now reads "refused a request sent by a page on
another origin (...)". The doc comment and the two test names that said
"another site" follow. What is admitted does not change.
@xintaofei

Copy link
Copy Markdown
Collaborator

@xintaofei
xintaofei merged commit 2244511 into spacering-net:main Oct 20, 2026
13 checks passed
xintaofei added a commit that referenced this pull request Oct 21, 2026
This one lets an agent write your commit messages, lets a command run on your own computer even from a remote folder, and lets codeg-server sign people in through a proxy such as Cloudflare Access instead of its access token.
Alongside that: Recent in the sidebar lists what you used last, on Windows an update waits for Restart to update and no longer stops at "Failed to kill codeg", the Linux AppImage no longer hands its own libraries to agents and the terminal, and a phone that comes back to the page gets the rest of the reply.

## New

- **An agent can write your commit message**: the ✨ button by the commit box, in the Changes tab and the commit window, has the agent of the conversation you're in write one from exactly what will be committed, taking anything you've typed as a hint, and adds no conversation to your list. (#931 requested by @trueai-org, #928 by @Time-w)
- **With untracked files around, the ✨ button first asks which of them the message covers**, and Enter then commits exactly those, with a line under the commit box saying how many it will take.
- **Commit message settings… in the More Git actions menu chooses who writes the message and how**: a fixed agent, model and options, instructions it always follows such as the language or format, and committing as soon as it's written, for all folders or just one project.
- **Manage commands has a Run on this computer switch**, so a command such as opening the remote folder in your local VS Code or Zed over SSH runs on the computer with the desktop app, and Show in all folders keeps it at hand across folders, worktrees and remote connections. (#974, @fengpanhe)
- **codeg-server can sign people in through a proxy such as Cloudflare Access**: with `CODEG_AUTH=proxy-jwt`, a browser the proxy has let in no longer asks for the access token, which codeg-cli and scripts that bypass the proxy keep using, as the [proxy sign-in guide](https://github.com/spacering-net/codeg/blob/main/docs/proxy-auth.md) explains. (#985, @MagTer)
- **Grok's custom model card can declare the model's reasoning-effort levels**, so the composer offers a reasoning-effort selector for a custom model too. (#993 requested by @HOMEFTW)

## Improved

- **Recent in the sidebar lists the conversations you used last instead of the ones created last**, ten at a time, with each row showing when it was last active, and Chat rows show their last activity too. (#1010, @Flyneen)
- **On Windows, a downloaded update waits for you to click Restart to update**, as on macOS and Linux, instead of closing codeg the moment the download finishes, and an install that fails says so and can be tried again.
- **The commit box in the Changes tab grows with the message**, with Shift+Enter for a new line while Enter still commits.
- **Clicking a delegation card unfolds the full task the sub-agent was given**, and a resumed sub-agent's card shows there why it was resumed.
- **The sidebar can be dragged narrower on Windows, Linux and in the browser**, since only the macOS app needs room for its window buttons.
- **Pi's model selector lists only the models your `enabledModels` setting allows.** (#989, @xyalter)
- **ZCode takes pasted and uploaded images and PDFs, and keeps each reply's token usage and time after you reopen a conversation.** (#1003, #1005, @jackie-cqz)
- **Claude Code moves to claude-agent-acp 0.94.0**, and along with newer CodeBuddy, Cursor, Kimi Code, Qoder, ZCode and Command Code, a Claude Code fork sub-agent's card now shows its real report instead of "Fork started".

## Fixed

- **Updating codeg on Windows no longer stops at "Failed to kill codeg"** while Windows or an antivirus is checking codeg.exe.
- **In the Linux AppImage, the agents, terminals and other programs codeg starts get your system's own environment instead of the AppImage's**, so on Ubuntu 26.04 installing and starting agents, npm and terminal commands no longer fail with "`LIBSYSTEMD_254` not found". (#983 reported by @yanvigdev)
- **A phone that puts the page in the background mid-reply gets the rest of the reply when it comes back**, without reloading the page. (#386 reported by @wuzhaozhongguo)
- **Behind a sign-in proxy such as Cloudflare Access, a page whose proxy sign-in has expired shows Sign-in expired with a Reload button**, instead of trying to reconnect forever. (#984, @MagTer)
- **A message sent on the canvas while the agent is still answering stays in the input with a notice**, instead of disappearing. (#991 reported by @SeeSeaStar)
- **A sub-agent waiting on you shows its permission, question or plan prompt in one card only**, instead of one more card each time the agent checks on it.
- **A new conversation no longer shows Load background activity right after its first message.**
- **An agent removed outside codeg, or lost with a switch to another Node version, no longer reads as installed in Settings.** (#990 reported by @BillyOutlast)
- **Grok installed with its own installer is found in `~/.grok/bin`**, also on Windows. (#994 reported by @HOMEFTW)
- **The Codex settings no longer write options current Codex ignores**, and the WebSocket switch is gone for Official Subscription, where it broke every turn, while Fast stays in the composer.
- **The forge panel's open repository button works for a self-hosted GitLab or similar served over http, on its own port or under a subpath.** (#997 reported by @run19910607)
- **A pull-request task whose pull request was force-pushed reports only its own changes**, on its card and in its done comment, instead of everything the base gained meanwhile.
- **A project's `.pi/mcp.json` now asks for project trust before Pi starts the servers it lists.**

## Note

- **Once 0.39.0 has run, 0.38.0 no longer starts on the same data**, because this version adds to codeg's database: codeg-server's Roll back would restore a 0.38.0 that won't start, and so would reinstalling an older desktop app, so copy codeg's data folder before you update if you may want to go back.
- **On Windows, the update from 0.38.0 still closes codeg to install as soon as the download finishes**, because the version you update from decides that, and later updates wait for Restart to update.
- **Run on this computer is only in the desktop app**, and a remote folder's host has to be on 0.39.0 to save the switch on a folder's command: until then, turn on Show in all folders to keep the command on your computer.

Thanks to @MagTer, @fengpanhe, @Flyneen, @jackie-cqz and @xyalter for contributing to this release, and to @yanvigdev, @trueai-org, @Time-w, @wuzhaozhongguo, @SeeSeaStar, @BillyOutlast, @HOMEFTW and @run19910607 for the reports.

-----------------------------

# 发布版本 0.39.0

这一版可以让智能体帮你写提交信息,远程文件夹里的命令也可以放到你自己的电脑上运行,codeg-server 还可以改用 Cloudflare Access 这类登录代理来登录,不必再输访问令牌。
同期还有:侧栏「最近」按最近使用排序;Windows 上的更新会等你点「重启以更新」再安装,也不会再卡在「Failed to kill codeg」;Linux AppImage 不再把自带的库带给智能体和终端;手机切回页面后能看到完整的回复。

## 新增

- **智能体可以帮你写提交信息了**——「变更」标签页和提交窗口的提交框旁新增 ✨ 按钮,会让当前会话的智能体按这次实际要提交的内容写一条提交信息,已经输入的文字会作为提示,也不会在会话列表里多出会话。(#931 由 @trueai-org、#928 由 @Time-w 提出)
- **有未跟踪的文件时,✨ 按钮会先问你这条提交信息包括其中哪些**,回车就只提交这些文件,提交框下方也会显示将带上几个未跟踪文件。
- **「更多 Git 操作」菜单里的「提交信息生成设置…」决定由谁、怎么写**——可以指定智能体、模型和选项,加上每次都遵循的附加说明(比如用什么语言、什么格式),还能在写好后自动提交;可以对所有文件夹统一设置,也可以给某个项目单独设置。
- **「管理命令」新增「在本机运行」开关**——比如用本机的 VS Code 或 Zed 通过 SSH 打开当前远程文件夹,这类命令会在运行桌面版的电脑上执行;打开「在所有文件夹中显示」后,切换文件夹、工作树或远程连接都能用到它。(#974,@fengpanhe)
- **codeg-server 可以通过 Cloudflare Access 这类登录代理来登录**——设置 `CODEG_AUTH=proxy-jwt` 后,已经通过代理登录的浏览器不用再输访问令牌,不经过代理的 codeg-cli 和脚本照常使用令牌,具体见[代理登录说明](https://github.com/spacering-net/codeg/blob/main/docs/proxy-auth.md)。(#985,@MagTer)
- **Grok 设置里的自定义模型可以声明支持的推理强度**,输入框里也就有了这个模型的推理强度选择。(#993 由 @HOMEFTW 提出)

## 改进

- **侧栏「最近」按最近使用而不是创建时间排序**,每次显示 10 个,每行显示的是最近活动时间;「对话」分组的每行也改为显示最近活动时间。(#1010,@Flyneen)
- **在 Windows 上,更新下载完后会等你点「重启以更新」再安装**,和 macOS、Linux 一样,不会再一下载完就关掉 codeg;安装失败会提示,可以重试。
- **「变更」标签页的提交框会随内容变高**,Shift+Enter 换行,回车仍然直接提交。
- **点开委派卡片能看到子智能体收到的完整任务**,恢复执行的子智能体也会在这里显示恢复的原因。
- **在 Windows、Linux 和浏览器里,侧栏可以拖得更窄**,只有 macOS 版需要给窗口按钮留出位置。
- **Pi 的模型选择器只列出 `enabledModels` 设置允许的模型。**(#989,@xyalter)
- **ZCode 支持粘贴和上传的图片、PDF,重新打开会话后每条回复的用量和耗时也还在。**(#1003、#1005,@jackie-cqz)
- **Claude Code 升级到 claude-agent-acp 0.94.0**,CodeBuddy、Cursor、Kimi Code、Qoder、ZCode 和 Command Code 也一并更新;Claude Code 的 fork 子智能体卡片现在显示它真正的结果,而不是「Fork started」。

## 修复

- **在 Windows 上更新 codeg 不会再停在「Failed to kill codeg」**,即使这时 Windows 或杀毒软件正在检查 codeg.exe。
- **Linux AppImage 版里,codeg 启动的智能体、终端和其他程序改用系统自己的环境,不再沿用 AppImage 的**,在 Ubuntu 26.04 上安装和启动智能体、运行 npm 和终端命令不会再报「`LIBSYSTEMD_254` not found」。(#983 由 @yanvigdev 反馈)
- **手机在回复途中把页面切到后台,切回来后能看到完整的回复**,不用再刷新页面。(#386 由 @wuzhaozhongguo 反馈)
- **在 Cloudflare Access 这类登录代理后面,代理登录过期时页面会显示「登录已过期」和「刷新」按钮**,不会再一直尝试重新连接。(#984,@MagTer)
- **在无限画布里给正在回答的对话发消息,消息会留在输入框里并给出提示**,不会再消失。(#991 由 @SeeSeaStar 反馈)
- **子智能体等你处理时,权限请求、提问或计划审批只显示在一张卡片里**,不会再每查询一次状态就多出一张。
- **新会话发出第一条消息后不会再马上出现「加载后台活动」提示。**
- **在 codeg 之外卸载的智能体,或者切换 Node 版本后找不到的智能体,设置里不会再显示为已安装。**(#990 由 @BillyOutlast 反馈)
- **用 Grok 官方安装程序装在 `~/.grok/bin` 的 Grok 能被识别了**,Windows 上也一样。(#994 由 @HOMEFTW 反馈)
- **Codex 设置不会再写入新版 Codex 已不认的配置**,官方订阅下也不再显示会让每轮对话都失败的 WebSocket 开关;快速模式仍在输入框里切换。
- **代码托管面板的「打开仓库」按钮支持走 http、用自定义端口或子路径的自建 GitLab 等服务。**(#997 由 @run19910607 反馈)
- **处理拉取请求的看板任务在对方强推后,只统计自己的改动**,卡片和完成评论里不会再算上基础分支期间新增的全部内容。
- **项目里的 `.pi/mcp.json` 现在要先信任项目,Pi 才会启动其中列出的服务。**

## 注意

- **0.39.0 运行过之后,0.38.0 就无法在同一份数据上启动了**,因为这一版给 codeg 的数据库加了内容:codeg-server 的「回滚」恢复出来的 0.38.0 起不来,重新安装旧版桌面版也一样;之后可能还要退回 0.38.0 的话,更新前请先复制一份 codeg 的数据目录。
- **在 Windows 上,从 0.38.0 更新的这一次仍会在下载完后直接关闭 codeg 安装**,因为安装时机由更新前的版本决定,之后的更新会等你点「重启以更新」。
- **「在本机运行」只在桌面版里可用**,远程文件夹的主机也要更新到 0.39.0 才能为文件夹命令保存这个开关;在此之前,打开「在所有文件夹中显示」就能把命令保存在你自己的电脑上。

感谢 @MagTer、@fengpanhe、@Flyneen、@jackie-cqz 和 @xyalter 为本次发布做出的贡献,也感谢 @yanvigdev、@trueai-org、@Time-w、@wuzhaozhongguo、@SeeSeaStar、@BillyOutlast、@HOMEFTW 和 @run19910607 的反馈。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants