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.
- package/bin/commands/login.js +17 -7
- package/bin/commands/logout.js +14 -6
- package/bin/commands/status.js +9 -3
- package/bin/commands/whoami.js +10 -4
- package/bin/mstro.js +11 -1
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/index.d.ts +1 -0
- package/dist/server/cli/headless/index.d.ts.map +1 -1
- package/dist/server/cli/headless/index.js +2 -0
- package/dist/server/cli/headless/index.js.map +1 -1
- package/dist/server/cli/headless/resilient-runner.d.ts +47 -0
- package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -0
- package/dist/server/cli/headless/resilient-runner.js +234 -0
- package/dist/server/cli/headless/resilient-runner.js.map +1 -0
- package/dist/server/cli/headless/retry-strategies.d.ts +44 -0
- package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -0
- package/dist/server/cli/headless/retry-strategies.js +262 -0
- package/dist/server/cli/headless/retry-strategies.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +5 -0
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +2 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +31 -4
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +1 -30
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +16 -3
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/prompt-builders.d.ts.map +1 -1
- package/dist/server/cli/prompt-builders.js +31 -13
- package/dist/server/cli/prompt-builders.js.map +1 -1
- package/dist/server/index.js +1 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +5 -4
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +1 -1
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +14 -8
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-patterns.js +1 -1
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +19 -9
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +6 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +158 -76
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts +1 -0
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +6 -0
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-classification.d.ts +11 -0
- package/dist/server/services/plan/issue-classification.d.ts.map +1 -0
- package/dist/server/services/plan/issue-classification.js +20 -0
- package/dist/server/services/plan/issue-classification.js.map +1 -0
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +10 -5
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +0 -5
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
- package/dist/server/services/plan/issue-retry.js +12 -241
- package/dist/server/services/plan/issue-retry.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +9 -6
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.js +11 -4
- package/dist/server/services/platform-credentials.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +7 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +2 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +2 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +18 -7
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +6 -6
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.js +90 -42
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +48 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +22 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +48 -1
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +74 -32
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +18 -18
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/skill-handlers.d.ts +3 -1
- package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-handlers.js +52 -41
- package/dist/server/services/websocket/skill-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-watcher.d.ts +17 -0
- package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -0
- package/dist/server/services/websocket/skill-watcher.js +85 -0
- package/dist/server/services/websocket/skill-watcher.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +2 -268
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +0 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-stream.ts +1 -0
- package/server/cli/headless/index.ts +2 -0
- package/server/cli/headless/resilient-runner.ts +354 -0
- package/server/cli/headless/retry-strategies.ts +330 -0
- package/server/cli/headless/stall-assessor.ts +5 -0
- package/server/cli/headless/tool-watchdog.ts +40 -4
- package/server/cli/improvisation-retry.ts +1 -32
- package/server/cli/improvisation-session-manager.ts +17 -3
- package/server/cli/prompt-builders.ts +33 -12
- package/server/index.ts +1 -9
- package/server/mcp/bouncer-cli.ts +5 -4
- package/server/mcp/bouncer-haiku.ts +1 -1
- package/server/mcp/bouncer-integration.ts +15 -8
- package/server/mcp/security-patterns.ts +1 -1
- package/server/services/plan/agents/code-review.md +109 -0
- package/server/services/plan/agents/commit-message.md +26 -0
- package/server/services/plan/agents/execute-issue.md +10 -1
- package/server/services/plan/agents/fix-quality.md +24 -0
- package/server/services/plan/agents/pr-description.md +28 -0
- package/server/services/plan/composer.ts +20 -9
- package/server/services/plan/executor.ts +160 -76
- package/server/services/plan/front-matter.ts +7 -0
- package/server/services/plan/issue-classification.ts +21 -0
- package/server/services/plan/issue-prompt-builder.ts +11 -5
- package/server/services/plan/issue-retry.ts +15 -330
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/review-gate.ts +9 -6
- package/server/services/plan/types.ts +3 -0
- package/server/services/platform-credentials.ts +10 -4
- package/server/services/terminal/pty-manager.ts +7 -1
- package/server/services/websocket/handler-context.ts +2 -0
- package/server/services/websocket/handler.ts +18 -8
- package/server/services/websocket/plan-execution-handlers.ts +7 -7
- package/server/services/websocket/quality-fix-agent.ts +86 -44
- package/server/services/websocket/quality-handlers.ts +48 -7
- package/server/services/websocket/quality-persistence.ts +75 -1
- package/server/services/websocket/quality-review-agent.ts +70 -31
- package/server/services/websocket/quality-tools.ts +16 -14
- package/server/services/websocket/skill-handlers.ts +50 -40
- package/server/services/websocket/skill-watcher.ts +79 -0
- package/server/services/websocket/types.ts +0 -311
- package/dist/server/services/deploy/ai-broker.d.ts +0 -63
- package/dist/server/services/deploy/ai-broker.d.ts.map +0 -1
- package/dist/server/services/deploy/ai-broker.js +0 -360
- package/dist/server/services/deploy/ai-broker.js.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.d.ts +0 -114
- package/dist/server/services/deploy/board-execution-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.js +0 -621
- package/dist/server/services/deploy/board-execution-handler.js.map +0 -1
- package/dist/server/services/deploy/credentials.d.ts +0 -35
- package/dist/server/services/deploy/credentials.d.ts.map +0 -1
- package/dist/server/services/deploy/credentials.js +0 -177
- package/dist/server/services/deploy/credentials.js.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.d.ts +0 -107
- package/dist/server/services/deploy/deploy-ai-service.d.ts.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.js +0 -294
- package/dist/server/services/deploy/deploy-ai-service.js.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.d.ts +0 -94
- package/dist/server/services/deploy/headless-session-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.js +0 -266
- package/dist/server/services/deploy/headless-session-handler.js.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.d.ts +0 -14
- package/dist/server/services/websocket/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.js +0 -409
- package/dist/server/services/websocket/deploy-handlers.js.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +0 -11
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.js +0 -176
- package/dist/server/services/websocket/handlers/deploy-handlers.js.map +0 -1
- package/server/cli/headless/RESEARCH.md +0 -627
- package/server/services/deploy/ai-broker.ts +0 -512
- package/server/services/deploy/board-execution-handler.ts +0 -847
- package/server/services/deploy/credentials.ts +0 -200
- package/server/services/deploy/deploy-ai-service.ts +0 -401
- package/server/services/deploy/headless-session-handler.ts +0 -414
- package/server/services/websocket/deploy-handlers.ts +0 -544
- 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
|
-
}
|