@koteye/kote-gateway-opencode
v0.1.1
Published
Fail-closed Kote Gateway transport plugin for OpenCode
Maintainers
Readme
KoteGateway for OpenCode
@koteye/kote-gateway-opencode routes OpenCode provider traffic either directly or through the KoteGateway HTTPS CONNECT proxy. The same package runs in the Bun-based OpenCode CLI and the Electron/Node-based OpenCode Desktop host. It is a standalone OpenCode plugin: it does not patch OpenCode, replace providers, copy credentials, or add UI.
Подробная инструкция на русском языке: установка, настройка и ручная проверка.
The route is selected by the exact model.providerID supplied to the public chat.headers hook. There is no provider allowlist. A new provider automatically uses the configured default when its HTTP transport reaches the process fetch interceptor.
Compatibility ceiling: OpenCode 1.18.18. The source audit used OpenCode 1.18.18 (
2cba7e227d68a7e7e4a2aa9c85b808e8ecb14daf),@opencode-ai/plugin1.18.18, and Bun 1.3.14. The Desktop path requires Node.js 18.17 or newer and is covered by a Node runtime smoke test. See Provider compatibility before enabling proxy mode for a new transport.
Routing model
direct: the provider's existing transport calls the fetch implementation that was present when the plugin loaded. KoteGateway is not consulted.proxy: the original provider URL is fetched with Bun's per-requestproxyoption in CLI, or anundiciProxyAgentdispatcher in Desktop's Electron/Node host. Bootstrap or proxy failure terminates the request. There is no automatic direct fallback.
For HTTPS targets, both runtime adapters open a CONNECT host:port tunnel and TLS remains end to end between OpenCode and the provider. Provider authorization stays in the encrypted target request; it is not repurposed as proxy authorization. Proxy errors have identical fail-closed behavior in CLI and Desktop.
Install
Add the package to the OpenCode configuration:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"@koteye/kote-gateway-opencode"
]
}OpenCode installs npm plugins with Bun. CLI executes the plugin under Bun, while Desktop may load it in its Electron/Node process. The audited 1.18.18 public plugin type also accepts a tuple with plugin options:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@koteye/kote-gateway-opencode",
{
"configPath": "~/.config/kote-gateway/config.json"
}
]
]
}Configure
Create config.json:
The example below is a strict proxy-by-default policy intended for provider transports that have already been audited. For a gradual rollout, start with default: "direct", test one explicit provider in direct mode, and then switch that provider to proxy.
{
"version": 1,
"default": "proxy",
"strict": true,
"providers": {
"openai": "proxy",
"anthropic": "proxy",
"openrouter": "direct",
"google": "proxy",
"amazon-bedrock": "proxy",
"github-copilot": "direct",
"ollama": "direct"
}
}The audited OpenAI origins https://auth.openai.com, https://api.openai.com, and https://chatgpt.com are merged automatically. auxiliaryOrigins is only needed for additional or custom-provider origins.
Configuration lookup order is:
KOTE_GATEWAY_CONFIG(absolute or relative path);- tuple option
configPath; - the platform default.
A relative configPath is resolved against PluginInput.directory, the project directory supplied by OpenCode, rather than the process launch directory. On Windows, a Desktop app started from the Start menu or taskbar may not inherit variables set in a terminal. Prefer the tuple option or the platform default for GUI launches, or set KOTE_GATEWAY_CONFIG persistently and restart Desktop completely.
| Platform | Default path |
| --- | --- |
| Linux | $XDG_CONFIG_HOME/kote-gateway/config.json, otherwise ~/.config/kote-gateway/config.json |
| macOS | ~/.config/kote-gateway/config.json |
| Windows | %APPDATA%\\KoteGateway\\config.json |
Only direct and proxy are valid route values. Provider IDs are arbitrary non-empty strings. Auxiliary entries must be bare HTTP(S) origins without credentials, path, query, or fragment. Conflicting ownership of an origin is rejected. Restart OpenCode after editing the file; v1 does not hot reload configuration.
See Configuration for the complete schema and validation behavior.
OAuth and provider-specific authentication
Continue to use OpenCode's normal /connect flow. The plugin does not read, store, refresh, or log OAuth tokens.
For OpenAI ChatGPT Plus/Pro OAuth, OpenCode remains responsible for refreshing the access token, setting Authorization and ChatGPT-Account-Id, and rewriting the model URL to the Codex endpoint. The final HTTP fetch is then routed. In-process requests to configured auxiliary origins, including token exchange and refresh, use the same route as openai.
The authorization page itself opens in an external browser. Browser traffic is outside the OpenCode process and therefore outside this plugin. A successful browser login does not prove that the browser connection traversed KoteGateway.
No real OpenAI OAuth credentials or completed real-OAuth E2E result are included in this repository. The automated suite uses synthetic request chains only; the release checklist is in Testing.
WebSocket and native transports
The public plugin API exposes HTTP headers, not arbitrary sockets.
- OpenAI WebSocket mode in OpenCode 1.18.18 can bypass
globalThis.fetch. For a proxied OpenAI route, the adapter requests that version's built-in HTTP fallback using the internalx-opencode-titlebehavior. This is not observable through the public API, so the package is pinned through the audited 1.18.18 release and the range must not be expanded without source review and a compatibility retest. OPENCODE_EXPERIMENTAL_NATIVE_LLM=trueis rejected for proxy routing because the public hook does not expose enough runtime state to prove that its HTTP client is intercepted. Direct routes remain OpenCode-owned. A programmatically supplied runtime-flag override is not observable through the public Plugin API, so the plugin cannot claim process-wide enforcement in that case.- Any third-party WebSocket, native socket, or custom client that never calls the captured process fetch is outside the verifiable routing boundary. Do not use proxy mode for it without a transport contract test or system-level egress policy.
- A wrapper that both removes the opaque marker and rewrites to an origin configured as another provider's auxiliary traffic is ambiguous to the public API. Do not rely on plugin-only fail-closed enforcement for that combination.
See Provider compatibility for the transport matrix.
Failure behavior
A proxy route is fail-closed after the plugin has initialized:
proxy selected
-> bootstrap unavailable or invalid
-> cached signed bootstrap unavailable or outside grace
-> model request fails
-> no direct retryAn invalid or missing required configuration is retained as blocked state so the next model request fails instead of escaping directly. However, the audited OpenCode 1.18.18 loader logs an external plugin load failure and continues. If the package is absent, cannot be installed, or throws before installing its interceptor, a public plugin cannot stop OpenCode itself. Use OS/container egress controls when that stronger guarantee is required.
Debugging
Set KOTE_GATEWAY_LOG_LEVEL=debug before starting OpenCode. Logs are intentionally sanitized: they may include configuration path, provider ID, route, target origin, bootstrap cache state, and gateway origin, but never authorization headers, cookies, request bodies, full URLs, query strings, or complete bootstrap documents.
Useful checks:
- Confirm OpenCode loaded
@koteye/kote-gateway-opencodewithout an install or compatibility error. - Confirm the resolved configuration path and route in debug output.
- For proxy failures, restore the signed bootstrap service or a still-valid last-known-good cache; do not expect a direct retry.
- If
KOTE_UNSUPPORTED_TRANSPORTappears, disable the experimental transport or use an explicitly direct route. - After changing configuration, restart OpenCode.
Uninstall
Remove @koteye/kote-gateway-opencode (or its tuple) from the OpenCode plugin array and restart OpenCode. The plugin's dispose() unregisters its instance and restores the captured fetch only when it is the last active instance and no later plugin has replaced the wrapper.
Configuration and bootstrap cache files are not automatically deleted. Remove them separately only if they are no longer needed.
Security summary
- Signed bootstrap v1 is verified with the embedded Ed25519 public key before
proxy.urlis used. - The target URL is never rewritten to a proxy API endpoint; HTTPS uses
CONNECTwithout MITM or a custom CA. - Opaque, short-lived route markers contain no provider ID, session ID, or credential.
- Proxy-specific headers, if a future descriptor supplies them, are passed only through the runtime's proxy transport (
proxy.headersin Bun orProxyAgentoptions in Node), never target headers. The current signed v1 wire format contains no proxy-header or proxy-credential field. - Bootstrap traffic, MCP, npm, Git, OpenCode service traffic, and arbitrary unmarked fetches are not globally proxied.
- Proxy failure never triggers a direct retry.
Read Security and Architecture for the full boundary.
Development
bun install
bun run lint
bun run typecheck
bun run test:unit
bun run test:integration
bun run build
bun run node:smoke
bun run test:opencode
npm packTests use synthetic transports plus local TLS target/proxy sockets. node:smoke imports the built package under Node, exercises Desktop-style initialization and relative configuration lookup, and proves Node CONNECT plus gateway-down no-fallback behavior. Ordinary CI does not contact real AI APIs or contain OAuth credentials.
