feat(cli): Server Edition server-mode — gateway login/workspace commands, remote transport, headless entry hardening - #81
Merged
deepagent-ai merged 8 commits intoJul 22, 2026
Conversation
- dacode login/logout: gateway /control/v1/auth/login, 凭证落盘 0600 - dacode workspace list/use: 选定 workspace, baseUrl 定格 /w/:id - Connection 服务统一 local daemon 与 server-mode 远程 transport - Bearer 注入自定义 fetch, 401→单飞 refresh→重试, TUI 经 fetch 注入复用 - DEEPAGENT_GATEWAY_URL 环境变量置顶 gateway (server-v1 §20.3)
- package.json 显式导出 ./server 子路径 (listen/openapi 稳定入口) - /global/capabilities 增加 commit 字段 (DEEPAGENT_CODE_COMMIT, CI 注入) - DEEPAGENT_SERVER_MODE=true 时 Auth.set/remove fail-close, 禁 key 落盘
- packages/cli/test/server-mode.test.ts: 真实 Bun.serve mock gateway, 覆盖 login(body/cookie 双通道 refresh token, 0600 持久化, 错误凭证)、 workspaces(401→refresh→重试, wrapped 响应形状, env 置顶不匹配)、 useWorkspace、transport(/w/:id URL, fetch Bearer 注入与刷新重试) - test/auth/auth.test.ts: DEEPAGENT_SERVER_MODE 阻断/恢复两态 - Flag.DEEPAGENT_SERVER_MODE 改为 getter 以便按用例切换 - cli 包补 test/test:ci 脚本, 接入 turbo test:ci
- login/refresh parse camelCase accessToken; capture rotated refresh cookie on refresh - workspaces use /control/v1/containers (one container per user) with idempotent POST ensure on 404; proxy base URL is the bare /w - replay a cloned Request on 401 retry so POST bodies survive refresh - normalize trailing slashes in DEEPAGENT_GATEWAY_URL before comparing against the stored gateway
…lFetch - connection.test.ts: server-mode gateway wins once logged in with a workspace; daemon fallback when inactive; fail fast with a workspace hint instead of silently falling back - handlers.test.ts: login/logout/workspace list+use against a mock gateway, including env fallback and unknown-id rejection - extract gracefulFetch from tui.ts into services/graceful-fetch.ts so the legacy-404 fallback is testable without loading the TUI
…back - graceful-fetch strips the bare /w proxy prefix so legacy config endpoints get their empty defaults in server mode too - a pinned DEEPAGENT_GATEWAY_URL without a matching login warns and falls back to the local daemon instead of bricking local usage - re-login against the same gateway keeps the selected workspace - unify the transport shape: Connection.Transport derives from ServerMode.Transport instead of redefining it - style: let -> const ternary, drop destructuring, for -> forEach
Pin the two highest-risk behaviors in the 401 retry path: concurrent 401s trigger exactly one refresh round, and a rejected refresh token surfaces a session-expired error instead of hanging or looping.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #
Type of change
What does this PR do?
This PR prepares the new CLI (
packages/cli,dacode) and the headless server to act as a client/data-plane for DeepAgent Server Edition (design: server-v1 §11/§13/§20). The CLI now connects to a gateway with JWT auth and routes workspace traffic through the/w/:workspaceIdtransparent proxy instead of a local daemon.CLI changes:
dacode login [gateway] [--email --password]/logout– authenticate viaPOST /control/v1/auth/login, store state in~/.deepagent/code/state/server-mode.json(0600, atomic temp+rename). Flags optional; fallback to interactive prompts. Refresh token read from response body, withSet-Cookieas fallback.dacode workspace list/dacode workspace use <id>– list gateway workspaces and pin a selection; remote base URL becomes{gateway}/w/{workspaceId}.Connectionservice switches to remote transport when server-mode is active; otherwise uses the existing local daemon (unchanged behavior).Authorization: Bearerand, on 401, performs a single-flight token refresh with one retry. Since the TUI already accepts a customfetch, all TUI/SDK traffic (including SSE) gets auth + reconnect without any changes topackages/tui.DEEPAGENT_GATEWAY_URLpins the gateway (server-v1 §20.3 auto-switch); a mismatch with stored login produces a clear error suggestingdacode login <url>.Server changes (
packages/deepagent-code):"./server"subpath export soimport { listen, openapi } from "deepagent-code/server"resolves as documented for the workspace-agent./global/capabilitiesgains an optionalcommitfield fromDEEPAGENT_CODE_COMMIT(injected by CI for version-checking, §13.3). Omitted in local builds; optional in schema for backward compatibility.DEEPAGENT_SERVER_MODE=truemakesAuth.set/Auth.removefail with a clear error – in gateway-managed containers, provider keys come via env and must not persist to volume (§20.4). Reads (includingDEEPAGENT_CODE_AUTH_CONTENT) are unaffected.How did you verify your code works?
bun typecheckpasses forpackages/cli,packages/core,packages/deepagent-code.Bun.serveimplementing auth endpoints, workspace listing, and proxy paths): covered login via args/env, state file permissions, workspace list/use with expired token refresh, remote transport URL and auth injection, logout, and gateway mismatch errors./global/capabilitiesreturns the injected commit andopenapi()works;Auth.set/Auth.removeare blocked underDEEPAGENT_SERVER_MODE=true; without itauth.jsonis written with mode 600.test/auth/auth.test.ts,test/server/httpapi-global.test.ts,test/server/httpapi-public-openapi.test.ts. (test/server/httpapi-listen.test.tshas PTY failures that reproduce identically on the base commit in this environment.)Screenshots / recordings
No UI changes.
Checklist