tokens.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. // 每个 Run 一个短期令牌:引擎调用网关时携带,网关据此确定会话、工作区与访问策略;Run 结束即作废。
  2. // Codex 的会话在 app-server 中保持加载,MCP 连接沿用第一次对话时的请求头(thread/resume 不会更新),
  3. // 所以 Codex 每个会话使用固定的令牌字符串:每次 Run 开始时重新绑定到当前 Run,结束后失效,下次 Run 再绑定
  4. import crypto from 'node:crypto'
  5. import type { AppSettings, KnowledgeRootDto, PermissionMode } from '@agent-server/shared'
  6. import type { ResolvedAccess } from '../services/access-policy'
  7. import type { InteractionOutcome, NewApproval } from '../services/interactions'
  8. export interface GatewayRunContext {
  9. sessionId: string
  10. runId: string
  11. provider: 'claude' | 'codex'
  12. workspaceDir: string
  13. access: ResolvedAccess
  14. permissionMode: PermissionMode
  15. settings: AppSettings
  16. knowledgeRoots: KnowledgeRootDto[]
  17. /** Profile 勾选的第三方 MCP 服务 */
  18. mcpServerIds: string[]
  19. audit: (kind: 'gateway_call' | 'network_denied' | 'file_access_denied' | 'approval', summary: string, detail?: unknown) => void
  20. /** 发起审批(与引擎侧的审批相同:显示交互卡片并等待回复) */
  21. interact: (req: NewApproval) => Promise<InteractionOutcome>
  22. }
  23. const MAX_AGE_MS = 24 * 3600 * 1000
  24. export class RunTokenRegistry {
  25. private tokens = new Map<string, { ctx: GatewayRunContext; issuedAt: number }>()
  26. /** 会话 → 固定令牌(只用于 Codex) */
  27. private sessionTokens = new Map<string, string>()
  28. issue(ctx: GatewayRunContext, opts: { stablePerSession?: boolean } = {}): string {
  29. let token = opts.stablePerSession ? this.sessionTokens.get(ctx.sessionId) : undefined
  30. if (!token) {
  31. token = `rt_${crypto.randomBytes(32).toString('base64url')}`
  32. if (opts.stablePerSession) this.sessionTokens.set(ctx.sessionId, token)
  33. }
  34. this.tokens.set(token, { ctx, issuedAt: Date.now() })
  35. return token
  36. }
  37. get(token: string | undefined): GatewayRunContext | null {
  38. if (!token) return null
  39. const e = this.tokens.get(token)
  40. if (!e) return null
  41. if (Date.now() - e.issuedAt > MAX_AGE_MS) {
  42. this.tokens.delete(token)
  43. return null
  44. }
  45. return e.ctx
  46. }
  47. /** Run 结束:令牌失效(固定令牌的字符串保留,下次 Run 重新绑定) */
  48. revoke(token: string) {
  49. this.tokens.delete(token)
  50. }
  51. /** 会话删除时调用 */
  52. forgetSession(sessionId: string) {
  53. const token = this.sessionTokens.get(sessionId)
  54. if (token) this.tokens.delete(token)
  55. this.sessionTokens.delete(sessionId)
  56. }
  57. }