Skip to content

feat(cli): Server Edition server-mode — gateway login/workspace commands, remote transport, headless entry hardening - #81

Merged
deepagent-ai merged 8 commits into
deepagent-ltd:devfrom
thomas-yanga:feat/cli-server-mode
Jul 22, 2026
Merged

feat(cli): Server Edition server-mode — gateway login/workspace commands, remote transport, headless entry hardening#81
deepagent-ai merged 8 commits into
deepagent-ltd:devfrom
thomas-yanga:feat/cli-server-mode

Conversation

@thomas-yanga

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

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/:workspaceId transparent proxy instead of a local daemon.

CLI changes:

  • dacode login [gateway] [--email --password] / logout – authenticate via POST /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, with Set-Cookie as fallback.
  • dacode workspace list / dacode workspace use <id> – list gateway workspaces and pin a selection; remote base URL becomes {gateway}/w/{workspaceId}.
  • The Connection service switches to remote transport when server-mode is active; otherwise uses the existing local daemon (unchanged behavior).
  • Remote transport is a fetch wrapper that injects Authorization: Bearer and, on 401, performs a single-flight token refresh with one retry. Since the TUI already accepts a custom fetch, all TUI/SDK traffic (including SSE) gets auth + reconnect without any changes to packages/tui.
  • DEEPAGENT_GATEWAY_URL pins the gateway (server-v1 §20.3 auto-switch); a mismatch with stored login produces a clear error suggesting dacode login <url>.

Server changes (packages/deepagent-code):

  • Add explicit "./server" subpath export so import { listen, openapi } from "deepagent-code/server" resolves as documented for the workspace-agent.
  • /global/capabilities gains an optional commit field from DEEPAGENT_CODE_COMMIT (injected by CI for version-checking, §13.3). Omitted in local builds; optional in schema for backward compatibility.
  • DEEPAGENT_SERVER_MODE=true makes Auth.set/Auth.remove fail with a clear error – in gateway-managed containers, provider keys come via env and must not persist to volume (§20.4). Reads (including DEEPAGENT_CODE_AUTH_CONTENT) are unaffected.

Note: The gateway itself (deepagent-code-server repo) is not implemented yet; the refresh-cookie contract may need a follow-up once the real gateway exists.

How did you verify your code works?

  • bun typecheck passes for packages/cli, packages/core, packages/deepagent-code.
  • End-to-end against a mock gateway (Bun.serve implementing 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.
  • Server tests: started the legacy server via the new subpath export, confirmed /global/capabilities returns the injected commit and openapi() works; Auth.set/Auth.remove are blocked under DEEPAGENT_SERVER_MODE=true; without it auth.json is written with mode 600.
  • Existing related suites pass: test/auth/auth.test.ts, test/server/httpapi-global.test.ts, test/server/httpapi-public-openapi.test.ts. (test/server/httpapi-listen.test.ts has PTY failures that reproduce identically on the base commit in this environment.)

Screenshots / recordings

No UI changes.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

- 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 落盘
thomas-yanga and others added 6 commits July 22, 2026 15:26
- 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.
@deepagent-ai
deepagent-ai merged commit 6906ee6 into deepagent-ltd:dev Jul 22, 2026
3 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants