Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/runbooks/skill-harness-proof.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ The outbound gateway allows HTTPS npm retrieval, GitHub source archives, and the
It blocks other destinations, hosted model tools, credential overrides, and model conversation reuse.
For OpenCode Go, only chat completions pass. Every tool must have the `function` type.
The gateway removes extra choices and service tier requests, because both raise the price.
It forwards the `x-opencode-*` session headers, because OpenCode Go rejects a request without `x-opencode-session`.
External documentation requests are blocked. Source files must carry the evidence for this proof.

If generation fails, read `state.result.detail` and `state.result.generation` in the saved result.
Expand Down
19 changes: 18 additions & 1 deletion workers/skill-harness/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ import { MAX_REQUEST_BYTES, parseJson, readBoundedBody } from './contracts'
export type ModelProvider = 'google' | 'anthropic' | 'opencode-go'

const OPENCODE_GO_CHAT_URL = 'https://opencode.ai/zen/go/v1/chat/completions'

/**
* OpenCode Go routes and caches by these headers, and answers 400 MissingSessionID
* without `x-opencode-session`. They name the session; none carries a credential.
*/
const OPENCODE_ROUTING_HEADERS = ['x-opencode-client', 'x-opencode-project', 'x-opencode-request', 'x-opencode-session'] as const
const ROUTING_VALUE = /^[\w.:-]{1,200}$/

function openCodeRoutingHeaders(request: Request): Record<string, string> {
const headers: Record<string, string> = {}
for (const name of OPENCODE_ROUTING_HEADERS) {
const value = request.headers.get(name)
if (value !== null && ROUTING_VALUE.test(value))
headers[name] = value
}
return headers
}
const OPENCODE_GO_MAX_OUTPUT_TOKENS = 8192
// Web search, hosted plugins, live search, and remote MCP run on the provider at a separate cost.
const OPENAI_COMPATIBLE_HOSTED_FIELDS = ['web_search_options', 'plugins', 'search_parameters', 'mcp_servers']
Expand Down Expand Up @@ -102,7 +119,7 @@ export async function forwardSandboxRequest(
delete standardInput.service_tier
return fetchClient(OPENCODE_GO_CHAT_URL, {
method: 'POST',
headers: { 'content-type': 'application/json', 'authorization': `Bearer ${options.apiKey}` },
headers: { 'content-type': 'application/json', 'authorization': `Bearer ${options.apiKey}`, ...openCodeRoutingHeaders(request) },
body: JSON.stringify({
...standardInput,
model: options.model,
Expand Down
23 changes: 20 additions & 3 deletions workers/skill-harness/test/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ function openCodeGo() {
return { ...fixture(), provider: 'opencode-go' as const, model: 'glm-5.3' }
}

function openCodeGoRequest(body: unknown, url = 'https://opencode.ai/zen/go/v1/chat/completions?key=untrusted') {
/** OpenCode sends these routing headers. OpenCode Go rejects a request without `x-opencode-session`. */
const openCodeRouting = { 'x-opencode-client': 'cli', 'x-opencode-project': 'global', 'x-opencode-request': 'msg_01K7Q3', 'x-opencode-session': 'ses_6f1c2b9a' }

function openCodeGoRequest(body: unknown, url = 'https://opencode.ai/zen/go/v1/chat/completions?key=untrusted', routing: Record<string, string> = openCodeRouting) {
return new Request(url, {
method: 'POST',
headers: { 'authorization': 'Bearer sandbox-placeholder', 'content-type': 'application/json', 'x-opencode-session': 'untrusted', 'cookie': 'private=cookie' },
headers: { 'authorization': 'Bearer sandbox-placeholder', 'content-type': 'application/json', 'cookie': 'private=cookie', ...routing },
body: typeof body === 'string' ? body : JSON.stringify(body),
})
}
Expand All @@ -48,14 +51,28 @@ describe('sandbox credential gateway', () => {
const [url, forwarded] = options.fetch.mock.calls[0]!
expect(String(url)).toBe('https://opencode.ai/zen/go/v1/chat/completions')
expect(forwarded?.redirect).toBe('manual')
expect([...new Headers(forwarded?.headers).entries()]).toEqual([['authorization', 'Bearer worker-only-secret'], ['content-type', 'application/json']])
expect([...new Headers(forwarded?.headers).entries()]).toEqual([
['authorization', 'Bearer worker-only-secret'],
['content-type', 'application/json'],
...Object.entries(openCodeRouting),
])
const body = JSON.parse(String(forwarded?.body))
expect(body).toMatchObject({ model: 'glm-5.3', max_tokens: 8192, max_completion_tokens: 8192, stream: true, tools: [{ type: 'function' }] })
expect(body).not.toHaveProperty('n')
expect(body).not.toHaveProperty('service_tier')
expect(options.consumeModelCall).toHaveBeenCalledOnce()
})

it('drops an OpenCode routing header whose value is not a plain identifier', async () => {
const options = openCodeGo()
await forwardSandboxRequest(openCodeGoRequest({ messages: [] }, undefined, { 'x-opencode-session': 'ses_ok', 'x-opencode-request': 'a b<c>', 'x-opencode-project': 'p'.repeat(300) }), options)
const [, forwarded] = options.fetch.mock.calls[0]!
const headers = new Headers(forwarded?.headers)
expect(headers.get('x-opencode-session')).toBe('ses_ok')
expect(headers.has('x-opencode-request')).toBe(false)
expect(headers.has('x-opencode-project')).toBe(false)
})

it.each([
[{ max_tokens: 1000 }, { max_tokens: 1000 }],
[{}, { max_tokens: 8192 }],
Expand Down
Loading