feat(llm): resolve the gate's selection Llm by tier, not by name #91

Open
michal wants to merge 2 commits from feat/llm-tier-resolution into main
Showing only changes of commit de906f9c75 - Show all commits

View File

@@ -32,6 +32,16 @@ function httpRequest(opts: {
headers?: Record<string, string>; headers?: Record<string, string>;
body?: string; body?: string;
timeout?: number; timeout?: number;
/**
* Resolve as soon as response headers arrive, without waiting for the body
* to end. Required for endpoints that never end: an SSE stream's socket only
* goes idle when nothing else is talking to mcplocal, so waiting for the
* inactivity timeout made this depend on whether other smoke files happened
* to be generating traffic concurrently. Resolving on headers is also a
* STRONGER assertion — the caller sees the real status instead of inferring
* "reachable" from a timeout, which would pass just as happily on a slow 401.
*/
resolveOnHeaders?: boolean;
}): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> { }): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const parsed = new URL(opts.url); const parsed = new URL(opts.url);
@@ -46,6 +56,12 @@ function httpRequest(opts: {
timeout: opts.timeout ?? 10_000, timeout: opts.timeout ?? 10_000,
}, },
(res) => { (res) => {
if (opts.resolveOnHeaders === true) {
resolve({ status: res.statusCode ?? 0, headers: res.headers, body: '' });
res.destroy();
req.destroy();
return;
}
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk)); res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => { res.on('end', () => {
@@ -92,22 +108,19 @@ describe('Smoke: Security — mcplocal unauthenticated endpoints', () => {
if (!available) return; if (!available) return;
// /inspect streams ALL MCP traffic (tool calls, arguments, responses) // /inspect streams ALL MCP traffic (tool calls, arguments, responses)
// for ALL projects to any unauthenticated local client // for ALL projects to any unauthenticated local client. The stream never
// ends, so take the status off the response headers and hang up.
const res = await httpRequest({ const res = await httpRequest({
url: `${MCPLOCAL_URL}/inspect`, url: `${MCPLOCAL_URL}/inspect`,
method: 'GET', method: 'GET',
headers: { 'Accept': 'text/event-stream' }, headers: { 'Accept': 'text/event-stream' },
timeout: 3_000, timeout: 5_000,
}).catch((err) => { resolveOnHeaders: true,
// Timeout is expected (SSE keeps connection open) — still means endpoint is accessible
if ((err as Error).message.includes('timed out')) {
return { status: 200, headers: {} as http.IncomingHttpHeaders, body: '' };
}
throw err;
}); });
// Should be accessible without auth (documenting the vulnerability) // Should be accessible without auth (documenting the vulnerability)
expect(res.status).toBeLessThan(400); expect(res.status).toBeLessThan(400);
expect(res.headers['content-type']).toContain('text/event-stream');
console.log(` ⚠ /inspect accessible without auth (status ${res.status})`); console.log(` ⚠ /inspect accessible without auth (status ${res.status})`);
}, 10_000); }, 10_000);