@jingyi0605/codingns 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.md +14 -0
  2. package/dist/public/assets/{TerminalPage-CVG1cGAJ.js → TerminalPage-Dr7knYq2.js} +19 -19
  3. package/dist/public/assets/index-BpUi6zoT.js +108 -0
  4. package/dist/public/assets/index-NMtdQNda.css +1 -0
  5. package/dist/public/index.html +2 -2
  6. package/dist/server/config/env.js +24 -3
  7. package/dist/server/config/env.js.map +1 -1
  8. package/dist/server/config/opencode-base-url-resolver.d.ts +13 -8
  9. package/dist/server/config/opencode-base-url-resolver.js +117 -147
  10. package/dist/server/config/opencode-base-url-resolver.js.map +1 -1
  11. package/dist/server/config/opencode-system-probe-helper-client.d.ts +18 -0
  12. package/dist/server/config/opencode-system-probe-helper-client.js +127 -0
  13. package/dist/server/config/opencode-system-probe-helper-client.js.map +1 -0
  14. package/dist/server/config/opencode-system-probe-helper-process.d.ts +1 -0
  15. package/dist/server/config/opencode-system-probe-helper-process.js +208 -0
  16. package/dist/server/config/opencode-system-probe-helper-process.js.map +1 -0
  17. package/dist/server/modules/git/git-command-helper-client.d.ts +25 -0
  18. package/dist/server/modules/git/git-command-helper-client.js +143 -0
  19. package/dist/server/modules/git/git-command-helper-client.js.map +1 -0
  20. package/dist/server/modules/git/git-command-helper-process.d.ts +1 -0
  21. package/dist/server/modules/git/git-command-helper-process.js +237 -0
  22. package/dist/server/modules/git/git-command-helper-process.js.map +1 -0
  23. package/dist/server/modules/git/git-command-runner.d.ts +8 -0
  24. package/dist/server/modules/git/git-command-runner.js +77 -6
  25. package/dist/server/modules/git/git-command-runner.js.map +1 -1
  26. package/dist/server/modules/git/git-controller.d.ts +4 -0
  27. package/dist/server/modules/git/git-controller.js +4 -1
  28. package/dist/server/modules/git/git-controller.js.map +1 -1
  29. package/dist/server/modules/git/git-read-service.d.ts +2 -1
  30. package/dist/server/modules/git/git-read-service.js +30 -0
  31. package/dist/server/modules/git/git-read-service.js.map +1 -1
  32. package/dist/server/modules/git/git-write-service.d.ts +1 -1
  33. package/dist/server/modules/git/git-write-service.js +8 -7
  34. package/dist/server/modules/git/git-write-service.js.map +1 -1
  35. package/dist/server/modules/git/types.d.ts +5 -0
  36. package/dist/server/modules/preferences/common.d.ts +2 -0
  37. package/dist/server/modules/preferences/common.js +13 -0
  38. package/dist/server/modules/preferences/common.js.map +1 -0
  39. package/dist/server/modules/preferences/profile-controller.d.ts +11 -0
  40. package/dist/server/modules/preferences/profile-controller.js +14 -0
  41. package/dist/server/modules/preferences/profile-controller.js.map +1 -0
  42. package/dist/server/modules/preferences/profile-service.d.ts +17 -0
  43. package/dist/server/modules/preferences/profile-service.js +213 -0
  44. package/dist/server/modules/preferences/profile-service.js.map +1 -0
  45. package/dist/server/modules/preferences/quick-phrase-controller.js +2 -12
  46. package/dist/server/modules/preferences/quick-phrase-controller.js.map +1 -1
  47. package/dist/server/modules/provider/codex-model-options.js +26 -165
  48. package/dist/server/modules/provider/codex-model-options.js.map +1 -1
  49. package/dist/server/modules/provider/opencode-model-options.js +6 -71
  50. package/dist/server/modules/provider/opencode-model-options.js.map +1 -1
  51. package/dist/server/modules/provider/provider-controller.d.ts +2 -0
  52. package/dist/server/modules/provider/provider-controller.js +21 -1
  53. package/dist/server/modules/provider/provider-controller.js.map +1 -1
  54. package/dist/server/modules/provider/provider-discovery-helper-client.d.ts +25 -0
  55. package/dist/server/modules/provider/provider-discovery-helper-client.js +114 -0
  56. package/dist/server/modules/provider/provider-discovery-helper-client.js.map +1 -0
  57. package/dist/server/modules/provider/provider-discovery-helper-process.d.ts +1 -0
  58. package/dist/server/modules/provider/provider-discovery-helper-process.js +296 -0
  59. package/dist/server/modules/provider/provider-discovery-helper-process.js.map +1 -0
  60. package/dist/server/modules/sessions/claude-runtime-helper-client.d.ts +28 -0
  61. package/dist/server/modules/sessions/claude-runtime-helper-client.js +221 -0
  62. package/dist/server/modules/sessions/claude-runtime-helper-client.js.map +1 -0
  63. package/dist/server/modules/sessions/claude-runtime-helper-process.d.ts +1 -0
  64. package/dist/server/modules/sessions/claude-runtime-helper-process.js +146 -0
  65. package/dist/server/modules/sessions/claude-runtime-helper-process.js.map +1 -0
  66. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +16 -0
  67. package/dist/server/modules/sessions/codex-app-server-helper-client.js +237 -0
  68. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -0
  69. package/dist/server/modules/sessions/codex-app-server-helper-process.d.ts +1 -0
  70. package/dist/server/modules/sessions/codex-app-server-helper-process.js +484 -0
  71. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -0
  72. package/dist/server/modules/sessions/session-activity-authority-service.d.ts +52 -0
  73. package/dist/server/modules/sessions/session-activity-authority-service.js +377 -0
  74. package/dist/server/modules/sessions/session-activity-authority-service.js.map +1 -0
  75. package/dist/server/modules/sessions/session-activity-inspector.js +80 -40
  76. package/dist/server/modules/sessions/session-activity-inspector.js.map +1 -1
  77. package/dist/server/modules/sessions/session-controller.d.ts +15 -1
  78. package/dist/server/modules/sessions/session-controller.js +14 -1
  79. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  80. package/dist/server/modules/sessions/session-history-service.d.ts +4 -2
  81. package/dist/server/modules/sessions/session-history-service.js +151 -29
  82. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  83. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +40 -4
  84. package/dist/server/modules/sessions/session-live-runtime-service.js +330 -44
  85. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  86. package/dist/server/modules/sessions/session-permission-request-service.d.ts +175 -0
  87. package/dist/server/modules/sessions/session-permission-request-service.js +1615 -0
  88. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -0
  89. package/dist/server/modules/sessions/session-provider-error-mapper.js +14 -0
  90. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  91. package/dist/server/modules/terminal/command-template-service.d.ts +2 -2
  92. package/dist/server/modules/terminal/command-template-service.js +3 -3
  93. package/dist/server/modules/terminal/command-template-service.js.map +1 -1
  94. package/dist/server/modules/terminal/runtime/adapters/conpty-control-helper-client.d.ts +24 -0
  95. package/dist/server/modules/terminal/runtime/adapters/conpty-control-helper-client.js +104 -0
  96. package/dist/server/modules/terminal/runtime/adapters/conpty-control-helper-client.js.map +1 -0
  97. package/dist/server/modules/terminal/runtime/adapters/conpty-control-helper-process.d.ts +1 -0
  98. package/dist/server/modules/terminal/runtime/adapters/conpty-control-helper-process.js +96 -0
  99. package/dist/server/modules/terminal/runtime/adapters/conpty-control-helper-process.js.map +1 -0
  100. package/dist/server/modules/terminal/runtime/adapters/conpty-runtime-adapter.d.ts +37 -0
  101. package/dist/server/modules/terminal/runtime/adapters/conpty-runtime-adapter.js +123 -0
  102. package/dist/server/modules/terminal/runtime/adapters/conpty-runtime-adapter.js.map +1 -0
  103. package/dist/server/modules/terminal/runtime/adapters/embedded-pty-runtime-adapter.d.ts +12 -5
  104. package/dist/server/modules/terminal/runtime/adapters/embedded-pty-runtime-adapter.js +44 -4
  105. package/dist/server/modules/terminal/runtime/adapters/embedded-pty-runtime-adapter.js.map +1 -1
  106. package/dist/server/modules/terminal/runtime/adapters/tmux-helper-client.d.ts +14 -0
  107. package/dist/server/modules/terminal/runtime/adapters/tmux-helper-client.js +105 -0
  108. package/dist/server/modules/terminal/runtime/adapters/tmux-helper-client.js.map +1 -0
  109. package/dist/server/modules/terminal/runtime/adapters/tmux-helper-process.d.ts +1 -0
  110. package/dist/server/modules/terminal/runtime/adapters/tmux-helper-process.js +67 -0
  111. package/dist/server/modules/terminal/runtime/adapters/tmux-helper-process.js.map +1 -0
  112. package/dist/server/modules/terminal/runtime/adapters/tmux-runtime-adapter.d.ts +8 -12
  113. package/dist/server/modules/terminal/runtime/adapters/tmux-runtime-adapter.js +112 -21
  114. package/dist/server/modules/terminal/runtime/adapters/tmux-runtime-adapter.js.map +1 -1
  115. package/dist/server/modules/terminal/runtime/conpty-runtime-shared.d.ts +25 -0
  116. package/dist/server/modules/terminal/runtime/conpty-runtime-shared.js +124 -0
  117. package/dist/server/modules/terminal/runtime/conpty-runtime-shared.js.map +1 -0
  118. package/dist/server/modules/terminal/runtime/conpty-session-agent-process.d.ts +1 -0
  119. package/dist/server/modules/terminal/runtime/conpty-session-agent-process.js +205 -0
  120. package/dist/server/modules/terminal/runtime/conpty-session-agent-process.js.map +1 -0
  121. package/dist/server/modules/terminal/runtime/conpty-session-attach-client.d.ts +1 -0
  122. package/dist/server/modules/terminal/runtime/conpty-session-attach-client.js +108 -0
  123. package/dist/server/modules/terminal/runtime/conpty-session-attach-client.js.map +1 -0
  124. package/dist/server/modules/terminal/runtime/conpty-session-control-client.d.ts +1 -0
  125. package/dist/server/modules/terminal/runtime/conpty-session-control-client.js +90 -0
  126. package/dist/server/modules/terminal/runtime/conpty-session-control-client.js.map +1 -0
  127. package/dist/server/modules/terminal/runtime/pty-broker-agent-process.d.ts +1 -0
  128. package/dist/server/modules/terminal/runtime/pty-broker-agent-process.js +179 -0
  129. package/dist/server/modules/terminal/runtime/pty-broker-agent-process.js.map +1 -0
  130. package/dist/server/modules/terminal/runtime/pty-broker-client.d.ts +29 -0
  131. package/dist/server/modules/terminal/runtime/pty-broker-client.js +169 -0
  132. package/dist/server/modules/terminal/runtime/pty-broker-client.js.map +1 -0
  133. package/dist/server/modules/terminal/runtime/pty-broker-shared.d.ts +22 -0
  134. package/dist/server/modules/terminal/runtime/pty-broker-shared.js +123 -0
  135. package/dist/server/modules/terminal/runtime/pty-broker-shared.js.map +1 -0
  136. package/dist/server/modules/terminal/runtime/pty-host-attachment-manager.d.ts +1 -0
  137. package/dist/server/modules/terminal/runtime/pty-host-attachment-manager.js +11 -1
  138. package/dist/server/modules/terminal/runtime/pty-host-attachment-manager.js.map +1 -1
  139. package/dist/server/modules/terminal/runtime/terminal-log-file-store.d.ts +1 -0
  140. package/dist/server/modules/terminal/runtime/terminal-log-file-store.js +7 -1
  141. package/dist/server/modules/terminal/runtime/terminal-log-file-store.js.map +1 -1
  142. package/dist/server/modules/terminal/runtime/terminal-log-spooler.js +3 -2
  143. package/dist/server/modules/terminal/runtime/terminal-log-spooler.js.map +1 -1
  144. package/dist/server/modules/terminal/runtime/terminal-runtime-adapter.d.ts +5 -3
  145. package/dist/server/modules/terminal/runtime/terminal-runtime-manager.d.ts +11 -5
  146. package/dist/server/modules/terminal/runtime/terminal-runtime-manager.js +110 -13
  147. package/dist/server/modules/terminal/runtime/terminal-runtime-manager.js.map +1 -1
  148. package/dist/server/modules/terminal/terminal-controller.js +7 -7
  149. package/dist/server/modules/terminal/terminal-controller.js.map +1 -1
  150. package/dist/server/modules/terminal/terminal-service.d.ts +30 -14
  151. package/dist/server/modules/terminal/terminal-service.js +260 -51
  152. package/dist/server/modules/terminal/terminal-service.js.map +1 -1
  153. package/dist/server/modules/terminal/terminal-shell.d.ts +4 -0
  154. package/dist/server/modules/terminal/terminal-shell.js +165 -18
  155. package/dist/server/modules/terminal/terminal-shell.js.map +1 -1
  156. package/dist/server/modules/workbench/workspace-file-watcher.d.ts +30 -0
  157. package/dist/server/modules/workbench/workspace-file-watcher.js +137 -0
  158. package/dist/server/modules/workbench/workspace-file-watcher.js.map +1 -0
  159. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +43 -7
  160. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  161. package/dist/server/routes/git.js +1 -0
  162. package/dist/server/routes/git.js.map +1 -1
  163. package/dist/server/routes/preferences.d.ts +2 -1
  164. package/dist/server/routes/preferences.js +3 -1
  165. package/dist/server/routes/preferences.js.map +1 -1
  166. package/dist/server/routes/sessions.js +2 -0
  167. package/dist/server/routes/sessions.js.map +1 -1
  168. package/dist/server/server/create-server.d.ts +4 -0
  169. package/dist/server/server/create-server.js +22 -5
  170. package/dist/server/server/create-server.js.map +1 -1
  171. package/dist/server/shared/utils/command-launch.d.ts +6 -0
  172. package/dist/server/shared/utils/command-launch.js +39 -0
  173. package/dist/server/shared/utils/command-launch.js.map +1 -0
  174. package/dist/server/shared/utils/perf-log.d.ts +1 -0
  175. package/dist/server/shared/utils/perf-log.js +8 -2
  176. package/dist/server/shared/utils/perf-log.js.map +1 -1
  177. package/dist/server/shared/utils/permission-debug-log.d.ts +2 -0
  178. package/dist/server/shared/utils/permission-debug-log.js +40 -0
  179. package/dist/server/shared/utils/permission-debug-log.js.map +1 -0
  180. package/dist/server/shared/utils/terminal-debug-log.d.ts +4 -0
  181. package/dist/server/shared/utils/terminal-debug-log.js +71 -0
  182. package/dist/server/shared/utils/terminal-debug-log.js.map +1 -0
  183. package/dist/server/storage/repositories/user-preference-profile-repository.d.ts +8 -0
  184. package/dist/server/storage/repositories/user-preference-profile-repository.js +46 -0
  185. package/dist/server/storage/repositories/user-preference-profile-repository.js.map +1 -0
  186. package/dist/server/storage/sqlite/schema.sql +13 -0
  187. package/dist/server/types/domain.d.ts +28 -1
  188. package/dist/server/ws/terminal-ws-hub.d.ts +2 -0
  189. package/dist/server/ws/terminal-ws-hub.js +42 -5
  190. package/dist/server/ws/terminal-ws-hub.js.map +1 -1
  191. package/dist/server/ws/workbench-ws-hub.d.ts +9 -1
  192. package/dist/server/ws/workbench-ws-hub.js +132 -21
  193. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  194. package/dist/server/ws/ws-server.d.ts +22 -1
  195. package/dist/server/ws/ws-server.js +79 -43
  196. package/dist/server/ws/ws-server.js.map +1 -1
  197. package/node_modules/@codingns/session-sync-core/dist/claude-message-utils.js +42 -0
  198. package/node_modules/@codingns/session-sync-core/dist/claude-message-utils.js.map +1 -1
  199. package/node_modules/@codingns/session-sync-core/dist/patch-builder.d.ts +30 -0
  200. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js +103 -0
  201. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js.map +1 -0
  202. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +1 -1
  203. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +2 -1
  204. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  205. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +1 -1
  206. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +1 -1
  207. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  208. package/node_modules/@codingns/session-sync-core/dist/providers/opencode-permissions.d.ts +1 -0
  209. package/node_modules/@codingns/session-sync-core/dist/providers/opencode-permissions.js +8 -0
  210. package/node_modules/@codingns/session-sync-core/dist/providers/opencode-permissions.js.map +1 -0
  211. package/node_modules/@codingns/session-sync-core/dist/providers/opencode-shared.js +31 -0
  212. package/node_modules/@codingns/session-sync-core/dist/providers/opencode-shared.js.map +1 -1
  213. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +1 -1
  214. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +8 -4
  215. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  216. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.d.ts +1 -0
  217. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +5 -0
  218. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
  219. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.d.ts +5 -0
  220. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js +82 -1
  221. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js.map +1 -1
  222. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +23 -0
  223. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +688 -17
  224. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  225. package/node_modules/@codingns/session-sync-core/dist/runtime/opencode-runtime.d.ts +3 -0
  226. package/node_modules/@codingns/session-sync-core/dist/runtime/opencode-runtime.js +106 -23
  227. package/node_modules/@codingns/session-sync-core/dist/runtime/opencode-runtime.js.map +1 -1
  228. package/node_modules/@codingns/session-sync-core/dist/runtime/provider-runtime-service.d.ts +1 -0
  229. package/node_modules/@codingns/session-sync-core/dist/runtime/provider-runtime-service.js +3 -0
  230. package/node_modules/@codingns/session-sync-core/dist/runtime/provider-runtime-service.js.map +1 -1
  231. package/node_modules/@codingns/session-sync-core/dist/services.d.ts +1 -1
  232. package/node_modules/@codingns/session-sync-core/dist/services.js +2 -2
  233. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  234. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +1 -1
  235. package/package.json +3 -2
  236. package/scripts/postinstall.mjs +15 -8
  237. package/dist/public/assets/index-BUPByQPG.css +0 -1
  238. package/dist/public/assets/index-D1CwTkI2.js +0 -106
@@ -0,0 +1,1615 @@
1
+ import { join } from "node:path";
2
+ import { AppError } from "../../shared/errors/app-error.js";
3
+ import { logPermissionDebug } from "../../shared/utils/permission-debug-log.js";
4
+ import { createId } from "../../shared/utils/id.js";
5
+ import { nowIso } from "../../shared/utils/time.js";
6
+ const CLAUDE_PRE_TOOL_USE_TIMEOUT_MS = 90_000;
7
+ const OPENCODE_RECONNECT_DELAY_MS = 1_500;
8
+ export class SessionPermissionRequestService {
9
+ sessionHistoryService;
10
+ sessionBindingRepository;
11
+ authUserRepository;
12
+ workspaceService;
13
+ config;
14
+ emitEnvelope;
15
+ resolveActiveClaudeSession;
16
+ requestsById = new Map();
17
+ requestIdsBySessionId = new Map();
18
+ opencodeWatcherAbortControllers = new Map();
19
+ claudeAllowedScopeKeysBySessionId = new Map();
20
+ constructor(sessionHistoryService, sessionBindingRepository, authUserRepository, workspaceService, config, emitEnvelope, resolveActiveClaudeSession) {
21
+ this.sessionHistoryService = sessionHistoryService;
22
+ this.sessionBindingRepository = sessionBindingRepository;
23
+ this.authUserRepository = authUserRepository;
24
+ this.workspaceService = workspaceService;
25
+ this.config = config;
26
+ this.emitEnvelope = emitEnvelope;
27
+ this.resolveActiveClaudeSession = resolveActiveClaudeSession;
28
+ }
29
+ async dispose() {
30
+ for (const controller of this.opencodeWatcherAbortControllers.values()) {
31
+ controller.abort();
32
+ }
33
+ this.opencodeWatcherAbortControllers.clear();
34
+ for (const request of this.requestsById.values()) {
35
+ if (request.source.kind === "claude-pre-tool-use" && request.source.timer) {
36
+ clearTimeout(request.source.timer);
37
+ }
38
+ }
39
+ this.requestsById.clear();
40
+ this.requestIdsBySessionId.clear();
41
+ this.claudeAllowedScopeKeysBySessionId.clear();
42
+ }
43
+ async listSessionPermissionRequests(sessionId, userId) {
44
+ const session = this.sessionHistoryService.getSession(sessionId, userId);
45
+ if (session.provider === "opencode") {
46
+ await this.startOpenCodeWatchers(session);
47
+ await this.refreshOpenCodePermissionRequests(session).catch(() => {
48
+ return;
49
+ });
50
+ }
51
+ const requests = this.getSessionRequestViews(sessionId);
52
+ logPermissionDebug("permission_request.list", {
53
+ sessionId,
54
+ provider: session.provider,
55
+ count: requests.length
56
+ });
57
+ return requests;
58
+ }
59
+ async replyToSessionPermissionRequest(sessionId, userId, requestId, input) {
60
+ this.sessionHistoryService.getSession(sessionId, userId);
61
+ const request = this.getRequestOrThrow(sessionId, requestId);
62
+ if (request.status !== "pending") {
63
+ throw new AppError({
64
+ statusCode: 409,
65
+ errorCode: "PERMISSION_REQUEST_ALREADY_RESOLVED",
66
+ detail: "该权限申请已经处理过了",
67
+ field: "requestId"
68
+ });
69
+ }
70
+ if (request.source.kind === "claude-pre-tool-use") {
71
+ const action = normalizeText(input.action);
72
+ if (action !== "allow" && action !== "allow_session" && action !== "deny") {
73
+ throw new AppError({
74
+ statusCode: 400,
75
+ errorCode: "INVALID_INPUT",
76
+ detail: "Claude 权限申请只支持 allow、allow_session 或 deny",
77
+ field: "action"
78
+ });
79
+ }
80
+ if (request.source.timer) {
81
+ clearTimeout(request.source.timer);
82
+ }
83
+ if (action === "allow_session") {
84
+ this.addClaudeAllowedScopeKey(request);
85
+ logPermissionDebug("claude_permission.pre_tool_use.session_default_allow", {
86
+ sessionId: request.sessionId,
87
+ providerSessionId: request.providerSessionId,
88
+ requestId: request.id,
89
+ scopeKey: buildClaudeAllowedScopeKey(request)
90
+ });
91
+ }
92
+ request.source.resolve({
93
+ action: action === "deny" ? "deny" : "allow"
94
+ });
95
+ return await this.markResolved(request, action === "deny" ? "declined" : "approved");
96
+ }
97
+ if (request.source.kind === "opencode") {
98
+ const action = normalizeText(input.action);
99
+ if (action !== "once" && action !== "always" && action !== "reject") {
100
+ throw new AppError({
101
+ statusCode: 400,
102
+ errorCode: "INVALID_INPUT",
103
+ detail: "OpenCode 权限申请只支持 once、always 或 reject",
104
+ field: "action"
105
+ });
106
+ }
107
+ await this.replyToOpenCodePermission(request.source.baseUrl, request.providerSessionId, request.source.permissionId, action);
108
+ return await this.markResolved(request, action === "reject" ? "declined" : "approved");
109
+ }
110
+ const responsePayload = buildCodexServerRequestResponsePayload(request, input);
111
+ if (!request.source.resolve) {
112
+ throw new AppError({
113
+ statusCode: 409,
114
+ errorCode: "PERMISSION_REQUEST_REPLY_NOT_SUPPORTED",
115
+ detail: "当前 Codex 请求没有挂起中的回写通道",
116
+ field: "requestId"
117
+ });
118
+ }
119
+ request.source.resolve(responsePayload);
120
+ return await this.markResolved(request, resolveCodexReplyStatus(request.kind, input.action));
121
+ }
122
+ async handleClaudePreToolUse(payload) {
123
+ logPermissionDebug("claude_permission.pre_tool_use.begin", {
124
+ providerSessionId: payload.session_id ?? null,
125
+ cwd: payload.cwd ?? null,
126
+ toolName: payload.tool_name ?? null,
127
+ transcriptPath: payload.transcript_path ?? null
128
+ });
129
+ const providerSessionId = requireNonEmptyText(payload.session_id, "session_id");
130
+ const workspacePath = requireNonEmptyText(payload.cwd, "cwd");
131
+ const workspace = this.workspaceService.findWorkspaceByPath(workspacePath);
132
+ if (!workspace) {
133
+ logPermissionDebug("claude_permission.pre_tool_use.workspace_not_found", {
134
+ providerSessionId,
135
+ cwd: workspacePath
136
+ });
137
+ return {
138
+ accepted: true,
139
+ ignored: true,
140
+ sessionId: null,
141
+ bridgeResponse: buildClaudePreToolUseBridgeResponse("ask", "未匹配到工作区,回退 Claude 原生确认")
142
+ };
143
+ }
144
+ const transcriptPath = normalizeText(payload.transcript_path) || null;
145
+ const binding = await this.resolveClaudeBinding(providerSessionId, workspace.id, workspace.path, transcriptPath).catch(() => null)
146
+ ?? this.resolveClaudeWorkspaceSessionFallback({
147
+ providerSessionId,
148
+ workspaceId: workspace.id,
149
+ workspacePath: workspace.path,
150
+ transcriptPath
151
+ })
152
+ ?? (this.resolveActiveClaudeSession
153
+ ? await this.resolveActiveClaudeSession({
154
+ providerSessionId,
155
+ workspaceId: workspace.id,
156
+ workspacePath: workspace.path,
157
+ transcriptPath
158
+ }).catch(() => null)
159
+ : null);
160
+ logPermissionDebug("claude_permission.pre_tool_use.binding_resolution", {
161
+ providerSessionId,
162
+ workspaceId: workspace.id,
163
+ sessionId: binding?.sessionId ?? null,
164
+ rawStoreRef: binding?.rawStoreRef ?? null
165
+ });
166
+ if (!binding) {
167
+ logPermissionDebug("claude_permission.pre_tool_use.binding_missing", {
168
+ providerSessionId,
169
+ workspaceId: workspace.id
170
+ });
171
+ return {
172
+ accepted: true,
173
+ ignored: true,
174
+ sessionId: null,
175
+ bridgeResponse: buildClaudePreToolUseBridgeResponse("ask", "CodingNS 未找到会话绑定,回退 Claude 原生确认")
176
+ };
177
+ }
178
+ const now = nowIso();
179
+ const normalized = normalizeClaudePreToolUseRequest({
180
+ sessionId: binding.sessionId,
181
+ providerSessionId,
182
+ payload,
183
+ createdAt: now
184
+ });
185
+ const allowedScopeKey = buildClaudeAllowedScopeKey(normalized);
186
+ if (allowedScopeKey && this.isClaudeScopeAllowed(binding.sessionId, allowedScopeKey)) {
187
+ logPermissionDebug("claude_permission.pre_tool_use.auto_allow_session", {
188
+ sessionId: binding.sessionId,
189
+ providerSessionId,
190
+ toolName: payload.tool_name ?? null,
191
+ scopeKey: allowedScopeKey
192
+ });
193
+ return {
194
+ accepted: true,
195
+ ignored: false,
196
+ sessionId: binding.sessionId,
197
+ bridgeResponse: buildClaudePreToolUseBridgeResponse("allow", "CodingNS 已按本会话默认允许自动放行")
198
+ };
199
+ }
200
+ let resolvedByTimeout = false;
201
+ const decision = await new Promise((resolve) => {
202
+ const timer = setTimeout(() => {
203
+ resolvedByTimeout = true;
204
+ resolve({ action: "ask" });
205
+ }, CLAUDE_PRE_TOOL_USE_TIMEOUT_MS);
206
+ const record = {
207
+ ...normalized,
208
+ source: {
209
+ kind: "claude-pre-tool-use",
210
+ resolve,
211
+ timer
212
+ }
213
+ };
214
+ this.upsertRequest(record);
215
+ logPermissionDebug("claude_permission.pre_tool_use.request_created", {
216
+ requestId: record.id,
217
+ sessionId: binding.sessionId,
218
+ providerSessionId,
219
+ title: record.title,
220
+ kind: record.kind
221
+ });
222
+ void this.emitEnvelope({
223
+ type: "session.permission_request",
224
+ sessionId: binding.sessionId,
225
+ request: this.toRequestView(record)
226
+ });
227
+ });
228
+ if (decision.action === "ask") {
229
+ const existing = this.requestsById.get(normalized.id);
230
+ if (existing) {
231
+ await this.markResolved(existing, "expired");
232
+ }
233
+ }
234
+ return {
235
+ accepted: true,
236
+ ignored: false,
237
+ sessionId: binding.sessionId,
238
+ bridgeResponse: buildClaudePreToolUseBridgeResponse(decision.action, buildClaudeDecisionReason(decision.action, normalized.title, resolvedByTimeout))
239
+ };
240
+ }
241
+ async handleClaudePermissionRequest(payload) {
242
+ logPermissionDebug("claude_permission.permission_request.begin", {
243
+ providerSessionId: payload.session_id ?? null,
244
+ cwd: payload.cwd ?? null,
245
+ toolName: payload.tool_name ?? null,
246
+ transcriptPath: payload.transcript_path ?? null,
247
+ suggestions: payload.permission_suggestions ?? null
248
+ });
249
+ const providerSessionId = requireNonEmptyText(payload.session_id, "session_id");
250
+ const workspacePath = requireNonEmptyText(payload.cwd, "cwd");
251
+ const workspace = this.workspaceService.findWorkspaceByPath(workspacePath);
252
+ if (!workspace) {
253
+ logPermissionDebug("claude_permission.permission_request.workspace_not_found", {
254
+ providerSessionId,
255
+ cwd: workspacePath
256
+ });
257
+ return {
258
+ accepted: true,
259
+ ignored: true,
260
+ sessionId: null,
261
+ bridgeResponse: buildClaudePermissionRequestBridgeResponse("deny", "未匹配到工作区,拒绝本次权限申请")
262
+ };
263
+ }
264
+ const transcriptPath = normalizeText(payload.transcript_path) || null;
265
+ const binding = await this.resolveClaudeBinding(providerSessionId, workspace.id, workspace.path, transcriptPath).catch(() => null)
266
+ ?? this.resolveClaudeWorkspaceSessionFallback({
267
+ providerSessionId,
268
+ workspaceId: workspace.id,
269
+ workspacePath: workspace.path,
270
+ transcriptPath
271
+ })
272
+ ?? (this.resolveActiveClaudeSession
273
+ ? await this.resolveActiveClaudeSession({
274
+ providerSessionId,
275
+ workspaceId: workspace.id,
276
+ workspacePath: workspace.path,
277
+ transcriptPath
278
+ }).catch(() => null)
279
+ : null);
280
+ logPermissionDebug("claude_permission.permission_request.binding_resolution", {
281
+ providerSessionId,
282
+ workspaceId: workspace.id,
283
+ sessionId: binding?.sessionId ?? null,
284
+ rawStoreRef: binding?.rawStoreRef ?? null
285
+ });
286
+ if (!binding) {
287
+ logPermissionDebug("claude_permission.permission_request.binding_missing", {
288
+ providerSessionId,
289
+ workspaceId: workspace.id
290
+ });
291
+ return {
292
+ accepted: true,
293
+ ignored: true,
294
+ sessionId: null,
295
+ bridgeResponse: buildClaudePermissionRequestBridgeResponse("deny", "CodingNS 未找到会话绑定,拒绝本次权限申请")
296
+ };
297
+ }
298
+ const now = nowIso();
299
+ const normalized = normalizeClaudePreToolUseRequest({
300
+ sessionId: binding.sessionId,
301
+ providerSessionId,
302
+ payload,
303
+ createdAt: now
304
+ });
305
+ let resolvedByTimeout = false;
306
+ const decision = await new Promise((resolve) => {
307
+ const timer = setTimeout(() => {
308
+ resolvedByTimeout = true;
309
+ resolve({ action: "deny" });
310
+ }, CLAUDE_PRE_TOOL_USE_TIMEOUT_MS);
311
+ const record = {
312
+ ...normalized,
313
+ source: {
314
+ kind: "claude-pre-tool-use",
315
+ resolve,
316
+ timer
317
+ }
318
+ };
319
+ this.upsertRequest(record);
320
+ logPermissionDebug("claude_permission.permission_request.request_created", {
321
+ requestId: record.id,
322
+ sessionId: binding.sessionId,
323
+ providerSessionId,
324
+ title: record.title,
325
+ kind: record.kind
326
+ });
327
+ void this.emitEnvelope({
328
+ type: "session.permission_request",
329
+ sessionId: binding.sessionId,
330
+ request: this.toRequestView(record)
331
+ });
332
+ });
333
+ const status = decision.action === "allow" ? "approved" : "declined";
334
+ const existing = this.requestsById.get(normalized.id);
335
+ if (existing) {
336
+ await this.markResolved(existing, resolvedByTimeout ? "expired" : status);
337
+ }
338
+ return {
339
+ accepted: true,
340
+ ignored: false,
341
+ sessionId: binding.sessionId,
342
+ bridgeResponse: buildClaudePermissionRequestBridgeResponse(decision.action === "allow" ? "allow" : "deny", decision.action === "allow"
343
+ ? "CodingNS 已批准本次权限申请"
344
+ : resolvedByTimeout
345
+ ? "CodingNS 审批超时,拒绝本次权限申请"
346
+ : "CodingNS 已拒绝本次权限申请")
347
+ };
348
+ }
349
+ ingestCodexServerRequest(sessionId, providerSessionId, request) {
350
+ const normalized = normalizeCodexServerRequest(sessionId, providerSessionId, request);
351
+ if (!normalized) {
352
+ return null;
353
+ }
354
+ const record = {
355
+ ...normalized,
356
+ source: {
357
+ kind: "codex-app-server",
358
+ method: normalizeText(request.method) || "unknown",
359
+ resolve: () => undefined
360
+ }
361
+ };
362
+ this.upsertRequest(record);
363
+ void this.emitEnvelope({
364
+ type: "session.permission_request",
365
+ sessionId,
366
+ request: this.toRequestView(record)
367
+ });
368
+ return this.toRequestView(record);
369
+ }
370
+ async handleCodexServerRequest(sessionId, providerSessionId, request) {
371
+ const normalized = normalizeCodexServerRequest(sessionId, providerSessionId, request);
372
+ if (!normalized) {
373
+ throw new AppError({
374
+ statusCode: 400,
375
+ errorCode: "UNSUPPORTED_CODEX_SERVER_REQUEST",
376
+ detail: "当前暂不支持这类 Codex app-server 请求"
377
+ });
378
+ }
379
+ return await new Promise((resolve) => {
380
+ const record = {
381
+ ...normalized,
382
+ source: {
383
+ kind: "codex-app-server",
384
+ method: normalizeText(request.method) || "unknown",
385
+ resolve
386
+ }
387
+ };
388
+ this.upsertRequest(record);
389
+ void this.emitEnvelope({
390
+ type: "session.permission_request",
391
+ sessionId,
392
+ request: this.toRequestView(record)
393
+ });
394
+ });
395
+ }
396
+ async startOpenCodeWatchers(session) {
397
+ const workspacePath = this.workspaceService.getWorkspaceOrThrow(session.workspaceId).path;
398
+ const baseUrls = await this.resolveOpenCodeBaseUrls(false, workspacePath);
399
+ logPermissionDebug("opencode_permission.watchers.ensure", {
400
+ sessionId: session.sessionId,
401
+ providerSessionId: session.providerSessionId,
402
+ workspacePath,
403
+ baseUrls
404
+ });
405
+ for (const baseUrl of baseUrls) {
406
+ if (this.opencodeWatcherAbortControllers.has(baseUrl)) {
407
+ continue;
408
+ }
409
+ const controller = new AbortController();
410
+ this.opencodeWatcherAbortControllers.set(baseUrl, controller);
411
+ void this.consumeOpenCodeEvents(baseUrl, workspacePath, controller.signal)
412
+ .finally(() => {
413
+ if (this.opencodeWatcherAbortControllers.get(baseUrl) === controller) {
414
+ this.opencodeWatcherAbortControllers.delete(baseUrl);
415
+ }
416
+ });
417
+ }
418
+ }
419
+ async consumeOpenCodeEvents(baseUrl, workspacePath, signal) {
420
+ while (!signal.aborted) {
421
+ try {
422
+ logPermissionDebug("opencode_permission.watch.connecting", {
423
+ baseUrl,
424
+ workspacePath
425
+ });
426
+ const url = new URL("/event", `${baseUrl}/`);
427
+ const response = await fetch(url, { signal });
428
+ if (!response.body) {
429
+ throw new Error("OPENCODE_EVENT_STREAM_UNAVAILABLE");
430
+ }
431
+ logPermissionDebug("opencode_permission.watch.connected", {
432
+ baseUrl,
433
+ workspacePath
434
+ });
435
+ const reader = response.body.getReader();
436
+ const decoder = new TextDecoder();
437
+ let buffer = "";
438
+ try {
439
+ while (!signal.aborted) {
440
+ const next = await reader.read();
441
+ if (next.done) {
442
+ break;
443
+ }
444
+ buffer += decoder.decode(next.value, { stream: true });
445
+ while (true) {
446
+ const separatorIndex = buffer.indexOf("\n\n");
447
+ if (separatorIndex < 0) {
448
+ break;
449
+ }
450
+ const frame = buffer.slice(0, separatorIndex);
451
+ buffer = buffer.slice(separatorIndex + 2);
452
+ const payload = extractSseData(frame);
453
+ if (!payload) {
454
+ continue;
455
+ }
456
+ const rawEvent = JSON.parse(payload);
457
+ const event = unwrapOpenCodeEventPayload(rawEvent);
458
+ if (!event) {
459
+ continue;
460
+ }
461
+ await this.handleOpenCodeEvent(event, {
462
+ baseUrl,
463
+ workspacePath
464
+ });
465
+ }
466
+ }
467
+ }
468
+ finally {
469
+ reader.releaseLock();
470
+ }
471
+ }
472
+ catch (error) {
473
+ if (signal.aborted) {
474
+ return;
475
+ }
476
+ logPermissionDebug("opencode_permission.watch.reconnect", {
477
+ baseUrl,
478
+ workspacePath,
479
+ detail: error instanceof Error ? error.message : "unknown"
480
+ });
481
+ await waitForDelay(OPENCODE_RECONNECT_DELAY_MS, signal).catch(() => {
482
+ return;
483
+ });
484
+ }
485
+ }
486
+ }
487
+ async handleOpenCodeEvent(event, input) {
488
+ const eventType = normalizeText(event.type);
489
+ logPermissionDebug("opencode_permission.event", {
490
+ eventType,
491
+ baseUrl: input.baseUrl,
492
+ workspacePath: input.workspacePath
493
+ });
494
+ if (eventType === "permission.updated" || eventType === "permission.asked") {
495
+ await this.handleOpenCodePermissionUpdated(event.properties, input);
496
+ return;
497
+ }
498
+ if (eventType === "permission.replied") {
499
+ const properties = toRecord(event.properties);
500
+ const providerSessionId = normalizeText(properties?.sessionID);
501
+ const requestKey = normalizeText(properties?.permissionID) ||
502
+ normalizeText(properties?.requestID);
503
+ if (!providerSessionId || !requestKey) {
504
+ return;
505
+ }
506
+ const request = this.findRequestByKey("opencode", providerSessionId, requestKey);
507
+ if (!request || request.status !== "pending") {
508
+ return;
509
+ }
510
+ const response = normalizeText(properties?.response) || "reject";
511
+ await this.markResolved(request, response === "reject" ? "declined" : "approved");
512
+ }
513
+ }
514
+ async handleOpenCodePermissionUpdated(rawPermission, input) {
515
+ const permission = toRecord(rawPermission);
516
+ const providerSessionId = normalizeText(permission?.sessionID);
517
+ const requestKey = normalizeText(permission?.id);
518
+ if (!permission || !providerSessionId || !requestKey) {
519
+ logPermissionDebug("opencode_permission.updated.invalid_payload", {
520
+ providerSessionId,
521
+ requestKey,
522
+ baseUrl: input.baseUrl,
523
+ rawPermission
524
+ });
525
+ return;
526
+ }
527
+ const session = await this.resolveOpenCodeSession(providerSessionId, input);
528
+ if (!session) {
529
+ return;
530
+ }
531
+ const normalized = normalizeOpenCodePermissionRequest({
532
+ sessionId: session.sessionId,
533
+ providerSessionId,
534
+ permission,
535
+ createdAt: extractOpenCodePermissionCreatedAt(permission) ?? nowIso()
536
+ });
537
+ const existing = this.findRequestByKey("opencode", providerSessionId, requestKey);
538
+ const record = {
539
+ ...(existing ?? normalized),
540
+ ...normalized,
541
+ id: existing?.id ?? normalized.id,
542
+ createdAt: existing?.createdAt ?? normalized.createdAt,
543
+ updatedAt: nowIso(),
544
+ source: {
545
+ kind: "opencode",
546
+ permissionId: requestKey,
547
+ baseUrl: input.baseUrl
548
+ }
549
+ };
550
+ this.upsertRequest(record);
551
+ logPermissionDebug("opencode_permission.request_created", {
552
+ requestId: record.id,
553
+ sessionId: session.sessionId,
554
+ providerSessionId,
555
+ requestKey,
556
+ baseUrl: input.baseUrl,
557
+ title: record.title,
558
+ kind: record.kind
559
+ });
560
+ await this.emitEnvelope({
561
+ type: "session.permission_request",
562
+ sessionId: session.sessionId,
563
+ request: this.toRequestView(record)
564
+ });
565
+ }
566
+ async refreshOpenCodePermissionRequests(session) {
567
+ const workspacePath = this.workspaceService.getWorkspaceOrThrow(session.workspaceId).path;
568
+ const permissions = await this.fetchOpenCodePermissions(session.providerSessionId, workspacePath);
569
+ logPermissionDebug("opencode_permission.refresh", {
570
+ sessionId: session.sessionId,
571
+ providerSessionId: session.providerSessionId,
572
+ workspacePath,
573
+ count: permissions.length,
574
+ baseUrls: permissions.map((entry) => entry.baseUrl)
575
+ });
576
+ for (const entry of permissions) {
577
+ await this.handleOpenCodePermissionUpdated(entry.permission, {
578
+ baseUrl: entry.baseUrl,
579
+ workspacePath
580
+ });
581
+ }
582
+ }
583
+ async replyToOpenCodePermission(baseUrl, providerSessionId, permissionId, action) {
584
+ const normalizedBaseUrl = `${baseUrl.replace(/\/+$/, "")}/`;
585
+ const requests = [
586
+ {
587
+ url: new URL(`/permission/${encodeURIComponent(permissionId)}/reply`, normalizedBaseUrl),
588
+ body: {
589
+ reply: action
590
+ }
591
+ },
592
+ {
593
+ url: new URL(`/session/${encodeURIComponent(providerSessionId)}/permissions/${encodeURIComponent(permissionId)}`, normalizedBaseUrl),
594
+ body: {
595
+ response: action
596
+ }
597
+ }
598
+ ];
599
+ for (const candidate of requests) {
600
+ const response = await fetch(candidate.url, {
601
+ method: "POST",
602
+ headers: {
603
+ "content-type": "application/json"
604
+ },
605
+ body: JSON.stringify(candidate.body)
606
+ });
607
+ if (response.ok) {
608
+ return;
609
+ }
610
+ if (response.status !== 404) {
611
+ throw new AppError({
612
+ statusCode: 502,
613
+ errorCode: "OPENCODE_PERMISSION_REPLY_FAILED",
614
+ detail: (await safeReadResponseText(response)) || "OpenCode 权限回复失败"
615
+ });
616
+ }
617
+ }
618
+ throw new AppError({
619
+ statusCode: 502,
620
+ errorCode: "OPENCODE_PERMISSION_REPLY_FAILED",
621
+ detail: "OpenCode 权限回复接口不存在"
622
+ });
623
+ }
624
+ async fetchOpenCodePermissions(providerSessionId, workspacePath) {
625
+ const baseUrls = await this.resolveOpenCodeBaseUrls(false, workspacePath);
626
+ const results = [];
627
+ const seenPermissionIds = new Set();
628
+ for (const baseUrl of baseUrls) {
629
+ const candidates = [
630
+ new URL(`/permission?sessionID=${encodeURIComponent(providerSessionId)}`, `${baseUrl}/`),
631
+ new URL(`/session/${encodeURIComponent(providerSessionId)}/permissions`, `${baseUrl}/`)
632
+ ];
633
+ for (const url of candidates) {
634
+ const response = await fetch(url);
635
+ if (!response.ok) {
636
+ if (response.status === 404) {
637
+ continue;
638
+ }
639
+ logPermissionDebug("opencode_permission.fetch.failed", {
640
+ providerSessionId,
641
+ workspacePath,
642
+ baseUrl,
643
+ endpoint: url.pathname,
644
+ status: response.status
645
+ });
646
+ break;
647
+ }
648
+ const payload = (await response.json());
649
+ const permissions = Array.isArray(payload) ? payload : [];
650
+ for (const permission of permissions) {
651
+ const permissionId = normalizeText(permission.id);
652
+ if (!permissionId || seenPermissionIds.has(permissionId)) {
653
+ continue;
654
+ }
655
+ seenPermissionIds.add(permissionId);
656
+ results.push({
657
+ permission,
658
+ baseUrl
659
+ });
660
+ }
661
+ logPermissionDebug("opencode_permission.fetch.success", {
662
+ providerSessionId,
663
+ workspacePath,
664
+ baseUrl,
665
+ endpoint: url.pathname,
666
+ count: permissions.length
667
+ });
668
+ break;
669
+ }
670
+ }
671
+ return results;
672
+ }
673
+ async fetchOpenCodeSession(providerSessionId, baseUrl) {
674
+ const url = new URL(`/session/${encodeURIComponent(providerSessionId)}`, `${baseUrl}/`);
675
+ const response = await fetch(url);
676
+ if (!response.ok) {
677
+ return null;
678
+ }
679
+ return (await response.json());
680
+ }
681
+ async resolveOpenCodeSession(providerSessionId, input) {
682
+ const binding = this.sessionBindingRepository.findByProviderSession("opencode", providerSessionId);
683
+ if (binding) {
684
+ const userId = this.authUserRepository.listIds()[0] ?? null;
685
+ if (!userId) {
686
+ return null;
687
+ }
688
+ return this.sessionHistoryService.getSession(binding.sessionId, userId);
689
+ }
690
+ const summary = await this.fetchOpenCodeSession(providerSessionId, input.baseUrl);
691
+ const workspacePath = normalizeText(summary?.directory);
692
+ if (!workspacePath) {
693
+ return null;
694
+ }
695
+ const workspace = this.workspaceService.findWorkspaceByPath(workspacePath);
696
+ const userId = this.authUserRepository.listIds()[0] ?? null;
697
+ if (!workspace || !userId) {
698
+ return null;
699
+ }
700
+ await this.sessionHistoryService.discoverWorkspaceSessions(workspace.id, userId, {
701
+ force: true,
702
+ refreshStateMode: "deferred"
703
+ }).catch(() => {
704
+ return;
705
+ });
706
+ const discoveredBinding = this.sessionBindingRepository.findByProviderSession("opencode", providerSessionId);
707
+ if (!discoveredBinding) {
708
+ return null;
709
+ }
710
+ return this.sessionHistoryService.getSession(discoveredBinding.sessionId, userId);
711
+ }
712
+ resolveClaudeWorkspaceSessionFallback(input) {
713
+ const userId = this.authUserRepository.listIds()[0] ?? null;
714
+ if (!userId) {
715
+ return null;
716
+ }
717
+ const activeClaudeSessions = this.sessionHistoryService
718
+ .listWorkspaceSessions(input.workspaceId, userId)
719
+ .filter((session) => session.provider === "claude-code");
720
+ const preferredSession = activeClaudeSessions.find((session) => session.runningState === "starting" || session.runningState === "running")
721
+ ?? [...activeClaudeSessions]
722
+ .filter((session) => session.isArchived !== true)
723
+ .sort((left, right) => (right.lastEventAt ?? right.updatedAt ?? "").localeCompare(left.lastEventAt ?? left.updatedAt ?? ""))[0]
724
+ ?? (activeClaudeSessions.length === 1 ? activeClaudeSessions[0] : null);
725
+ if (!preferredSession) {
726
+ return null;
727
+ }
728
+ const rawStoreRef = input.transcriptPath ??
729
+ preferredSession.rawStoreRef ??
730
+ buildClaudeRawStoreRef(this.config.claudeCodeHomeDir, input.workspacePath, input.providerSessionId);
731
+ this.sessionHistoryService.persistSessionBinding(preferredSession.sessionId, input.workspaceId, {
732
+ provider: "claude-code",
733
+ providerSessionId: input.providerSessionId,
734
+ rawStoreRef
735
+ });
736
+ return {
737
+ sessionId: preferredSession.sessionId,
738
+ rawStoreRef
739
+ };
740
+ }
741
+ async resolveClaudeBinding(providerSessionId, workspaceId, workspacePath, transcriptPath) {
742
+ const rawStoreRef = transcriptPath ??
743
+ this.sessionBindingRepository.findByProviderSession("claude-code", providerSessionId)?.rawStoreRef ??
744
+ buildClaudeRawStoreRef(this.config.claudeCodeHomeDir, workspacePath, providerSessionId);
745
+ const existing = this.sessionBindingRepository.findByProviderSession("claude-code", providerSessionId) ??
746
+ this.sessionBindingRepository.findByRawStoreRef("claude-code", rawStoreRef);
747
+ if (existing) {
748
+ return {
749
+ sessionId: existing.sessionId,
750
+ rawStoreRef: existing.rawStoreRef
751
+ };
752
+ }
753
+ const userId = this.authUserRepository.listIds()[0] ?? null;
754
+ if (userId) {
755
+ await this.sessionHistoryService.discoverWorkspaceSessions(workspaceId, userId, {
756
+ force: true,
757
+ refreshStateMode: "deferred"
758
+ }).catch(() => {
759
+ return;
760
+ });
761
+ }
762
+ const refreshed = this.sessionBindingRepository.findByProviderSession("claude-code", providerSessionId) ??
763
+ this.sessionBindingRepository.findByRawStoreRef("claude-code", rawStoreRef);
764
+ if (!refreshed) {
765
+ throw new AppError({
766
+ statusCode: 404,
767
+ errorCode: "CLAUDE_SESSION_NOT_FOUND",
768
+ detail: "没有找到对应的 Claude 会话绑定"
769
+ });
770
+ }
771
+ return {
772
+ sessionId: refreshed.sessionId,
773
+ rawStoreRef: refreshed.rawStoreRef
774
+ };
775
+ }
776
+ async resolveOpenCodeBaseUrl(refresh, workspacePath) {
777
+ const resolved = this.config.opencodeBaseUrlResolver
778
+ ? await this.config.opencodeBaseUrlResolver.resolve({
779
+ refresh,
780
+ workspacePath
781
+ })
782
+ : this.config.opencodeBaseUrl;
783
+ const normalized = resolved?.trim() ?? "";
784
+ if (!normalized) {
785
+ throw new Error("OPENCODE_BASE_URL_UNAVAILABLE");
786
+ }
787
+ return normalized.replace(/\/+$/, "");
788
+ }
789
+ async resolveOpenCodeBaseUrls(refresh, workspacePath) {
790
+ const resolver = this.config.opencodeBaseUrlResolver;
791
+ if (resolver) {
792
+ const reachable = await resolver.listReachableBaseUrls({
793
+ refresh,
794
+ workspacePath
795
+ });
796
+ if (reachable.length > 0) {
797
+ return reachable.map((value) => value.replace(/\/+$/, ""));
798
+ }
799
+ }
800
+ return [await this.resolveOpenCodeBaseUrl(refresh, workspacePath)];
801
+ }
802
+ addClaudeAllowedScopeKey(request) {
803
+ const scopeKey = buildClaudeAllowedScopeKey(request);
804
+ if (!scopeKey) {
805
+ return;
806
+ }
807
+ const scopeKeys = this.claudeAllowedScopeKeysBySessionId.get(request.sessionId) ?? new Set();
808
+ scopeKeys.add(scopeKey);
809
+ this.claudeAllowedScopeKeysBySessionId.set(request.sessionId, scopeKeys);
810
+ }
811
+ isClaudeScopeAllowed(sessionId, scopeKey) {
812
+ return this.claudeAllowedScopeKeysBySessionId.get(sessionId)?.has(scopeKey) ?? false;
813
+ }
814
+ upsertRequest(request) {
815
+ const existing = this.requestsById.get(request.id);
816
+ if (existing?.source.kind === "claude-pre-tool-use" && existing.source.timer) {
817
+ clearTimeout(existing.source.timer);
818
+ }
819
+ this.requestsById.set(request.id, request);
820
+ const requestIds = this.requestIdsBySessionId.get(request.sessionId) ?? [];
821
+ if (!requestIds.includes(request.id)) {
822
+ requestIds.push(request.id);
823
+ this.requestIdsBySessionId.set(request.sessionId, requestIds);
824
+ }
825
+ logPermissionDebug("permission_request.upsert", {
826
+ requestId: request.id,
827
+ sessionId: request.sessionId,
828
+ provider: request.provider,
829
+ providerSessionId: request.providerSessionId,
830
+ kind: request.kind,
831
+ status: request.status,
832
+ title: request.title
833
+ });
834
+ }
835
+ async markResolved(request, status) {
836
+ request.status = status;
837
+ request.updatedAt = nowIso();
838
+ request.resolvedAt = request.updatedAt;
839
+ this.requestsById.set(request.id, request);
840
+ const view = this.toRequestView(request);
841
+ logPermissionDebug("permission_request.resolved", {
842
+ requestId: request.id,
843
+ sessionId: request.sessionId,
844
+ provider: request.provider,
845
+ kind: request.kind,
846
+ status
847
+ });
848
+ await this.emitEnvelope({
849
+ type: "session.permission_request_resolved",
850
+ sessionId: request.sessionId,
851
+ request: view
852
+ });
853
+ return view;
854
+ }
855
+ getSessionRequestViews(sessionId) {
856
+ const requestIds = this.requestIdsBySessionId.get(sessionId) ?? [];
857
+ return requestIds
858
+ .map((requestId) => this.requestsById.get(requestId) ?? null)
859
+ .filter((request) => request !== null)
860
+ .sort((left, right) => {
861
+ if (left.status !== right.status) {
862
+ return left.status === "pending" ? -1 : 1;
863
+ }
864
+ return right.createdAt.localeCompare(left.createdAt);
865
+ })
866
+ .map((request) => this.toRequestView(request));
867
+ }
868
+ toRequestView(request) {
869
+ return {
870
+ id: request.id,
871
+ sessionId: request.sessionId,
872
+ provider: request.provider,
873
+ providerSessionId: request.providerSessionId,
874
+ requestKey: request.requestKey,
875
+ kind: request.kind,
876
+ status: request.status,
877
+ title: request.title,
878
+ summary: request.summary,
879
+ detail: request.detail,
880
+ reason: request.reason,
881
+ toolName: request.toolName,
882
+ command: request.command,
883
+ cwd: request.cwd,
884
+ paths: [...request.paths],
885
+ permissionProfile: request.permissionProfile
886
+ ? {
887
+ readPaths: [...request.permissionProfile.readPaths],
888
+ writePaths: [...request.permissionProfile.writePaths],
889
+ networkEnabled: request.permissionProfile.networkEnabled
890
+ }
891
+ : null,
892
+ questions: request.questions.map((question) => ({
893
+ ...question,
894
+ options: question.options.map((option) => ({ ...option }))
895
+ })),
896
+ actions: request.actions.map((action) => ({ ...action })),
897
+ rawPayload: request.rawPayload,
898
+ createdAt: request.createdAt,
899
+ updatedAt: request.updatedAt,
900
+ resolvedAt: request.resolvedAt
901
+ };
902
+ }
903
+ getRequestOrThrow(sessionId, requestId) {
904
+ const request = this.requestsById.get(requestId);
905
+ if (!request || request.sessionId !== sessionId) {
906
+ throw new AppError({
907
+ statusCode: 404,
908
+ errorCode: "PERMISSION_REQUEST_NOT_FOUND",
909
+ detail: "没有找到对应的权限申请",
910
+ field: "requestId"
911
+ });
912
+ }
913
+ return request;
914
+ }
915
+ findRequestByKey(provider, providerSessionId, requestKey) {
916
+ for (const request of this.requestsById.values()) {
917
+ if (request.provider === provider &&
918
+ request.providerSessionId === providerSessionId &&
919
+ request.requestKey === requestKey) {
920
+ return request;
921
+ }
922
+ }
923
+ return null;
924
+ }
925
+ }
926
+ export function normalizeClaudePreToolUseRequest(input) {
927
+ const toolName = normalizeText(input.payload.tool_name) || "tool";
928
+ const toolInput = input.payload.tool_input;
929
+ const rawPayload = stringifyPayload(input.payload);
930
+ const normalized = buildClaudeKind(toolName, toolInput);
931
+ const requestKey = normalizeText(toRecord(toolInput)?.tool_use_id) ||
932
+ normalizeText(toRecord(toolInput)?.id) ||
933
+ `${toolName}:${hashLike(rawPayload)}`;
934
+ return {
935
+ id: `permission-${createId()}`,
936
+ sessionId: input.sessionId,
937
+ provider: "claude-code",
938
+ providerSessionId: input.providerSessionId,
939
+ requestKey,
940
+ kind: normalized.kind,
941
+ status: "pending",
942
+ title: normalized.title,
943
+ summary: normalized.summary,
944
+ detail: normalized.detail,
945
+ reason: normalizeText(input.payload.reason) || null,
946
+ toolName,
947
+ command: normalized.command,
948
+ cwd: normalizeText(toRecord(toolInput)?.cwd) || null,
949
+ paths: normalized.paths,
950
+ permissionProfile: null,
951
+ questions: [],
952
+ actions: buildClaudeActions(normalized.kind),
953
+ rawPayload,
954
+ createdAt: input.createdAt,
955
+ updatedAt: input.createdAt,
956
+ resolvedAt: null,
957
+ source: {
958
+ kind: "claude-pre-tool-use",
959
+ resolve: () => undefined,
960
+ timer: null
961
+ }
962
+ };
963
+ }
964
+ export function normalizeOpenCodePermissionRequest(input) {
965
+ const requestKey = normalizeText(input.permission.id) || createId();
966
+ const title = normalizeText(input.permission.title) || "OpenCode 请求权限";
967
+ const pattern = normalizeOpenCodePattern(input.permission.pattern ?? input.permission.patterns);
968
+ const metadata = toRecord(input.permission.metadata);
969
+ const normalized = buildOpenCodeKind(title, metadata, pattern, normalizeText(input.permission.tool) || normalizeText(input.permission.permission));
970
+ return {
971
+ id: `permission-${createId()}`,
972
+ sessionId: input.sessionId,
973
+ provider: "opencode",
974
+ providerSessionId: input.providerSessionId,
975
+ requestKey,
976
+ kind: normalized.kind,
977
+ status: "pending",
978
+ title,
979
+ summary: normalized.summary,
980
+ detail: normalized.detail,
981
+ reason: normalizeText(metadata?.reason) || null,
982
+ toolName: normalized.toolName,
983
+ command: normalized.command,
984
+ cwd: normalizeText(metadata?.cwd) || null,
985
+ paths: normalized.paths,
986
+ permissionProfile: normalized.permissionProfile,
987
+ questions: [],
988
+ actions: [
989
+ createAction("once", "允许一次", "primary", "只放行这一次"),
990
+ createAction("always", "总是允许", "neutral", "后续匹配请求也默认放行"),
991
+ createAction("reject", "拒绝", "danger", "阻止这次权限申请")
992
+ ],
993
+ rawPayload: stringifyPayload(input.permission),
994
+ createdAt: input.createdAt,
995
+ updatedAt: input.createdAt,
996
+ resolvedAt: null,
997
+ source: {
998
+ kind: "opencode",
999
+ permissionId: requestKey,
1000
+ baseUrl: ""
1001
+ }
1002
+ };
1003
+ }
1004
+ export function normalizeCodexServerRequest(sessionId, providerSessionId, request) {
1005
+ const method = normalizeText(request.method);
1006
+ const params = toRecord(request.params);
1007
+ const createdAt = nowIso();
1008
+ if (!method || !params) {
1009
+ return null;
1010
+ }
1011
+ if (method === "item/commandExecution/requestApproval") {
1012
+ const actions = readCodexCommandActions(params.commandActions);
1013
+ const command = normalizeText(params.command);
1014
+ const host = normalizeText(toRecord(params.networkApprovalContext)?.host);
1015
+ const reason = normalizeText(params.reason);
1016
+ const summary = command || host || "Codex 请求执行命令";
1017
+ return {
1018
+ id: `permission-${createId()}`,
1019
+ sessionId,
1020
+ provider: "codex",
1021
+ providerSessionId,
1022
+ requestKey: normalizeText(params.itemId) || createId(),
1023
+ kind: "command",
1024
+ status: "pending",
1025
+ title: host ? `Codex 请求访问 ${host}` : "Codex 请求执行命令",
1026
+ summary,
1027
+ detail: actions.length > 0 ? stringifyPayload(actions) : reason || null,
1028
+ reason,
1029
+ toolName: null,
1030
+ command,
1031
+ cwd: normalizeText(params.cwd) || null,
1032
+ paths: actions
1033
+ .map((action) => normalizeText(action.path))
1034
+ .filter((value) => Boolean(value)),
1035
+ permissionProfile: null,
1036
+ questions: [],
1037
+ actions: [
1038
+ createAction("accept", "允许", "primary", "只放行这次命令"),
1039
+ createAction("acceptForSession", "本会话放行", "neutral", "相同会话后续不再询问"),
1040
+ createAction("decline", "拒绝", "danger", "拒绝但继续当前轮次"),
1041
+ createAction("cancel", "拒绝并中断", "danger", "拒绝并立即中断当前轮次")
1042
+ ],
1043
+ rawPayload: stringifyPayload(params),
1044
+ createdAt,
1045
+ updatedAt: createdAt,
1046
+ resolvedAt: null,
1047
+ source: {
1048
+ kind: "codex-app-server",
1049
+ method
1050
+ }
1051
+ };
1052
+ }
1053
+ if (method === "item/fileChange/requestApproval") {
1054
+ const path = normalizeText(params.grantRoot);
1055
+ return {
1056
+ id: `permission-${createId()}`,
1057
+ sessionId,
1058
+ provider: "codex",
1059
+ providerSessionId,
1060
+ requestKey: normalizeText(params.itemId) || createId(),
1061
+ kind: "file_change",
1062
+ status: "pending",
1063
+ title: "Codex 请求写入文件",
1064
+ summary: path || "请求放行文件改动",
1065
+ detail: normalizeText(params.reason) || null,
1066
+ reason: normalizeText(params.reason) || null,
1067
+ toolName: null,
1068
+ command: null,
1069
+ cwd: null,
1070
+ paths: path ? [path] : [],
1071
+ permissionProfile: null,
1072
+ questions: [],
1073
+ actions: [
1074
+ createAction("accept", "允许", "primary", "只放行这次改动"),
1075
+ createAction("acceptForSession", "本会话放行", "neutral", "后续相同改动不再询问"),
1076
+ createAction("decline", "拒绝", "danger", "拒绝但继续当前轮次"),
1077
+ createAction("cancel", "拒绝并中断", "danger", "拒绝并立即中断当前轮次")
1078
+ ],
1079
+ rawPayload: stringifyPayload(params),
1080
+ createdAt,
1081
+ updatedAt: createdAt,
1082
+ resolvedAt: null,
1083
+ source: {
1084
+ kind: "codex-app-server",
1085
+ method
1086
+ }
1087
+ };
1088
+ }
1089
+ if (method === "item/permissions/requestApproval") {
1090
+ const permissions = toRecord(params.permissions);
1091
+ const fileSystem = toRecord(permissions?.fileSystem);
1092
+ const network = toRecord(permissions?.network);
1093
+ return {
1094
+ id: `permission-${createId()}`,
1095
+ sessionId,
1096
+ provider: "codex",
1097
+ providerSessionId,
1098
+ requestKey: normalizeText(params.itemId) || createId(),
1099
+ kind: "permissions",
1100
+ status: "pending",
1101
+ title: "Codex 请求附加权限",
1102
+ summary: normalizeText(params.reason) || "请求扩大文件或网络权限",
1103
+ detail: stringifyPayload(params.permissions),
1104
+ reason: normalizeText(params.reason) || null,
1105
+ toolName: null,
1106
+ command: null,
1107
+ cwd: null,
1108
+ paths: [
1109
+ ...readStringArray(fileSystem?.read),
1110
+ ...readStringArray(fileSystem?.write)
1111
+ ],
1112
+ permissionProfile: {
1113
+ readPaths: readStringArray(fileSystem?.read),
1114
+ writePaths: readStringArray(fileSystem?.write),
1115
+ networkEnabled: typeof network?.enabled === "boolean" ? network.enabled : null
1116
+ },
1117
+ questions: [],
1118
+ actions: [
1119
+ createAction("allow_turn", "仅当前轮次放行", "primary", "只把新增权限授予当前轮次"),
1120
+ createAction("allow_session", "本会话放行", "neutral", "把新增权限授予当前会话"),
1121
+ createAction("deny", "拒绝", "danger", "拒绝这次权限扩展")
1122
+ ],
1123
+ rawPayload: stringifyPayload(params),
1124
+ createdAt,
1125
+ updatedAt: createdAt,
1126
+ resolvedAt: null,
1127
+ source: {
1128
+ kind: "codex-app-server",
1129
+ method
1130
+ }
1131
+ };
1132
+ }
1133
+ if (method === "item/tool/requestUserInput") {
1134
+ const questions = Array.isArray(params.questions)
1135
+ ? params.questions
1136
+ .map((question) => toRecord(question))
1137
+ .filter((question) => question !== null)
1138
+ .map((question) => ({
1139
+ id: normalizeText(question.id) || createId(),
1140
+ header: normalizeText(question.header) || "问题",
1141
+ question: normalizeText(question.question) || "请输入答案",
1142
+ allowOther: Boolean(question.isOther),
1143
+ secret: Boolean(question.isSecret),
1144
+ options: Array.isArray(question.options)
1145
+ ? question.options
1146
+ .map((option) => toRecord(option))
1147
+ .filter((option) => option !== null)
1148
+ .map((option) => ({
1149
+ label: normalizeText(option.label) || "选项",
1150
+ description: normalizeText(option.description) || null
1151
+ }))
1152
+ : []
1153
+ }))
1154
+ : [];
1155
+ return {
1156
+ id: `permission-${createId()}`,
1157
+ sessionId,
1158
+ provider: "codex",
1159
+ providerSessionId,
1160
+ requestKey: normalizeText(params.itemId) || createId(),
1161
+ kind: "user_input",
1162
+ status: "pending",
1163
+ title: "Codex 请求补充输入",
1164
+ summary: questions[0]?.question ?? "需要你补充工具输入",
1165
+ detail: null,
1166
+ reason: null,
1167
+ toolName: null,
1168
+ command: null,
1169
+ cwd: null,
1170
+ paths: [],
1171
+ permissionProfile: null,
1172
+ questions,
1173
+ actions: [
1174
+ createAction("submit", "提交", "primary", "提交补充输入")
1175
+ ],
1176
+ rawPayload: stringifyPayload(params),
1177
+ createdAt,
1178
+ updatedAt: createdAt,
1179
+ resolvedAt: null,
1180
+ source: {
1181
+ kind: "codex-app-server",
1182
+ method
1183
+ }
1184
+ };
1185
+ }
1186
+ return null;
1187
+ }
1188
+ function buildClaudePreToolUseBridgeResponse(action, reason) {
1189
+ return {
1190
+ hookSpecificOutput: {
1191
+ hookEventName: "PreToolUse",
1192
+ permissionDecision: action,
1193
+ permissionDecisionReason: reason
1194
+ }
1195
+ };
1196
+ }
1197
+ function buildClaudePermissionRequestBridgeResponse(action, message) {
1198
+ return {
1199
+ hookSpecificOutput: {
1200
+ hookEventName: "PermissionRequest",
1201
+ decision: {
1202
+ behavior: action,
1203
+ message
1204
+ }
1205
+ }
1206
+ };
1207
+ }
1208
+ function buildClaudeDecisionReason(action, title, resolvedByTimeout) {
1209
+ if (action === "allow") {
1210
+ return `CodingNS 已批准:${title}`;
1211
+ }
1212
+ if (action === "deny") {
1213
+ return `CodingNS 已拒绝:${title}`;
1214
+ }
1215
+ return resolvedByTimeout
1216
+ ? "CodingNS 未在超时时间内处理,回退 Claude 原生确认"
1217
+ : "CodingNS 请求回退 Claude 原生确认";
1218
+ }
1219
+ function extractOpenCodePermissionCreatedAt(permission) {
1220
+ const created = toRecord(permission.time)?.created;
1221
+ const numeric = typeof created === "number"
1222
+ ? created
1223
+ : Number.parseInt(normalizeText(created) || "", 10);
1224
+ if (Number.isFinite(numeric) && numeric > 0) {
1225
+ return new Date(numeric).toISOString();
1226
+ }
1227
+ return null;
1228
+ }
1229
+ function buildCodexServerRequestResponsePayload(request, input) {
1230
+ const action = normalizeText(input.action);
1231
+ if (!action) {
1232
+ throw new AppError({
1233
+ statusCode: 400,
1234
+ errorCode: "INVALID_INPUT",
1235
+ detail: "Codex 审批回复必须包含 action",
1236
+ field: "action"
1237
+ });
1238
+ }
1239
+ if (request.kind === "command") {
1240
+ if (action !== "accept" &&
1241
+ action !== "acceptForSession" &&
1242
+ action !== "decline" &&
1243
+ action !== "cancel") {
1244
+ throw new AppError({
1245
+ statusCode: 400,
1246
+ errorCode: "INVALID_INPUT",
1247
+ detail: "命令审批只支持 accept、acceptForSession、decline、cancel",
1248
+ field: "action"
1249
+ });
1250
+ }
1251
+ return {
1252
+ decision: action
1253
+ };
1254
+ }
1255
+ if (request.kind === "file_change") {
1256
+ if (action !== "accept" &&
1257
+ action !== "acceptForSession" &&
1258
+ action !== "decline" &&
1259
+ action !== "cancel") {
1260
+ throw new AppError({
1261
+ statusCode: 400,
1262
+ errorCode: "INVALID_INPUT",
1263
+ detail: "文件改动审批只支持 accept、acceptForSession、decline、cancel",
1264
+ field: "action"
1265
+ });
1266
+ }
1267
+ return {
1268
+ decision: action
1269
+ };
1270
+ }
1271
+ if (request.kind === "permissions") {
1272
+ if (action !== "allow_turn" && action !== "allow_session" && action !== "deny") {
1273
+ throw new AppError({
1274
+ statusCode: 400,
1275
+ errorCode: "INVALID_INPUT",
1276
+ detail: "权限扩展审批只支持 allow_turn、allow_session、deny",
1277
+ field: "action"
1278
+ });
1279
+ }
1280
+ if (action === "deny") {
1281
+ return {
1282
+ permissions: {},
1283
+ scope: "turn"
1284
+ };
1285
+ }
1286
+ return {
1287
+ permissions: {
1288
+ fileSystem: request.permissionProfile
1289
+ ? {
1290
+ read: request.permissionProfile.readPaths,
1291
+ write: request.permissionProfile.writePaths
1292
+ }
1293
+ : null,
1294
+ network: request.permissionProfile?.networkEnabled === null
1295
+ ? null
1296
+ : {
1297
+ enabled: request.permissionProfile?.networkEnabled ?? null
1298
+ }
1299
+ },
1300
+ scope: action === "allow_session" ? "session" : "turn"
1301
+ };
1302
+ }
1303
+ if (request.kind === "user_input") {
1304
+ if (action !== "submit") {
1305
+ throw new AppError({
1306
+ statusCode: 400,
1307
+ errorCode: "INVALID_INPUT",
1308
+ detail: "补充输入请求只支持 submit",
1309
+ field: "action"
1310
+ });
1311
+ }
1312
+ const questionIds = new Set(request.questions.map((question) => question.id));
1313
+ const answers = Object.fromEntries(Object.entries(input.answers ?? {})
1314
+ .filter(([questionId]) => questionIds.has(questionId))
1315
+ .map(([questionId, values]) => [
1316
+ questionId,
1317
+ {
1318
+ answers: values
1319
+ }
1320
+ ]));
1321
+ return {
1322
+ answers
1323
+ };
1324
+ }
1325
+ throw new AppError({
1326
+ statusCode: 409,
1327
+ errorCode: "PERMISSION_REQUEST_REPLY_NOT_SUPPORTED",
1328
+ detail: "当前 Codex 请求类型还不支持直接回复",
1329
+ field: "requestId"
1330
+ });
1331
+ }
1332
+ function resolveCodexReplyStatus(kind, action) {
1333
+ const normalizedAction = normalizeText(action);
1334
+ if (kind === "user_input") {
1335
+ return "approved";
1336
+ }
1337
+ if (normalizedAction === "deny" ||
1338
+ normalizedAction === "decline" ||
1339
+ normalizedAction === "cancel") {
1340
+ return "declined";
1341
+ }
1342
+ return "approved";
1343
+ }
1344
+ function buildClaudeKind(toolName, toolInput) {
1345
+ const normalizedToolName = toolName.trim().toLowerCase();
1346
+ const inputRecord = toRecord(toolInput);
1347
+ const command = normalizeText(inputRecord?.command) ||
1348
+ normalizeText(inputRecord?.cmd) ||
1349
+ null;
1350
+ if (normalizedToolName === "bash" || normalizedToolName === "shell") {
1351
+ return {
1352
+ kind: "command",
1353
+ title: "Claude 请求执行命令",
1354
+ summary: command ?? "Bash 工具需要确认",
1355
+ detail: stringifyPayload(toolInput),
1356
+ command,
1357
+ paths: []
1358
+ };
1359
+ }
1360
+ if (normalizedToolName === "edit" ||
1361
+ normalizedToolName === "write" ||
1362
+ normalizedToolName === "multiedit") {
1363
+ const paths = readClaudePaths(inputRecord);
1364
+ return {
1365
+ kind: "file_change",
1366
+ title: "Claude 请求改动文件",
1367
+ summary: paths[0] ?? `${toolName} 工具需要确认`,
1368
+ detail: stringifyPayload(toolInput),
1369
+ command: null,
1370
+ paths
1371
+ };
1372
+ }
1373
+ return {
1374
+ kind: "tool_call",
1375
+ title: `Claude 请求调用 ${toolName}`,
1376
+ summary: toolName,
1377
+ detail: stringifyPayload(toolInput),
1378
+ command,
1379
+ paths: []
1380
+ };
1381
+ }
1382
+ function buildOpenCodeKind(title, metadata, pattern, fallbackToolName) {
1383
+ const command = normalizeText(metadata?.command);
1384
+ const toolName = normalizeText(metadata?.tool) || fallbackToolName;
1385
+ const path = normalizeText(metadata?.path);
1386
+ const readPaths = readStringArray(toRecord(metadata?.fileSystem)?.read);
1387
+ const writePaths = readStringArray(toRecord(metadata?.fileSystem)?.write);
1388
+ const network = toRecord(metadata?.network);
1389
+ const summary = command || toolName || path || pattern[0] || title;
1390
+ if (command) {
1391
+ return {
1392
+ kind: "command",
1393
+ summary,
1394
+ detail: stringifyPayload(metadata),
1395
+ toolName,
1396
+ command,
1397
+ paths: path ? [path] : [],
1398
+ permissionProfile: null
1399
+ };
1400
+ }
1401
+ if (path || pattern.length > 0 || writePaths.length > 0) {
1402
+ return {
1403
+ kind: "file_change",
1404
+ summary,
1405
+ detail: stringifyPayload(metadata),
1406
+ toolName,
1407
+ command: null,
1408
+ paths: [
1409
+ ...(path ? [path] : []),
1410
+ ...pattern,
1411
+ ...writePaths
1412
+ ],
1413
+ permissionProfile: null
1414
+ };
1415
+ }
1416
+ if (readPaths.length > 0 || writePaths.length > 0 || typeof network?.enabled === "boolean") {
1417
+ return {
1418
+ kind: "permissions",
1419
+ summary,
1420
+ detail: stringifyPayload(metadata),
1421
+ toolName,
1422
+ command: null,
1423
+ paths: [...readPaths, ...writePaths],
1424
+ permissionProfile: {
1425
+ readPaths,
1426
+ writePaths,
1427
+ networkEnabled: typeof network?.enabled === "boolean" ? network.enabled : null
1428
+ }
1429
+ };
1430
+ }
1431
+ return {
1432
+ kind: "tool_call",
1433
+ summary,
1434
+ detail: stringifyPayload(metadata),
1435
+ toolName,
1436
+ command: null,
1437
+ paths: [],
1438
+ permissionProfile: null
1439
+ };
1440
+ }
1441
+ function readClaudePaths(inputRecord) {
1442
+ if (!inputRecord) {
1443
+ return [];
1444
+ }
1445
+ const directPath = normalizeText(inputRecord.file_path) || normalizeText(inputRecord.path);
1446
+ const paths = directPath ? [directPath] : [];
1447
+ const edits = Array.isArray(inputRecord.edits) ? inputRecord.edits : [];
1448
+ for (const edit of edits) {
1449
+ const editPath = normalizeText(toRecord(edit)?.file_path) || normalizeText(toRecord(edit)?.path);
1450
+ if (editPath && !paths.includes(editPath)) {
1451
+ paths.push(editPath);
1452
+ }
1453
+ }
1454
+ return paths;
1455
+ }
1456
+ function readCodexCommandActions(value) {
1457
+ if (!Array.isArray(value)) {
1458
+ return [];
1459
+ }
1460
+ return value
1461
+ .map((item) => toRecord(item))
1462
+ .filter((item) => item !== null);
1463
+ }
1464
+ function normalizeOpenCodePattern(value) {
1465
+ if (typeof value === "string") {
1466
+ return value.trim() ? [value.trim()] : [];
1467
+ }
1468
+ if (!Array.isArray(value)) {
1469
+ return [];
1470
+ }
1471
+ return value
1472
+ .map((item) => normalizeText(item))
1473
+ .filter((item) => Boolean(item));
1474
+ }
1475
+ function createAction(value, label, tone, description) {
1476
+ return {
1477
+ value,
1478
+ label,
1479
+ tone,
1480
+ description
1481
+ };
1482
+ }
1483
+ function buildClaudeAllowedScopeKey(request) {
1484
+ if (request.kind === "command") {
1485
+ const normalizedCommand = normalizeText(request.command);
1486
+ return normalizedCommand ? `command:${normalizedCommand}` : null;
1487
+ }
1488
+ if (request.kind === "file_change") {
1489
+ const normalizedPaths = request.paths
1490
+ .map((path) => normalizeText(path))
1491
+ .filter((path) => Boolean(path))
1492
+ .sort();
1493
+ return normalizedPaths.length > 0 ? `file_change:${normalizedPaths.join("|")}` : null;
1494
+ }
1495
+ return null;
1496
+ }
1497
+ function buildClaudeActions(kind) {
1498
+ const actions = [
1499
+ createAction("allow", "允许", "primary", "只允许这一次")
1500
+ ];
1501
+ if (kind === "command" || kind === "file_change") {
1502
+ actions.push(createAction("allow_session", "本会话默认允许", "neutral", "仅对同类操作默认放行"));
1503
+ }
1504
+ actions.push(createAction("deny", "拒绝", "danger", "阻止这次工具操作继续执行"));
1505
+ return actions;
1506
+ }
1507
+ function stringifyPayload(value) {
1508
+ if (value === undefined) {
1509
+ return null;
1510
+ }
1511
+ if (typeof value === "string") {
1512
+ return value.trim() || null;
1513
+ }
1514
+ try {
1515
+ return JSON.stringify(value, null, 2);
1516
+ }
1517
+ catch {
1518
+ return String(value);
1519
+ }
1520
+ }
1521
+ function normalizeText(value) {
1522
+ if (typeof value === "string") {
1523
+ const normalized = value.trim();
1524
+ return normalized || null;
1525
+ }
1526
+ if (typeof value === "number" || typeof value === "boolean") {
1527
+ return String(value);
1528
+ }
1529
+ return null;
1530
+ }
1531
+ function toRecord(value) {
1532
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1533
+ return null;
1534
+ }
1535
+ return value;
1536
+ }
1537
+ function readStringArray(value) {
1538
+ if (!Array.isArray(value)) {
1539
+ return [];
1540
+ }
1541
+ return value
1542
+ .map((item) => normalizeText(item))
1543
+ .filter((item) => Boolean(item));
1544
+ }
1545
+ function hashLike(input) {
1546
+ const text = input ?? "";
1547
+ let hash = 0;
1548
+ for (let index = 0; index < text.length; index += 1) {
1549
+ hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
1550
+ }
1551
+ return hash.toString(16);
1552
+ }
1553
+ function requireNonEmptyText(value, field) {
1554
+ const normalized = normalizeText(value);
1555
+ if (!normalized) {
1556
+ throw new AppError({
1557
+ statusCode: 400,
1558
+ errorCode: "INVALID_INPUT",
1559
+ detail: `${field} 不能为空`,
1560
+ field
1561
+ });
1562
+ }
1563
+ return normalized;
1564
+ }
1565
+ async function safeReadResponseText(response) {
1566
+ try {
1567
+ return (await response.text()).trim();
1568
+ }
1569
+ catch {
1570
+ return "";
1571
+ }
1572
+ }
1573
+ function extractSseData(frame) {
1574
+ const lines = frame.split(/\r?\n/);
1575
+ const dataLines = lines
1576
+ .filter((line) => line.startsWith("data:"))
1577
+ .map((line) => line.slice(5).trimStart());
1578
+ if (dataLines.length === 0) {
1579
+ return null;
1580
+ }
1581
+ return dataLines.join("\n");
1582
+ }
1583
+ function unwrapOpenCodeEventPayload(rawEvent) {
1584
+ const properties = toRecord(rawEvent.properties);
1585
+ const nestedEvent = toRecord(properties?.event);
1586
+ return nestedEvent ?? rawEvent;
1587
+ }
1588
+ async function waitForDelay(delayMs, signal) {
1589
+ await new Promise((resolve, reject) => {
1590
+ const timer = setTimeout(() => {
1591
+ cleanup();
1592
+ resolve();
1593
+ }, delayMs);
1594
+ const cleanup = () => {
1595
+ clearTimeout(timer);
1596
+ signal.removeEventListener("abort", onAbort);
1597
+ };
1598
+ const onAbort = () => {
1599
+ cleanup();
1600
+ reject(new Error("ABORTED"));
1601
+ };
1602
+ signal.addEventListener("abort", onAbort, { once: true });
1603
+ });
1604
+ }
1605
+ function buildClaudeRawStoreRef(homeDir, workspacePath, sessionId) {
1606
+ return join(homeDir, "projects", workspaceSlug(workspacePath), `${sessionId}.jsonl`);
1607
+ }
1608
+ function workspaceSlug(workspacePath) {
1609
+ return workspacePath
1610
+ .replace(/:/g, "")
1611
+ .replace(/[\\/]+/g, "-")
1612
+ .replace(/^-+|-+$/g, "")
1613
+ .replace(/[^A-Za-z0-9._-]/g, "-");
1614
+ }
1615
+ //# sourceMappingURL=session-permission-request-service.js.map