Skip to content

Commit 832c8c8

Browse files
lilyshen0722kai
andauthored
[TASK-190] Log refused OpenCode proxy routes (#2122)
* fix(cli): log refused OpenCode proxy routes Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid> Agent-Model: codex/gpt-6-luna/max * fix(cli): bump release for proxy diagnostics Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid> Agent-Model: codex/gpt-6-luna/max --------- Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
1 parent 868256c commit 832c8c8

4 files changed

Lines changed: 73 additions & 9 deletions

File tree

‎cli/__tests__/adapters.opencode.test.mjs‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,55 @@ describe('opencode external provider config', () => {
364364
});
365365
});
366366

367+
test('logs each refused provider route to wrapper stderr using only its normalized path', async () => {
368+
await withTemp(async (root) => {
369+
const keyFile = join(root, 'provider-key');
370+
const proxyTokenFile = join(root, 'proxy-token');
371+
await writeFile(keyFile, 'upstream-secret');
372+
await chmod(keyFile, 0o600);
373+
let upstreamCalls = 0;
374+
const upstream = createServer((req, res) => {
375+
upstreamCalls += 1;
376+
req.resume();
377+
res.writeHead(200).end('{}');
378+
});
379+
await new Promise((resolve) => upstream.listen(0, '127.0.0.1', resolve));
380+
const upstreamPort = upstream.address().port;
381+
const proxy = await startProviderProxy({
382+
provider: { baseURL: `http://127.0.0.1:${upstreamPort}/v1` },
383+
keyFile,
384+
tokenFile: proxyTokenFile,
385+
});
386+
const proxyToken = readFileSync(proxyTokenFile, 'utf8');
387+
const stderrWrite = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
388+
try {
389+
const unsupported = await fetch(`${proxy.baseURL}/embeddings?secret=do-not-log`, {
390+
method: 'POST',
391+
headers: { authorization: `Bearer ${proxyToken}` },
392+
body: 'request body must not be logged',
393+
});
394+
expect(unsupported.status).toBe(403);
395+
await unsupported.text();
396+
397+
const malformed = await requestProxy(proxy.port, '/v1/%2fadmin?secret=do-not-log', {
398+
authorization: `Bearer ${proxyToken}`,
399+
});
400+
expect(malformed).toBe(400);
401+
402+
expect(stderrWrite.mock.calls.map(([line]) => String(line))).toEqual([
403+
'[opencode] provider proxy refused POST /v1/embeddings\n',
404+
'[opencode] provider proxy refused POST /v1/%2fadmin\n',
405+
]);
406+
expect(stderrWrite.mock.calls).toHaveLength(2);
407+
expect(upstreamCalls).toBe(0);
408+
} finally {
409+
stderrWrite.mockRestore();
410+
await proxy.close();
411+
await new Promise((resolve) => upstream.close(resolve));
412+
}
413+
});
414+
});
415+
367416
test('uses file-backed provider auth and materializes only the selected model when models are omitted', () => {
368417
const config = buildProviderConfig({
369418
id: 'litellm',

‎cli/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cli/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@commonlyai/cli",
3-
"version": "0.1.92",
3+
"version": "0.1.93",
44
"license": "Apache-2.0",
55
"description": "The Commonly CLI — connect agents, manage pods, iterate fast",
66
"type": "module",

‎cli/src/lib/adapters/opencode.js‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,18 @@ const startProviderProxy = async ({ provider, keyFile, tokenFile }) => {
342342
await writeFile(tokenFile, proxyToken, { encoding: 'utf8', mode: 0o600 });
343343
await chmod(tokenFile, 0o600);
344344

345+
const logRefusedRequest = (method, requestPath) => {
346+
let normalizedPath = '/';
347+
try {
348+
normalizedPath = new URL(requestPath || '/', 'http://127.0.0.1').pathname || '/';
349+
} catch {
350+
// Do not include an unparseable raw request target in logs.
351+
}
352+
const safeMethod = typeof method === 'string' && /^[A-Z]+$/.test(method)
353+
? method : 'UNKNOWN';
354+
process.stderr.write(`[opencode] provider proxy refused ${safeMethod} ${normalizedPath}\n`);
355+
};
356+
345357
const server = createServer((incoming, outgoing) => {
346358
const supplied = Buffer.from(incoming.headers.authorization || '');
347359
const expected = Buffer.from(`Bearer ${proxyToken}`);
@@ -356,22 +368,26 @@ const startProviderProxy = async ({ provider, keyFile, tokenFile }) => {
356368
return;
357369
}
358370
const rejectRoute = (path) => {
371+
logRefusedRequest(incoming.method, path);
359372
incoming.resume();
360373
outgoing.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
361374
outgoing.end(`Unsupported provider request: ${incoming.method || 'UNKNOWN'} ${path}`);
362375
};
376+
const rejectBadRequest = () => {
377+
logRefusedRequest(incoming.method, incoming.url);
378+
incoming.resume();
379+
outgoing.writeHead(400).end();
380+
};
363381

364382
let target;
365383
try {
366384
const requestPath = incoming.url || '/';
367385
if (!requestPath.startsWith('/') || requestPath.startsWith('//')) {
368-
incoming.resume();
369-
outgoing.writeHead(400).end();
386+
rejectBadRequest();
370387
return;
371388
}
372389
if (/%(?:2f|5c|2e)/i.test(requestPath)) {
373-
incoming.resume();
374-
outgoing.writeHead(400).end();
390+
rejectBadRequest();
375391
return;
376392
}
377393
// OpenCode owns this local request path, so never use it as an outbound
@@ -405,8 +421,7 @@ const startProviderProxy = async ({ provider, keyFile, tokenFile }) => {
405421
host: upstream.host,
406422
};
407423
} catch {
408-
incoming.resume();
409-
outgoing.writeHead(400).end();
424+
rejectBadRequest();
410425
return;
411426
}
412427

0 commit comments

Comments
 (0)