返回知识库
0

第十五章:凭证与授权

本章目标:帮助你理解 dsh 的凭证接缝(Credential Seam)、环境变量 → .env → Settings 的优先级链、OAuth/授权流的人类交互、以及 API Key 的安全存储与轮换。阅读本章后,你应该能回答"凭证是如何被管理和使用的"以及"如何添加新的凭证类型"。


15.1 凭证架构

15.1.1 凭证接缝

预览
源码

credentials/ 能力

credentials/ (Service Definition)

credentials-env/ (Provider)

credentials-settings/ (Provider)

消费者: llm, tools, web

graph TB
    subgraph "credentials/ 能力"
        A["credentials/ (Service Definition)"]
        B["credentials-env/ (Provider)"]
        C["credentials-settings/ (Provider)"]
        D["消费者: llm, tools, web"]
    end

    A --> B
    A --> C
    D --> A

15.1.2 凭证类型

// 来自 packages/credentials/credentials/src/index.ts
type CredentialType =
  | 'api-key'           // API 密钥
  | 'oauth-token'       // OAuth 令牌
  | 'certificate'       // 证书
  | 'password'          // 密码
  | 'ssh-key'           // SSH 密钥

15.2 凭证服务

15.2.1 CredentialsService

// 来自 packages/credentials/credentials/src/index.ts
class CredentialsService {
  /** 获取凭证 */
  async get(name: string): Promise<Credential | null> {
    // 按优先级查找
    for (const provider of this.providers) {
      const credential = await provider.get(name)
      if (credential) return credential
    }
    
    return null
  }
  
  /** 存储凭证 */
  async set(name: string, credential: Credential): Promise<void> {
    // 存储到默认提供商
    await this.defaultProvider.set(name, credential)
  }
  
  /** 删除凭证 */
  async delete(name: string): Promise<void> {
    await this.defaultProvider.delete(name)
  }
  
  /** 列出所有凭证 */
  async list(): Promise<CredentialEntry[]> {
    const entries: CredentialEntry[] = []
    
    for (const provider of this.providers) {
      const providerEntries = await provider.list()
      entries.push(...providerEntries)
    }
    
    return entries
  }
}

15.2.2 Credential

interface Credential {
  /** 凭证名称 */
  name: string
  /** 凭证类型 */
  type: CredentialType
  /** 凭证值 */
  value: string
  /** 元数据 */
  metadata?: Record<string, unknown>
  /** 过期时间 */
  expiresAt?: Date
}

15.3 凭证提供者

15.3.1 环境变量提供者

// 来自 packages/credentials/credentials-env/src/index.ts
class EnvCredentialProvider implements CredentialProvider {
  /** 获取凭证 */
  async get(name: string): Promise<Credential | null> {
    // 转换名称格式:my-api-key → MY_API_KEY
    const envName = name.replace(/-/g, '_').toUpperCase()
    const value = process.env[envName]
    
    if (!value) return null
    
    return {
      name,
      type: 'api-key',
      value
    }
  }
  
  /** 列出所有凭证 */
  async list(): Promise<CredentialEntry[]> {
    return Object.entries(process.env)
      .filter(([key]) => key.startsWith('DSH_'))
      .map(([key, value]) => ({
        name: key.replace(/^DSH_/, '').toLowerCase().replace(/_/g, '-'),
        type: 'api-key',
        provider: 'env'
      }))
  }
}

15.3.2 .env 文件提供者

// 来自 packages/credentials/credentials-env/src/index.ts
class DotEnvCredentialProvider implements CredentialProvider {
  constructor(private envPath: string) {}
  
  /** 获取凭证 */
  async get(name: string): Promise<Credential | null> {
    // 读取 .env 文件
    const env = await this.loadEnvFile()
    const value = env[name]
    
    if (!value) return null
    
    return {
      name,
      type: 'api-key',
      value
    }
  }
  
  /** 加载 .env 文件 */
  private async loadEnvFile(): Promise<Record<string, string>> {
    const content = await readFile(this.envPath, 'utf-8')
    const env: Record<string, string> = {}
    
    for (const line of content.split('\n')) {
      const trimmed = line.trim()
      if (!trimmed || trimmed.startsWith('#')) continue
      
      const [key, ...valueParts] = trimmed.split('=')
      env[key.trim()] = valueParts.join('=').trim()
    }
    
    return env
  }
}

15.3.3 Settings 提供者

// 来自 packages/credentials/credentials-settings/src/index.ts
class SettingsCredentialProvider implements CredentialProvider {
  constructor(private ctx: Context) {}
  
  /** 获取凭证 */
  async get(name: string): Promise<Credential | null> {
    // 从用户设置中获取
    const settings = await this.ctx.settings.get('credentials')
    const value = settings?.[name]
    
    if (!value) return null
    
    return {
      name,
      type: 'api-key',
      value
    }
  }
  
  /** 存储凭证 */
  async set(name: string, credential: Credential): Promise<void> {
    const settings = await this.ctx.settings.get('credentials') ?? {}
    settings[name] = credential.value
    await this.ctx.settings.set('credentials', settings)
  }
}

15.4 凭证解析优先级

15.4.1 优先级链

预览
源码

找到

未找到

找到

未找到

找到

未找到

提供

拒绝

凭证请求

环境变量?

返回凭证

.env 文件?

Settings?

用户输入?

存储到 Settings

返回 null

graph TB
    A[凭证请求] --> B{环境变量?}
    B -->|"找到"| C[返回凭证]
    B -->|"未找到"| D{.env 文件?}
    D -->|"找到"| C
    D -->|"未找到"| E{Settings?}
    E -->|"找到"| C
    E -->|"未找到"| F{用户输入?}
    F -->|"提供"| G[存储到 Settings]
    G --> C
    F -->|"拒绝"| H[返回 null]

15.4.2 实现

// 概念性描述
class CredentialResolver {
  /** 解析凭证 */
  async resolve(name: string, options?: ResolveOptions): Promise<Credential | null> {
    // 1. 尝试环境变量
    let credential = await this.envProvider.get(name)
    if (credential) return credential
    
    // 2. 尝试 .env 文件
    credential = await this.dotEnvProvider.get(name)
    if (credential) return credential
    
    // 3. 尝试 Settings
    credential = await this.settingsProvider.get(name)
    if (credential) return credential
    
    // 4. 如果需要,询问用户
    if (options?.askUser) {
      const value = await this.askUserForCredential(name)
      if (value) {
        credential = { name, type: 'api-key', value }
        await this.settingsProvider.set(name, credential)
        return credential
      }
    }
    
    return null
  }
}

15.5 OAuth 授权流

15.5.1 概念

对于需要 OAuth 的服务,dsh 支持人类交互式的授权流。

预览
源码
OAuth ProviderUserAuthorizationServiceAgentOAuth ProviderUserAuthorizationServiceAgentrequestAuthorization(provider)生成授权 URLauthorizationUrl显示授权 URL授权authorizationCode交换令牌accessTokencredential
sequenceDiagram
    participant Agent
    participant Auth as AuthorizationService
    participant User
    participant Provider as OAuth Provider

    Agent->>Auth: requestAuthorization(provider)
    Auth->>Provider: 生成授权 URL
    Provider-->>Auth: authorizationUrl
    Auth->>User: 显示授权 URL
    User->>Provider: 授权
    Provider-->>Auth: authorizationCode
    Auth->>Provider: 交换令牌
    Provider-->>Auth: accessToken
    Auth-->>Agent: credential

15.5.2 AuthorizationService

// 来自 packages/credentials/authorization/src/index.ts
class AuthorizationService {
  /** 注册授权流 */
  registerFlow(flow: AuthorizationFlow): Disposable {
    this.flows.set(flow.provider, flow)
    
    return () => {
      this.flows.delete(flow.provider)
    }
  }
  
  /** 请求授权 */
  async requestAuthorization(provider: string): Promise<Credential> {
    const flow = this.flows.get(provider)
    if (!flow) throw new Error(`No authorization flow for provider: ${provider}`)
    
    // 1. 生成授权 URL
    const authUrl = await flow.generateAuthorizationUrl()
    
    // 2. 显示给用户
    await this.showAuthorizationUrl(authUrl)
    
    // 3. 等待回调
    const code = await this.waitForCallback(flow.callbackUrl)
    
    // 4. 交换令牌
    const token = await flow.exchangeCode(code)
    
    return token
  }
}

15.6 API Key 安全

15.6.1 安全存储

// 概念性描述
class SecureCredentialStorage {
  /** 加密存储 */
  async store(credential: Credential): Promise<void> {
    // 1. 加密凭证值
    const encrypted = await this.encrypt(credential.value)
    
    // 2. 存储到安全位置
    await this.secureStore.set(credential.name, {
      ...credential,
      value: encrypted
    })
  }
  
  /** 解密读取 */
  async retrieve(name: string): Promise<Credential | null> {
    const stored = await this.secureStore.get(name)
    if (!stored) return null
    
    // 解密
    const decrypted = await this.decrypt(stored.value)
    
    return {
      ...stored,
      value: decrypted
    }
  }
}

15.6.2 密钥轮换

// 概念性描述
class CredentialRotation {
  /** 检查是否需要轮换 */
  async needsRotation(credential: Credential): Promise<boolean> {
    if (!credential.expiresAt) return false
    
    // 检查是否即将过期(7 天内)
    const daysUntilExpiry = (credential.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
    return daysUntilExpiry < 7
  }
  
  /** 轮换密钥 */
  async rotate(name: string): Promise<Credential> {
    // 1. 获取当前凭证
    const current = await this.credentials.get(name)
    if (!current) throw new Error(`Credential not found: ${name}`)
    
    // 2. 生成新凭证
    const newCredential = await this.generateNewCredential(current)
    
    // 3. 存储新凭证
    await this.credentials.set(name, newCredential)
    
    // 4. 删除旧凭证(可选)
    // await this.credentials.delete(`${name}-old`)
    
    return newCredential
  }
}

15.7 配置

15.7.1 凭证配置

# cordis.patch.yml
- id: credentials
  config:
    # 提供者优先级
    providers:
      - 'env'
      - 'dot-env'
      - 'settings'
    
    # .env 文件路径
    dotEnvPath: '${DSH_HOME}/.env'
    
    # 安全存储
    secureStorage:
      enabled: true
      type: 'keychain'  # 'keychain' | 'file' | 'memory'

15.8 小结

概念一句话解释
Credential Seam凭证管理的能力接缝
Env Provider环境变量凭证提供者
DotEnv Provider.env 文件凭证提供者
Settings Provider用户设置凭证提供者
Authorization FlowOAuth 授权流
密钥轮换自动轮换即将过期的凭证

下一步第十六章:Webhook 与外部事件——深入理解验证外部事件的签名机制和可信规则。

DeepSeek-Harness / 15-凭证与授权 0 0 iliuqi
2026-09-04T07:48:53.495134837Z 2026-09-04T07:58:27.702620227Z