mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
fix(mcp): require the panel key for panel requests
panel_request now requires a key that each MCP server generates and places in its panel page, which keeps the tool from clients that list it to the model without loading the panel. The key is not access control: any MCP client can read the panel page. Routes the panel never loads (provider settings, credential and model endpoints, the MCP endpoint and development server internals) are refused to every caller, regardless of letter case or encoding. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
b1a958242c
commit
3318390e3f
9 files changed
+209
-23
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
} from './panelRequest.js';
|
||||
|
||||
const fromBase64 = (text) => Buffer.from(text, 'base64');
|
||||
const KEY = 'panel-key-for-tests';
|
||||
const gunzip = async (bytes) =>
|
||||
new Uint8Array(
|
||||
await new Response(
|
||||
@@ -25,7 +26,11 @@ function catalogWith(handler) {
|
||||
catalog: composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
app: { baseUrl: 'http://localhost:4173/', fetch: fetchImpl },
|
||||
app: {
|
||||
baseUrl: 'http://localhost:4173/',
|
||||
fetch: fetchImpl,
|
||||
panelKey: KEY,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -46,6 +51,7 @@ test('a small response returns whole, compressed when that helps', async () => {
|
||||
}),
|
||||
);
|
||||
const { data } = await catalog.call('panel_request', {
|
||||
key: KEY,
|
||||
path: '/panel/assets/style.css',
|
||||
headers: { Accept: 'text/css', Cookie: 'secret', 'X-Other': '1' },
|
||||
});
|
||||
@@ -66,12 +72,16 @@ test('a large response comes in parts that join to the original', async () => {
|
||||
const { catalog } = catalogWith(
|
||||
() => new Response(bytes, { headers: { 'content-type': 'image/png' } }),
|
||||
);
|
||||
let { data } = await catalog.call('panel_request', { path: '/a.png' });
|
||||
let { data } = await catalog.call('panel_request', {
|
||||
key: KEY,
|
||||
path: '/a.png',
|
||||
});
|
||||
assert.equal(data.encoding, 'identity');
|
||||
assert.equal(data.totalBytes, bytes.length);
|
||||
const parts = [fromBase64(data.body)];
|
||||
while (data.nextOffset !== undefined) {
|
||||
({ data } = await catalog.call('panel_request', {
|
||||
key: KEY,
|
||||
id: data.id,
|
||||
offset: data.nextOffset,
|
||||
}));
|
||||
@@ -86,7 +96,8 @@ test('a request body and method pass through; settings and other sites do not',
|
||||
() => new Response('{}', { status: 201 }),
|
||||
);
|
||||
const { data } = await catalog.call('panel_request', {
|
||||
path: '/api/openai/hud-summary',
|
||||
key: KEY,
|
||||
path: '/api/overpass',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: Buffer.from('{"a":1}').toString('base64'),
|
||||
@@ -103,12 +114,12 @@ test('a request body and method pass through; settings and other sites do not',
|
||||
'http://evil.example/',
|
||||
])
|
||||
await assert.rejects(
|
||||
catalog.call('panel_request', { path }),
|
||||
catalog.call('panel_request', { key: KEY, path }),
|
||||
(error) => error.code === 'invalid_arguments',
|
||||
path,
|
||||
);
|
||||
await assert.rejects(
|
||||
catalog.call('panel_request', { id: 'unknown' }),
|
||||
catalog.call('panel_request', { key: KEY, id: 'unknown' }),
|
||||
(error) => error.code === 'invalid_arguments',
|
||||
);
|
||||
assert.equal(requests.length, 1);
|
||||
@@ -116,7 +127,11 @@ test('a request body and method pass through; settings and other sites do not',
|
||||
|
||||
test('the path is requested as the parser reads it, without following redirects', async () => {
|
||||
const { catalog, requests } = catalogWith(() => new Response('ok'));
|
||||
await catalog.call('panel_request', { path: '/panel/a/../b.js?x=1' });
|
||||
await catalog.call('panel_request', {
|
||||
key: KEY,
|
||||
key: KEY,
|
||||
path: '/panel/a/../b.js?x=1',
|
||||
});
|
||||
assert.equal(requests[0].path, '/panel/b.js?x=1');
|
||||
assert.equal(requests[0].redirect, 'manual');
|
||||
assert.ok(requests[0].signal instanceof AbortSignal);
|
||||
@@ -137,7 +152,7 @@ test('a response larger than the panel may load is refused while reading', async
|
||||
),
|
||||
);
|
||||
await assert.rejects(
|
||||
catalog.call('panel_request', { path: '/huge' }),
|
||||
catalog.call('panel_request', { key: KEY, path: '/huge' }),
|
||||
(error) => error.code === 'unavailable' && /too large/.test(error.message),
|
||||
);
|
||||
assert.ok(sent <= PANEL_RESPONSE_LIMIT_BYTES + 2 * chunk.length, `${sent}`);
|
||||
@@ -154,16 +169,85 @@ test('large responses read in turn keep their own parts', async () => {
|
||||
headers: { 'content-type': 'image/png' },
|
||||
}),
|
||||
);
|
||||
const first = (await catalog.call('panel_request', { path: '/a.png' })).data;
|
||||
const second = (await catalog.call('panel_request', { path: '/b.png' })).data;
|
||||
const first = (
|
||||
await catalog.call('panel_request', { key: KEY, path: '/a.png' })
|
||||
).data;
|
||||
const second = (
|
||||
await catalog.call('panel_request', { key: KEY, path: '/b.png' })
|
||||
).data;
|
||||
for (const [start, fill] of [
|
||||
[first, 1],
|
||||
[second, 2],
|
||||
]) {
|
||||
const { data } = await catalog.call('panel_request', {
|
||||
key: KEY,
|
||||
id: start.id,
|
||||
offset: start.nextOffset,
|
||||
});
|
||||
assert.deepEqual([...fromBase64(data.body)], [1, 1, 1, 1, 1].fill(fill));
|
||||
}
|
||||
});
|
||||
|
||||
test('a call without the panel key is refused before any request', async () => {
|
||||
const { catalog, requests } = catalogWith(() => new Response('ok'));
|
||||
for (const key of [undefined, '', 'wrong', `${KEY}x`, KEY.toUpperCase()])
|
||||
await assert.rejects(
|
||||
catalog.call('panel_request', {
|
||||
...(key === undefined ? {} : { key }),
|
||||
path: '/panel/index.html',
|
||||
}),
|
||||
(error) => error.code === 'invalid_arguments',
|
||||
String(key),
|
||||
);
|
||||
await assert.rejects(
|
||||
catalog.call('panel_request', { id: 'held-elsewhere', offset: 0 }),
|
||||
(error) => error.code === 'invalid_arguments',
|
||||
);
|
||||
assert.equal(requests.length, 0);
|
||||
});
|
||||
|
||||
test('without a panel key in its services the tool refuses every call', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
app: {
|
||||
baseUrl: 'http://localhost:4173/',
|
||||
fetch: async () => new Response('ok'),
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
catalog.call('panel_request', { key: '', path: '/panel/index.html' }),
|
||||
(error) => error.code === 'invalid_arguments',
|
||||
);
|
||||
});
|
||||
|
||||
test('routes the panel never loads are refused in any case or encoding', async () => {
|
||||
const { catalog, requests } = catalogWith(() => new Response('ok'));
|
||||
for (const path of [
|
||||
'/API/setup/status',
|
||||
'/Api/Setup',
|
||||
'/api/realtime/token',
|
||||
'/API/REALTIME/token',
|
||||
'/api/openai/hud-summary',
|
||||
'/api/%4fpenai/hud-summary',
|
||||
'/mcp',
|
||||
'/MCP',
|
||||
'/@fs/etc/passwd',
|
||||
'/@vite/client',
|
||||
'/%40fs/x',
|
||||
'/__open-in-editor?file=x',
|
||||
])
|
||||
await assert.rejects(
|
||||
catalog.call('panel_request', { key: KEY, path }),
|
||||
(error) => error.code === 'invalid_arguments',
|
||||
path,
|
||||
);
|
||||
// Similar names the panel does load stay available.
|
||||
await catalog.call('panel_request', { key: KEY, path: '/api/setups-help' });
|
||||
await catalog.call('panel_request', { key: KEY, path: '/mcpx.json' });
|
||||
assert.deepEqual(
|
||||
requests.map((request) => request.path),
|
||||
['/api/setups-help', '/mcpx.json'],
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user