mstro-app 0.4.39 → 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 (197) 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 +7 -4
  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/fix-quality.md +24 -0
  139. package/server/services/plan/agents/pr-description.md +28 -0
  140. package/server/services/plan/composer.ts +20 -9
  141. package/server/services/plan/executor.ts +160 -76
  142. package/server/services/plan/front-matter.ts +7 -0
  143. package/server/services/plan/issue-classification.ts +21 -0
  144. package/server/services/plan/issue-prompt-builder.ts +8 -4
  145. package/server/services/plan/issue-retry.ts +15 -330
  146. package/server/services/plan/parser-core.ts +1 -0
  147. package/server/services/plan/review-gate.ts +9 -6
  148. package/server/services/plan/types.ts +3 -0
  149. package/server/services/platform-credentials.ts +10 -4
  150. package/server/services/terminal/pty-manager.ts +7 -1
  151. package/server/services/websocket/handler-context.ts +2 -0
  152. package/server/services/websocket/handler.ts +18 -8
  153. package/server/services/websocket/plan-execution-handlers.ts +7 -7
  154. package/server/services/websocket/quality-fix-agent.ts +86 -44
  155. package/server/services/websocket/quality-handlers.ts +48 -7
  156. package/server/services/websocket/quality-persistence.ts +75 -1
  157. package/server/services/websocket/quality-review-agent.ts +70 -31
  158. package/server/services/websocket/quality-tools.ts +16 -14
  159. package/server/services/websocket/skill-handlers.ts +50 -40
  160. package/server/services/websocket/skill-watcher.ts +79 -0
  161. package/server/services/websocket/types.ts +0 -311
  162. package/dist/server/services/deploy/ai-broker.d.ts +0 -63
  163. package/dist/server/services/deploy/ai-broker.d.ts.map +0 -1
  164. package/dist/server/services/deploy/ai-broker.js +0 -360
  165. package/dist/server/services/deploy/ai-broker.js.map +0 -1
  166. package/dist/server/services/deploy/board-execution-handler.d.ts +0 -114
  167. package/dist/server/services/deploy/board-execution-handler.d.ts.map +0 -1
  168. package/dist/server/services/deploy/board-execution-handler.js +0 -621
  169. package/dist/server/services/deploy/board-execution-handler.js.map +0 -1
  170. package/dist/server/services/deploy/credentials.d.ts +0 -35
  171. package/dist/server/services/deploy/credentials.d.ts.map +0 -1
  172. package/dist/server/services/deploy/credentials.js +0 -177
  173. package/dist/server/services/deploy/credentials.js.map +0 -1
  174. package/dist/server/services/deploy/deploy-ai-service.d.ts +0 -107
  175. package/dist/server/services/deploy/deploy-ai-service.d.ts.map +0 -1
  176. package/dist/server/services/deploy/deploy-ai-service.js +0 -294
  177. package/dist/server/services/deploy/deploy-ai-service.js.map +0 -1
  178. package/dist/server/services/deploy/headless-session-handler.d.ts +0 -94
  179. package/dist/server/services/deploy/headless-session-handler.d.ts.map +0 -1
  180. package/dist/server/services/deploy/headless-session-handler.js +0 -266
  181. package/dist/server/services/deploy/headless-session-handler.js.map +0 -1
  182. package/dist/server/services/websocket/deploy-handlers.d.ts +0 -14
  183. package/dist/server/services/websocket/deploy-handlers.d.ts.map +0 -1
  184. package/dist/server/services/websocket/deploy-handlers.js +0 -409
  185. package/dist/server/services/websocket/deploy-handlers.js.map +0 -1
  186. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +0 -11
  187. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +0 -1
  188. package/dist/server/services/websocket/handlers/deploy-handlers.js +0 -176
  189. package/dist/server/services/websocket/handlers/deploy-handlers.js.map +0 -1
  190. package/server/cli/headless/RESEARCH.md +0 -627
  191. package/server/services/deploy/ai-broker.ts +0 -512
  192. package/server/services/deploy/board-execution-handler.ts +0 -847
  193. package/server/services/deploy/credentials.ts +0 -200
  194. package/server/services/deploy/deploy-ai-service.ts +0 -401
  195. package/server/services/deploy/headless-session-handler.ts +0 -414
  196. package/server/services/websocket/deploy-handlers.ts +0 -544
  197. package/server/services/websocket/handlers/deploy-handlers.ts +0 -228
@@ -1,200 +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
- * Deploy Credentials — AES-256-GCM encrypted API key storage.
6
- *
7
- * Stores the developer's Anthropic API key at ~/.mstro/deploy-credentials.json,
8
- * encrypted with a machine-specific secret (hostname + username).
9
- *
10
- * The key derivation and storage format must remain compatible with
11
- * deploy-ai-service.ts which reads credentials at execution time.
12
- */
13
-
14
- import {
15
- createCipheriv,
16
- createDecipheriv,
17
- randomBytes,
18
- scryptSync,
19
- } from 'node:crypto';
20
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
21
- import { homedir, hostname, userInfo } from 'node:os';
22
- import { dirname, join } from 'node:path';
23
-
24
- const DEPLOY_CREDENTIALS_PATH = join(homedir(), '.mstro', 'deploy-credentials.json');
25
-
26
- // ── Validation rate limiter (max 10 attempts per minute) ────
27
-
28
- const RATE_LIMIT_MAX = 10;
29
- const RATE_LIMIT_WINDOW_MS = 60_000;
30
- const validationTimestamps: number[] = [];
31
-
32
- /**
33
- * Check if a validation attempt is allowed under the rate limit.
34
- * Returns true if allowed, false if rate-limited.
35
- */
36
- export function checkValidationRateLimit(): boolean {
37
- const now = Date.now();
38
- // Prune timestamps outside the window
39
- while (validationTimestamps.length > 0 && validationTimestamps[0]! < now - RATE_LIMIT_WINDOW_MS) {
40
- validationTimestamps.shift();
41
- }
42
- if (validationTimestamps.length >= RATE_LIMIT_MAX) {
43
- return false;
44
- }
45
- validationTimestamps.push(now);
46
- return true;
47
- }
48
-
49
- interface StoredDeployCredentials {
50
- iv: string;
51
- authTag: string;
52
- encrypted: string;
53
- lastFour: string;
54
- }
55
-
56
- /**
57
- * Derive the encryption key from machine-specific secret.
58
- * Must match the derivation used by deploy-ai-service.ts.
59
- */
60
- function deriveEncryptionKey(salt: string): Buffer {
61
- const machineSecret = `${hostname()}${userInfo().username}`;
62
- return scryptSync(machineSecret, salt, 32);
63
- }
64
-
65
- /**
66
- * Encrypt an API key with AES-256-GCM.
67
- */
68
- function encryptKey(apiKey: string): StoredDeployCredentials {
69
- const iv = randomBytes(16);
70
- const key = deriveEncryptionKey(iv.toString('hex')); // salt = iv hex for simplicity
71
- const cipher = createCipheriv('aes-256-gcm', key, iv);
72
- let encrypted = cipher.update(apiKey, 'utf-8', 'hex');
73
- encrypted += cipher.final('hex');
74
- const authTag = cipher.getAuthTag();
75
-
76
- return {
77
- iv: iv.toString('hex'),
78
- authTag: authTag.toString('hex'),
79
- encrypted,
80
- lastFour: apiKey.slice(-4),
81
- };
82
- }
83
-
84
- /**
85
- * Decrypt a stored API key.
86
- */
87
- function decryptKey(stored: StoredDeployCredentials): string | null {
88
- try {
89
- const iv = Buffer.from(stored.iv, 'hex');
90
- const authTag = Buffer.from(stored.authTag, 'hex');
91
- const key = deriveEncryptionKey(stored.iv); // salt = iv for simplicity
92
- const decipher = createDecipheriv('aes-256-gcm', key, iv);
93
- decipher.setAuthTag(authTag);
94
- let decrypted = decipher.update(stored.encrypted, 'hex', 'utf-8');
95
- decrypted += decipher.final('utf-8');
96
- return decrypted;
97
- } catch {
98
- return null;
99
- }
100
- }
101
-
102
- /**
103
- * Store an encrypted API key to ~/.mstro/deploy-credentials.json.
104
- * Creates the directory if it doesn't exist. File is mode 0600.
105
- */
106
- export function storeApiKey(apiKey: string): { lastFour: string } {
107
- const stored = encryptKey(apiKey);
108
- const dir = dirname(DEPLOY_CREDENTIALS_PATH);
109
- if (!existsSync(dir)) {
110
- mkdirSync(dir, { recursive: true });
111
- }
112
- writeFileSync(DEPLOY_CREDENTIALS_PATH, JSON.stringify(stored, null, 2), {
113
- mode: 0o600,
114
- });
115
- return { lastFour: stored.lastFour };
116
- }
117
-
118
- /**
119
- * Read and decrypt the stored API key. Returns null if missing or corrupted.
120
- */
121
- export function readApiKey(): string | null {
122
- if (!existsSync(DEPLOY_CREDENTIALS_PATH)) {
123
- return null;
124
- }
125
- try {
126
- const raw = readFileSync(DEPLOY_CREDENTIALS_PATH, 'utf-8');
127
- const stored: StoredDeployCredentials = JSON.parse(raw);
128
- if (!stored.encrypted || !stored.iv || !stored.authTag) {
129
- return null;
130
- }
131
- return decryptKey(stored);
132
- } catch {
133
- return null;
134
- }
135
- }
136
-
137
- /**
138
- * Get API key status without exposing the key.
139
- * Checks env var first (highest priority), then stored credentials.
140
- */
141
- export function getApiKeyStatus(): { status: 'valid' | 'missing'; lastFour?: string; source?: 'env' | 'stored' } {
142
- // 1. Check environment variable (highest priority)
143
- const envKey = process.env.ANTHROPIC_API_KEY;
144
- if (envKey && envKey.trim().length > 0) {
145
- return {
146
- status: 'valid',
147
- lastFour: envKey.trim().slice(-4),
148
- source: 'env',
149
- };
150
- }
151
-
152
- // 2. Check stored credentials
153
- if (!existsSync(DEPLOY_CREDENTIALS_PATH)) {
154
- return { status: 'missing' };
155
- }
156
- try {
157
- const raw = readFileSync(DEPLOY_CREDENTIALS_PATH, 'utf-8');
158
- const stored: StoredDeployCredentials = JSON.parse(raw);
159
- if (!stored.encrypted || !stored.iv || !stored.authTag) {
160
- return { status: 'missing' };
161
- }
162
- // Verify we can still decrypt (machine identity unchanged)
163
- const decrypted = decryptKey(stored);
164
- if (!decrypted) {
165
- return { status: 'missing' };
166
- }
167
- return { status: 'valid', lastFour: stored.lastFour, source: 'stored' };
168
- } catch {
169
- return { status: 'missing' };
170
- }
171
- }
172
-
173
- /**
174
- * Delete stored API key.
175
- */
176
- export function deleteApiKey(): void {
177
- if (existsSync(DEPLOY_CREDENTIALS_PATH)) {
178
- unlinkSync(DEPLOY_CREDENTIALS_PATH);
179
- }
180
- }
181
-
182
- /**
183
- * Validate an Anthropic API key by calling GET /v1/models.
184
- * Returns true if the key is valid, false otherwise.
185
- */
186
- export async function validateAnthropicKey(apiKey: string): Promise<boolean> {
187
- try {
188
- const response = await fetch('https://api.anthropic.com/v1/models', {
189
- method: 'GET',
190
- headers: {
191
- 'x-api-key': apiKey,
192
- 'anthropic-version': '2023-06-01',
193
- },
194
- signal: AbortSignal.timeout(10_000),
195
- });
196
- return response.ok;
197
- } catch {
198
- return false;
199
- }
200
- }
@@ -1,401 +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
- * Deploy AI Service
6
- *
7
- * Core service managing Deploy AI execution. Every execution is gated on the
8
- * developer's Anthropic API key — subscription (OAuth) auth is rejected.
9
- *
10
- * The API key is injected into each spawned Claude Code process via `extraEnv`
11
- * and is never logged or sent to the platform server.
12
- */
13
-
14
- import { createDecipheriv, scryptSync } from 'node:crypto';
15
- import { EventEmitter } from 'node:events';
16
- import { existsSync, readFileSync } from 'node:fs';
17
- import { homedir, hostname, userInfo } from 'node:os';
18
- import { join } from 'node:path';
19
- import { HeadlessRunner } from '../../cli/headless/runner.js';
20
- import type { HeadlessConfig, SessionResult, ToolUseEvent } from '../../cli/headless/types.js';
21
- import { getCredentials } from '../platform-credentials.js';
22
-
23
- // ========== Credential Detection ==========
24
-
25
- /**
26
- * Credential types returned by readOwnerApiCredential().
27
- *
28
- * - 'api-key': Developer has an explicit Anthropic API key (env var or stored).
29
- * This is the ONLY type that allows Deploy execution.
30
- * - 'oauth': Developer is authenticated via Claude subscription (platform creds
31
- * exist but no API key). Deploy rejects this.
32
- * - null: No authentication found at all. Deploy rejects this.
33
- */
34
- export type OwnerCredentialResult =
35
- | { type: 'api-key'; key: string; source: 'env' | 'stored' }
36
- | { type: 'oauth' }
37
- | null;
38
-
39
- const DEPLOY_CREDENTIALS_PATH = join(homedir(), '.mstro', 'deploy-credentials.json');
40
-
41
- interface StoredDeployCredentials {
42
- iv: string;
43
- authTag: string;
44
- encrypted: string;
45
- lastFour: string;
46
- }
47
-
48
- /**
49
- * Derive the encryption key from machine-specific secret.
50
- * Must match the derivation used by IS-026 (credential storage).
51
- */
52
- function deriveEncryptionKey(salt: string): Buffer {
53
- const machineSecret = `${hostname()}${userInfo().username}`;
54
- return scryptSync(machineSecret, salt, 32);
55
- }
56
-
57
- /**
58
- * Decrypt a stored API key from ~/.mstro/deploy-credentials.json.
59
- */
60
- function decryptStoredKey(stored: StoredDeployCredentials): string | null {
61
- try {
62
- const iv = Buffer.from(stored.iv, 'hex');
63
- const authTag = Buffer.from(stored.authTag, 'hex');
64
- const key = deriveEncryptionKey(stored.iv); // salt = iv for simplicity
65
- const decipher = createDecipheriv('aes-256-gcm', key, iv);
66
- decipher.setAuthTag(authTag);
67
- let decrypted = decipher.update(stored.encrypted, 'hex', 'utf-8');
68
- decrypted += decipher.final('utf-8');
69
- return decrypted;
70
- } catch {
71
- return null;
72
- }
73
- }
74
-
75
- /**
76
- * Read the owner's API credential. Resolution order:
77
- * 1. ANTHROPIC_API_KEY environment variable
78
- * 2. Locally encrypted storage (~/.mstro/deploy-credentials.json)
79
- * 3. Platform credentials exist (OAuth/subscription) -> { type: 'oauth' }
80
- * 4. Nothing -> null
81
- *
82
- * Deploy ONLY proceeds if result.type === 'api-key'.
83
- */
84
- export function readOwnerApiCredential(): OwnerCredentialResult {
85
- // 1. Check environment variable (highest priority)
86
- const envKey = process.env.ANTHROPIC_API_KEY;
87
- if (envKey && envKey.trim().length > 0) {
88
- return { type: 'api-key', key: envKey.trim(), source: 'env' };
89
- }
90
-
91
- // 2. Check locally encrypted storage
92
- if (existsSync(DEPLOY_CREDENTIALS_PATH)) {
93
- try {
94
- const raw = readFileSync(DEPLOY_CREDENTIALS_PATH, 'utf-8');
95
- const stored: StoredDeployCredentials = JSON.parse(raw);
96
- if (stored.encrypted && stored.iv && stored.authTag) {
97
- const decrypted = decryptStoredKey(stored);
98
- if (decrypted) {
99
- return { type: 'api-key', key: decrypted, source: 'stored' };
100
- }
101
- }
102
- } catch {
103
- // Corrupted credentials file — fall through
104
- }
105
- }
106
-
107
- // 3. Platform credentials exist -> user has subscription (OAuth) but no API key
108
- const platformCreds = getCredentials();
109
- if (platformCreds) {
110
- return { type: 'oauth' };
111
- }
112
-
113
- // 4. No authentication at all
114
- return null;
115
- }
116
-
117
- // ========== Deploy Error ==========
118
-
119
- const DEPLOY_API_KEY_ERROR =
120
- 'Deploy requires an Anthropic API key. Add your key in Deploy \u2192 AI Config or set ANTHROPIC_API_KEY in your environment.';
121
-
122
- export class DeployApiKeyError extends Error {
123
- constructor() {
124
- super(DEPLOY_API_KEY_ERROR);
125
- this.name = 'DeployApiKeyError';
126
- }
127
- }
128
-
129
- // ========== Deploy Session Types ==========
130
-
131
- export interface DeployExecutionOptions {
132
- /** Unique identifier for this deployment */
133
- deploymentId: string;
134
- /** The prompt/task to execute */
135
- prompt: string;
136
- /** Working directory for the Claude Code instance */
137
- workingDir: string;
138
- /** Optional model override */
139
- model?: string;
140
- /** Callback for streaming output */
141
- outputCallback?: (text: string) => void;
142
- /** Callback for thinking output */
143
- thinkingCallback?: (text: string) => void;
144
- /** Callback for tool use events */
145
- toolUseCallback?: (event: ToolUseEvent) => void;
146
- /** Tools to disallow in this session */
147
- disallowedTools?: string[];
148
- }
149
-
150
- export interface DeploySession {
151
- id: string;
152
- deploymentId: string;
153
- runner: HeadlessRunner;
154
- startedAt: number;
155
- status: 'running' | 'completed' | 'failed' | 'cancelled';
156
- }
157
-
158
- export interface DeployExecutionResult {
159
- sessionId: string;
160
- completed: boolean;
161
- error?: string;
162
- totalTokens: number;
163
- assistantResponse?: string;
164
- durationMs: number;
165
- }
166
-
167
- // ========== Deploy AI Service (Singleton) ==========
168
-
169
- const MAX_CONCURRENT_SESSIONS_PER_DEPLOYMENT = 3;
170
- const MAX_TOTAL_SESSIONS = 10;
171
-
172
- export class DeployAiService extends EventEmitter {
173
- private static instance: DeployAiService | null = null;
174
- private sessions: Map<string, DeploySession> = new Map();
175
-
176
- private constructor() {
177
- super();
178
- }
179
-
180
- static getInstance(): DeployAiService {
181
- if (!DeployAiService.instance) {
182
- DeployAiService.instance = new DeployAiService();
183
- }
184
- return DeployAiService.instance;
185
- }
186
-
187
- /**
188
- * Execute a deploy AI task. Hard-gates on API key authentication.
189
- *
190
- * @throws DeployApiKeyError if credential type is 'oauth' or null
191
- */
192
- async execute(options: DeployExecutionOptions): Promise<DeployExecutionResult> {
193
- const startTime = Date.now();
194
- const sessionId = `deploy-${options.deploymentId}-${Date.now()}`;
195
-
196
- // ===== CRITICAL: API key auth enforcement =====
197
- const credential = readOwnerApiCredential();
198
-
199
- if (!credential || credential.type !== 'api-key') {
200
- throw new DeployApiKeyError();
201
- }
202
-
203
- const deployApiKey = credential.key;
204
- // ===== END auth gate =====
205
-
206
- // Enforce concurrency limits
207
- this.enforceSessionLimits(options.deploymentId);
208
-
209
- // Create isolated HeadlessRunner with the developer's API key injected.
210
- // deployMode activates additional Security Bouncer patterns for end-user-driven sessions.
211
- const runnerConfig: Partial<HeadlessConfig> = {
212
- workingDir: options.workingDir,
213
- directPrompt: options.prompt,
214
- model: options.model,
215
- outputCallback: options.outputCallback,
216
- thinkingCallback: options.thinkingCallback,
217
- toolUseCallback: options.toolUseCallback,
218
- disallowedTools: options.disallowedTools,
219
- extraEnv: { ANTHROPIC_API_KEY: deployApiKey },
220
- verbose: false,
221
- deployMode: true,
222
- };
223
-
224
- const runner = new HeadlessRunner(runnerConfig);
225
-
226
- const session: DeploySession = {
227
- id: sessionId,
228
- deploymentId: options.deploymentId,
229
- runner,
230
- startedAt: startTime,
231
- status: 'running',
232
- };
233
-
234
- this.sessions.set(sessionId, session);
235
- this.emit('sessionStart', { sessionId, deploymentId: options.deploymentId });
236
-
237
- try {
238
- const result: SessionResult = await runner.run();
239
-
240
- session.status = result.completed ? 'completed' : 'failed';
241
-
242
- const executionResult: DeployExecutionResult = {
243
- sessionId,
244
- completed: result.completed,
245
- error: result.error,
246
- totalTokens: result.totalTokens,
247
- assistantResponse: result.assistantResponse,
248
- durationMs: Date.now() - startTime,
249
- };
250
-
251
- this.emit('sessionComplete', {
252
- sessionId,
253
- deploymentId: options.deploymentId,
254
- completed: result.completed,
255
- durationMs: executionResult.durationMs,
256
- });
257
-
258
- return executionResult;
259
- } catch (error: unknown) {
260
- session.status = 'failed';
261
- const errorMessage = error instanceof Error ? error.message : String(error);
262
-
263
- this.emit('sessionError', {
264
- sessionId,
265
- deploymentId: options.deploymentId,
266
- error: errorMessage,
267
- });
268
-
269
- return {
270
- sessionId,
271
- completed: false,
272
- error: errorMessage,
273
- totalTokens: 0,
274
- durationMs: Date.now() - startTime,
275
- };
276
- } finally {
277
- // Cleanup: remove from active sessions after a brief delay
278
- // to allow status queries on recently-completed sessions
279
- setTimeout(() => {
280
- this.sessions.delete(sessionId);
281
- }, 30_000);
282
- }
283
- }
284
-
285
- /**
286
- * Stop a specific deploy session.
287
- */
288
- stopSession(sessionId: string): boolean {
289
- const session = this.sessions.get(sessionId);
290
- if (!session || session.status !== 'running') {
291
- return false;
292
- }
293
-
294
- session.runner.cleanup();
295
- session.status = 'cancelled';
296
-
297
- this.emit('sessionCancelled', {
298
- sessionId,
299
- deploymentId: session.deploymentId,
300
- });
301
-
302
- return true;
303
- }
304
-
305
- /**
306
- * Stop all sessions for a specific deployment.
307
- */
308
- stopDeployment(deploymentId: string): number {
309
- let stopped = 0;
310
- for (const [, session] of this.sessions) {
311
- if (session.deploymentId === deploymentId && session.status === 'running') {
312
- session.runner.cleanup();
313
- session.status = 'cancelled';
314
- stopped++;
315
- }
316
- }
317
- return stopped;
318
- }
319
-
320
- /**
321
- * Get the current status of a deploy session.
322
- */
323
- getSession(sessionId: string): DeploySession | undefined {
324
- return this.sessions.get(sessionId);
325
- }
326
-
327
- /**
328
- * Get all active sessions for a deployment.
329
- */
330
- getDeploymentSessions(deploymentId: string): DeploySession[] {
331
- const result: DeploySession[] = [];
332
- for (const session of this.sessions.values()) {
333
- if (session.deploymentId === deploymentId) {
334
- result.push(session);
335
- }
336
- }
337
- return result;
338
- }
339
-
340
- /**
341
- * Count active (running) sessions for a deployment.
342
- */
343
- getActiveSessionCount(deploymentId: string): number {
344
- let count = 0;
345
- for (const session of this.sessions.values()) {
346
- if (session.deploymentId === deploymentId && session.status === 'running') {
347
- count++;
348
- }
349
- }
350
- return count;
351
- }
352
-
353
- /**
354
- * Cleanup all sessions — call on process exit.
355
- */
356
- cleanup(): void {
357
- for (const session of this.sessions.values()) {
358
- if (session.status === 'running') {
359
- session.runner.cleanup();
360
- session.status = 'cancelled';
361
- }
362
- }
363
- this.sessions.clear();
364
- }
365
-
366
- /**
367
- * Sweep completed/failed sessions that have exceeded the retention window.
368
- */
369
- sweepStaleSessions(): number {
370
- let swept = 0;
371
- const now = Date.now();
372
- for (const [sessionId, session] of this.sessions) {
373
- if (session.status !== 'running' && now - session.startedAt > 60_000) {
374
- this.sessions.delete(sessionId);
375
- swept++;
376
- }
377
- }
378
- return swept;
379
- }
380
-
381
- // ========== Internal ==========
382
-
383
- private enforceSessionLimits(deploymentId: string): void {
384
- const activeForDeployment = this.getActiveSessionCount(deploymentId);
385
- if (activeForDeployment >= MAX_CONCURRENT_SESSIONS_PER_DEPLOYMENT) {
386
- throw new Error(
387
- `Deployment ${deploymentId} has reached the maximum of ${MAX_CONCURRENT_SESSIONS_PER_DEPLOYMENT} concurrent sessions.`
388
- );
389
- }
390
-
391
- let totalActive = 0;
392
- for (const session of this.sessions.values()) {
393
- if (session.status === 'running') totalActive++;
394
- }
395
- if (totalActive >= MAX_TOTAL_SESSIONS) {
396
- throw new Error(
397
- `Maximum total concurrent deploy sessions (${MAX_TOTAL_SESSIONS}) reached. Wait for existing sessions to complete.`
398
- );
399
- }
400
- }
401
- }