mirror of
https://github.com/larsbaunwall/vscode-copilot-bridge.git
synced 2025-10-05 22:22:59 +00:00
Bridge hardening: 400 payload validation, 429 concurrency cap (configurable), and verbose logging option
Co-Authored-By: Lars Baunwall <larslb@thinkability.dk>
This commit is contained in:
parent
52bd219586
commit
4bc97d81ac
2 changed files with 36 additions and 6 deletions
26
package.json
26
package.json
|
|
@ -28,24 +28,40 @@
|
|||
"properties": {
|
||||
"bridge.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
"default": false,
|
||||
"description": "Start the Copilot Bridge automatically when VS Code starts."
|
||||
},
|
||||
"bridge.host": {
|
||||
"type": "string",
|
||||
"default": "127.0.0.1"
|
||||
"default": "127.0.0.1",
|
||||
"description": "Bind address for the local HTTP server. For security, keep this on loopback."
|
||||
},
|
||||
"bridge.port": {
|
||||
"type": "number",
|
||||
"default": 0
|
||||
"default": 0,
|
||||
"description": "Port for the local HTTP server. 0 picks a random ephemeral port."
|
||||
},
|
||||
"bridge.token": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
"default": "",
|
||||
"description": "Optional bearer token required in Authorization header. Leave empty to disable."
|
||||
},
|
||||
"bridge.historyWindow": {
|
||||
"type": "number",
|
||||
"default": 3,
|
||||
"minimum": 0
|
||||
"minimum": 0,
|
||||
"description": "Number of user/assistant turns to include (system message is kept separately)."
|
||||
},
|
||||
"bridge.maxConcurrent": {
|
||||
"type": "number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
"description": "Maximum concurrent /v1/chat/completions requests. Excess requests return 429."
|
||||
},
|
||||
"bridge.verbose": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Verbose logging to the 'Copilot Bridge' output channel."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ let access: vscode.ChatAccess | undefined;
|
|||
let statusItem: vscode.StatusBarItem | undefined;
|
||||
let output: vscode.OutputChannel | undefined;
|
||||
let running = false;
|
||||
let activeRequests = 0;
|
||||
|
||||
export async function activate(ctx: vscode.ExtensionContext) {
|
||||
output = vscode.window.createOutputChannel('Copilot Bridge');
|
||||
|
|
@ -47,6 +48,7 @@ async function startBridge() {
|
|||
const portCfg = cfg.get<number>('port') ?? 0;
|
||||
const token = (cfg.get<string>('token') ?? '').trim();
|
||||
const hist = cfg.get<number>('historyWindow') ?? 3;
|
||||
const verbose = cfg.get<boolean>('verbose') ?? false;
|
||||
|
||||
try {
|
||||
try {
|
||||
|
|
@ -57,6 +59,7 @@ async function startBridge() {
|
|||
|
||||
server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
if (verbose) output?.appendLine(`HTTP ${req.method} ${req.url}`);
|
||||
if (token && req.headers.authorization !== `Bearer ${token}`) {
|
||||
writeJson(res, 401, { error: { message: 'unauthorized', type: 'invalid_request_error', code: 'unauthorized' } });
|
||||
return;
|
||||
|
|
@ -79,7 +82,15 @@ async function startBridge() {
|
|||
}
|
||||
|
||||
const body = await readJson(req);
|
||||
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
||||
const messages = Array.isArray(body?.messages) ? body.messages : null;
|
||||
if (!messages || messages.length === 0 || !messages.every((m: any) =>
|
||||
m && typeof m.role === 'string' &&
|
||||
/^(system|user|assistant)$/.test(m.role) &&
|
||||
m.content !== undefined && m.content !== null
|
||||
)) {
|
||||
writeJson(res, 400, { error: { message: 'invalid request', type: 'invalid_request_error', code: 'invalid_payload' } });
|
||||
return;
|
||||
}
|
||||
const prompt = normalizeMessages(messages, hist);
|
||||
const streamMode = body?.stream !== false;
|
||||
|
||||
|
|
@ -93,6 +104,7 @@ async function startBridge() {
|
|||
'Connection': 'keep-alive'
|
||||
});
|
||||
const id = `cmp_${Math.random().toString(36).slice(2)}`;
|
||||
if (verbose) output?.appendLine(`SSE start id=${id}`);
|
||||
const h1 = chatStream.onDidProduceContent((chunk) => {
|
||||
const payload = {
|
||||
id,
|
||||
|
|
@ -102,6 +114,7 @@ async function startBridge() {
|
|||
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
||||
});
|
||||
const endAll = () => {
|
||||
if (verbose) output?.appendLine(`SSE end id=${id}`);
|
||||
res.write('data: [DONE]\n\n');
|
||||
res.end();
|
||||
h1.dispose();
|
||||
|
|
@ -120,6 +133,7 @@ async function startBridge() {
|
|||
resolve();
|
||||
});
|
||||
});
|
||||
if (verbose) output?.appendLine(`Non-stream complete len=${buf.length}`);
|
||||
writeJson(res, 200, {
|
||||
id: `cmpl_${Math.random().toString(36).slice(2)}`,
|
||||
object: 'chat.completion',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue