(6).fill('test-platform-grant'))
+ expect((await f.account.getState()).status).toBe('credential-stored')
+ expect(await f.ctx.credentials.readRecord(credentialKey('deepseek-account-platform', 'default')))
+ .toMatchObject({ kind: 'grant', payload: { token: 'new-account-token' } })
+})
+
+it.each([['en', 'en_US'], ['zh-CN', 'zh_CN']])('passes %s to Platform and keeps the active attempt language', async (locale, platformLocale) => {
+ const f = await fixture()
+ await f.account.startSignIn(locale, f.callbackOrigin, 'desktop')
+ const first = await f.wait('waiting-browser')
+ expect(f.init().locale).toBe(platformLocale)
+ expect((await f.account.startSignIn('zh', f.callbackOrigin, 'desktop')).attempt?.id).toBe(first.attempt?.id)
+ expect(f.init().locale).toBe(platformLocale)
+ await f.account.cancelSignIn(first.attempt!.id)
+ await f.account.startSignIn('zh', f.callbackOrigin, 'desktop')
+ await f.wait('waiting-browser')
+ expect(f.init().locale).toBe('zh_CN')
+})
+
+it('adds private deployment cookies to every Platform request without exposing them in account state', async () => {
+ const f = await fixture(undefined, { Cookie: 'test_gate=synthetic' })
+ await f.account.startSignIn('en', f.callbackOrigin, 'desktop')
+ await f.wait('waiting-browser')
+ await fetch(f.callback(), { redirect: 'manual' })
+ await readDetails(f.account)
+ expect(JSON.stringify(await f.account.getState())).not.toContain('test_gate')
+ await f.account.signOut()
+ await expect.poll(f.logoutCount).toBe(1)
+ expect(f.receivedHeaders.map(item => item.path)).toEqual([
+ '/auth-api/v0/dsh/auth_init', '/auth-api/v0/dsh/auth_exchange',
+ '/auth-api/v0/users/current', '/api/v0/users/get_user_summary', '/auth-api/v0/users/logout',
+ ])
+ expect(f.receivedHeaders.every(item => item.cookie === 'test_gate=synthetic')).toBe(true)
+ expect(f.receivedHeaders.slice(2).every(item => item.authorization === 'dsh_mock_test')).toBe(true)
+})
+
+it('rejects reserved, duplicate and malformed deployment headers without disclosing values', () => {
+ for (const headers of [
+ { Authorization: 'secret-value' }, { 'X-DSH-Auth-Token': 'secret-value' }, { HOST: 'secret-value' }, { 'Content-Length': '5' },
+ { Cookie: 'secret-value', cookie: 'other' }, { 'bad name': 'secret-value' },
+ { Cookie: 'secret-value\r\ninjected: x' },
+ ]) {
+ expect(() => platformHeaders(headers)).toThrow(/^account: requestHeaders/)
+ }
+ expect(platformHeaders({ Cookie: 'test_gate=synthetic' })).toEqual({ cookie: 'test_gate=synthetic' })
+})
+
+it('does not forward deployment cookies through a Platform redirect', async () => {
+ const f = await fixture(undefined, { Cookie: 'test_gate=synthetic' })
+ f.redirect()
+ await f.account.startSignIn('en', f.callbackOrigin, 'desktop')
+ await f.wait('failed')
+ expect(f.receivedHeaders.map(item => item.path)).toEqual(['/auth-api/v0/dsh/auth_init'])
+})
+
+it('closes the failed Web authorization tab without redirecting and keeps the shared HTTP server alive', async () => {
+ const f = await fixture()
+ const dispose = f.ctx.webServer.register({ kind: 'exact', path: '/health', handler: (_req, res) => { res.end('alive') } })
+ cleanups.push(async () => { dispose() })
+ await f.account.startSignIn('en', f.callbackOrigin, 'web')
+ await f.wait('waiting-browser')
+ expect(new URL(f.init().redirect_uri!).origin).toBe(f.callbackOrigin)
+ f.fail(17)
+ const response = await fetch(f.callback(), { redirect: 'manual' })
+ expect(response.status).toBe(200)
+ expect(response.headers.get('location')).toBeNull()
+ const page = await response.text()
+ expect(page).toContain('window.close()')
+ const nonce = /nonce="([^"]+)"/.exec(page)![1]
+ expect(response.headers.get('content-security-policy')).toContain(`script-src 'nonce-${nonce}'`)
+ await expect(`${page.match(/(.*?)<\/p>/)![1]}\n`)
+ .toMatchFileSnapshot('./expected/web-exchange-failure.txt')
+ expect(f.init().client_type).toBe('web')
+ expect(await f.wait('failed')).toMatchObject({ status: 'signed-out', attempt: { errorCode: 'protocol' } })
+ expect(f.count()).toBe(1)
+ expect(await (await fetch(`${f.callbackOrigin}/health`)).text()).toBe('alive')
+ f.fail(0)
+ await f.account.startSignIn('en', f.callbackOrigin, 'web')
+ await f.wait('waiting-browser')
+ const success = await fetch(f.callback(), { redirect: 'manual' })
+ expect(success.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=test&locale=zh_CN`)
+ expect(f.count()).toBe(2)
+})
+
+it('completes login through a local TCP forward using the browser port rather than the Host port', async () => {
+ const f = await fixture()
+ const sockets = new Set()
+ const forward = createTcpServer((socket) => {
+ const upstream = connect(f.ctx.webServer.port, '127.0.0.1')
+ for (const connection of [socket, upstream]) {
+ sockets.add(connection)
+ connection.on('close', () => { sockets.delete(connection) })
+ connection.on('error', () => { socket.destroy(); upstream.destroy() })
+ }
+ socket.pipe(upstream).pipe(socket)
+ })
+ cleanups.push(async () => {
+ for (const socket of sockets) socket.destroy()
+ await new Promise(resolve => forward.close(() => { resolve() }))
+ })
+ await new Promise(resolve => forward.listen(0, '127.0.0.1', resolve))
+ const address = forward.address()
+ if (address === null || typeof address === 'string') throw new Error('missing forward listener')
+ const forwardedOrigin = `http://127.0.0.1:${address.port}`
+ expect(forwardedOrigin).not.toBe(f.callbackOrigin)
+ await f.account.startSignIn('en', forwardedOrigin, 'web')
+ await f.wait('waiting-browser')
+ expect(f.init().redirect_uri).toBe(`${forwardedOrigin}/oauth/callback`)
+ const response = await fetch(f.callback(), { redirect: 'manual' })
+ expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=test&locale=zh_CN`)
+ expect((await f.account.getState()).status).toBe('credential-stored')
+})
+
+it('keeps cancellation authoritative without reopening WebUI after a delayed exchange', async () => {
+ const f = await fixture()
+ f.hold()
+ await f.account.startSignIn('en', f.callbackOrigin, 'web')
+ const state = await f.wait('waiting-browser')
+ const response = fetch(f.callback(), { redirect: 'manual' })
+ await f.exchanged.promise
+ await f.account.cancelSignIn(state.attempt!.id)
+ expect((await response).status).toBe(204)
+ f.release.resolve(undefined)
+ expect(await f.account.getState()).toMatchObject({ status: 'signed-out', attempt: { phase: 'cancelled' } })
+})
+
+it.each(['https://example.com', 'http://example.com', 'http://127.0.0.1.evil.test',
+ 'http://user@localhost', 'http://localhost/path', 'http://localhost/?x=1', 'http://localhost/#x', 'invalid'])
+('rejects unsupported callback origin %s before starting authorization', async (origin) => {
+ const f = await fixture()
+ await expect(f.account.startSignIn('en', origin, 'web')).rejects.toThrow('account: protocol')
+ expect(f.init()).toEqual({})
+})
+
+it('normalizes supported loopback callback origins', () => {
+ expect(loginOrigin('http://localhost:8080/')).toBe('http://localhost:8080')
+ expect(loginOrigin('http://127.0.0.1:8080')).toBe('http://127.0.0.1:8080')
+})
+
+it('cancels remotely with the original PKCE verifier without waiting for acknowledgment', async () => {
+ const f = await fixture()
+ f.holdCancel()
+ await f.account.startSignIn('en', f.callbackOrigin, 'web')
+ const state = await f.wait('waiting-browser')
+ expect((await f.account.cancelSignIn(state.attempt!.id)).attempt?.phase).toBe('cancelled')
+ await f.cancellationReceived.promise
+ expect(f.cancellations).toHaveLength(1)
+ const body = f.cancellations[0]!
+ expect(Object.keys(body).sort()).toEqual(['code_verifier', 'request_id'])
+ expect(body.request_id).toBe('test')
+ expect(createHash('sha256').update(body.code_verifier!).digest('base64url')).toBe(f.init().code_challenge)
+ expect(f.receivedHeaders.find(row => row.path?.endsWith('auth_cancel'))?.authorization).toBeUndefined()
+ await f.account.cancelSignIn(state.attempt!.id)
+ expect(f.cancellations).toHaveLength(1)
+ f.fail(1)
+ f.release.resolve(undefined)
+ expect(await f.account.getState()).toMatchObject({ status: 'signed-out', attempt: { phase: 'cancelled' } })
+})
+
+it('preserves localhost and the browser-visible port in authorization initialization', async () => {
+ const f = await fixture()
+ const origin = f.callbackOrigin.replace('127.0.0.1', 'localhost')
+ await f.account.startSignIn('en', origin, 'web')
+ const state = await f.wait('waiting-browser')
+ expect(f.init().redirect_uri).toBe(`${origin}/oauth/callback`)
+ await f.account.cancelSignIn(state.attempt!.id)
+})
+
+it('never exports an embedded Platform token to a different configured issuer', async () => {
+ const f = await fixture()
+ await f.ctx.credentials.modifyRecord(credentialKey('deepseek-account-platform', 'default'), () => Promise.resolve({
+ kind: 'grant', payload: { version: 1, token: 'fixture-secret', issuer: 'https://platform.deepseek.com' },
+ }))
+ await expect(f.account.getPlatformSession()).rejects.toThrow()
+})
+
+async function readDetails(account: Pick) {
+ const [profile, balance] = await Promise.all([account.getProfile(), account.getBalance()])
+ return profile === null || balance === null ? null : { profile, balance }
+}
+
+it('returns the profile while the balance request is still pending', async () => {
+ const f = await fixture()
+ await storeAccount(f)
+ f.holdBalance()
+ let balanceSettled = false
+ const balance = f.account.getBalance().then((value) => { balanceSettled = true; return value })
+ try {
+ await f.detailsStarted.promise
+ expect(await f.account.getProfile()).toMatchObject({ status: 'ready', value: { name: 'Test Account' } })
+ expect(balanceSettled).toBe(false)
+ } finally { f.release.resolve(undefined) }
+ expect(await balance).toMatchObject({ status: 'ready' })
+})
diff --git a/packages/credentials/deepseek-account-platform/tests/diagnostics.spec.ts b/packages/credentials/deepseek-account-platform/tests/diagnostics.spec.ts
new file mode 100644
index 0000000000..f3979fdbac
--- /dev/null
+++ b/packages/credentials/deepseek-account-platform/tests/diagnostics.spec.ts
@@ -0,0 +1,55 @@
+import { expect, it, vi } from 'vitest'
+import { browserUrl, requestPlatform } from '../src/protocol.ts'
+
+it.each(['/dsh/authorize', '/dsh/authorized'])('maps %s to the configured development origin and preserves query bytes', (path) => {
+ const origin = 'http://localhost:8081'
+ const query = '?request_id=fixture&value=a%2Fb&value=two+words&empty='
+ const url = `https://platform.deepseek.com${path}${query}`
+ expect(() => browserUrl(url, origin, path)).toThrow()
+ expect(browserUrl(url, origin, path, true)).toBe(`${origin}${path}${query}`)
+ expect(browserUrl(`${origin}${path}${query}`, origin, path, true)).toBe(`${origin}${path}${query}`)
+ for (const invalid of [
+ `http://other.example${path}`,
+ 'javascript:alert(1)',
+ 'https://platform.deepseek.com/other',
+ `https://user:password@platform.deepseek.com${path}`,
+ `https://platform.deepseek.com${path}#fragment`,
+ ]) expect(() => browserUrl(invalid, origin, path, true)).toThrow()
+})
+
+it('reports business failure codes without credentials or response messages', async () => {
+ const output = vi.spyOn(console, 'info').mockImplementation(() => undefined)
+ const fetcher = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
+ code: 0, data: { biz_code: 40123, biz_data: { token: 'response-secret' } }, message: 'private-message',
+ })))
+ try {
+ await expect(requestPlatform('https://platform.deepseek.com', 'auth_init',
+ { code_verifier: 'request-secret' }, new AbortController().signal,
+ { Cookie: 'cookie-secret' })).rejects.toThrow('account: protocol')
+ const logged = JSON.stringify(output.mock.calls)
+ expect(logged).toContain('/auth-api/v0/dsh/auth_init')
+ expect(logged).toContain('40123')
+ expect(logged).toContain('200')
+ for (const secret of ['response-secret', 'private-message', 'request-secret', 'cookie-secret']) {
+ expect(logged).not.toContain(secret)
+ }
+ } finally {
+ fetcher.mockRestore()
+ output.mockRestore()
+ }
+})
+
+it('reports transport failure without exposing the thrown error', async () => {
+ const output = vi.spyOn(console, 'info').mockImplementation(() => undefined)
+ const fetcher = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('private-network-detail'))
+ try {
+ await expect(requestPlatform('https://platform.deepseek.com', 'auth_init', {},
+ new AbortController().signal, {})).rejects.toThrow('account: network')
+ const logged = JSON.stringify(output.mock.calls)
+ expect(logged).toContain('network')
+ expect(logged).not.toContain('private-network-detail')
+ } finally {
+ fetcher.mockRestore()
+ output.mockRestore()
+ }
+})
diff --git a/packages/credentials/deepseek-account-platform/tests/expected/web-exchange-failure.txt b/packages/credentials/deepseek-account-platform/tests/expected/web-exchange-failure.txt
new file mode 100644
index 0000000000..49037a1dfd
--- /dev/null
+++ b/packages/credentials/deepseek-account-platform/tests/expected/web-exchange-failure.txt
@@ -0,0 +1 @@
+Sign-in failed. Close this tab and try again in the original tab.
diff --git a/packages/credentials/deepseek-account-platform/tests/logout.spec.ts b/packages/credentials/deepseek-account-platform/tests/logout.spec.ts
new file mode 100644
index 0000000000..e41d950e0b
--- /dev/null
+++ b/packages/credentials/deepseek-account-platform/tests/logout.spec.ts
@@ -0,0 +1,75 @@
+import { afterEach, beforeEach, expect, it, vi } from 'vitest'
+import { setTimeout } from 'node:timers/promises'
+import { logoutAccount } from '../src/protocol.ts'
+import { revokeAccount } from '../src/logout.ts'
+
+vi.mock('node:timers/promises', () => ({ setTimeout: vi.fn() }))
+vi.mock('../src/protocol.ts', () => ({ logoutAccount: vi.fn() }))
+beforeEach(() => {
+ vi.resetAllMocks()
+ vi.mocked(setTimeout).mockResolvedValue(undefined)
+})
+afterEach(() => { vi.restoreAllMocks() })
+
+const policy = { maxRetries: 5, delayMs: 1_000, requestTimeoutMs: 30_000 }
+
+it('stops after one successful request without waiting', async () => {
+ await revokeAccount('https://platform.deepseek.com', 'old-token', policy, new AbortController().signal, {})
+ expect(logoutAccount).toHaveBeenCalledTimes(1)
+ expect(setTimeout).not.toHaveBeenCalled()
+})
+
+it('makes at most six requests with five exponentially increasing delays and the captured token', async () => {
+ vi.mocked(logoutAccount).mockRejectedValue(new Error('unavailable'))
+ const controller = new AbortController()
+ const headers = { cookie: 'test-gate' }
+ await expect(revokeAccount('https://platform.deepseek.com', 'old-token', policy, controller.signal, headers))
+ .resolves.toBeUndefined()
+ expect(logoutAccount).toHaveBeenCalledTimes(6)
+ expect(vi.mocked(setTimeout).mock.calls.map(args => args[0])).toEqual([1_000, 2_000, 4_000, 8_000, 16_000])
+ for (const args of vi.mocked(logoutAccount).mock.calls) {
+ expect(args).toEqual(['https://platform.deepseek.com', 'old-token', expect.any(AbortSignal), headers])
+ }
+})
+
+it('stops retrying as soon as a request succeeds', async () => {
+ vi.mocked(logoutAccount).mockRejectedValueOnce(new Error('offline')).mockResolvedValue(undefined)
+ await revokeAccount('https://platform.deepseek.com', 'old-token', policy, new AbortController().signal, {})
+ expect(logoutAccount).toHaveBeenCalledTimes(2)
+ expect(setTimeout).toHaveBeenCalledTimes(1)
+})
+
+it('honors a disabled retry policy', async () => {
+ vi.mocked(logoutAccount).mockRejectedValue(new Error('offline'))
+ await revokeAccount('https://platform.deepseek.com', 'old-token', { ...policy, maxRetries: 0 }, new AbortController().signal, {})
+ expect(logoutAccount).toHaveBeenCalledTimes(1)
+ expect(setTimeout).not.toHaveBeenCalled()
+})
+
+it('cancels an in-flight request when the provider closes', async () => {
+ const controller = new AbortController()
+ vi.mocked(logoutAccount).mockImplementation((_origin, _token, signal) => new Promise((_resolve, reject) => {
+ signal.addEventListener('abort', () => { reject(new Error('cancelled')) }, { once: true })
+ controller.abort()
+ }))
+ await revokeAccount('https://platform.deepseek.com', 'old-token', policy, controller.signal, {})
+ expect(logoutAccount).toHaveBeenCalledTimes(1)
+ expect(setTimeout).not.toHaveBeenCalled()
+})
+
+it('cancels a pending delay and never starts another request', async () => {
+ const controller = new AbortController()
+ vi.mocked(logoutAccount).mockRejectedValue(new Error('offline'))
+ vi.mocked(setTimeout).mockImplementation(async (_delay, _value, options) => {
+ expect(options?.signal).toBe(controller.signal)
+ controller.abort()
+ throw controller.signal.reason
+ })
+ await revokeAccount('https://platform.deepseek.com', 'old-token', policy, controller.signal, {})
+ expect(logoutAccount).toHaveBeenCalledTimes(1)
+})
+
+it('does not start work for an already closed provider', async () => {
+ await revokeAccount('https://platform.deepseek.com', 'old-token', policy, AbortSignal.abort(), {})
+ expect(logoutAccount).not.toHaveBeenCalled()
+})
diff --git a/packages/credentials/deepseek-account-platform/tsconfig.json b/packages/credentials/deepseek-account-platform/tsconfig.json
new file mode 100644
index 0000000000..63c81daaea
--- /dev/null
+++ b/packages/credentials/deepseek-account-platform/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "lib/types"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../../../vendor/cosmokit"
+ },
+ {
+ "path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../deepseek-account"
+ },
+ {
+ "path": "../authorization"
+ },
+ {
+ "path": "../credentials"
+ },
+ {
+ "path": "../../../vendor/schemastery"
+ },
+ {
+ "path": "../../host/webserver"
+ }
+ ]
+}
diff --git a/packages/credentials/deepseek-account/README.i18n.yaml b/packages/credentials/deepseek-account/README.i18n.yaml
new file mode 100644
index 0000000000..e6714ca1a7
--- /dev/null
+++ b/packages/credentials/deepseek-account/README.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write packages/credentials/deepseek-account/README.md
+README.md: 4183c786f92a53bb7a90ba565a354da2d6ee2374
+README.zh.md: 3ad85f452ccb6f0eec9fad11c7eac2d29350ebf2
diff --git a/packages/credentials/deepseek-account/README.md b/packages/credentials/deepseek-account/README.md
new file mode 100644
index 0000000000..4183c786f9
--- /dev/null
+++ b/packages/credentials/deepseek-account/README.md
@@ -0,0 +1,55 @@
+---
+description: "Account consumers read stored login state, start or cancel a browser login, and sign out without editing API keys. Host model consumers resolve account credentials only for the official API origin."
+kind: "package-reference"
+---
+
+# @deepseek-ai/dsh-deepseek-account
+
+English | [中文](README.zh.md)
+
+getPlatformSession returns a Host-only origin/token snapshot for native Platform embedding, or null when signed out. It is absent from account-controller RPC and Client state. Consumers destroy documents holding a snapshot when the account changes.
+
+## Summary
+
+Account consumers read stored login state, start or cancel a browser login, and sign out without editing API keys. Host model consumers resolve account credentials only for the official API origin.
+
+## Table of Contents
+
+- [Use this package](#use-this-package)
+- [Model Experience](#model-experience)
+- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
+
+
+## Use this package
+
+The service defines account operations and reconnectable state snapshots. The platform provider owns the protocol and stored grant. Credentials are Host-only; the API controller exports state and commands without resolveToken.
+
+
+## Understand the implementation
+
+The service defines account operations without maintaining a second credential index; no invariant companion is published. The provider owns persistence and login lifecycle checks.
+
+
+## Further Exploration
+
+The [credentials subsystem](../../../docs/subsystems/credentials.md) owns storage APIs; the [architecture](../../../docs/architecture.md) explains application composition.
+
+
+## Model Experience
+
+None, as account credentials affect HTTP authentication and never enter model prompts, Session logs, or tool results.
+
+#### KV Cache effect
+
+No model request prefix changes.
+
+## Known Limitations and Deferred Work
+
+
+
+- Account tokens have no expiry or refresh flow. Sign-out removes the local grant before background provider revocation; remote logout failures never restore local login. Profile and balance query failures retain the stored grant. getProfile / getBalance reports profile and balance query outcomes independently.
+
+
+### Dev Note
+
+The [desktop login decision](../../../.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md) records cancellation and storage ownership.
diff --git a/packages/credentials/deepseek-account/README.zh.md b/packages/credentials/deepseek-account/README.zh.md
new file mode 100644
index 0000000000..3ad85f452c
--- /dev/null
+++ b/packages/credentials/deepseek-account/README.zh.md
@@ -0,0 +1,55 @@
+---
+description: "账号使用方可读取本地登录状态、发起或取消浏览器登录,并在保留 API Key 的情况下退出。Host 模型使用方仅能为官方 API 来源解析账号凭证。"
+kind: "package-reference"
+---
+
+# @deepseek-ai/dsh-deepseek-account
+
+[English](README.md) | 中文
+
+getPlatformSession 为原生 Platform 内嵌提供仅限 Host 的 origin/token 快照,退登时返回 null。账号控制器 RPC 和 Client 状态不包含此方法及快照。账号变化时,使用方销毁持有旧快照的文档。
+
+## 概述
+
+账号使用方可读取本地登录状态、发起或取消浏览器登录,并在保留 API Key 的情况下退出。Host 模型使用方仅能为官方 API 来源解析账号凭证。
+
+## 目录
+
+- [使用此包](#use-this-package)
+- [模型体验](#model-experience)
+- [已知限制与后续工作](#known-limitations-and-deferred-work)
+
+
+## 使用此包
+
+该服务定义账号操作和可重连的状态快照。平台提供者负责协议和授权记录。凭证仅限 Host;API 控制器只导出状态与操作,不导出 resolveToken。
+
+
+## 理解实现
+
+服务只定义账号操作,不维护第二份凭证索引,因此不发布 invariant。提供者负责持久化和登录生命周期检查。
+
+
+## 深入探索
+
+[凭证子系统](../../../docs/subsystems/credentials.zh.md)定义存储接口;[架构](../../../docs/architecture.zh.md)说明应用组合。
+
+
+## 模型体验
+
+无,因为账号凭证只影响 HTTP 认证,不进入模型提示、Session 日志或工具结果。
+
+#### KV Cache effect
+
+不改变模型请求前缀。
+
+## 已知限制与后续工作
+
+
+
+- 账号 token 没有过期或刷新流程。退出时先移除本地授权,再由提供者在后台撤销;远程退登失败不会恢复本地登录态。资料和余额查询失败保留已存储授权。getProfile / getBalance 分别报告资料和余额的查询结果。
+
+
+### 开发备注
+
+[桌面登录决策](../../../.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md)记录取消和存储的职责。
diff --git a/packages/credentials/deepseek-account/package.json b/packages/credentials/deepseek-account/package.json
new file mode 100644
index 0000000000..d85e2e0168
--- /dev/null
+++ b/packages/credentials/deepseek-account/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@deepseek-ai/dsh-deepseek-account",
+ "description": "Read account state and resolve official API credentials",
+ "version": "0.1.6-alpha.1",
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
+ "directory": "packages/credentials/deepseek-account"
+ },
+ "type": "module",
+ "main": "lib/index.js",
+ "types": "lib/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/types/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./types": {
+ "types": "./lib/types/types.d.ts",
+ "default": "./lib/types/types.js"
+ },
+ "./src/*": "./src/*",
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "lib/index.js",
+ "lib/types/**/*.js",
+ "lib/types/**/*.d.ts"
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-brand": "workspace:^"
+ },
+ "devDependencies": {
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/dsh-brand": "workspace:^"
+ }
+}
diff --git a/packages/credentials/deepseek-account/src/index.ts b/packages/credentials/deepseek-account/src/index.ts
new file mode 100644
index 0000000000..5e47bdd899
--- /dev/null
+++ b/packages/credentials/deepseek-account/src/index.ts
@@ -0,0 +1,74 @@
+/** Account Service Definition shared by platform, API, and model consumers. */
+import { Context, Service } from '@deepseek-ai/cordis'
+import type { AccountDetails, AccountView, SignInAttemptId } from './types.ts'
+export type { AccountDetails, AccountProfile, AccountWallet, AccountLinks, AccountView, SignInAttemptId, SignInAttemptView, SignInErrorCode } from './types.ts'
+
+declare module '@deepseek-ai/cordis' {
+ interface Context {
+ deepseekAccount: DeepSeekAccount
+ }
+}
+
+/** Host-only credentials for an embedded Platform document; never expose through account RPC. */
+export interface PlatformSession {
+ readonly origin: string
+ readonly token: string
+}
+
+/** Account operations; only Host consumers can obtain a request credential. */
+export abstract class DeepSeekAccount extends Service {
+ /** @param ctx - context owning this account implementation. */
+ constructor(ctx: Context) { super(ctx, 'deepseekAccount') }
+ /**
+ * Read stored-account presence and the latest login attempt.
+ * @returns a snapshot without credentials or PKCE secrets.
+ */
+ abstract getState(): Promise
+ /**
+ * Query Platform profile independently of wallet balances.
+ * @returns profile outcome, or null if signed out or the grant changed during the query.
+ */
+ abstract getProfile(): Promise
+ /**
+ * Query Platform recharge-wallet balances independently of profile data.
+ * @returns balance outcome, or null if signed out or the grant changed during the query.
+ */
+ abstract getBalance(): Promise
+ /**
+ * Join an active attempt or start browser authorization.
+ * @param locale - active UI language for a new attempt; joining retains its original language.
+ * @param callbackOrigin - browser-accessible loopback HTTP origin, including any SSH local port.
+ * @param client - initiating UI, used to return from a failed exchange.
+ * @returns the initial snapshot without waiting for browser approval.
+ */
+ abstract startSignIn(locale: string, callbackOrigin: string, client: 'web' | 'desktop'): Promise
+ /**
+ * Cancel only the named attempt; committing attempts settle before returning.
+ * @param id - attempt identity from this Host.
+ * @returns state after cancellation or an already-started commit.
+ */
+ abstract cancelSignIn(id: SignInAttemptId): Promise
+ /**
+ * Remove the local grant while retaining API keys and tasks; the provider revokes it in the background.
+ * @returns the signed-out state after local removal; remote failures never restore the grant.
+ */
+ abstract signOut(): Promise
+ /**
+ * Subscribe to snapshots including a complete initial state.
+ * @param signal - subscription lifetime; ending it never cancels login.
+ * @returns complete snapshots as account state changes.
+ */
+ abstract watch(signal: AbortSignal): AsyncIterable
+ /**
+ * Resolve a credential only for the official HTTPS API origin.
+ * @param url - actual request destination or API base URL.
+ * @returns stored token, or undefined for other origins or a signed-out account.
+ */
+ abstract resolveToken(url: string): Promise
+ /**
+ * Read credentials for the configured Platform origin, bound to their issuing environment.
+ * @returns a Host-only snapshot, or null while signed out.
+ */
+ abstract getPlatformSession(): Promise
+}
+export default DeepSeekAccount
diff --git a/packages/credentials/deepseek-account/src/types.ts b/packages/credentials/deepseek-account/src/types.ts
new file mode 100644
index 0000000000..aebeaa4a6d
--- /dev/null
+++ b/packages/credentials/deepseek-account/src/types.ts
@@ -0,0 +1,45 @@
+/** Client-safe account state; credentials never cross this projection. */
+import type { Branded } from '@deepseek-ai/dsh-brand'
+
+/** Identity of one local login attempt, unrelated to the platform request ID. */
+export type SignInAttemptId = Branded<'SignInAttemptId'>
+/** Safe failure codes rendered through the caller's locale dictionary. */
+export type SignInErrorCode = 'network' | 'protocol' | 'expired' | 'storage'
+/** Latest login attempt, including terminal outcomes until the next attempt. */
+export interface SignInAttemptView {
+ readonly id: SignInAttemptId
+ readonly phase: 'initializing' | 'waiting-browser' | 'exchanging' | 'committing' | 'succeeded' | 'cancelled' | 'expired' | 'failed'
+ readonly authorizeUrl?: string
+ readonly expiresAt?: number
+ readonly errorCode?: SignInErrorCode
+}
+/** Browser destinations derived from the Host's platform configuration; never carry tokens. */
+export interface AccountLinks {
+ readonly usageUrl: string
+ readonly topUpUrl: string
+}
+/** Stored-account presence is not a claim that the server has validated its token. */
+export interface AccountView {
+ readonly status: 'signed-out' | 'credential-stored'
+ readonly links: AccountLinks
+ readonly attempt: SignInAttemptView | null
+}
+
+/** Platform account identifier supplied by the current-user endpoint. */
+export type AccountUserId = Branded<'AccountUserId'>
+/** Display identity and support UID; contacts retain Platform masking. */
+export interface AccountProfile {
+ readonly id: AccountUserId | null
+ readonly name: string | null
+ readonly contact: string | null
+}
+/** Platform recharge-wallet balance; decimal strings retain server precision. */
+export interface AccountWallet {
+ readonly currency: 'CNY' | 'USD'
+ readonly balance: string
+}
+/** Independent query outcomes allow profile and balance failures to render separately. */
+export interface AccountDetails {
+ readonly profile: { readonly status: 'ready'; readonly value: AccountProfile } | { readonly status: 'failed' }
+ readonly balance: { readonly status: 'ready'; readonly value: readonly AccountWallet[] } | { readonly status: 'failed' }
+}
diff --git a/packages/credentials/deepseek-account/tsconfig.json b/packages/credentials/deepseek-account/tsconfig.json
new file mode 100644
index 0000000000..342f636170
--- /dev/null
+++ b/packages/credentials/deepseek-account/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "lib/types"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../../../vendor/cosmokit"
+ },
+ {
+ "path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../../util/brand"
+ }
+ ]
+}
diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
index 927527cd5c..2f01eea6cd 100644
--- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
+++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
@@ -1913,7 +1913,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.action\', () => ctx.slots.register(\n { name: \'settings.action\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings/src/client/contract/slots.ts:36',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:39',
},
{
key: 'settings.close',
@@ -1944,7 +1944,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.close\', () => ctx.slots.register(\n { name: \'settings.close\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings/src/client/contract/slots.ts:42',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:45',
},
{
key: 'settings.general.item',
@@ -2001,7 +2001,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.general.item\', () => ctx.slots.register(\n { name: \'settings.general.item\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings/src/client/contract/slots.ts:89',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:92',
},
{
key: 'settings.header',
@@ -2032,7 +2032,37 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.header\', () => ctx.slots.register(\n { name: \'settings.header\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings/src/client/contract/slots.ts:30',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:33',
+ },
+ {
+ key: 'settings.launcher',
+ kind: 'single',
+ scope: 'root',
+ summary: 'Optional sidebar account launcher; opens the shell-owned settings panel.',
+ doc: 'Optional sidebar account launcher; opens the shell-owned settings panel.',
+ registerOptions: [],
+ ownerProps: [
+ '/** Sidebar launcher geometry and settings navigation. */\nexport interface SettingsLauncherOwnerProps {\n /** Whether the sidebar shows labels. */\n wide: boolean\n /** Open the settings panel. */\n openSettings: () => void\n /** @param id - registered onboarding editor to open explicitly. */\n openOnboarding: (id: string) => void\n}',
+ ],
+ ownerPropsReferences: [],
+ standardProps: [
+ 'useResource: UseResource',
+ 'useWorkspaces: SnapshotSelectorHook',
+ 'usePanelInfo: UsePanelInfo',
+ 'useSessions: UseSessions',
+ 'useSessionPendingInteraction: UseSessionPendingInteraction',
+ 'useWorkspaces: SnapshotSelectorHook',
+ ],
+ keyDomain: '',
+ hookContext: '',
+ slotInject: '',
+ declaredBy: 'an entry in \'sidebar.settings\' (client-ui-settings-general), so it exists while that entry is mounted',
+ occupants: [
+ 'client-ui-settings-account AccountMenu',
+ ],
+ replaceRisk: 'shadows-shipped-ui',
+ example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.launcher\', () => ctx.slots.register(\n { name: \'settings.launcher\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:17',
},
{
key: 'settings.models.footer',
@@ -2080,7 +2110,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.models.footer\', () => ctx.slots.register(\n { name: \'settings.models.footer\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings-models/src/client/slot-contract.ts:38',
+ source: 'packages/client/ui-settings-models/src/client/slot-contract.ts:40',
},
{
key: 'settings.models.provider-card',
@@ -2120,6 +2150,34 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.models.provider-card\', () => ctx.slots.register(\n { name: \'settings.models.provider-card\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-settings-models/src/client/slot-contract.ts:33',
},
+ {
+ key: 'settings.models.sign-in',
+ kind: 'single',
+ scope: 'root',
+ summary: 'Optional account login choice before the credential editor.',
+ doc: 'Optional account login choice before the credential editor.',
+ registerOptions: [],
+ ownerProps: [],
+ ownerPropsReferences: [],
+ standardProps: [
+ 'useResource: UseResource',
+ 'useWorkspaces: SnapshotSelectorHook',
+ 'usePanelInfo: UsePanelInfo',
+ 'useSessions: UseSessions',
+ 'useSessionPendingInteraction: UseSessionPendingInteraction',
+ 'useWorkspaces: SnapshotSelectorHook',
+ ],
+ keyDomain: '',
+ hookContext: '',
+ slotInject: '',
+ declaredBy: 'an entry in \'settings.onboarding\' (client-ui-settings-models), so it exists while that entry is mounted',
+ occupants: [
+ 'client-ui-settings-account AccountOnboarding',
+ ],
+ replaceRisk: 'shadows-shipped-ui',
+ example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.models.sign-in\', () => ctx.slots.register(\n { name: \'settings.models.sign-in\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
+ source: 'packages/client/ui-settings-models/src/client/slot-contract.ts:35',
+ },
{
key: 'settings.onboarding',
kind: 'list',
@@ -2147,7 +2205,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
},
],
ownerProps: [
- '/** Owner share of the currently active settings-backed onboarding step. */\nexport interface SettingsOnboardingOwnerProps {\n /** Stable id of the step currently selected by the coordinator. */\n stepId: string\n /** Complete or skip this step and transfer ownership to the next entry. */\n complete: () => void\n /** Open the settings panel directly on one registered section. */\n openSection: (id: string) => void\n}',
+ '/** Owner share of the currently active settings-backed onboarding step. */\nexport interface SettingsOnboardingOwnerProps {\n /** Stable id of the step currently selected by the coordinator. */\n stepId: string\n /** User explicitly reopened this step outside first-run onboarding. */\n explicit?: boolean\n /** Complete or skip this step and transfer ownership to the next entry. */\n complete: () => void\n /** Open the settings panel directly on one registered section. */\n openSection: (id: string) => void\n}',
],
ownerPropsReferences: [],
standardProps: [
@@ -2169,7 +2227,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.onboarding\', () => ctx.slots.register(\n { name: \'settings.onboarding\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings/src/client/contract/slots.ts:74',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:77',
},
{
key: 'settings.plugins.tab',
@@ -2219,7 +2277,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.plugins.tab\', () => ctx.slots.register(\n { name: \'settings.plugins.tab\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings/src/client/contract/slots.ts:63',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:66',
},
{
key: 'settings.section',
@@ -2266,6 +2324,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
declaredBy: 'an entry in \'sidebar.settings\' (client-ui-settings-general), so it exists while that entry is mounted',
occupants: [
'client-ui-agent-preset AgentPresetSection id \'agent-presets\'',
+ 'client-ui-settings-account AccountSection id \'account\'',
'client-ui-settings-general GeneralSection id \'general\'',
'client-ui-settings-models ModelsSection id \'models\'',
'client-ui-settings-plugins PluginsSettingsSection id \'plugins\'',
@@ -2273,7 +2332,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.section\', () => ctx.slots.register(\n { name: \'settings.section\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings/src/client/contract/slots.ts:54',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:57',
},
{
key: 'settings.trigger',
@@ -2304,7 +2363,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'settings.trigger\', () => ctx.slots.register(\n { name: \'settings.trigger\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-settings/src/client/contract/slots.ts:24',
+ source: 'packages/client/ui-settings/src/client/contract/slots.ts:27',
},
{
key: 'shell.overlay',
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index 643ff6bed0..5fe26323d7 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/packages/extensions/tool-cordis/src/api-catalog.ts
@@ -858,6 +858,67 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
],
},
+ {
+ key: 'deepseekAccount',
+ summary: 'Account operations; only Host consumers can obtain a request credential.',
+ description: 'Account operations; only Host consumers can obtain a request credential.',
+ methods: [
+ {
+ signature: 'abstract getState(): Promise',
+ description: 'Read stored-account presence and the latest login attempt.',
+ parameters: [],
+ returns: 'a snapshot without credentials or PKCE secrets.',
+ },
+ {
+ signature: 'abstract getProfile(): Promise',
+ description: 'Query Platform profile independently of wallet balances.',
+ parameters: [],
+ returns: 'profile outcome, or null if signed out or the grant changed during the query.',
+ },
+ {
+ signature: 'abstract getBalance(): Promise',
+ description: 'Query Platform recharge-wallet balances independently of profile data.',
+ parameters: [],
+ returns: 'balance outcome, or null if signed out or the grant changed during the query.',
+ },
+ {
+ signature: 'abstract startSignIn(locale: string, callbackOrigin: string, client: \'web\' | \'desktop\'): Promise',
+ description: 'Join an active attempt or start browser authorization.',
+ parameters: [{ name: 'locale', description: 'active UI language for a new attempt; joining retains its original language.' }, { name: 'callbackOrigin', description: 'browser-accessible loopback HTTP origin, including any SSH local port.' }, { name: 'client', description: 'initiating UI, used to return from a failed exchange.' }],
+ returns: 'the initial snapshot without waiting for browser approval.',
+ },
+ {
+ signature: 'abstract cancelSignIn(id: SignInAttemptId): Promise',
+ description: 'Cancel only the named attempt; committing attempts settle before returning.',
+ parameters: [{ name: 'id', description: 'attempt identity from this Host.' }],
+ returns: 'state after cancellation or an already-started commit.',
+ },
+ {
+ signature: 'abstract signOut(): Promise',
+ description: 'Remove the local grant while retaining API keys and tasks; the provider revokes it in the background.',
+ parameters: [],
+ returns: 'the signed-out state after local removal; remote failures never restore the grant.',
+ },
+ {
+ signature: 'abstract watch(signal: AbortSignal): AsyncIterable',
+ description: 'Subscribe to snapshots including a complete initial state.',
+ parameters: [{ name: 'signal', description: 'subscription lifetime; ending it never cancels login.' }],
+ returns: 'complete snapshots as account state changes.',
+ },
+ {
+ signature: 'abstract resolveToken(url: string): Promise',
+ description: 'Resolve a credential only for the official HTTPS API origin.',
+ parameters: [{ name: 'url', description: 'actual request destination or API base URL.' }],
+ returns: 'stored token, or undefined for other origins or a signed-out account.',
+ },
+ {
+ signature: 'abstract getPlatformSession(): Promise',
+ description: 'Read credentials for the configured Platform origin, bound to their issuing environment.',
+ parameters: [],
+ returns: 'a Host-only snapshot, or null while signed out.',
+ },
+ ],
+ },
{
key: 'deepseekLlmApiExtensions',
summary: 'Registry of independently owned top-level fields for official DeepSeek requests.',
@@ -3988,6 +4049,30 @@ export const EVENT_API: readonly EventApiEntry[] = [
/** Shapes of every exported type the Service and Event signatures reference (transitively), sorted by name. */
export const TYPE_API: readonly TypeApiEntry[] = [
+ {
+ name: 'AccountDetails',
+ declaration: 'export interface AccountDetails {\n readonly profile: {\n readonly status: \'ready\';\n readonly value: AccountProfile;\n } | {\n readonly status: \'failed\';\n };\n readonly balance: {\n readonly status: \'ready\';\n readonly value: readonly AccountWallet[];\n } | {\n readonly status: \'failed\';\n };\n}',
+ },
+ {
+ name: 'AccountLinks',
+ declaration: 'export interface AccountLinks {\n readonly usageUrl: string;\n readonly topUpUrl: string;\n}',
+ },
+ {
+ name: 'AccountProfile',
+ declaration: 'export interface AccountProfile {\n readonly id: AccountUserId | null;\n readonly name: string | null;\n readonly contact: string | null;\n}',
+ },
+ {
+ name: 'AccountUserId',
+ declaration: 'export type AccountUserId = Branded<\'AccountUserId\'>;',
+ },
+ {
+ name: 'AccountView',
+ declaration: 'export interface AccountView {\n readonly status: \'signed-out\' | \'credential-stored\';\n readonly links: AccountLinks;\n readonly attempt: SignInAttemptView | null;\n}',
+ },
+ {
+ name: 'AccountWallet',
+ declaration: 'export interface AccountWallet {\n readonly currency: \'CNY\' | \'USD\';\n readonly balance: string;\n}',
+ },
{
name: 'AdapterRegistrationHandle',
declaration: 'export interface AdapterRegistrationHandle {\n (): void;\n replace(providers: string[]): void;\n}',
@@ -4198,7 +4283,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'AuthorizationSession',
- declaration: 'export interface AuthorizationSession {\n readonly method: string;\n readonly signal: AbortSignal;\n notify(notice: AuthorizationNotice): void;\n prompt(prompt: AuthorizationPrompt): Promise;\n}',
+ declaration: 'export interface AuthorizationSession {\n readonly method: string;\n readonly signal: AbortSignal;\n commit(record: CredentialRecord): Promise;\n notify(notice: AuthorizationNotice): void;\n prompt(prompt: AuthorizationPrompt): Promise;\n}',
},
{
name: 'AuthorizationSettlement',
@@ -5272,6 +5357,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'PermissionCatalog',
declaration: 'export interface PermissionCatalog {\n options: PresetOption[];\n}',
},
+ {
+ name: 'PlatformSession',
+ declaration: 'export interface PlatformSession {\n readonly origin: string;\n readonly token: string;\n}',
+ },
{
name: 'PluginChange',
declaration: 'export interface PluginChange {\n readonly reason: \'plugin\' | \'bundle\' | \'install\' | \'remove\';\n}',
@@ -6240,6 +6329,18 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'ShellSandboxInfo',
declaration: 'export interface ShellSandboxInfo {\n mode: SandboxMode;\n denied: boolean;\n enforcement?: SandboxEnforcement;\n runnerFailed?: boolean;\n}',
},
+ {
+ name: 'SignInAttemptId',
+ declaration: 'export type SignInAttemptId = Branded<\'SignInAttemptId\'>;',
+ },
+ {
+ name: 'SignInAttemptView',
+ declaration: 'export interface SignInAttemptView {\n readonly id: SignInAttemptId;\n readonly phase: \'initializing\' | \'waiting-browser\' | \'exchanging\' | \'committing\' | \'succeeded\' | \'cancelled\' | \'expired\' | \'failed\';\n readonly authorizeUrl?: string;\n readonly expiresAt?: number;\n readonly errorCode?: SignInErrorCode;\n}',
+ },
+ {
+ name: 'SignInErrorCode',
+ declaration: 'export type SignInErrorCode = \'network\' | \'protocol\' | \'expired\' | \'storage\';',
+ },
{
name: 'SkillCandidate',
declaration: 'export interface SkillCandidate extends SkillSummary {\n readonly rank: number;\n readonly locator: unknown;\n readonly metadata?: Readonly>;\n}',
diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml
index cc43d12297..dd136efc05 100644
--- a/packages/llm/llm-deepseek/README.i18n.yaml
+++ b/packages/llm/llm-deepseek/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
-README.md: 33a76a316fd6f89e5d011ab3aa7a933331bd1e7b
-README.zh.md: dbc7282140268985f215efcac0b4e602cd5b0e44
+README.md: 19482112747b80033d57790e4dc5247fc0a305f7
+README.zh.md: 7254d7eae8a48e4f373597b0dc31c6f7dcd14c0b
diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md
index 33a76a316f..1948211274 100644
--- a/packages/llm/llm-deepseek/README.md
+++ b/packages/llm/llm-deepseek/README.md
@@ -227,3 +227,7 @@ This Dev Note is non-authoritative working context: undecided directions and not
**Runtime invariant:** No companion is published. This package exposes no independent event sequence or mutable data relation beyond contracts enforced at its owning seam.
+
+When the account provider is mounted, its stored token takes priority only for https://api.deepseek.com. Other origins and signed-out accounts retain the configured API-key reference. Chat and Files requests reject redirects. Signing out removes only the account grant and preserves API keys.
+
+DSH account tokens use x-dsh-auth-token without a Bearer prefix for both inference protocols and all Files operations. API keys retain each protocol’s existing authentication header.
diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md
index dbc7282140..7254d7eae8 100644
--- a/packages/llm/llm-deepseek/README.zh.md
+++ b/packages/llm/llm-deepseek/README.zh.md
@@ -227,3 +227,7 @@ loop 保留的响应块会追加到下一个请求,并保留其更早的可复
**运行时不变式:** 不发布伴生入口。本包没有独立事件序列或可变数据关系,相关约定在所属 seam 强制执行。
+
+挂载账号提供者时,其已保存 token 仅对 https://api.deepseek.com 优先生效。其他来源和已退出的账号继续使用配置的 API Key 引用。聊天和文件请求拒绝重定向。退出登录只删除账号授权记录,保留 API Key。
+
+DSH 账号 token 在两种推理协议和所有 Files 操作中使用 x-dsh-auth-token,不加 Bearer 前缀。API Key 保持各协议现有的认证请求头。
diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json
index e3f69ca464..9f9d194442 100644
--- a/packages/llm/llm-deepseek/package.json
+++ b/packages/llm/llm-deepseek/package.json
@@ -38,7 +38,8 @@
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
- "@deepseek-ai/dsh-timeout": "workspace:^"
+ "@deepseek-ai/dsh-timeout": "workspace:^",
+ "@deepseek-ai/dsh-deepseek-account": "workspace:^"
},
"dependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
@@ -72,6 +73,12 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-credentials-local": "workspace:^",
- "@deepseek-ai/dsh-settings-file": "workspace:^"
+ "@deepseek-ai/dsh-settings-file": "workspace:^",
+ "@deepseek-ai/dsh-deepseek-account": "workspace:^"
+ },
+ "peerDependenciesMeta": {
+ "@deepseek-ai/dsh-deepseek-account": {
+ "optional": true
+ }
}
}
diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts
index 8e46799185..722f69bc3f 100644
--- a/packages/llm/llm-deepseek/src/adapter.ts
+++ b/packages/llm/llm-deepseek/src/adapter.ts
@@ -23,6 +23,7 @@ export class DeepSeekAdapter extends LlmAdapter {
return new DeepSeekMessagesAdapter({
connection: () => connection,
apiKey: this.dependencies.resolveApiKey,
+ ...this.dependencies.resolveAccountToken === undefined ? {} : { accountToken: this.dependencies.resolveAccountToken },
userId: this.dependencies.resolveUserId,
attachments: () => this.dependencies.resolveAttachments?.(),
imageAccess: (ref) => {
diff --git a/packages/llm/llm-deepseek/src/common/file-store.ts b/packages/llm/llm-deepseek/src/common/file-store.ts
index 5fcab0cbe1..a6dff81008 100644
--- a/packages/llm/llm-deepseek/src/common/file-store.ts
+++ b/packages/llm/llm-deepseek/src/common/file-store.ts
@@ -24,6 +24,8 @@ export interface DeepSeekFilePolicy {
export interface DeepSeekFileConnection {
baseURL: string
apiKey: string
+ /** Use the DSH account header; omitted for ordinary API keys. */
+ accountCredential?: boolean
/** Files wire protocol selected by the resolved connection. */
protocol: DeepSeekProtocol
}
@@ -139,6 +141,7 @@ export class DeepSeekFileStore {
return new DeepSeekFilesClient({
baseURL: connection.baseURL,
apiKey: connection.apiKey,
+ ...connection.accountCredential === undefined ? {} : { accountCredential: connection.accountCredential },
protocol: connection.protocol,
...this.fetchImpl === undefined ? {} : { fetch: this.fetchImpl },
})
diff --git a/packages/llm/llm-deepseek/src/common/files-api.ts b/packages/llm/llm-deepseek/src/common/files-api.ts
index 5420871bf0..0cbc8eacdf 100644
--- a/packages/llm/llm-deepseek/src/common/files-api.ts
+++ b/packages/llm/llm-deepseek/src/common/files-api.ts
@@ -74,6 +74,8 @@ export function isFilesQuotaError(error: unknown): error is DeepSeekFilesError {
interface FilesApiOptions {
baseURL: string
apiKey: string
+ /** Use the DSH account header; omitted for ordinary API keys. */
+ accountCredential?: boolean
protocol: DeepSeekProtocol
fetch?: typeof fetch
}
@@ -144,6 +146,7 @@ function providerErrorDetail(value: unknown): { message?: string; detail: string
/** Direct Files client retaining the configured URL root and refusing redirects before credentials can leave its origin. */
export class DeepSeekFilesClient {
private readonly baseURL: string
+ private readonly accountCredential: boolean
private readonly apiKey: string
private readonly fetchImpl: typeof fetch
private readonly protocol: DeepSeekProtocol
@@ -154,6 +157,7 @@ export class DeepSeekFilesClient {
*/
constructor(options: FilesApiOptions) {
this.apiKey = options.apiKey
+ this.accountCredential = options.accountCredential === true
this.fetchImpl = options.fetch ?? globalThis.fetch
this.protocol = options.protocol
this.baseURL = this.protocol === 'messages'
@@ -171,10 +175,11 @@ export class DeepSeekFilesClient {
try {
const headers = new Headers(attributionHeaders())
if (this.protocol === 'messages') {
- headers.set('x-api-key', this.apiKey)
+ if (!this.accountCredential) headers.set('x-api-key', this.apiKey)
headers.set('anthropic-version', '2023-06-01')
headers.set('anthropic-beta', MESSAGES_FILES_BETA)
- } else headers.set('authorization', `Bearer ${this.apiKey}`)
+ } else if (!this.accountCredential) headers.set('authorization', `Bearer ${this.apiKey}`)
+ if (this.accountCredential) headers.set('x-dsh-auth-token', this.apiKey)
response = await this.fetchImpl(`${this.baseURL}${path}`, {
...init,
redirect: 'error',
diff --git a/packages/llm/llm-deepseek/src/common/types.ts b/packages/llm/llm-deepseek/src/common/types.ts
index d1c4932317..21fe8f37a7 100644
--- a/packages/llm/llm-deepseek/src/common/types.ts
+++ b/packages/llm/llm-deepseek/src/common/types.ts
@@ -100,6 +100,8 @@ export interface DeepSeekAdapterOptions {
* `MISSING_CREDENTIAL` when no key is available anywhere.
*/
resolveApiKey: (connection: DeepSeekConnectionOptions) => Promise
+ /** Resolve a DSH account token only for an eligible official endpoint. */
+ resolveAccountToken?: (connection: DeepSeekConnectionOptions) => Promise
/** Resolve the harness-home anonymous id shared with telemetry and feedback. */
resolveUserId: () => AnonymousUserId
/** Resolve the current durable attachment service; absence rejects image input. */
diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts
index d38246b971..6cf7922f6c 100644
--- a/packages/llm/llm-deepseek/src/index.ts
+++ b/packages/llm/llm-deepseek/src/index.ts
@@ -1,4 +1,5 @@
/** Register DeepSeek with protocol selection and request-local settings and credentials. */
+import type {} from '@deepseek-ai/dsh-deepseek-account'
import type { Context } from '@deepseek-ai/cordis'
import { assertUsableApiKey, LlmError, resolveImageAttachmentAccess } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-fs'
@@ -112,6 +113,7 @@ export function apply(ctx: Context, config: Config): void {
ctx.logger.warn(`llm-deepseek: unusable Messages replay state on assistant history for route "${provider}/${model}"; sending provider-neutral content (${reason})`)
},
resolveApiKey,
+ resolveAccountToken: connection => ctx.get('deepseekAccount')?.resolveToken(connection.baseURL) ?? Promise.resolve(undefined),
resolveUserId,
resolveAttachments: () => ctx.get('attachments'),
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(
diff --git a/packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts b/packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts
index 3d2e50ea45..a8db882bf6 100644
--- a/packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts
+++ b/packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts
@@ -194,7 +194,8 @@ export class ChatCompletionsAdapter extends LlmAdapter {
)
}
}
- const apiKey = await this.config.resolveApiKey(connection)
+ const accountToken = await this.config.resolveAccountToken?.(connection)
+ const apiKey = accountToken ?? await this.config.resolveApiKey(connection)
const userId = this.config.resolveUserId()
const consumer = new AbortController()
const upstream = options.signal === undefined
@@ -206,6 +207,7 @@ export class ChatCompletionsAdapter extends LlmAdapter {
watchdog.signal,
connection,
apiKey,
+ accountToken !== undefined,
userId,
attachments,
() => { watchdog.pulse() },
@@ -250,12 +252,13 @@ export class ChatCompletionsAdapter extends LlmAdapter {
signal: AbortSignal,
connection: DeepSeekConnectionOptions,
apiKey: string,
+ accountCredential: boolean,
userId: AnonymousUserId,
attachments: AttachmentStore | undefined,
onActivity: () => void,
): AsyncIterable {
const headers = {
- 'authorization': `Bearer ${apiKey}`,
+ ...accountCredential ? { 'x-dsh-auth-token': apiKey } : { authorization: `Bearer ${apiKey}` },
'content-type': 'application/json',
'accept': 'text/event-stream',
...attributionHeaders(),
@@ -268,7 +271,7 @@ export class ChatCompletionsAdapter extends LlmAdapter {
: {},
}
- const fileConnection = { baseURL: connection.baseURL, apiKey, protocol: connection.protocol }
+ const fileConnection = { baseURL: connection.baseURL, apiKey, accountCredential, protocol: connection.protocol }
const model = connection.models.find(entry => entry.id === options.model)
const resolveImageAccess = attachments === undefined
? undefined
@@ -328,6 +331,7 @@ export class ChatCompletionsAdapter extends LlmAdapter {
let response: Response
try {
response = await fetch(`${connection.baseURL}/chat/completions`, {
+ redirect: 'error',
method: 'POST',
headers,
body: extensions.payload,
diff --git a/packages/llm/llm-deepseek/src/protocols/messages/adapter.ts b/packages/llm/llm-deepseek/src/protocols/messages/adapter.ts
index e5304b8380..f669be7ed3 100644
--- a/packages/llm/llm-deepseek/src/protocols/messages/adapter.ts
+++ b/packages/llm/llm-deepseek/src/protocols/messages/adapter.ts
@@ -23,6 +23,8 @@ export interface AdapterDependencies {
connection(): Connection
/** Resolve the key named by that same generation. */
apiKey(connection: Connection): Promise
+ /** DSH account token for an eligible official endpoint. */
+ accountToken?(connection: Connection): Promise
/** Stable anonymous Harness identity. */
userId(): string
/** Current attachment service; absence is valid for text requests. */
@@ -92,9 +94,12 @@ export class DeepSeekMessagesAdapter extends LlmAdapter {
const { messages, versions } = await prepareImages(
options.messages, connection, options.model, this.dependencies.attachments(), this.dependencies.imageAccess, signal,
)
- const key = await this.dependencies.apiKey(connection)
- const files = new RequestFiles(this.dependencies.files(), { baseURL: connection.baseURL, apiKey: key, protocol: 'messages' },
- connection.filePolicy, connection.filesApiTimeoutMs, signal, activity)
+ const accountToken = await this.dependencies.accountToken?.(connection)
+ const key = accountToken ?? await this.dependencies.apiKey(connection)
+ const files = new RequestFiles(this.dependencies.files(), {
+ baseURL: connection.baseURL, apiKey: key, accountCredential: accountToken !== undefined, protocol: 'messages',
+ },
+ connection.filePolicy, connection.filesApiTimeoutMs, signal, activity)
let inline = false
while (true) {
signal.throwIfAborted()
@@ -124,7 +129,8 @@ export class DeepSeekMessagesAdapter extends LlmAdapter {
headers: {
...attributionHeaders(),
'content-type': 'application/json', 'accept': 'text/event-stream',
- 'x-api-key': key, 'anthropic-version': '2023-06-01',
+ ...accountToken === undefined ? { 'x-api-key': key } : { 'x-dsh-auth-token': accountToken },
+ 'anthropic-version': '2023-06-01',
...fileIds === undefined || fileIds.size === 0 ? {} : { 'anthropic-beta': MESSAGES_FILES_BETA },
'x-deepseek-harness-user-id': this.dependencies.userId(),
...options.sessionId === undefined ? {} : { 'x-deepseek-harness-session-id': String(options.sessionId) },
diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts
index 767c0d5c73..97987d66ea 100644
--- a/packages/llm/llm-deepseek/tests/adapter.spec.ts
+++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts
@@ -1,3 +1,4 @@
+import type { DeepSeekAccount } from '@deepseek-ai/dsh-deepseek-account'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
@@ -2333,3 +2334,32 @@ describe('plugin registration and config', () => {
expect(ctx.llm.listProviders()).toEqual([])
})
})
+
+
+it.each([
+ ['chat-completions', 'https://api.deepseek.com', 'account-token'],
+ ['messages', 'https://api.deepseek.com', 'account-token'],
+ ['chat-completions', 'https://custom.example.test', 'ambient-key'],
+ ['messages', 'https://custom.example.test', 'ambient-key'],
+] as const)('selects account or API-key credentials from the actual endpoint %s', async (protocol, baseURL, expected) => {
+ vi.stubEnv('DEEPSEEK_API_KEY', 'ambient-key')
+ const ctx = new Context()
+ // This consumer uses only resolveToken; the real provider owns origin validation in its own suite.
+ ctx.provide('deepseekAccount', {
+ resolveToken: (url: string) => Promise.resolve(url === 'https://api.deepseek.com' ? 'account-token' : undefined),
+ } as DeepSeekAccount)
+ await ctx.plugin(LlmRuntime)
+ await ctx.plugin(LlmDeepSeek, { baseURL, protocol })
+ const request = vi.spyOn(globalThis, 'fetch').mockImplementation((_input, init) => {
+ const headers = new Headers(init?.headers)
+ expect(headers.get('authorization')).toBe(expected === 'account-token' || protocol === 'messages' ? null : `Bearer ${expected}`)
+ expect(headers.get('x-api-key')).toBe(expected !== 'account-token' && protocol === 'messages' ? expected : null)
+ expect(headers.get('x-dsh-auth-token')).toBe(expected === 'account-token' ? expected : null)
+ expect(init?.redirect).toBe('error')
+ return Promise.resolve(new Response('fixture rejection', { status: 400 }))
+ })
+ try {
+ await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
+ expect(request).toHaveBeenCalledOnce()
+ } finally { request.mockRestore() }
+})
diff --git a/packages/llm/llm-deepseek/tests/files-api.spec.ts b/packages/llm/llm-deepseek/tests/files-api.spec.ts
index b8857bd0dc..df66abcfae 100644
--- a/packages/llm/llm-deepseek/tests/files-api.spec.ts
+++ b/packages/llm/llm-deepseek/tests/files-api.spec.ts
@@ -149,6 +149,7 @@ describe('DeepSeekFilesClient', () => {
const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
expect(requestUrl(url)).toBe('https://api.deepseek.com/files')
expect(init?.method).toBe('POST')
+ expect(init?.redirect).toBe('error')
const headers = new Headers(init?.headers)
expect(headers.get('authorization')).toBe('Bearer key')
expect(headers.get('user-agent')).toBe(userAgent())
@@ -382,3 +383,17 @@ describe('DeepSeekFilesClient', () => {
await expect(client.delete(DeepSeekFileId('file-api-one'))).rejects.toMatchObject({ code: 'INVALID_RESPONSE' })
})
})
+
+it.each(['chat-completions', 'messages'] as const)('authenticates %s Files with the raw DSH token', async (protocol) => {
+ const client = new DeepSeekFilesClient({
+ baseURL: 'https://api.deepseek.com', apiKey: 'account-token', accountCredential: true, protocol,
+ fetch: (_url, init) => {
+ const headers = new Headers(init?.headers)
+ expect(headers.get('x-dsh-auth-token')).toBe('account-token')
+ expect(headers.has('authorization')).toBe(false)
+ expect(headers.has('x-api-key')).toBe(false)
+ return Promise.resolve(new Response(JSON.stringify({ object: 'list', data: [], has_more: false }), { status: 200 }))
+ },
+ })
+ await client.list()
+})
diff --git a/packages/llm/llm-deepseek/tsconfig.json b/packages/llm/llm-deepseek/tsconfig.json
index 876332451e..b1bae1c4bc 100644
--- a/packages/llm/llm-deepseek/tsconfig.json
+++ b/packages/llm/llm-deepseek/tsconfig.json
@@ -55,6 +55,9 @@
},
{
"path": "../../util/http-proxy"
+ },
+ {
+ "path": "../../credentials/deepseek-account"
}
]
}
diff --git a/packages/test-support/client-runtime/README.i18n.yaml b/packages/test-support/client-runtime/README.i18n.yaml
index 20d4d80537..c2c00bcdde 100644
--- a/packages/test-support/client-runtime/README.i18n.yaml
+++ b/packages/test-support/client-runtime/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/test-support/client-runtime/README.md
-README.md: 7eca8e599cf36dbfb635ec2766e7daf15034eab0
-README.zh.md: ee63f4d930410f7f5069be9027327559249b56ca
+README.md: 53e5633eda3f0eba58af858fafb808a1e076f4f3
+README.zh.md: 55adda165d679939dc400556f3940b7a94e931b0
diff --git a/packages/test-support/client-runtime/README.md b/packages/test-support/client-runtime/README.md
index 7eca8e599c..53e5633eda 100644
--- a/packages/test-support/client-runtime/README.md
+++ b/packages/test-support/client-runtime/README.md
@@ -11,6 +11,8 @@ English | [中文](README.zh.md)
`SlotTestRuntime.create()` lets Vitest suites drive production slots, stores, typed Session and Workspace fixtures, and local DOM assertions in jsdom. For plugin activation, reload, reconnect, and cleanup tests, `createClientTest` starts the web profile's bundle roster with endpoint-named Remote mocks, without a business Host. Missing services and unstubbed calls fail explicitly. The whole-client fixture owns startup and disposal; the local runtime provides idempotent disposal. Use this package through `devDependencies` for client tests; it is not a product plugin.
+Linked bundles resolve dependencies from their real package directories; dependencies next to any bundle take precedence over packages inherited from ancestor directories.
+
## Table of Contents
- [Use this package](#use-this-package)
diff --git a/packages/test-support/client-runtime/README.zh.md b/packages/test-support/client-runtime/README.zh.md
index ee63f4d930..55adda165d 100644
--- a/packages/test-support/client-runtime/README.zh.md
+++ b/packages/test-support/client-runtime/README.zh.md
@@ -11,6 +11,8 @@ kind: "package-library"
`SlotTestRuntime.create()` 让 Vitest 套件在 jsdom 中驱动生产 slot、store、带类型的 Session 与 Workspace fixture,并对局部 DOM 断言。面向插件激活、重载、重连与清理的测试,`createClientTest` 使用具名端点 Remote mock 启动 web profile 的 bundle roster,无需业务 Host。缺失服务与未打桩调用会明确失败。整机 fixture 拥有启动和销毁,局部 runtime 提供幂等销毁。通过 `devDependencies` 将本包用于客户端测试;它不是产品插件。
+链接到工作区的 bundle 从实际包目录解析依赖;各 bundle 自身目录中的依赖优先于祖先目录中继承的包。
+
## 目录
- [使用本包](#use-this-package)
diff --git a/packages/test-support/client-runtime/src/assembly/bundle-roster.ts b/packages/test-support/client-runtime/src/assembly/bundle-roster.ts
index 14284556f5..dae5e214cd 100644
--- a/packages/test-support/client-runtime/src/assembly/bundle-roster.ts
+++ b/packages/test-support/client-runtime/src/assembly/bundle-roster.ts
@@ -13,7 +13,7 @@
* repository.
* @module @deepseek-ai/dsh-client-test-runtime/src/assembly/bundle-roster
*/
-import { existsSync, readFileSync } from 'node:fs'
+import { existsSync, readFileSync, realpathSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -117,12 +117,15 @@ function readManifest(path: string): PackageManifest {
return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
}
-/** Locate `/package.json` on the resolution paths of any anchor, without requiring a `./package.json` export. */
+/** Resolve package manifests to real paths so linked bundles use their own dependency directories. */
function locateManifest(anchors: readonly string[], name: string): string | undefined {
- for (const anchor of anchors) {
- for (const searchPath of createRequire(anchor).resolve.paths(name) ?? []) {
- const candidate = join(searchPath, name, 'package.json')
- if (existsSync(candidate)) return candidate
+ const paths = anchors.map(anchor => createRequire(anchor).resolve.paths(name) ?? [])
+ for (let depth = 0; depth < Math.max(...paths.map(search => search.length)); depth++) {
+ for (const search of paths) {
+ const directory = search[depth]
+ if (directory === undefined) continue
+ const candidate = join(directory, name, 'package.json')
+ if (existsSync(candidate)) return realpathSync(candidate)
}
}
return undefined
diff --git a/packages/test-support/client-runtime/src/assembly/remote-default-responses.ts b/packages/test-support/client-runtime/src/assembly/remote-default-responses.ts
index 707028af66..c75fe7cb29 100644
--- a/packages/test-support/client-runtime/src/assembly/remote-default-responses.ts
+++ b/packages/test-support/client-runtime/src/assembly/remote-default-responses.ts
@@ -32,6 +32,9 @@ export const remoteDefaultResponses: RemoteTable = {
'credentials/describe': ok({}),
// ui-permission-presets `PermissionCatalogDirectory` on its first read for a connection generation.
'permissionPresets/catalog': ok({ options: [] }),
+ // ui-settings-account refreshes details after a stored-grant snapshot.
+ 'account/getProfile': ok(null),
+ 'account/getBalance': ok(null),
},
// Stream endpoints the roster opens later than boot; declared so a spec that forgets the script gets a stream miss.
streams: [
@@ -41,6 +44,8 @@ export const remoteDefaultResponses: RemoteTable = {
stream: {
// api-session-controller client `apply`: the control stream's opening baseline, then open.
'session/control': openStream([{ type: 'baseline', value: { jobs: {}, projections: {} } }]),
+ // ui-settings-account shares the account snapshot across settings and the sidebar menu.
+ 'account/watch': openStream([{ status: 'signed-out', attempt: null, links: { usageUrl: 'https://platform.deepseek.com/usage', topUpUrl: 'https://platform.deepseek.com/top_up' } }]),
// api-workspace-controller client `apply`: the follow stream's opening baseline, then open.
'workspace/follow': openStream([{ type: 'baseline', value: { items: [], archivedSessionIds: [] } }]),
},
diff --git a/packages/test-support/client-runtime/tests/assembly-bundle-roster.client.spec.ts b/packages/test-support/client-runtime/tests/assembly-bundle-roster.client.spec.ts
index e73ce00818..44d980b362 100644
--- a/packages/test-support/client-runtime/tests/assembly-bundle-roster.client.spec.ts
+++ b/packages/test-support/client-runtime/tests/assembly-bundle-roster.client.spec.ts
@@ -1,9 +1,9 @@
/** bundleRoster: the real web profile read from its bundles, and every reader decision on a scratch installation. */
-import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { getStaticModules } from '@deepseek-ai/dsh-client-web/src/seed.ts'
-import { afterAll, describe, expect, it } from 'vitest'
+import { afterAll, describe, expect, it, onTestFinished } from 'vitest'
import { MODULES_PACKAGE } from '../src/assembly/modules.ts'
import { WEB_PROFILE_BUNDLES, bundleRoster, webApp } from '../src/assembly/bundle-roster.ts'
@@ -66,6 +66,21 @@ describe('bundleRoster on a scratch installation', () => {
const scratch = new Scratch()
afterAll(() => { rmSync(scratch.root, { recursive: true, force: true }) })
+ it('resolves a linked bundle dependency before an unrelated ancestor package', () => {
+ const linked = new Scratch()
+ onTestFinished(() => { rmSync(linked.root, { recursive: true, force: true }) })
+ const bundle = join(linked.root, 'workspace', 'bundle')
+ const dependency = join(bundle, 'node_modules', '@t', 'theme')
+ mkdirSync(dependency, { recursive: true })
+ writeFileSync(join(bundle, 'package.json'), JSON.stringify({ name: '@t/linked', dsh: { bundle: { patch: './cordis.patch.yml' } } }))
+ writeFileSync(join(bundle, 'cordis.patch.yml'), "- insert:\n - id: theme\n name: '@t/theme'\n")
+ writeFileSync(join(dependency, 'package.json'), JSON.stringify({ name: '@t/theme', dsh: { client: { platform: 'web' } } }))
+ linked.pkg('@t/theme', {})
+ symlinkSync(bundle, join(linked.root, 'app', 'node_modules', '@t', 'linked'), 'junction')
+ linked.bundle('@t/base', '- insert: []\n')
+ expect(linked.roster(['@t/base', '@t/linked'])).toEqual(['@t/theme'])
+ })
+
it('applies the layers in order and keeps enabled browser rows once, with their dsh.client declaration', () => {
scratch.web('@t/a', { inject: ['@t/b'], immediately: true })
scratch.web('@t/b')
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d4374d8a7c..bcf9585ca5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -526,16 +526,25 @@ importers:
apps/desktop:
dependencies:
+ '@deepseek-ai/dsh-api-gateway':
+ specifier: workspace:^
+ version: link:../../packages/api/gateway
electron-updater:
specifier: ^6.8.9
version: 6.8.9
semver:
specifier: ^7.8.5
version: 7.8.5
+ ws:
+ specifier: ^8.21.0
+ version: 8.21.0
devDependencies:
'@deepseek-ai/dsh-app-boot':
specifier: workspace:^
version: link:../../packages/boot/app-boot
+ '@deepseek-ai/dsh-deepseek-account':
+ specifier: workspace:^
+ version: link:../../packages/credentials/deepseek-account
'@deepseek-ai/dsh-home-paths':
specifier: workspace:^
version: link:../../packages/util/home-paths
@@ -554,6 +563,9 @@ importers:
'@types/semver':
specifier: ^7.8.0
version: 7.8.0
+ '@types/ws':
+ specifier: ^8.18.1
+ version: 8.18.1
app-builder-lib:
specifier: 26.15.3
version: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)
@@ -602,6 +614,9 @@ importers:
'@deepseek-ai/dsh-client-connection':
specifier: workspace:^
version: link:../../packages/client/connection
+ '@deepseek-ai/dsh-deepseek-account':
+ specifier: workspace:^
+ version: link:../../packages/credentials/deepseek-account
'@deepseek-ai/dsh-home-paths':
specifier: workspace:^
version: link:../../packages/util/home-paths
@@ -897,6 +912,18 @@ importers:
specifier: workspace:^
version: link:../../interaction/user-approval
+ packages/api/account-controller:
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:^
+ version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-deepseek-account':
+ specifier: workspace:^
+ version: link:../../credentials/deepseek-account
+ '@deepseek-ai/dsh-typert-protocol':
+ specifier: workspace:^
+ version: link:../../typert/protocol
+
packages/api/gateway:
dependencies:
'@deepseek-ai/dsh-deque':
@@ -964,6 +991,9 @@ importers:
'@deepseek-ai/dsh-agent-presets':
specifier: workspace:^
version: link:../../preset/agent-presets
+ '@deepseek-ai/dsh-api-account-controller':
+ specifier: workspace:^
+ version: link:../account-controller
'@deepseek-ai/dsh-api-gateway':
specifier: workspace:^
version: link:../gateway
@@ -1634,6 +1664,9 @@ importers:
'@deepseek-ai/dsh-attachment-local':
specifier: workspace:^
version: link:../../attachment/attachment-local
+ '@deepseek-ai/dsh-authorization':
+ specifier: workspace:^
+ version: link:../../credentials/authorization
'@deepseek-ai/dsh-bash-sandbox':
specifier: workspace:^
version: link:../../shell/bash-sandbox
@@ -1661,6 +1694,9 @@ importers:
'@deepseek-ai/dsh-credentials-local':
specifier: workspace:^
version: link:../../credentials/credentials-local
+ '@deepseek-ai/dsh-deepseek-account-platform':
+ specifier: workspace:^
+ version: link:../../credentials/deepseek-account-platform
'@deepseek-ai/dsh-deepseek-llm-api-extensions':
specifier: workspace:^
version: link:../../llm/deepseek-llm-api-extensions
@@ -2049,6 +2085,9 @@ importers:
'@deepseek-ai/dsh-agent-presets':
specifier: workspace:^
version: link:../../preset/agent-presets
+ '@deepseek-ai/dsh-api-account-controller':
+ specifier: workspace:^
+ version: link:../../api/account-controller
'@deepseek-ai/dsh-api-remotes':
specifier: workspace:^
version: link:../../api/remotes
@@ -2166,6 +2205,9 @@ importers:
'@deepseek-ai/dsh-client-ui-settings':
specifier: workspace:^
version: link:../../client/ui-settings
+ '@deepseek-ai/dsh-client-ui-settings-account':
+ specifier: workspace:^
+ version: link:../../client/ui-settings-account
'@deepseek-ai/dsh-client-ui-settings-general':
specifier: workspace:^
version: link:../../client/ui-settings-general
@@ -4051,6 +4093,67 @@ importers:
specifier: ^18.2.0
version: 18.3.1
+ packages/client/ui-settings-account:
+ dependencies:
+ '@deepseek-ai/schemastery':
+ specifier: link:../../../vendor/schemastery
+ version: link:../../../vendor/schemastery
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:^
+ version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-api-remotes':
+ specifier: workspace:^
+ version: link:../../api/remotes
+ '@deepseek-ai/dsh-client-locale':
+ specifier: workspace:^
+ version: link:../locale
+ '@deepseek-ai/dsh-client-test-runtime':
+ specifier: workspace:^
+ version: link:../../test-support/client-runtime
+ '@deepseek-ai/dsh-client-ui-primitives':
+ specifier: workspace:^
+ version: link:../ui-primitives
+ '@deepseek-ai/dsh-client-ui-renderer':
+ specifier: workspace:^
+ version: link:../ui-renderer
+ '@deepseek-ai/dsh-client-ui-settings':
+ specifier: workspace:^
+ version: link:../ui-settings
+ '@deepseek-ai/dsh-client-ui-settings-models':
+ specifier: workspace:^
+ version: link:../ui-settings-models
+ '@deepseek-ai/dsh-client-ui-slots':
+ specifier: workspace:^
+ version: link:../ui-slots
+ '@deepseek-ai/dsh-deepseek-account':
+ specifier: workspace:^
+ version: link:../../credentials/deepseek-account
+ '@deepseek-ai/dsh-host-webserver':
+ specifier: workspace:^
+ version: link:../../host/webserver
+ '@testing-library/react':
+ specifier: ^16.1.0
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@types/big.js':
+ specifier: ^6.2.2
+ version: 6.2.2
+ '@types/react':
+ specifier: ~18.3.1
+ version: 18.3.31
+ '@types/react-dom':
+ specifier: ~18.3.0
+ version: 18.3.7(@types/react@18.3.31)
+ big.js:
+ specifier: ^7.0.1
+ version: 7.0.1
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
+
packages/client/ui-settings-general:
dependencies:
'@deepseek-ai/schemastery':
@@ -6048,6 +6151,40 @@ importers:
specifier: workspace:^
version: link:../../util/launch-environment
+ packages/credentials/deepseek-account:
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:^
+ version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-brand':
+ specifier: workspace:^
+ version: link:../../util/brand
+
+ packages/credentials/deepseek-account-platform:
+ dependencies:
+ '@deepseek-ai/schemastery':
+ specifier: link:../../../vendor/schemastery
+ version: link:../../../vendor/schemastery
+ zod:
+ specifier: ^4.4.3
+ version: 4.4.3
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:^
+ version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-authorization':
+ specifier: workspace:^
+ version: link:../authorization
+ '@deepseek-ai/dsh-credentials':
+ specifier: workspace:^
+ version: link:../credentials
+ '@deepseek-ai/dsh-deepseek-account':
+ specifier: workspace:^
+ version: link:../deepseek-account
+ '@deepseek-ai/dsh-host-webserver':
+ specifier: workspace:^
+ version: link:../../host/webserver
+
packages/deliverables/tool-present:
dependencies:
'@deepseek-ai/schemastery':
@@ -8220,6 +8357,9 @@ importers:
'@deepseek-ai/dsh-credentials-local':
specifier: workspace:^
version: link:../../credentials/credentials-local
+ '@deepseek-ai/dsh-deepseek-account':
+ specifier: workspace:^
+ version: link:../../credentials/deepseek-account
'@deepseek-ai/dsh-deepseek-llm-api-extensions':
specifier: workspace:^
version: link:../deepseek-llm-api-extensions
@@ -15602,6 +15742,9 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+ '@types/big.js@6.2.2':
+ resolution: {integrity: sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==}
+
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@@ -16370,6 +16513,9 @@ packages:
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+ big.js@7.0.1:
+ resolution: {integrity: sha512-iFgV784tD8kq4ccF1xtNMZnXeZzVuXWWM+ERFzKQjv+A5G9HC8CY3DuV45vgzFFcW+u2tIvmF95+AzWgs6BjCg==}
+
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
@@ -22389,6 +22535,8 @@ snapshots:
dependencies:
'@babel/types': 7.29.7
+ '@types/big.js@6.2.2': {}
+
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
@@ -23257,6 +23405,8 @@ snapshots:
dependencies:
require-from-string: 2.0.2
+ big.js@7.0.1: {}
+
bignumber.js@9.3.1: {}
birpc@2.9.0: {}
diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts
index 2c7f7968cb..f1bb62873b 100644
--- a/scripts/gen-cordis-catalog.ts
+++ b/scripts/gen-cordis-catalog.ts
@@ -76,6 +76,7 @@ export const SERVICE_PAGE: Record = {
compaction: 'compaction.md',
cordisInspect: 'extensions.md',
authorization: 'credentials.md',
+ deepseekAccount: 'credentials.md',
credentials: 'credentials.md',
credentialsController: 'credentials.md',
settingsController: 'settings.md',
@@ -635,6 +636,10 @@ export const LINK_MAP: Readonly> = {
AuthorizationMethod: 'credentials.md',
AuthorizationNotice: 'credentials.md',
AuthorizationOutcome: 'credentials.md',
+ AccountView: 'credentials.md',
+ AccountDetails: 'credentials.md',
+ PlatformSession: 'credentials.md',
+ SignInAttemptId: 'credentials.md',
AuthorizationPrompt: 'credentials.md',
AuthorizationRequest: 'credentials.md',
AuthorizationSession: 'credentials.md',
diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts
index 5b32da720a..7d15999f89 100644
--- a/scripts/gen-doc-graphs.ts
+++ b/scripts/gen-doc-graphs.ts
@@ -350,6 +350,15 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['api-settings-controller', 'llm-deepseek', 'llm-pi-ai'],
note: 'Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the settings controller exposes value-free views and write-only storage.',
},
+ {
+ key: 'deepseekAccount',
+ pkg: 'deepseek-account',
+ title: 'DeepSeek account',
+ mode: 'seam',
+ implementations: ['deepseek-account-platform'],
+ consumers: ['api-account-controller', 'llm-deepseek'],
+ note: 'The Host owns browser authorization and local credentials; UI consumers receive state without tokens.',
+ },
{
key: 'authorization',
pkg: 'authorization',
diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts
index 33363b380e..cce35e13f2 100644
--- a/scripts/verify-package-readme-model-experience.ts
+++ b/scripts/verify-package-readme-model-experience.ts
@@ -54,6 +54,12 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = {
'packages/experimental/browser-use-runtime': { kind: 'indirect', reason: 'Browser providers call the library to own resources and expose upstream MCP tools.' },
'packages/computer-use/computer-use': { kind: 'none', reason: 'The registry only reserves provider names; providers own all model-facing tools and guidance.' },
'packages/document/office-to-pdf': { kind: 'none', reason: 'The provider returns PDF bytes without constructing model input.' },
+
+ 'packages/credentials/deepseek-account': { kind: 'none', reason: 'Account authorization registers no model context or tools; credentials affect HTTP authentication only.' },
+ 'packages/credentials/deepseek-account-platform': { kind: 'none', reason: 'Account authorization registers no model context or tools; credentials affect HTTP authentication only.' },
+ 'packages/api/account-controller': { kind: 'none', reason: 'Account authorization registers no model context or tools; credentials affect HTTP authentication only.' },
+ 'packages/client/ui-settings-account': { kind: 'none', reason: 'Account authorization registers no model context or tools; credentials affect HTTP authentication only.' },
+
'packages/attachment/attachment': { kind: 'indirect', reason: 'The storage seam delegates model request rendering to provider adapters.' },
'packages/attachment/attachment-local': { kind: 'indirect', reason: 'The local backend delegates model request rendering to provider adapters.' },
'packages/shell/shell': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 3cace3bae1..44b0393439 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -28,6 +28,15 @@
// not declaration path aliases, keep each package/vendor source compiled
// under its own tsconfig boundary.
"paths": {
+ "@deepseek-ai/dsh-client-ui-settings-account": ["./packages/client/ui-settings-account/src/index.ts"],
+ "@deepseek-ai/dsh-client-ui-settings-account/client": ["./packages/client/ui-settings-account/src/client/index.ts"],
+ "@deepseek-ai/dsh-api-gateway/stream-protocol": ["./packages/api/gateway/src/stream-protocol.ts"],
+ "@deepseek-ai/dsh-api-account-controller": ["./packages/api/account-controller/src/index.ts"],
+ "@deepseek-ai/dsh-api-account-controller/types": ["./packages/api/account-controller/src/types.ts"],
+ "@deepseek-ai/dsh-deepseek-account-platform": ["./packages/credentials/deepseek-account-platform/src/index.ts"],
+ "@deepseek-ai/dsh-deepseek-account-platform/types": ["./packages/credentials/deepseek-account-platform/src/types.ts"],
+ "@deepseek-ai/dsh-deepseek-account": ["./packages/credentials/deepseek-account/src/index.ts"],
+ "@deepseek-ai/dsh-deepseek-account/types": ["./packages/credentials/deepseek-account/src/types.ts"],
"@deepseek-ai/dsh/profile-boot": ["./apps/cli/src/profile-boot.ts"],
"@deepseek-ai/dsh-browser-use": ["./packages/browser-use/browser-use/src/index.ts"],
"@deepseek-ai/dsh-browser-use/brand": ["./packages/browser-use/browser-use/src/brand.ts"],
diff --git a/tsconfig.client.json b/tsconfig.client.json
index b830d85b2f..313b80a967 100644
--- a/tsconfig.client.json
+++ b/tsconfig.client.json
@@ -46,6 +46,7 @@
{ "path": "./packages/api/terminal-controller/tsconfig.client.json" },
// Producer-to-UI spill tests use the ptc-runtime contract with the real tool registry.
{ "path": "./packages/ptc-runtime/ptc-runtime" },
+ { "path": "./packages/client/ui-settings-account" },
// Shared leaf: web e2e boots the real host webserver (fixture + real-host
// smoke policy). webserver has zero workspace deps and no cordis merge,
// so it cannot drag host-side Context augmentation into this program.
diff --git a/tsconfig.host.json b/tsconfig.host.json
index aafd4ece8f..100f244354 100644
--- a/tsconfig.host.json
+++ b/tsconfig.host.json
@@ -178,6 +178,10 @@
{ "path": "./packages/experimental/computer-use-cua-driver-native" },
{ "path": "./packages/client/ui-sidebar-documentpreview/tsconfig.host.json" },
{ "path": "./packages/document/office-to-pdf" },
+
+ { "path": "./packages/api/account-controller" },
+ { "path": "./packages/credentials/deepseek-account-platform" },
+ { "path": "./packages/credentials/deepseek-account" },
{ "path": "./packages/client/ui-deliverables/tsconfig.host.json" },
{ "path": "./vendor/cosmokit" },
{ "path": "./vendor/schemastery" },