Skip to content

feat: add Trae CLI client support (bili trae) - #669

Open
ranxianglei wants to merge 3 commits into
masterfrom
2026-09-10_trae-client-support
Open

feat: add Trae CLI client support (bili trae)#669
ranxianglei wants to merge 3 commits into
masterfrom
2026-09-10_trae-client-support

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Adds a bili trae launcher for Trae CLI (ByteDance's closed traecli agent). Trae CLI is a locally-signed-in agent that carries its own model quota (the issue's use case: a company only provides a traecli quota).

Why cert-MITM (not /bili/ rewrites)

Trae CLI is a stripped Go binary with no base-URL override — its model host is baked in, so the /bili/ path-rewrite mechanism (used by pi/omp/opencode) cannot reach it. The one route that works is cert-MITM: the Go runtime honors HTTPS_PROXY, so the launcher routes its model traffic through the bili proxy and injects the bili CA.

Verified by decompiling traecli v0.120.52 (see #655): honors HTTPS_PROXY (Go net/http.ProxyFromEnvironment); CA via the Go system trust store → SSL_CERT_FILE; model host overridable via TRAE_CLI_API_HOST; wire is the proprietary POST /api/ide/v2/llm_raw_chat (SSE, OpenAI-shaped body).

What changed

  • src/client-config.tsTraeConfig, resolveTraeHome (~/.trae), readTraeConfig (reads TRAE_CLI_API_HOST), TRAE_DEFAULT_MODEL_HOSTS (console.enterprise.trae.cn, www.trae.cn).
  • src/launcher.tsbili trae (proxy mode): buildTraeEnv sets HTTPS_PROXY + SSL_CERT_FILE (the combined CA bundle, since it replaces Go's system trust store — same shape as codex) + BILLION_CONTEXT_PROXY; whitelists the model host(s) for MITM (default hosts, or the TRAE_CLI_API_HOST override); resolves the traecli/trae-cli/trae binary. No budget/transport env (Trae manages its own context window internally).
  • src/server.ts — recognizes the proprietary /api/ide/v2/llm_raw_chat wire as OpenAI (per decompilation the body is OpenAI-shaped). Safe: if the body is unparseable (non-JSON/encrypted), the existing gate falls through to verbatim passthrough — no compression, no breakage.
  • src/discover.ts — trae hosts in MITM domain auto-discovery.
  • src/cli.ts + README/AGENTS/CONFIGURATION (en + zh-CN) — help + docs.
  • tests — launcher + discover coverage mirroring the qoder tests.

Residual risk (needs a real-machine capture)

Decompilation shows chatmodel.decryptMessage/decryptAK exist. The evidence (an OpenAI-response converter + a community bridge that reads the SSE directly) indicates the conversation body is plaintext, so decryptMessage most likely covers a signed-metadata field, not the whole body. If the body is encrypted, the proxy forwards it unchanged (no compression, no breakage) — so the worst case is "no value", not "broken". A real bili trae run against a company traecli confirms it.

The default MITM hosts are best-effort from the binary; if the company tenant is on a different host, set TRAE_CLI_API_HOST or --mitm-domain.

Verification

  • npm run typecheck — clean
  • npm test — 1242 pass (1 pre-existing sandbox failure in the codex/claude resolveClientCommand test, unrelated to this change — it hardcodes PATH=/usr/bin assuming codex is absent, but this sandbox has it; passes in CI)
  • npm run build — success

Fixes #655

Trae CLI (ByteDance) is a closed Go binary with no base-URL override, so
/bili/ rewrites cannot reach it — cert-MITM is the only route (the Go
runtime honors HTTPS_PROXY; the CA rides SSL_CERT_FILE, the combined
bundle, since it replaces Go's system trust store).

- client-config: TraeConfig, resolveTraeHome (~/.trae), readTraeConfig
  (TRAE_CLI_API_HOST), TRAE_DEFAULT_MODEL_HOSTS
- launcher: bili trae (proxy mode) — HTTPS_PROXY + SSL_CERT_FILE +
  BILLION_CONTEXT_PROXY, default model hosts whitelisted for MITM (or the
  TRAE_CLI_API_HOST override), traecli/trae-cli/trae binary resolution
- server: recognize the proprietary /api/ide/v2/llm_raw_chat wire as
  OpenAI (OpenAI-shaped body per decompilation); an unparseable body falls
  to verbatim passthrough (no compression, no breakage)
- discover: trae hosts in MITM domain auto-discovery
- cli/README/AGENTS/CONFIGURATION: help + docs (en + zh-CN)

Residual risk (needs a real-machine capture): the wire body may carry a
signed/encrypted field (chatmodel.decryptMessage); if so the proxy
forwards it unchanged rather than compressing.

Fixes #655
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

📦 Built Package Artifact

Branch: 2026-09-10_trae-client-support (6a28250)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-669

Each push to this PR publishes a new version under the pr-669 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr669.tgz
npm install -g package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Reviewing PR #669 (bili trae) now — status so far:

  • Diff is clean and focused (12 files, +240/−17): launcher + client-config + server + discover + cli + docs + tests. No unrelated churn, no whitespace noise.
  • Verified the key safety claim in src/server.ts: /api/ide/v2/llm_raw_chat is recognized as OpenAI, and when the body is unparseable (encrypted / non-JSON) parsed stays null → the compression gate (protocol && parsed && typeof parsed === "object") is skipped → prepared stays null → the request is forwarded verbatim (original bytes). Buffer bodies also skip the string-only compat.roles rewrite in forward(). So worst case is "no compression", never breakage — matches your description.
  • Pre-flight: npm run typecheck clean, npm run build success, npm test 1242/1243 pass. The single failure is the pre-existing resolveClientCommand: codex/claude resolve to themselves sandbox failure you flagged (it hardcodes PATH assuming codex is absent; this environment has it, so it fails here but passes in CI) — unrelated to this change.

One small inconsistency I'm fixing directly: in discoverRoutes, the trae branch pushes modelApiHost verbatim, so a TRAE_CLI_API_HOST=host:port would whitelist host:port — but the MITM whitelist matches on the SNI hostname (port-less, isMitmHost compares h === d || h.endsWith("."+d)), so that entry would never match. Every other client branch uses url.hostname. Stripping the port for consistency.

Full review + the fix coming shortly.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review complete — full assessment of PR #669 (bili trae).

Verdict: ✅ Approve — ready to merge (merge stays human-only)

Diff hygiene

Clean and focused: 12 files, +240/−17. No unrelated churn, no whitespace/reformat noise, no lockfile drift. Every line serves the stated purpose.

Correctness — verified against the source, not just the description

  1. /llm_raw_chat passthrough safety (the load-bearing claim). Confirmed in src/server.ts: the path is recognized as openai at protocol-detect; when the body is unparseable (encrypted / non-JSON) parsed stays null, so the compression gate (!opts.passthrough && hopMarker === undefined && protocol && parsed && typeof parsed === "object") is skipped, prepared stays null, and the request is forwarded verbatim via the original bytes. Buffer bodies also skip the string-only compat.roles rewrite in forward(). Worst case = "no compression", never breakage — exactly as described, and it's the same proven path Follow-up from #617: robust session identity + no-spurious-400 for header-less clients (e.g. ZCode) #619 uses for decode failures.
  2. Launcher env shape. buildTraeEnv mirrors buildCodexEnv (Go binary → CA rides SSL_CERT_FILE, combined bundle since it replaces Go's trust store). Note: trae passes stripInheritedProxy(process.env) where codex passes raw process.env — that's more correct for trae (a user-set NO_PROXY can't bypass bili for the model host), not a bug.
  3. No budget/transport env. Correct: collectModelWindows(config, "trae") returns {}; the ModelWindowScope type extension is precisely what makes collectModelWindows(config, base) type-valid for base === "trae". Matches "Trae manages its own context window internally."
  4. MCP injection off. launcherInjectMcp returns false for trae (closed binary, no MCP surface). Correct.
  5. Binary resolution. traeclitrae-clitrae, falling back to bare traecli (fails loudly at spawn if absent). Reasonable.
  6. CLI dispatch + docs. isLaunchClient picks up trae via LAUNCH_CLIENTS; help text + README/CONFIGURATION (en/zh-CN) all updated consistently.

Pre-flight (ran on this branch)

  • npm run typecheck — clean
  • npm run build — success
  • npm test1242/1243 pass. The single failure is the pre-existing resolveClientCommand: codex/claude resolve to themselves sandbox failure you flagged (hardcodes PATH assuming codex is absent; this environment has it → resolves to a full path; passes in CI). Unrelated to this change.

One small inconsistency — fixed directly on the branch

Pushed commit 6965c22 on top of your head:

  • Problem: discoverRoutes's trae branch pushed modelApiHost verbatim. With a TRAE_CLI_API_HOST=host:port, that whitelisted host:port — but isMitmHost matches on the port-less SNI hostname (h === d || h.endsWith("."+d)), so a host:port entry would never match and the host would silently fall through to a blind tunnel (no compression). Every other client branch reduces to url.hostname.
  • Fix: strip the port (h.split(":", 2)[0]) so the entry actually matches, consistent with the rest of the file; added a test locking in host:port → host. Default hosts are unaffected (no port). Typecheck clean; launcher+discover tests green.

It was masked in practice by the server-side auto-discovery fallback (extractHttpsHosts already strips the port via url.hostname), so it wasn't a functional break — just dead weight in the launcher's explicit whitelist and an inconsistency worth closing.

Residual risk (unchanged — needs a real-machine capture)

Your decompilation-based assessment holds. The only outcome this PR produces on an encrypted body is "forward unchanged" (no breakage). A real bili trae run against a company traecli is the confirmation step; nothing here alters that.


中文摘要:修复了 discoverRoutes 里 Trae 分支把带端口的 modelApiHost(host:port)原样塞进 MITM 白名单、而 MITM 按无端口的 SNI 主机名匹配导致该条目永不命中(退化成盲隧道、失去压缩)的不一致——改为剥离端口(与其余客户端分支一致)并补了测试;核心的加密体透传安全性、env 形态、无预算 env、CLI/文档都已逐项核验通过,typecheck/build/test 全绿(仅 1 个与本改动无关的既有沙箱失败),可以合并。

@ranxianglei

Copy link
Copy Markdown
Owner Author

解决冲突

Resolve launcher.ts import/re-export conflict: union of master's
resolveCodexHome and the PR's trae symbols (resolveTraeHome, readTraeConfig,
TRAE_DEFAULT_MODEL_HOSTS, type TraeConfig). All other files auto-merged.

Verified on merged tree: typecheck clean, build ok, 1334 tests pass / 0 fail.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

已解决冲突并推回 PR 分支。

冲突来源:自本 PR 分叉后 master 前进了约 40 个提交(含 acp-kernel 版本升级、Windows 启动/spawn 修复、turn-integrity、release v0.1.98–v0.1.102 等)。把 master 合入 PR 分支后,只有 src/launcher.ts 存在真实文本冲突,且集中在两处——client-config.jsimport 行re-export 块:master 新增了 resolveCodexHome,本 PR 新增了 trae 符号(resolveTraeHomereadTraeConfigTRAE_DEFAULT_MODEL_HOSTStype TraeConfig)。其余文件全部自动合并,无语义冲突。

解决方式:取两者并集——同时保留 master 的 resolveCodexHome 与本 PR 的全部 trae 符号(import 与 re-export 各一处,顺序对齐现有分组)。

合并后验证(在合并后的完整树上重跑,非仅冲突文件):

  • npm run typecheck:clean
  • npm run build:成功
  • npm test1334 pass / 0 fail / 2 skipped。之前沙箱里那个 codex/claude resolveClientCommand 的预存失败这次也不再出现(现在 0 fail)。

推送:fast-forward 推到 PR 分支 2026-09-10_trae-client-support6965c22..6a28250)。PR #669 现在 mergeable: true(内容冲突已清除);mergeable_state: blocked 只是 CI / 分支保护在 gate「实际合并」这一步,属正常流程,合并仍由你手动触发(我不会代点)。

另外说明:未改动 version(合并后沿用 master 当前版本;本内容为功能 PR,不 bump 版本号)。


一句话:把 master 合入 PR 分支,解决了 src/launcher.ts 里 import/re-export 的符号冲突(取并集),合并树 typecheck / build / 1334 项测试全绿并已推回,PR 现已可合并,等你点合并按钮。

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.

能否新增对traecli的支持

1 participant