mstro-app 0.4.38 → 0.4.43

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 (198) hide show
  1. package/bin/commands/login.js +17 -7
  2. package/bin/commands/logout.js +14 -6
  3. package/bin/commands/status.js +9 -3
  4. package/bin/commands/whoami.js +10 -4
  5. package/bin/mstro.js +11 -1
  6. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
  7. package/dist/server/cli/headless/claude-invoker-stream.js +1 -0
  8. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
  9. package/dist/server/cli/headless/index.d.ts +1 -0
  10. package/dist/server/cli/headless/index.d.ts.map +1 -1
  11. package/dist/server/cli/headless/index.js +2 -0
  12. package/dist/server/cli/headless/index.js.map +1 -1
  13. package/dist/server/cli/headless/resilient-runner.d.ts +47 -0
  14. package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -0
  15. package/dist/server/cli/headless/resilient-runner.js +234 -0
  16. package/dist/server/cli/headless/resilient-runner.js.map +1 -0
  17. package/dist/server/cli/headless/retry-strategies.d.ts +44 -0
  18. package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -0
  19. package/dist/server/cli/headless/retry-strategies.js +262 -0
  20. package/dist/server/cli/headless/retry-strategies.js.map +1 -0
  21. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  22. package/dist/server/cli/headless/stall-assessor.js +5 -0
  23. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  24. package/dist/server/cli/headless/tool-watchdog.d.ts +2 -0
  25. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  26. package/dist/server/cli/headless/tool-watchdog.js +31 -4
  27. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  28. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  29. package/dist/server/cli/improvisation-retry.js +1 -30
  30. package/dist/server/cli/improvisation-retry.js.map +1 -1
  31. package/dist/server/cli/improvisation-session-manager.d.ts +1 -0
  32. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  33. package/dist/server/cli/improvisation-session-manager.js +16 -3
  34. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  35. package/dist/server/cli/prompt-builders.d.ts.map +1 -1
  36. package/dist/server/cli/prompt-builders.js +31 -13
  37. package/dist/server/cli/prompt-builders.js.map +1 -1
  38. package/dist/server/index.js +1 -9
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/mcp/bouncer-cli.js +5 -4
  41. package/dist/server/mcp/bouncer-cli.js.map +1 -1
  42. package/dist/server/mcp/bouncer-haiku.js +1 -1
  43. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  44. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  45. package/dist/server/mcp/bouncer-integration.js +14 -8
  46. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  47. package/dist/server/mcp/security-patterns.js +1 -1
  48. package/dist/server/mcp/security-patterns.js.map +1 -1
  49. package/dist/server/services/plan/composer.d.ts.map +1 -1
  50. package/dist/server/services/plan/composer.js +19 -9
  51. package/dist/server/services/plan/composer.js.map +1 -1
  52. package/dist/server/services/plan/executor.d.ts +6 -1
  53. package/dist/server/services/plan/executor.d.ts.map +1 -1
  54. package/dist/server/services/plan/executor.js +158 -76
  55. package/dist/server/services/plan/executor.js.map +1 -1
  56. package/dist/server/services/plan/front-matter.d.ts +1 -0
  57. package/dist/server/services/plan/front-matter.d.ts.map +1 -1
  58. package/dist/server/services/plan/front-matter.js +6 -0
  59. package/dist/server/services/plan/front-matter.js.map +1 -1
  60. package/dist/server/services/plan/issue-classification.d.ts +11 -0
  61. package/dist/server/services/plan/issue-classification.d.ts.map +1 -0
  62. package/dist/server/services/plan/issue-classification.js +20 -0
  63. package/dist/server/services/plan/issue-classification.js.map +1 -0
  64. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  65. package/dist/server/services/plan/issue-prompt-builder.js +10 -5
  66. package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
  67. package/dist/server/services/plan/issue-retry.d.ts +0 -5
  68. package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
  69. package/dist/server/services/plan/issue-retry.js +12 -241
  70. package/dist/server/services/plan/issue-retry.js.map +1 -1
  71. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  72. package/dist/server/services/plan/parser-core.js +1 -0
  73. package/dist/server/services/plan/parser-core.js.map +1 -1
  74. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  75. package/dist/server/services/plan/review-gate.js +9 -6
  76. package/dist/server/services/plan/review-gate.js.map +1 -1
  77. package/dist/server/services/plan/types.d.ts +1 -0
  78. package/dist/server/services/plan/types.d.ts.map +1 -1
  79. package/dist/server/services/platform-credentials.d.ts.map +1 -1
  80. package/dist/server/services/platform-credentials.js +11 -4
  81. package/dist/server/services/platform-credentials.js.map +1 -1
  82. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  83. package/dist/server/services/terminal/pty-manager.js +7 -1
  84. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  85. package/dist/server/services/websocket/handler-context.d.ts +2 -0
  86. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  87. package/dist/server/services/websocket/handler.d.ts +2 -0
  88. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  89. package/dist/server/services/websocket/handler.js +18 -7
  90. package/dist/server/services/websocket/handler.js.map +1 -1
  91. package/dist/server/services/websocket/plan-execution-handlers.js +6 -6
  92. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
  93. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -1
  94. package/dist/server/services/websocket/quality-fix-agent.js +90 -42
  95. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -1
  96. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  97. package/dist/server/services/websocket/quality-handlers.js +48 -7
  98. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  99. package/dist/server/services/websocket/quality-persistence.d.ts +22 -0
  100. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  101. package/dist/server/services/websocket/quality-persistence.js +48 -1
  102. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  103. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  104. package/dist/server/services/websocket/quality-review-agent.js +74 -32
  105. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  106. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  107. package/dist/server/services/websocket/quality-tools.js +18 -18
  108. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  109. package/dist/server/services/websocket/skill-handlers.d.ts +3 -1
  110. package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
  111. package/dist/server/services/websocket/skill-handlers.js +52 -41
  112. package/dist/server/services/websocket/skill-handlers.js.map +1 -1
  113. package/dist/server/services/websocket/skill-watcher.d.ts +17 -0
  114. package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -0
  115. package/dist/server/services/websocket/skill-watcher.js +85 -0
  116. package/dist/server/services/websocket/skill-watcher.js.map +1 -0
  117. package/dist/server/services/websocket/types.d.ts +2 -268
  118. package/dist/server/services/websocket/types.d.ts.map +1 -1
  119. package/dist/server/services/websocket/types.js +0 -4
  120. package/dist/server/services/websocket/types.js.map +1 -1
  121. package/package.json +1 -1
  122. package/server/cli/headless/claude-invoker-stream.ts +1 -0
  123. package/server/cli/headless/index.ts +2 -0
  124. package/server/cli/headless/resilient-runner.ts +354 -0
  125. package/server/cli/headless/retry-strategies.ts +330 -0
  126. package/server/cli/headless/stall-assessor.ts +5 -0
  127. package/server/cli/headless/tool-watchdog.ts +40 -4
  128. package/server/cli/improvisation-retry.ts +1 -32
  129. package/server/cli/improvisation-session-manager.ts +17 -3
  130. package/server/cli/prompt-builders.ts +33 -12
  131. package/server/index.ts +1 -9
  132. package/server/mcp/bouncer-cli.ts +5 -4
  133. package/server/mcp/bouncer-haiku.ts +1 -1
  134. package/server/mcp/bouncer-integration.ts +15 -8
  135. package/server/mcp/security-patterns.ts +1 -1
  136. package/server/services/plan/agents/code-review.md +109 -0
  137. package/server/services/plan/agents/commit-message.md +26 -0
  138. package/server/services/plan/agents/execute-issue.md +10 -1
  139. package/server/services/plan/agents/fix-quality.md +24 -0
  140. package/server/services/plan/agents/pr-description.md +28 -0
  141. package/server/services/plan/composer.ts +20 -9
  142. package/server/services/plan/executor.ts +160 -76
  143. package/server/services/plan/front-matter.ts +7 -0
  144. package/server/services/plan/issue-classification.ts +21 -0
  145. package/server/services/plan/issue-prompt-builder.ts +11 -5
  146. package/server/services/plan/issue-retry.ts +15 -330
  147. package/server/services/plan/parser-core.ts +1 -0
  148. package/server/services/plan/review-gate.ts +9 -6
  149. package/server/services/plan/types.ts +3 -0
  150. package/server/services/platform-credentials.ts +10 -4
  151. package/server/services/terminal/pty-manager.ts +7 -1
  152. package/server/services/websocket/handler-context.ts +2 -0
  153. package/server/services/websocket/handler.ts +18 -8
  154. package/server/services/websocket/plan-execution-handlers.ts +7 -7
  155. package/server/services/websocket/quality-fix-agent.ts +86 -44
  156. package/server/services/websocket/quality-handlers.ts +48 -7
  157. package/server/services/websocket/quality-persistence.ts +75 -1
  158. package/server/services/websocket/quality-review-agent.ts +70 -31
  159. package/server/services/websocket/quality-tools.ts +16 -14
  160. package/server/services/websocket/skill-handlers.ts +50 -40
  161. package/server/services/websocket/skill-watcher.ts +79 -0
  162. package/server/services/websocket/types.ts +0 -311
  163. package/dist/server/services/deploy/ai-broker.d.ts +0 -63
  164. package/dist/server/services/deploy/ai-broker.d.ts.map +0 -1
  165. package/dist/server/services/deploy/ai-broker.js +0 -360
  166. package/dist/server/services/deploy/ai-broker.js.map +0 -1
  167. package/dist/server/services/deploy/board-execution-handler.d.ts +0 -114
  168. package/dist/server/services/deploy/board-execution-handler.d.ts.map +0 -1
  169. package/dist/server/services/deploy/board-execution-handler.js +0 -621
  170. package/dist/server/services/deploy/board-execution-handler.js.map +0 -1
  171. package/dist/server/services/deploy/credentials.d.ts +0 -35
  172. package/dist/server/services/deploy/credentials.d.ts.map +0 -1
  173. package/dist/server/services/deploy/credentials.js +0 -177
  174. package/dist/server/services/deploy/credentials.js.map +0 -1
  175. package/dist/server/services/deploy/deploy-ai-service.d.ts +0 -107
  176. package/dist/server/services/deploy/deploy-ai-service.d.ts.map +0 -1
  177. package/dist/server/services/deploy/deploy-ai-service.js +0 -294
  178. package/dist/server/services/deploy/deploy-ai-service.js.map +0 -1
  179. package/dist/server/services/deploy/headless-session-handler.d.ts +0 -94
  180. package/dist/server/services/deploy/headless-session-handler.d.ts.map +0 -1
  181. package/dist/server/services/deploy/headless-session-handler.js +0 -266
  182. package/dist/server/services/deploy/headless-session-handler.js.map +0 -1
  183. package/dist/server/services/websocket/deploy-handlers.d.ts +0 -14
  184. package/dist/server/services/websocket/deploy-handlers.d.ts.map +0 -1
  185. package/dist/server/services/websocket/deploy-handlers.js +0 -409
  186. package/dist/server/services/websocket/deploy-handlers.js.map +0 -1
  187. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +0 -11
  188. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +0 -1
  189. package/dist/server/services/websocket/handlers/deploy-handlers.js +0 -176
  190. package/dist/server/services/websocket/handlers/deploy-handlers.js.map +0 -1
  191. package/server/cli/headless/RESEARCH.md +0 -627
  192. package/server/services/deploy/ai-broker.ts +0 -512
  193. package/server/services/deploy/board-execution-handler.ts +0 -847
  194. package/server/services/deploy/credentials.ts +0 -200
  195. package/server/services/deploy/deploy-ai-service.ts +0 -401
  196. package/server/services/deploy/headless-session-handler.ts +0 -414
  197. package/server/services/websocket/deploy-handlers.ts +0 -544
  198. package/server/services/websocket/handlers/deploy-handlers.ts +0 -228
@@ -1,414 +0,0 @@
1
- // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
-
4
- /**
5
- * Headless Session Handler
6
- *
7
- * Handles headless Claude Code session requests from a developer's backend
8
- * on behalf of end users. Each session is isolated — no shared context
9
- * between end users.
10
- *
11
- * Security: End-user prompts are untrusted input. They are always passed as
12
- * user messages, never injected into system prompts or tool parameters.
13
- * The Security Bouncer governs tool access within each session.
14
- */
15
-
16
- import type { ToolUseEvent } from '../../cli/headless/types.js';
17
- import { DeployAiService, type DeployExecutionResult } from './deploy-ai-service.js';
18
-
19
- // ========== Types ==========
20
-
21
- export interface HeadlessSessionRequest {
22
- /** The end user's prompt (untrusted input) */
23
- prompt: string;
24
- /** Override the deployment's default system prompt */
25
- systemPrompt?: string;
26
- /** Restrict which tools Claude can use in this session */
27
- allowedTools?: string[];
28
- /** Override the deployment's default model */
29
- model?: string;
30
- /** Unique identifier for the end user (for isolation + rate tracking) */
31
- endUserId: string;
32
- }
33
-
34
- export interface DeploymentAiConfig {
35
- deploymentId: string;
36
- aiEnabled: boolean;
37
- allowedAiCapabilities: string[];
38
- maxTokensPerRequest: number | null;
39
- maxRequestsPerMinute: number | null;
40
- maxConcurrentSessions: number;
41
- defaultSystemPrompt: string | null;
42
- defaultModel: string;
43
- workingDir: string;
44
- }
45
-
46
- export type HeadlessSessionErrorCode =
47
- | 'CAPABILITY_DENIED'
48
- | 'AI_DISABLED'
49
- | 'RATE_LIMIT_EXCEEDED'
50
- | 'CONCURRENT_LIMIT_EXCEEDED'
51
- | 'INVALID_REQUEST'
52
- | 'EXECUTION_FAILED';
53
-
54
- export interface HeadlessSessionError {
55
- code: HeadlessSessionErrorCode;
56
- message: string;
57
- }
58
-
59
- export interface HeadlessSessionStreamCallbacks {
60
- onOutput?: (text: string) => void;
61
- onThinking?: (text: string) => void;
62
- onToolUse?: (event: ToolUseEvent) => void;
63
- onUsageReport?: (report: UsageReportData) => void;
64
- onHealthUpdate?: (update: HealthUpdateData) => void;
65
- }
66
-
67
- export interface UsageReportData {
68
- deploymentId: string;
69
- endUserId: string;
70
- capability: 'headless' | 'pm-board';
71
- tokensUsed: number;
72
- model: string;
73
- durationMs: number;
74
- boardId?: string;
75
- }
76
-
77
- export interface HealthUpdateData {
78
- deploymentId: string;
79
- status: 'healthy' | 'invalid_key' | 'no_credits' | 'rate_limited' | 'unknown_error';
80
- message: string;
81
- aiDisabled: boolean;
82
- }
83
-
84
- export type HeadlessSessionResult =
85
- | { ok: true; result: DeployExecutionResult }
86
- | { ok: false; error: HeadlessSessionError };
87
-
88
- // ========== Rate Limiter ==========
89
-
90
- interface RateBucket {
91
- timestamps: number[];
92
- activeSessions: number;
93
- }
94
-
95
- const rateBuckets = new Map<string, RateBucket>();
96
-
97
- function getBucket(deploymentId: string): RateBucket {
98
- let bucket = rateBuckets.get(deploymentId);
99
- if (!bucket) {
100
- bucket = { timestamps: [], activeSessions: 0 };
101
- rateBuckets.set(deploymentId, bucket);
102
- }
103
- return bucket;
104
- }
105
-
106
- function pruneTimestamps(bucket: RateBucket): void {
107
- const oneMinuteAgo = Date.now() - 60_000;
108
- // Remove timestamps older than 1 minute
109
- while (bucket.timestamps.length > 0 && bucket.timestamps[0] < oneMinuteAgo) {
110
- bucket.timestamps.shift();
111
- }
112
- }
113
-
114
- function checkRateLimit(
115
- config: DeploymentAiConfig,
116
- ): HeadlessSessionError | null {
117
- const bucket = getBucket(config.deploymentId);
118
-
119
- // Check concurrent sessions
120
- if (bucket.activeSessions >= config.maxConcurrentSessions) {
121
- return {
122
- code: 'CONCURRENT_LIMIT_EXCEEDED',
123
- message: `Deployment has reached the maximum of ${config.maxConcurrentSessions} concurrent sessions. Wait for an existing session to complete.`,
124
- };
125
- }
126
-
127
- // Check requests per minute
128
- if (config.maxRequestsPerMinute !== null) {
129
- pruneTimestamps(bucket);
130
- if (bucket.timestamps.length >= config.maxRequestsPerMinute) {
131
- return {
132
- code: 'RATE_LIMIT_EXCEEDED',
133
- message: `Deployment has exceeded the rate limit of ${config.maxRequestsPerMinute} requests per minute. Try again shortly.`,
134
- };
135
- }
136
- }
137
-
138
- return null;
139
- }
140
-
141
- function recordRequestStart(deploymentId: string): void {
142
- const bucket = getBucket(deploymentId);
143
- bucket.timestamps.push(Date.now());
144
- bucket.activeSessions++;
145
- }
146
-
147
- function recordRequestEnd(deploymentId: string): void {
148
- const bucket = getBucket(deploymentId);
149
- bucket.activeSessions = Math.max(0, bucket.activeSessions - 1);
150
- }
151
-
152
- // ========== Prompt Composition ==========
153
-
154
- /**
155
- * Compose the final prompt sent to Claude. The system prompt (from deployment
156
- * config or per-request override) is prepended as a system instruction block.
157
- * The end-user prompt follows as a clearly delimited user message.
158
- *
159
- * SECURITY: The end-user prompt is always in the user-message section,
160
- * never interpolated into the system instruction.
161
- */
162
- function composePrompt(systemPrompt: string | null, userPrompt: string): string {
163
- if (!systemPrompt) {
164
- return userPrompt;
165
- }
166
-
167
- return [
168
- '<system-instruction>',
169
- systemPrompt,
170
- '</system-instruction>',
171
- '',
172
- userPrompt,
173
- ].join('\n');
174
- }
175
-
176
- // ========== Validation ==========
177
-
178
- /** Validate request fields and deployment config. Returns an error or null if valid. */
179
- function validateRequest(
180
- request: HeadlessSessionRequest,
181
- config: DeploymentAiConfig,
182
- ): HeadlessSessionError | null {
183
- if (!request.prompt || request.prompt.trim().length === 0) {
184
- return { code: 'INVALID_REQUEST', message: 'prompt is required and must not be empty.' };
185
- }
186
- if (!request.endUserId || request.endUserId.trim().length === 0) {
187
- return { code: 'INVALID_REQUEST', message: 'endUserId is required.' };
188
- }
189
- if (!config.aiEnabled) {
190
- return { code: 'AI_DISABLED', message: 'AI features are not enabled for this deployment.' };
191
- }
192
- if (!config.allowedAiCapabilities.includes('headless')) {
193
- return {
194
- code: 'CAPABILITY_DENIED',
195
- message: "This deployment does not have the 'headless' AI capability enabled.",
196
- };
197
- }
198
- return null;
199
- }
200
-
201
- /** Check estimated input tokens against the per-request cap. Returns an error or null. */
202
- function checkTokenLimit(
203
- promptLength: number,
204
- maxTokensPerRequest: number | null,
205
- ): HeadlessSessionError | null {
206
- if (maxTokensPerRequest === null) return null;
207
- const estimatedInputTokens = Math.ceil(promptLength / 4);
208
- if (estimatedInputTokens > maxTokensPerRequest) {
209
- return {
210
- code: 'RATE_LIMIT_EXCEEDED',
211
- message: `Estimated input tokens (${estimatedInputTokens}) exceeds maxTokensPerRequest (${maxTokensPerRequest}). Shorten your prompt.`,
212
- };
213
- }
214
- return null;
215
- }
216
-
217
- /** Emit health update and usage report callbacks after execution. */
218
- function emitPostExecutionCallbacks(
219
- result: DeployExecutionResult,
220
- config: DeploymentAiConfig,
221
- request: HeadlessSessionRequest,
222
- effectiveModel: string,
223
- callbacks?: HeadlessSessionStreamCallbacks,
224
- ): void {
225
- callbacks?.onUsageReport?.({
226
- deploymentId: config.deploymentId,
227
- endUserId: request.endUserId,
228
- capability: 'headless',
229
- tokensUsed: result.totalTokens,
230
- model: effectiveModel,
231
- durationMs: result.durationMs,
232
- });
233
-
234
- const healthStatus = detectAiHealthIssue(result.error);
235
- if (healthStatus) {
236
- callbacks?.onHealthUpdate?.({
237
- deploymentId: config.deploymentId,
238
- ...healthStatus,
239
- });
240
- }
241
- }
242
-
243
- // ========== Handler ==========
244
-
245
- /**
246
- * Handle a headless session request for an end user.
247
- *
248
- * Validates the deployment config, checks rate limits, composes the prompt
249
- * with the system instruction, and launches an isolated headless session
250
- * via DeployAiService. Streams results back through the provided callbacks.
251
- *
252
- * @returns Structured result with either the execution result or an error.
253
- */
254
- export async function handleHeadlessSession(
255
- request: HeadlessSessionRequest,
256
- config: DeploymentAiConfig,
257
- callbacks?: HeadlessSessionStreamCallbacks,
258
- ): Promise<HeadlessSessionResult> {
259
- // ── Validate request ───────────────────────────────────────
260
- const validationError = validateRequest(request, config);
261
- if (validationError) return { ok: false, error: validationError };
262
-
263
- // ── Rate limit checks ─────────────────────────────────────
264
- const rateLimitError = checkRateLimit(config);
265
- if (rateLimitError) return { ok: false, error: rateLimitError };
266
-
267
- // ── Token limit pre-check ─────────────────────────────────
268
- const tokenError = checkTokenLimit(request.prompt.length, config.maxTokensPerRequest);
269
- if (tokenError) return { ok: false, error: tokenError };
270
-
271
- // ── Compose prompt ─────────────────────────────────────────
272
- // Use per-request system prompt if provided, otherwise deployment default
273
- const effectiveSystemPrompt = request.systemPrompt ?? config.defaultSystemPrompt;
274
- const composedPrompt = composePrompt(effectiveSystemPrompt, request.prompt);
275
-
276
- // Use per-request model if provided, otherwise deployment default
277
- const effectiveModel = request.model ?? config.defaultModel;
278
-
279
- // ── Launch isolated session ────────────────────────────────
280
- const service = DeployAiService.getInstance();
281
-
282
- recordRequestStart(config.deploymentId);
283
-
284
- try {
285
- const result = await service.execute({
286
- deploymentId: config.deploymentId,
287
- prompt: composedPrompt,
288
- workingDir: config.workingDir,
289
- model: effectiveModel,
290
- outputCallback: callbacks?.onOutput,
291
- thinkingCallback: callbacks?.onThinking,
292
- toolUseCallback: callbacks?.onToolUse,
293
- // allowedTools from request are inverted: any tool NOT in the list is disallowed.
294
- // If allowedTools is not specified, no additional restrictions are applied
295
- // (Security Bouncer still governs tool access).
296
- disallowedTools: request.allowedTools
297
- ? invertAllowedTools(request.allowedTools)
298
- : undefined,
299
- });
300
-
301
- // Token overage is informational — session already ran, don't fail the response.
302
- // The developer can use usage reports for billing or to tighten limits.
303
-
304
- emitPostExecutionCallbacks(result, config, request, effectiveModel, callbacks);
305
-
306
- return { ok: true, result };
307
- } catch (error: unknown) {
308
- const message = error instanceof Error ? error.message : String(error);
309
-
310
- // Check for API key health issues from caught errors
311
- const healthStatus = detectAiHealthIssue(message);
312
- if (healthStatus) {
313
- callbacks?.onHealthUpdate?.({
314
- deploymentId: config.deploymentId,
315
- ...healthStatus,
316
- });
317
- }
318
-
319
- return {
320
- ok: false,
321
- error: { code: 'EXECUTION_FAILED', message },
322
- };
323
- } finally {
324
- recordRequestEnd(config.deploymentId);
325
- }
326
- }
327
-
328
- // ========== Health Detection ==========
329
-
330
- /**
331
- * Detect API key health issues from error messages returned by Claude Code.
332
- *
333
- * Anthropic API errors that indicate credential/billing problems:
334
- * - 401: Invalid API key
335
- * - 402/insufficient_funds: Account has no credits
336
- * - 429: Rate limited by Anthropic
337
- */
338
- function detectAiHealthIssue(
339
- errorMessage: string | undefined,
340
- ): { status: HealthUpdateData['status']; message: string; aiDisabled: boolean } | null {
341
- if (!errorMessage) return null;
342
-
343
- const lower = errorMessage.toLowerCase();
344
-
345
- if (lower.includes('invalid api key') || lower.includes('invalid x-api-key') || lower.includes('authentication_error')) {
346
- return { status: 'invalid_key', message: 'Anthropic API key is invalid or revoked.', aiDisabled: true };
347
- }
348
-
349
- if (lower.includes('insufficient_funds') || lower.includes('no credits') || lower.includes('billing') || lower.includes('credit balance')) {
350
- return { status: 'no_credits', message: 'Anthropic account has insufficient credits.', aiDisabled: true };
351
- }
352
-
353
- if (lower.includes('rate_limit') || lower.includes('rate limit') || lower.includes('too many requests')) {
354
- return { status: 'rate_limited', message: 'Anthropic API rate limit exceeded.', aiDisabled: false };
355
- }
356
-
357
- return null;
358
- }
359
-
360
- // ========== Helpers ==========
361
-
362
- /**
363
- * The DeployAiService accepts `disallowedTools` (blocklist), but the
364
- * headless session API exposes `allowedTools` (allowlist) for a better
365
- * developer UX. This converts an allowlist into a blocklist by marking
366
- * everything outside the allowlist as disallowed.
367
- *
368
- * We use a known set of standard Claude Code tool names. Tools not in
369
- * the known set are left unrestricted (the Security Bouncer handles them).
370
- */
371
- const KNOWN_TOOLS = [
372
- 'Read',
373
- 'Write',
374
- 'Edit',
375
- 'MultiEdit',
376
- 'Bash',
377
- 'Glob',
378
- 'Grep',
379
- 'WebFetch',
380
- 'WebSearch',
381
- 'TodoRead',
382
- 'TodoWrite',
383
- 'NotebookEdit',
384
- 'Agent',
385
- ] as const;
386
-
387
- function invertAllowedTools(allowedTools: string[]): string[] {
388
- const allowed = new Set(allowedTools);
389
- return KNOWN_TOOLS.filter((tool) => !allowed.has(tool));
390
- }
391
-
392
- /**
393
- * Get the current rate limit state for a deployment.
394
- * Useful for status/monitoring endpoints.
395
- */
396
- export function getDeploymentRateLimitState(deploymentId: string): {
397
- requestsInLastMinute: number;
398
- activeSessions: number;
399
- } {
400
- const bucket = getBucket(deploymentId);
401
- pruneTimestamps(bucket);
402
- return {
403
- requestsInLastMinute: bucket.timestamps.length,
404
- activeSessions: bucket.activeSessions,
405
- };
406
- }
407
-
408
- /**
409
- * Reset rate limit state for a deployment. Call when a deployment
410
- * is deleted or all its sessions are force-stopped.
411
- */
412
- export function resetDeploymentRateLimit(deploymentId: string): void {
413
- rateBuckets.delete(deploymentId);
414
- }