@hellcoder/companion 0.96.0

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 (242) hide show
  1. package/bin/cli.ts +168 -0
  2. package/bin/ctl.ts +528 -0
  3. package/bin/generate-token.ts +28 -0
  4. package/dist/apple-touch-icon.png +0 -0
  5. package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
  6. package/dist/assets/CronManager-EGwLJONv.js +1 -0
  7. package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
  8. package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
  9. package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
  10. package/dist/assets/Playground-BV3k0RbV.js +109 -0
  11. package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
  12. package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
  13. package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
  14. package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
  15. package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
  16. package/dist/assets/index-BhUa1e6X.css +1 -0
  17. package/dist/assets/index-DkqeP-R9.js +134 -0
  18. package/dist/assets/sw-register-BibwRdvC.js +1 -0
  19. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
  20. package/dist/favicon.svg +8 -0
  21. package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
  22. package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
  23. package/dist/icon-192.png +0 -0
  24. package/dist/icon-512.png +0 -0
  25. package/dist/index.html +20 -0
  26. package/dist/logo-codex.svg +14 -0
  27. package/dist/logo-docker.svg +4 -0
  28. package/dist/logo.svg +14 -0
  29. package/dist/manifest.json +24 -0
  30. package/dist/sw.js +2 -0
  31. package/package.json +104 -0
  32. package/server/agent-cron-migrator.test.ts +610 -0
  33. package/server/agent-cron-migrator.ts +85 -0
  34. package/server/agent-executor.test.ts +1108 -0
  35. package/server/agent-executor.ts +346 -0
  36. package/server/agent-store.test.ts +588 -0
  37. package/server/agent-store.ts +185 -0
  38. package/server/agent-types.ts +138 -0
  39. package/server/ai-validation-settings.test.ts +128 -0
  40. package/server/ai-validation-settings.ts +35 -0
  41. package/server/ai-validator.test.ts +387 -0
  42. package/server/ai-validator.ts +271 -0
  43. package/server/auth-manager.test.ts +83 -0
  44. package/server/auth-manager.ts +150 -0
  45. package/server/auto-namer.test.ts +252 -0
  46. package/server/auto-namer.ts +78 -0
  47. package/server/backend-adapter.test.ts +38 -0
  48. package/server/backend-adapter.ts +54 -0
  49. package/server/cache-headers.test.ts +98 -0
  50. package/server/cache-headers.ts +61 -0
  51. package/server/claude-adapter.test.ts +1363 -0
  52. package/server/claude-adapter.ts +889 -0
  53. package/server/claude-container-auth.test.ts +44 -0
  54. package/server/claude-container-auth.ts +30 -0
  55. package/server/claude-protocol-contract.test.ts +71 -0
  56. package/server/claude-protocol-drift.test.ts +78 -0
  57. package/server/claude-session-discovery.test.ts +132 -0
  58. package/server/claude-session-discovery.ts +157 -0
  59. package/server/claude-session-history.test.ts +158 -0
  60. package/server/claude-session-history.ts +410 -0
  61. package/server/cli-launcher.test.ts +1343 -0
  62. package/server/cli-launcher.ts +1298 -0
  63. package/server/cli.test.ts +16 -0
  64. package/server/codex-adapter.test.ts +5545 -0
  65. package/server/codex-adapter.ts +3062 -0
  66. package/server/codex-container-auth.test.ts +50 -0
  67. package/server/codex-container-auth.ts +24 -0
  68. package/server/codex-home.test.ts +61 -0
  69. package/server/codex-home.ts +26 -0
  70. package/server/codex-protocol-contract.test.ts +96 -0
  71. package/server/codex-protocol-drift.test.ts +123 -0
  72. package/server/codex-ws-proxy.cjs +226 -0
  73. package/server/commands-discovery.test.ts +179 -0
  74. package/server/commands-discovery.ts +81 -0
  75. package/server/constants.ts +7 -0
  76. package/server/container-manager.test.ts +1211 -0
  77. package/server/container-manager.ts +1053 -0
  78. package/server/cron-scheduler.test.ts +957 -0
  79. package/server/cron-scheduler.ts +243 -0
  80. package/server/cron-store.test.ts +422 -0
  81. package/server/cron-store.ts +148 -0
  82. package/server/cron-types.ts +63 -0
  83. package/server/env-manager.test.ts +268 -0
  84. package/server/env-manager.ts +161 -0
  85. package/server/event-bus-types.ts +64 -0
  86. package/server/event-bus.test.ts +244 -0
  87. package/server/event-bus.ts +124 -0
  88. package/server/execution-store.test.ts +307 -0
  89. package/server/execution-store.ts +170 -0
  90. package/server/fs-utils.ts +15 -0
  91. package/server/git-utils.test.ts +938 -0
  92. package/server/git-utils.ts +421 -0
  93. package/server/github-pr.test.ts +498 -0
  94. package/server/github-pr.ts +379 -0
  95. package/server/image-pull-manager.test.ts +303 -0
  96. package/server/image-pull-manager.ts +279 -0
  97. package/server/index.ts +396 -0
  98. package/server/linear-agent-bridge.test.ts +1157 -0
  99. package/server/linear-agent-bridge.ts +629 -0
  100. package/server/linear-agent.test.ts +473 -0
  101. package/server/linear-agent.ts +479 -0
  102. package/server/linear-cache.test.ts +136 -0
  103. package/server/linear-cache.ts +113 -0
  104. package/server/linear-connections.test.ts +350 -0
  105. package/server/linear-connections.ts +231 -0
  106. package/server/linear-credential-migration.test.ts +337 -0
  107. package/server/linear-credential-migration.ts +63 -0
  108. package/server/linear-oauth-connections-migration.test.ts +268 -0
  109. package/server/linear-oauth-connections.test.ts +365 -0
  110. package/server/linear-oauth-connections.ts +294 -0
  111. package/server/linear-project-manager.test.ts +162 -0
  112. package/server/linear-project-manager.ts +111 -0
  113. package/server/linear-prompt-builder.test.ts +74 -0
  114. package/server/linear-prompt-builder.ts +61 -0
  115. package/server/linear-staging.test.ts +276 -0
  116. package/server/linear-staging.ts +142 -0
  117. package/server/logger.test.ts +393 -0
  118. package/server/logger.ts +259 -0
  119. package/server/metrics-collector.test.ts +413 -0
  120. package/server/metrics-collector.ts +350 -0
  121. package/server/metrics-types.ts +108 -0
  122. package/server/middleware/managed-auth.test.ts +264 -0
  123. package/server/middleware/managed-auth.ts +195 -0
  124. package/server/novnc-proxy.test.ts +333 -0
  125. package/server/novnc-proxy.ts +99 -0
  126. package/server/path-resolver.test.ts +552 -0
  127. package/server/path-resolver.ts +186 -0
  128. package/server/paths.test.ts +31 -0
  129. package/server/paths.ts +11 -0
  130. package/server/pr-poller.test.ts +191 -0
  131. package/server/pr-poller.ts +162 -0
  132. package/server/prompt-manager.test.ts +211 -0
  133. package/server/prompt-manager.ts +211 -0
  134. package/server/protocol/claude-upstream/README.md +19 -0
  135. package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
  136. package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
  137. package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
  138. package/server/protocol/codex-upstream/README.md +18 -0
  139. package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
  140. package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
  141. package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
  142. package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
  143. package/server/protocol-monitor.ts +50 -0
  144. package/server/recorder.test.ts +454 -0
  145. package/server/recorder.ts +374 -0
  146. package/server/recording-hub/compat-validator.test.ts +150 -0
  147. package/server/recording-hub/compat-validator.ts +284 -0
  148. package/server/recording-hub/diagnostics.test.ts +140 -0
  149. package/server/recording-hub/diagnostics.ts +299 -0
  150. package/server/recording-hub/hub-config.test.ts +44 -0
  151. package/server/recording-hub/hub-config.ts +19 -0
  152. package/server/recording-hub/hub-routes.test.ts +417 -0
  153. package/server/recording-hub/hub-routes.ts +236 -0
  154. package/server/recording-hub/hub-store.test.ts +262 -0
  155. package/server/recording-hub/hub-store.ts +265 -0
  156. package/server/recording-hub/replay-adapter.test.ts +294 -0
  157. package/server/recording-hub/replay-adapter.ts +207 -0
  158. package/server/relay-client.test.ts +337 -0
  159. package/server/relay-client.ts +320 -0
  160. package/server/replay.test.ts +200 -0
  161. package/server/replay.ts +78 -0
  162. package/server/routes/agent-routes.test.ts +1400 -0
  163. package/server/routes/agent-routes.ts +409 -0
  164. package/server/routes/cron-routes.test.ts +881 -0
  165. package/server/routes/cron-routes.ts +103 -0
  166. package/server/routes/env-routes.test.ts +383 -0
  167. package/server/routes/env-routes.ts +95 -0
  168. package/server/routes/fs-routes.test.ts +1198 -0
  169. package/server/routes/fs-routes.ts +605 -0
  170. package/server/routes/git-routes.test.ts +813 -0
  171. package/server/routes/git-routes.ts +97 -0
  172. package/server/routes/linear-agent-routes.test.ts +721 -0
  173. package/server/routes/linear-agent-routes.ts +304 -0
  174. package/server/routes/linear-connection-routes.test.ts +927 -0
  175. package/server/routes/linear-connection-routes.ts +244 -0
  176. package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
  177. package/server/routes/linear-oauth-connection-routes.ts +129 -0
  178. package/server/routes/linear-routes.test.ts +1510 -0
  179. package/server/routes/linear-routes.ts +953 -0
  180. package/server/routes/metrics-routes.test.ts +103 -0
  181. package/server/routes/metrics-routes.ts +13 -0
  182. package/server/routes/prompt-routes.ts +67 -0
  183. package/server/routes/sandbox-routes.test.ts +513 -0
  184. package/server/routes/sandbox-routes.ts +127 -0
  185. package/server/routes/settings-routes.ts +270 -0
  186. package/server/routes/skills-routes.test.ts +690 -0
  187. package/server/routes/skills-routes.ts +100 -0
  188. package/server/routes/system-routes.test.ts +637 -0
  189. package/server/routes/system-routes.ts +228 -0
  190. package/server/routes/tailscale-routes.test.ts +176 -0
  191. package/server/routes/tailscale-routes.ts +22 -0
  192. package/server/routes.test.ts +4655 -0
  193. package/server/routes.ts +1277 -0
  194. package/server/sandbox-manager.test.ts +378 -0
  195. package/server/sandbox-manager.ts +168 -0
  196. package/server/service.test.ts +1419 -0
  197. package/server/service.ts +718 -0
  198. package/server/session-creation-service.test.ts +661 -0
  199. package/server/session-creation-service.ts +473 -0
  200. package/server/session-git-info.ts +104 -0
  201. package/server/session-linear-issues.test.ts +118 -0
  202. package/server/session-linear-issues.ts +88 -0
  203. package/server/session-names.test.ts +94 -0
  204. package/server/session-names.ts +67 -0
  205. package/server/session-orchestrator.test.ts +1784 -0
  206. package/server/session-orchestrator.ts +973 -0
  207. package/server/session-state-machine.test.ts +606 -0
  208. package/server/session-state-machine.ts +207 -0
  209. package/server/session-store.test.ts +290 -0
  210. package/server/session-store.ts +146 -0
  211. package/server/session-types.ts +509 -0
  212. package/server/settings-manager.test.ts +275 -0
  213. package/server/settings-manager.ts +173 -0
  214. package/server/tailscale-manager.test.ts +553 -0
  215. package/server/tailscale-manager.ts +451 -0
  216. package/server/terminal-manager.ts +240 -0
  217. package/server/update-checker.test.ts +306 -0
  218. package/server/update-checker.ts +197 -0
  219. package/server/usage-limits.test.ts +536 -0
  220. package/server/usage-limits.ts +225 -0
  221. package/server/worktree-tracker.test.ts +243 -0
  222. package/server/worktree-tracker.ts +84 -0
  223. package/server/ws-auth.test.ts +59 -0
  224. package/server/ws-auth.ts +41 -0
  225. package/server/ws-bridge-browser-ingest.test.ts +272 -0
  226. package/server/ws-bridge-browser-ingest.ts +72 -0
  227. package/server/ws-bridge-browser.ts +112 -0
  228. package/server/ws-bridge-cli-ingest.test.ts +302 -0
  229. package/server/ws-bridge-cli-ingest.ts +81 -0
  230. package/server/ws-bridge-codex.test.ts +1837 -0
  231. package/server/ws-bridge-codex.ts +266 -0
  232. package/server/ws-bridge-controls.test.ts +124 -0
  233. package/server/ws-bridge-controls.ts +20 -0
  234. package/server/ws-bridge-persist.test.ts +296 -0
  235. package/server/ws-bridge-persist.ts +66 -0
  236. package/server/ws-bridge-publish.test.ts +234 -0
  237. package/server/ws-bridge-publish.ts +79 -0
  238. package/server/ws-bridge-replay.test.ts +44 -0
  239. package/server/ws-bridge-replay.ts +61 -0
  240. package/server/ws-bridge-types.ts +106 -0
  241. package/server/ws-bridge.test.ts +4777 -0
  242. package/server/ws-bridge.ts +1279 -0
@@ -0,0 +1,266 @@
1
+ import type {
2
+ BrowserIncomingMessage,
3
+ BrowserOutgoingMessage,
4
+ PermissionRequest,
5
+ } from "./session-types.js";
6
+ import type { CodexAdapter } from "./codex-adapter.js";
7
+ import type { Session } from "./ws-bridge-types.js";
8
+ import { appendHistory } from "./ws-bridge-persist.js";
9
+ import { validatePermission } from "./ai-validator.js";
10
+ import { getEffectiveAiValidation } from "./ai-validation-settings.js";
11
+ import { companionBus } from "./event-bus.js";
12
+
13
+ /**
14
+ * @deprecated This file is no longer used in production. Codex adapters are now
15
+ * wired through the unified `attachBackendAdapter()` pipeline in `ws-bridge.ts`.
16
+ * This file is kept only for its test coverage which validates Codex-specific
17
+ * adapter handler logic patterns. It will be removed in a future cleanup pass.
18
+ */
19
+
20
+ export interface CodexAttachDeps {
21
+ persistSession: (session: Session) => void;
22
+ refreshGitInfo: (
23
+ session: Session,
24
+ options?: { broadcastUpdate?: boolean; notifyPoller?: boolean },
25
+ ) => void;
26
+ broadcastToBrowsers: (session: Session, msg: BrowserIncomingMessage) => void;
27
+ autoNamingAttempted: Set<string>;
28
+ }
29
+
30
+ export function attachCodexAdapterHandlers(
31
+ sessionId: string,
32
+ session: Session,
33
+ adapter: CodexAdapter,
34
+ deps: CodexAttachDeps,
35
+ ): void {
36
+ adapter.onBrowserMessage((msg) => {
37
+ // Track activity for idle detection — mirrors routeCLIMessage logic for
38
+ // Claude Code NDJSON. Without this, Codex sessions get incorrectly
39
+ // idle-killed because lastCliActivityTs is never updated.
40
+ session.lastCliActivityTs = Date.now();
41
+
42
+ if (msg.type === "session_init") {
43
+ // Preserve pre-populated commands/skills when adapter sends empty arrays
44
+ // (Codex does not provide its own commands/skills)
45
+ // Exclude session_id: the adapter may report its own internal session ID
46
+ // which differs from the Companion's session ID. Allowing it to overwrite
47
+ // session.state.session_id causes duplicate sidebar entries.
48
+ const { slash_commands, skills, session_id: _cliSessionId, ...rest } = msg.session;
49
+ session.state = {
50
+ ...session.state,
51
+ ...rest,
52
+ ...(slash_commands?.length ? { slash_commands } : {}),
53
+ ...(skills?.length ? { skills } : {}),
54
+ backend_type: "codex",
55
+ };
56
+ deps.refreshGitInfo(session, { notifyPoller: true });
57
+ deps.persistSession(session);
58
+ session.stateMachine.transition("ready", "codex_session_init");
59
+ } else if (msg.type === "session_update") {
60
+ // Exclude session_id — same rationale as session_init above.
61
+ const { slash_commands, skills, session_id: _cliSessionId, ...rest } = msg.session;
62
+ session.state = {
63
+ ...session.state,
64
+ ...rest,
65
+ ...(slash_commands?.length ? { slash_commands } : {}),
66
+ ...(skills?.length ? { skills } : {}),
67
+ backend_type: "codex",
68
+ };
69
+ deps.refreshGitInfo(session, { notifyPoller: true });
70
+ deps.persistSession(session);
71
+ } else if (msg.type === "status_change") {
72
+ session.state.is_compacting = msg.status === "compacting";
73
+ if (msg.status === "compacting") {
74
+ session.stateMachine.transition("compacting", "codex_compacting_started");
75
+ } else {
76
+ session.stateMachine.transition("ready", "codex_compacting_ended");
77
+ }
78
+ deps.persistSession(session);
79
+ }
80
+
81
+ if (msg.type === "assistant") {
82
+ const assistantMsg = { ...msg, timestamp: msg.timestamp || Date.now() };
83
+ appendHistory(session, assistantMsg);
84
+ deps.persistSession(session);
85
+ companionBus.emit("message:assistant", { sessionId, message: assistantMsg });
86
+ } else if (msg.type === "result") {
87
+ appendHistory(session, msg);
88
+ deps.persistSession(session);
89
+ companionBus.emit("message:result", { sessionId, message: msg });
90
+ session.stateMachine.transition("ready", "codex_turn_completed");
91
+ }
92
+
93
+ if (msg.type === "assistant") {
94
+ const content = (msg as { message?: { content?: Array<{ type: string }> } }).message?.content;
95
+ const hasToolUse = content?.some((b) => b.type === "tool_use");
96
+ if (hasToolUse) {
97
+ console.log(`[ws-bridge] Broadcasting tool_use assistant to ${session.browserSockets.size} browser(s) for session ${session.id}`);
98
+ }
99
+ }
100
+
101
+ if (msg.type === "permission_cancelled") {
102
+ const reqId = (msg as { request_id: string }).request_id;
103
+ session.pendingPermissions.delete(reqId);
104
+ // If no more pending permissions, transition back to streaming
105
+ if (session.pendingPermissions.size === 0 && session.stateMachine.phase === "awaiting_permission") {
106
+ session.stateMachine.transition("streaming", "permission_cancelled");
107
+ }
108
+ deps.persistSession(session);
109
+ }
110
+
111
+ if (msg.type === "permission_request") {
112
+ const perm = msg.request;
113
+
114
+ // AI Validation Mode for Codex sessions
115
+ const aiSettings = getEffectiveAiValidation(session.state);
116
+ if (
117
+ aiSettings.enabled
118
+ && aiSettings.anthropicApiKey
119
+ && perm.tool_name !== "AskUserQuestion"
120
+ && perm.tool_name !== "ExitPlanMode"
121
+ ) {
122
+ // Run AI validation async — don't broadcast yet
123
+ handleCodexAiValidation(session, adapter, perm, deps).catch((err) => {
124
+ console.warn(`[ws-bridge-codex] AI validation error for tool=${perm.tool_name} request_id=${perm.request_id} session=${session.id}, falling through to manual:`, err);
125
+ // On error, fall through to normal permission flow
126
+ session.pendingPermissions.set(perm.request_id, perm);
127
+ session.stateMachine.transition("awaiting_permission", "ai_validation_error_fallback");
128
+ deps.persistSession(session);
129
+ deps.broadcastToBrowsers(session, msg);
130
+ });
131
+ return;
132
+ }
133
+
134
+ session.pendingPermissions.set(perm.request_id, perm);
135
+ deps.persistSession(session);
136
+ session.stateMachine.transition("awaiting_permission", "codex_permission_requested");
137
+ }
138
+
139
+ deps.broadcastToBrowsers(session, msg);
140
+
141
+ if (
142
+ msg.type === "result" &&
143
+ !(msg.data as { is_error?: boolean }).is_error &&
144
+ !deps.autoNamingAttempted.has(session.id)
145
+ ) {
146
+ deps.autoNamingAttempted.add(session.id);
147
+ const firstUserMsg = session.messageHistory.find((m) => m.type === "user_message");
148
+ if (firstUserMsg && firstUserMsg.type === "user_message") {
149
+ companionBus.emit("session:first-turn-completed", { sessionId: session.id, firstUserMessage: firstUserMsg.content });
150
+ }
151
+ }
152
+ });
153
+
154
+ adapter.onSessionMeta((meta) => {
155
+ if (meta.cliSessionId) {
156
+ companionBus.emit("session:cli-id-received", { sessionId: session.id, cliSessionId: meta.cliSessionId });
157
+ }
158
+ if (meta.model) session.state.model = meta.model;
159
+ if (meta.cwd) session.state.cwd = meta.cwd;
160
+ session.state.backend_type = "codex";
161
+ deps.refreshGitInfo(session, { broadcastUpdate: true, notifyPoller: true });
162
+ deps.persistSession(session);
163
+ });
164
+
165
+ adapter.onDisconnect(() => {
166
+ // Guard: only clear the adapter reference if THIS adapter is still the active
167
+ // one. During relaunch, a NEW adapter is attached before the OLD one fires
168
+ // its disconnect callback — without this check the new adapter gets nulled out.
169
+ if (session.backendAdapter !== adapter) {
170
+ console.log(`[ws-bridge] Ignoring stale disconnect for session ${sessionId} (adapter replaced)`);
171
+ return;
172
+ }
173
+ for (const [reqId] of session.pendingPermissions) {
174
+ deps.broadcastToBrowsers(session, { type: "permission_cancelled", request_id: reqId });
175
+ }
176
+ session.pendingPermissions.clear();
177
+ session.backendAdapter = null;
178
+ deps.persistSession(session);
179
+ console.log(`[ws-bridge] Codex adapter disconnected for session ${sessionId}`);
180
+ deps.broadcastToBrowsers(session, { type: "cli_disconnected" });
181
+
182
+ // Auto-relaunch if browsers are still connected (don't leave users staring
183
+ // at a dead session when the transport drops mid-conversation).
184
+ if (session.browserSockets.size > 0) {
185
+ console.log(`[ws-bridge] Auto-relaunching Codex for session ${sessionId} (${session.browserSockets.size} browser(s) connected)`);
186
+ companionBus.emit("session:relaunch-needed", { sessionId });
187
+ }
188
+ });
189
+
190
+ if (session.pendingMessages.length > 0) {
191
+ console.log(`[ws-bridge] Flushing ${session.pendingMessages.length} queued message(s) to Codex adapter for session ${sessionId}`);
192
+ const queued = session.pendingMessages.splice(0);
193
+ for (const raw of queued) {
194
+ try {
195
+ const msg = JSON.parse(raw) as BrowserOutgoingMessage;
196
+ adapter.sendBrowserMessage(msg);
197
+ } catch {
198
+ console.warn(`[ws-bridge] Failed to parse queued message for Codex: ${raw.substring(0, 100)}`);
199
+ }
200
+ }
201
+ }
202
+
203
+ deps.broadcastToBrowsers(session, { type: "cli_connected" });
204
+ console.log(`[ws-bridge] Codex adapter attached for session ${sessionId}`);
205
+ }
206
+
207
+ async function handleCodexAiValidation(
208
+ session: Session,
209
+ adapter: CodexAdapter,
210
+ perm: PermissionRequest,
211
+ deps: CodexAttachDeps,
212
+ ): Promise<void> {
213
+ const aiSettings = getEffectiveAiValidation(session.state);
214
+ const result = await validatePermission(
215
+ perm.tool_name,
216
+ perm.input,
217
+ perm.description,
218
+ );
219
+
220
+ perm.ai_validation = {
221
+ verdict: result.verdict,
222
+ reason: result.reason,
223
+ ruleBasedOnly: result.ruleBasedOnly,
224
+ };
225
+
226
+ // Auto-approve safe tools
227
+ if (result.verdict === "safe" && aiSettings.autoApprove) {
228
+ deps.broadcastToBrowsers(session, {
229
+ type: "permission_auto_resolved",
230
+ request: perm,
231
+ behavior: "allow",
232
+ reason: result.reason,
233
+ });
234
+ adapter.sendBrowserMessage({
235
+ type: "permission_response",
236
+ request_id: perm.request_id,
237
+ behavior: "allow",
238
+ });
239
+ return;
240
+ }
241
+
242
+ // Auto-deny dangerous tools
243
+ if (result.verdict === "dangerous" && aiSettings.autoDeny) {
244
+ deps.broadcastToBrowsers(session, {
245
+ type: "permission_auto_resolved",
246
+ request: perm,
247
+ behavior: "deny",
248
+ reason: result.reason,
249
+ });
250
+ adapter.sendBrowserMessage({
251
+ type: "permission_response",
252
+ request_id: perm.request_id,
253
+ behavior: "deny",
254
+ });
255
+ return;
256
+ }
257
+
258
+ // Uncertain or auto-action disabled: fall through to manual
259
+ session.pendingPermissions.set(perm.request_id, perm);
260
+ session.stateMachine.transition("awaiting_permission", "ai_validation_manual_fallback");
261
+ deps.persistSession(session);
262
+ deps.broadcastToBrowsers(session, {
263
+ type: "permission_request",
264
+ request: perm,
265
+ });
266
+ }
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ // Mock settings-manager to avoid reading real settings files during tests.
4
+ vi.mock("./settings-manager.js", () => ({
5
+ getSettings: () => ({
6
+ aiValidationEnabled: false,
7
+ aiValidationAutoApprove: false,
8
+ aiValidationAutoDeny: false,
9
+ anthropicApiKey: "",
10
+ }),
11
+ DEFAULT_ANTHROPIC_MODEL: "claude-sonnet-4-6",
12
+ }));
13
+
14
+ import { handleSetAiValidation } from "./ws-bridge-controls.js";
15
+ import type { Session } from "./ws-bridge-types.js";
16
+ import { makeDefaultState } from "./ws-bridge-types.js";
17
+ import { SessionStateMachine } from "./session-state-machine.js";
18
+
19
+ /** Create a minimal Session object for testing handleSetAiValidation. */
20
+ function makeMockSession(overrides: Partial<Session["state"]> = {}): Session {
21
+ const state = { ...makeDefaultState("test-session"), ...overrides };
22
+ return {
23
+ id: "test-session",
24
+ backendType: "claude",
25
+ backendAdapter: null,
26
+ browserSockets: new Set(),
27
+ state,
28
+ pendingPermissions: new Map(),
29
+ messageHistory: [],
30
+ pendingMessages: [],
31
+ nextEventSeq: 1,
32
+ eventBuffer: [],
33
+ lastAckSeq: 0,
34
+ processedClientMessageIds: [],
35
+ processedClientMessageIdSet: new Set(),
36
+ lastCliActivityTs: Date.now(),
37
+ stateMachine: new SessionStateMachine("test-session"),
38
+ };
39
+ }
40
+
41
+ describe("handleSetAiValidation", () => {
42
+ it("sets aiValidationEnabled on session state", () => {
43
+ // When aiValidationEnabled is provided, it should be written to state.
44
+ const session = makeMockSession({ aiValidationEnabled: false });
45
+ handleSetAiValidation(session, { aiValidationEnabled: true });
46
+ expect(session.state.aiValidationEnabled).toBe(true);
47
+ });
48
+
49
+ it("sets aiValidationAutoApprove on session state", () => {
50
+ // When aiValidationAutoApprove is provided, it should be written to state.
51
+ const session = makeMockSession({ aiValidationAutoApprove: false });
52
+ handleSetAiValidation(session, { aiValidationAutoApprove: true });
53
+ expect(session.state.aiValidationAutoApprove).toBe(true);
54
+ });
55
+
56
+ it("sets aiValidationAutoDeny on session state", () => {
57
+ // When aiValidationAutoDeny is provided, it should be written to state.
58
+ const session = makeMockSession({ aiValidationAutoDeny: false });
59
+ handleSetAiValidation(session, { aiValidationAutoDeny: true });
60
+ expect(session.state.aiValidationAutoDeny).toBe(true);
61
+ });
62
+
63
+ it("does not overwrite existing state when values are undefined", () => {
64
+ // When a field is undefined in the message, handleSetAiValidation should
65
+ // leave the existing value in session.state untouched. This is important
66
+ // because the browser may send only the fields that changed.
67
+ const session = makeMockSession({
68
+ aiValidationEnabled: true,
69
+ aiValidationAutoApprove: true,
70
+ aiValidationAutoDeny: true,
71
+ });
72
+ handleSetAiValidation(session, {});
73
+ expect(session.state.aiValidationEnabled).toBe(true);
74
+ expect(session.state.aiValidationAutoApprove).toBe(true);
75
+ expect(session.state.aiValidationAutoDeny).toBe(true);
76
+ });
77
+
78
+ it("sets values to null when explicitly provided", () => {
79
+ // null is a valid value (different from undefined) and should be written.
80
+ const session = makeMockSession({
81
+ aiValidationEnabled: true,
82
+ aiValidationAutoApprove: true,
83
+ aiValidationAutoDeny: true,
84
+ });
85
+ handleSetAiValidation(session, {
86
+ aiValidationEnabled: null,
87
+ aiValidationAutoApprove: null,
88
+ aiValidationAutoDeny: null,
89
+ });
90
+ expect(session.state.aiValidationEnabled).toBeNull();
91
+ expect(session.state.aiValidationAutoApprove).toBeNull();
92
+ expect(session.state.aiValidationAutoDeny).toBeNull();
93
+ });
94
+
95
+ it("sets values to false when explicitly provided", () => {
96
+ // false is a valid value and should overwrite true.
97
+ const session = makeMockSession({
98
+ aiValidationEnabled: true,
99
+ aiValidationAutoApprove: true,
100
+ aiValidationAutoDeny: true,
101
+ });
102
+ handleSetAiValidation(session, {
103
+ aiValidationEnabled: false,
104
+ aiValidationAutoApprove: false,
105
+ aiValidationAutoDeny: false,
106
+ });
107
+ expect(session.state.aiValidationEnabled).toBe(false);
108
+ expect(session.state.aiValidationAutoApprove).toBe(false);
109
+ expect(session.state.aiValidationAutoDeny).toBe(false);
110
+ });
111
+
112
+ it("handles partial updates (only some fields provided)", () => {
113
+ // When only one field is provided, only that field should change.
114
+ const session = makeMockSession({
115
+ aiValidationEnabled: false,
116
+ aiValidationAutoApprove: false,
117
+ aiValidationAutoDeny: false,
118
+ });
119
+ handleSetAiValidation(session, { aiValidationEnabled: true });
120
+ expect(session.state.aiValidationEnabled).toBe(true);
121
+ expect(session.state.aiValidationAutoApprove).toBe(false);
122
+ expect(session.state.aiValidationAutoDeny).toBe(false);
123
+ });
124
+ });
@@ -0,0 +1,20 @@
1
+ import type { Session } from "./ws-bridge-types.js";
2
+
3
+ export function handleSetAiValidation(
4
+ session: Session,
5
+ msg: {
6
+ aiValidationEnabled?: boolean | null;
7
+ aiValidationAutoApprove?: boolean | null;
8
+ aiValidationAutoDeny?: boolean | null;
9
+ },
10
+ ): void {
11
+ if (msg.aiValidationEnabled !== undefined) {
12
+ session.state.aiValidationEnabled = msg.aiValidationEnabled;
13
+ }
14
+ if (msg.aiValidationAutoApprove !== undefined) {
15
+ session.state.aiValidationAutoApprove = msg.aiValidationAutoApprove;
16
+ }
17
+ if (msg.aiValidationAutoDeny !== undefined) {
18
+ session.state.aiValidationAutoDeny = msg.aiValidationAutoDeny;
19
+ }
20
+ }