aegis-bridge 2.2.2 → 2.2.5

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/dist/server.js CHANGED
@@ -35,7 +35,7 @@ import { registerHookRoutes } from './hooks.js';
35
35
  import { registerWsTerminalRoute } from './ws-terminal.js';
36
36
  import { SwarmMonitor } from './swarm-monitor.js';
37
37
  import { execSync } from 'node:child_process';
38
- import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, parseIntSafe, } from './validation.js';
38
+ import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, parseIntSafe, isValidUUID, } from './validation.js';
39
39
  const __filename = fileURLToPath(import.meta.url);
40
40
  const __dirname = path.dirname(__filename);
41
41
  // ── Configuration ────────────────────────────────────────────────────
@@ -211,6 +211,13 @@ function setupAuth(authManager) {
211
211
  });
212
212
  }
213
213
  // ── v1 API Routes ───────────────────────────────────────────────────
214
+ // #412: Reject non-UUID session IDs at the routing layer
215
+ app.addHook('onRequest', async (req, reply) => {
216
+ const id = req.params.id;
217
+ if (id !== undefined && !isValidUUID(id)) {
218
+ return reply.status(400).send({ error: 'Invalid session ID — must be a UUID' });
219
+ }
220
+ });
214
221
  // #226: Zod schema for session creation
215
222
  const createSessionSchema = z.object({
216
223
  workDir: z.string().min(1),
@@ -1378,7 +1385,11 @@ async function main() {
1378
1385
  await app.register(fastifyWebsocket);
1379
1386
  registerWsTerminalRoute(app, sessions, tmux, auth);
1380
1387
  // #217: CORS configuration — restrictive by default
1388
+ // #413: Reject wildcard CORS_ORIGIN — * is insecure and allows any origin
1381
1389
  const corsOrigin = process.env.CORS_ORIGIN;
1390
+ if (corsOrigin === '*') {
1391
+ throw new Error('CORS_ORIGIN=* wildcard is not allowed. Specify explicit origins (comma-separated) or leave unset to disable CORS.');
1392
+ }
1382
1393
  await app.register(fastifyCors, {
1383
1394
  origin: corsOrigin ? corsOrigin.split(',').map(s => s.trim()) : false,
1384
1395
  });
package/dist/session.d.ts CHANGED
@@ -99,6 +99,12 @@ export declare class SessionManager {
99
99
  }>;
100
100
  /** Wait for CC idle prompt, then send. Single attempt. */
101
101
  private waitForReadyAndSend;
102
+ /**
103
+ * Issue #561: After sending an initial prompt, verify CC actually accepted it
104
+ * by polling for a state transition away from idle/unknown.
105
+ * Returns true if CC transitions to a recognized active state within the timeout.
106
+ */
107
+ private verifyPromptAccepted;
102
108
  createSession(opts: {
103
109
  workDir: string;
104
110
  name?: string;
package/dist/session.js CHANGED
@@ -293,16 +293,52 @@ export class SessionManager {
293
293
  // At session creation, no other code is writing to this pane,
294
294
  // so queue serialization is unnecessary and adds latency.
295
295
  const paneText = await this.tmux.capturePaneDirect(session.windowId);
296
- // CC shows (U+276F) when ready for input. Avoid checking for plain >
297
- // which appears frequently in tool output, diffs, and prompts.
298
- if (paneText && paneText.includes('❯')) {
299
- return this.sendMessageDirect(sessionId, prompt);
296
+ // Issue #561: Use detectUIState for robust readiness detection.
297
+ // Requires both prompt AND chrome separators (─────) to confirm idle.
298
+ // Naive includes('❯') matched splash/startup output, causing premature sends.
299
+ if (paneText && detectUIState(paneText) === 'idle') {
300
+ const result = await this.sendMessageDirect(sessionId, prompt);
301
+ if (!result.delivered)
302
+ return result;
303
+ // Issue #561: Post-send verification. Wait for CC to transition to a
304
+ // recognized active state. If CC stays in idle/unknown, the prompt was
305
+ // swallowed — report as undelivered so the retry loop can re-attempt.
306
+ const verified = await this.verifyPromptAccepted(session.windowId);
307
+ return verified
308
+ ? result
309
+ : { delivered: false, attempts: result.attempts };
300
310
  }
301
311
  await new Promise(r => setTimeout(r, pollInterval));
302
312
  pollInterval = Math.min(pollInterval * 2, MAX_POLL_MS);
303
313
  }
304
314
  return { delivered: false, attempts: 0 };
305
315
  }
316
+ /**
317
+ * Issue #561: After sending an initial prompt, verify CC actually accepted it
318
+ * by polling for a state transition away from idle/unknown.
319
+ * Returns true if CC transitions to a recognized active state within the timeout.
320
+ */
321
+ async verifyPromptAccepted(windowId) {
322
+ const VERIFY_TIMEOUT_MS = 5_000;
323
+ const VERIFY_POLL_MS = 500;
324
+ const verifyStart = Date.now();
325
+ while (Date.now() - verifyStart < VERIFY_TIMEOUT_MS) {
326
+ const paneText = await this.tmux.capturePaneDirect(windowId);
327
+ const state = detectUIState(paneText);
328
+ // Active states mean CC received and is processing the prompt.
329
+ // waiting_for_input = CC accepted prompt, awaiting follow-up (no chrome yet).
330
+ if (state === 'working' || state === 'permission_prompt' ||
331
+ state === 'bash_approval' || state === 'plan_mode' ||
332
+ state === 'ask_question' || state === 'compacting' ||
333
+ state === 'context_warning' || state === 'waiting_for_input') {
334
+ return true;
335
+ }
336
+ // idle or unknown — keep polling
337
+ await new Promise(r => setTimeout(r, VERIFY_POLL_MS));
338
+ }
339
+ console.warn(`verifyPromptAccepted: CC did not transition from idle/unknown within ${VERIFY_TIMEOUT_MS}ms`);
340
+ return false;
341
+ }
306
342
  async createSession(opts) {
307
343
  const id = crypto.randomUUID();
308
344
  const windowName = opts.name || `cc-${id.slice(0, 8)}`;
@@ -12,6 +12,8 @@ export declare const authKeySchema: z.ZodObject<{
12
12
  name: z.ZodString;
13
13
  rateLimit: z.ZodOptional<z.ZodNumber>;
14
14
  }, z.core.$strict>;
15
+ /** Maximum length for user-supplied prompts/commands (Issue #411). */
16
+ export declare const MAX_INPUT_LENGTH = 10000;
15
17
  /** POST /v1/sessions/:id/send */
16
18
  export declare const sendMessageSchema: z.ZodObject<{
17
19
  text: z.ZodString;
@@ -15,17 +15,19 @@ export const authKeySchema = z.object({
15
15
  name: z.string().min(1),
16
16
  rateLimit: z.number().int().positive().optional(),
17
17
  }).strict();
18
+ /** Maximum length for user-supplied prompts/commands (Issue #411). */
19
+ export const MAX_INPUT_LENGTH = 10_000;
18
20
  /** POST /v1/sessions/:id/send */
19
21
  export const sendMessageSchema = z.object({
20
- text: z.string().min(1),
22
+ text: z.string().min(1).max(MAX_INPUT_LENGTH),
21
23
  }).strict();
22
24
  /** POST /v1/sessions/:id/command */
23
25
  export const commandSchema = z.object({
24
- command: z.string().min(1),
26
+ command: z.string().min(1).max(MAX_INPUT_LENGTH),
25
27
  }).strict();
26
28
  /** POST /v1/sessions/:id/bash */
27
29
  export const bashSchema = z.object({
28
- command: z.string().min(1),
30
+ command: z.string().min(1).max(MAX_INPUT_LENGTH),
29
31
  }).strict();
30
32
  /** POST /v1/sessions/:id/screenshot */
31
33
  export const screenshotSchema = z.object({
@@ -70,7 +72,7 @@ export const batchSessionSchema = z.object({
70
72
  const pipelineStageSchema = z.object({
71
73
  name: z.string().min(1),
72
74
  workDir: z.string().min(1).optional(),
73
- prompt: z.string().min(1),
75
+ prompt: z.string().min(1).max(MAX_INPUT_LENGTH),
74
76
  dependsOn: z.array(z.string()).optional(),
75
77
  permissionMode: z.enum(['default', 'bypassPermissions', 'plan']).optional(),
76
78
  autoApprove: z.boolean().optional(),
@@ -19,7 +19,7 @@
19
19
  * - Shared tmux capture polls (one per session, not per connection)
20
20
  * - Ping/pong keep-alive with dead connection detection
21
21
  */
22
- import { clamp, wsInboundMessageSchema } from './validation.js';
22
+ import { clamp, wsInboundMessageSchema, isValidUUID } from './validation.js';
23
23
  const POLL_INTERVAL_MS = 500;
24
24
  const KEEPALIVE_INTERVAL_TICKS = 60; // 30s at 500ms intervals
25
25
  const KEEPALIVE_TIMEOUT_MS = 35_000; // 30s interval + 5s grace
@@ -69,6 +69,12 @@ export function registerWsTerminalRoute(app, sessions, tmux, auth) {
69
69
  },
70
70
  }, (socket, req) => {
71
71
  const sessionId = req.params.id;
72
+ // #412: Validate session ID is a UUID before lookup
73
+ if (!isValidUUID(sessionId)) {
74
+ sendError(socket, 'Invalid session ID — must be a UUID');
75
+ socket.close();
76
+ return;
77
+ }
72
78
  const session = sessions.getSession(sessionId);
73
79
  if (!session) {
74
80
  sendError(socket, 'Session not found');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.2.2",
3
+ "version": "2.2.5",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",