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 +12 -1
- package/dist/session.d.ts +6 -0
- package/dist/session.js +40 -4
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +6 -4
- package/dist/ws-terminal.js +7 -1
- package/package.json +1 -1
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
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
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)}`;
|
package/dist/validation.d.ts
CHANGED
|
@@ -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;
|
package/dist/validation.js
CHANGED
|
@@ -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(),
|
package/dist/ws-terminal.js
CHANGED
|
@@ -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');
|