feat: headroom wrap opencode / unwrap opencode CLI (#1105)

## Summary

This PR implements transparent `headroom wrap opencode` support without
asking users to edit OpenCode provider URLs, choose an extra CLI flag,
or maintain a static provider list.

The wrapper now lives at the runtime transport boundary: OpenCode keeps
its user/provider config, while Headroom intercepts outbound provider
traffic in-process and routes it through the local Headroom proxy.

## What changed

### Transparent OpenCode wrapping

- `headroom wrap opencode` injects the `headroom-opencode` plugin
through `OPENCODE_CONFIG_CONTENT`.
- Existing OpenCode provider URLs are preserved. We do not rewrite user
config URLs to point at Headroom.
- Existing `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` env vars are
preserved.
- Local OpenCode traffic, localhost traffic, and Headroom proxy traffic
bypass the shim to avoid loops.

### Runtime transport interception

- Added an OpenCode plugin transport shim that wraps:
  - `globalThis.fetch`
  - `http.request` / `http.get`
  - `https.request` / `https.get`
- External provider calls are routed to the local Headroom proxy.
- The original upstream origin is passed through `x-headroom-base-url`,
so the proxy can forward to the real provider without changing OpenCode
config.
- External `http2.connect` is blocked loudly instead of allowing direct
provider traffic to leak outside Headroom.

### Live provider additions

Provider coverage is no longer based on a static config scan. Because
routing happens at outbound request time, providers added mid-session
are routed through Headroom automatically as long as they use the
covered Node transport paths.

### Subagent and child-process coverage

- The parent OpenCode plugin sets a packaged Node preload shim through
`NODE_OPTIONS=--import=.../hook-shim/handler.js`.
- The transport shim patches `child_process.spawn`, `exec`, `execFile`,
and `fork` so child Node processes receive the Headroom preload even
when OpenCode passes a custom `env`.
- The child-process shim fails closed if it loads without
`HEADROOM_OPENCODE_TRANSPORT_PROXY_URL`.
- This closes the subagent leak path where a child Node process could
otherwise start without Headroom transport interception.

## Why this goes beyond PR #1089

PR #1089 improves OpenCode provider registration, but it still focuses
on provider config shape. This PR moves the enforcement boundary to
runtime transport interception.

This PR goes further because:

- No provider URL rewriting is required.
- New providers added mid-session are covered automatically.
- Subagents and child Node processes inherit the Headroom transport
shim.
- Direct external HTTP/2 paths fail loudly instead of leaking.
- The wrap remains transparent to the user's OpenCode provider config.
- The wrapper is fail-closed for unsupported child-process preload
state.

## Additional robustness fixes

While validating the change in Docker, the full Python suite exposed
unrelated Linux/container robustness issues. These are fixed in this PR
so the suite is green:

- Binary cache handling now treats cache paths under a non-writable
existing parent as unavailable, including when tests run as root in
Docker.
- `release_version.py` honors `MANUAL_VER` before git calls so direct
script execution works outside a `.git` checkout.
- Test logger isolation now resets relevant Headroom child loggers so
proxy logging setup cannot poison later `caplog` tests.
- The scanner missing-path test now uses a guaranteed missing `tmp_path`
child instead of relying on `/nonexistent/path`.

## Validation

All implementation validation was run inside Docker.

- Full Python suite from a fresh Docker copy: `6605 passed, 523
skipped`.
- Ruff on changed Python/OpenCode paths: passed.
- OpenCode plugin typecheck: passed.
- OpenCode plugin tests: `9 passed`.
- OpenCode plugin build: passed.
- Hook shim preload smoke test: passed.

## Notes

This PR intentionally does not add a CLI option. `headroom wrap
opencode` means full wrap. Either Headroom wraps OpenCode transparently,
or the path fails loudly instead of silently leaking provider traffic.

---------

Co-authored-by: Rudimar Ronsoni <6081613+rudironsoni@users.noreply.github.com>
This commit is contained in:
Rudimar Ronsoni 2026-06-22 18:07:12 +02:00 committed by GitHub
parent 95b2333ee5
commit b4571cc346
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 4655 additions and 62 deletions

3
plugins/opencode/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
*.log

127
plugins/opencode/README.md Normal file
View file

@ -0,0 +1,127 @@
# headroom-opencode
Headroom proxy integration for [OpenCode](https://opencode.ai). Routes LLM traffic through the Headroom proxy for token compression, provides CCR retrieval, and handles provider configuration.
## Install
```bash
npm install headroom-opencode
```
## Quick start
### Create a Headroom provider for opencode.json
```ts
import { createHeadroomProvider } from "headroom-opencode";
const provider = createHeadroomProvider({
proxyPort: 8787,
});
// Write to opencode.json:
// {
// "provider": { "headroom": provider },
// "model": "headroom/claude-sonnet-4-6"
// }
```
### Build OPENCODE_CONFIG_CONTENT
```ts
import { buildOpencodeConfigContentJson } from "headroom-opencode";
const json = buildOpencodeConfigContentJson({
proxyPort: 8787,
defaultModel: "claude-sonnet-4-6",
});
// Set as env var: process.env.OPENCODE_CONFIG_CONTENT = json;
```
### Compress messages through the proxy
```ts
import { compressWithHeadroom } from "headroom-opencode";
const result = await compressWithHeadroom(messages, {
model: "gpt-4o",
proxyUrl: "http://localhost:8787",
});
console.log(`Saved ${result.tokensSaved} tokens`);
```
### CCR retrieve tool
```ts
import { createHeadroomRetrieveTool } from "headroom-opencode";
const retrieveTool = createHeadroomRetrieveTool({
proxyBaseUrl: "http://localhost:8787",
});
// Register in OpenCode's MCP config under mcp.headroom_retrieve
```
## API
### `createHeadroomProvider(options?)`
Creates a provider object compatible with OpenCode's `@ai-sdk/openai-compatible` format.
| Option | Default | Description |
|---|---|---|
| `proxyBaseUrl` | `http://127.0.0.1:8787` | Full proxy base URL |
| `proxyPort` | `8787` | Proxy port (ignored if proxyBaseUrl is set) |
| `models` | See below | Custom model mappings |
| `defaultModel` | `claude-sonnet-4-6` | Default model ID |
### `buildOpencodeConfigContent(options?)`
Returns a full `OPENCODE_CONFIG_CONTENT` JSON object with provider and model.
### `buildOpencodeConfigContentJson(options?)`
Same as above but returns a JSON string ready for the `OPENCODE_CONFIG_CONTENT` env var.
### `compressWithHeadroom(messages, options?)`
Compresses an array of messages through the Headroom proxy. Returns compression stats and compressed messages.
### `createHeadroomRetrieveTool(config)`
Creates a CCR retrieve tool for OpenCode's MCP system.
### `setDefaultProxyUrl(url)` / `getDefaultProxyUrl()`
Set or get the default proxy URL for all operations. Defaults to `HEADROOM_BASE_URL` env var or `http://localhost:8787`.
## Default models
| Model ID | Context | Output |
|---|---|---|
| `claude-sonnet-4-6` | 200K | 16K |
| `claude-opus-4-6` | 200K | 16K |
| `claude-haiku-4-5-20251001` | 200K | 8K |
| `gpt-4o` | 128K | 16K |
| `gpt-4.1` | 1M | 32K |
## OpenCode plugin
The package default export is an OpenCode plugin. It adds a `headroom_retrieve`
tool and Headroom metadata for shell commands:
```json
{
"plugin": [["headroom-opencode", { "proxyUrl": "http://127.0.0.1:8787" }]]
}
```
The plugin does not set `OPENAI_BASE_URL` or `ANTHROPIC_BASE_URL`. Model traffic
is routed by the `headroom` provider config generated by
`buildOpencodeConfigContent` or `headroom wrap opencode`.
## License
Apache-2.0

View file

@ -0,0 +1,8 @@
import { installHeadroomTransport } from "../dist/index.js";
const proxyUrl = process.env.HEADROOM_OPENCODE_TRANSPORT_PROXY_URL;
if (!proxyUrl) {
throw new Error("Headroom OpenCode transport shim loaded without HEADROOM_OPENCODE_TRANSPORT_PROXY_URL");
}
installHeadroomTransport({ proxyUrl });

View file

@ -0,0 +1,46 @@
{
"name": "headroom-opencode",
"version": "0.1.0",
"description": "Headroom proxy integration plugin for OpenCode - routes LLM traffic through the Headroom proxy for token compression",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"hook-shim",
"README.md"
],
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@opencode-ai/plugin": "^1.17.8",
"headroom-ai": "^0.22.3"
},
"peerDependencies": {
"@ai-sdk/openai-compatible": "*",
"@ai-sdk/provider": "*",
"ai": "*"
},
"peerDependenciesMeta": {
"@ai-sdk/openai-compatible": {
"optional": true
},
"@ai-sdk/provider": {
"optional": true
},
"ai": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^22.10.0",
"tsup": "^8.0.0",
"typescript": "^5.5.0",
"vitest": "^4.1.5"
},
"license": "Apache-2.0"
}

View file

@ -0,0 +1,23 @@
export {
DEFAULT_MODEL,
DEFAULT_MODELS,
buildOpencodeConfigContent,
buildOpencodeConfigContentJson,
createHeadroomProvider,
} from "./provider.js";
export type {
HeadroomModelMapping,
HeadroomProvider,
HeadroomProviderOptions,
} from "./provider.js";
export {
compressWithHeadroom,
createHeadroomRetrieveTool,
getDefaultProxyUrl,
setDefaultProxyUrl,
} from "./retrieve.js";
export type { RetrieveToolConfig } from "./retrieve.js";
export { HeadroomPlugin, default } from "./plugin.js";
export type { HeadroomOpenCodePluginOptions } from "./plugin.js";
export { installHeadroomTransport } from "./transport.js";

View file

@ -0,0 +1,68 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { HeadroomPlugin } from "./plugin.js";
function pluginInput() {
return {
client: {},
project: { id: "project-1" },
directory: "/repo",
worktree: "/repo",
experimental_workspace: {
register: vi.fn(),
},
$: {},
} as never;
}
afterEach(() => {
vi.restoreAllMocks();
});
describe("HeadroomPlugin", () => {
it("adds only Headroom metadata to shell env", async () => {
const plugin = await HeadroomPlugin(pluginInput(), {
proxyUrl: "http://127.0.0.1:8787/",
backend: "litellm",
});
const output = {
env: {
OPENAI_BASE_URL: "https://deepseek.example/v1",
ANTHROPIC_BASE_URL: "https://anthropic.example",
},
};
await plugin["shell.env"]?.({ cwd: "/repo" }, output);
expect(output.env).toMatchObject({
HEADROOM_ACTIVE: "1",
HEADROOM_PROXY_URL: "http://127.0.0.1:8787",
HEADROOM_PROJECT: "project-1",
HEADROOM_BACKEND: "litellm",
OPENAI_BASE_URL: "https://deepseek.example/v1",
ANTHROPIC_BASE_URL: "https://anthropic.example",
});
});
it("exposes a headroom_retrieve tool backed by the proxy", async () => {
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => "original content",
}));
vi.stubGlobal("fetch", fetchMock);
const plugin = await HeadroomPlugin(pluginInput(), {
proxyUrl: "http://127.0.0.1:8787",
});
const result = await plugin.tool?.headroom_retrieve.execute(
{ hash: "0123456789abcdef01234567", query: "needle" },
{} as never,
);
expect(result).toBe("original content");
expect(fetchMock).toHaveBeenCalledWith(
"http://127.0.0.1:8787/v1/retrieve/0123456789abcdef01234567?query=needle",
expect.any(Object),
);
});
});

View file

@ -0,0 +1,69 @@
import type { Plugin } from "@opencode-ai/plugin";
import { tool } from "@opencode-ai/plugin";
import { z } from "zod";
import { createHeadroomRetrieveTool, getDefaultProxyUrl } from "./retrieve.js";
import { installHeadroomTransport } from "./transport.js";
export interface HeadroomOpenCodePluginOptions {
proxyUrl?: string;
project?: string;
backend?: string;
debug?: boolean;
}
function normalizeProxyUrl(url: string): string {
return url.replace(/\/+$/, "");
}
function resolveProxyUrl(options?: HeadroomOpenCodePluginOptions): string {
return normalizeProxyUrl(
options?.proxyUrl ??
process.env.HEADROOM_PROXY_URL ??
process.env.HEADROOM_BASE_URL ??
getDefaultProxyUrl(),
);
}
export const HeadroomPlugin: Plugin = async (input, options = {}) => {
const pluginOptions = options as HeadroomOpenCodePluginOptions;
const proxyUrl = resolveProxyUrl(pluginOptions);
const retrieveTool = createHeadroomRetrieveTool({ proxyBaseUrl: proxyUrl });
const uninstallTransport = installHeadroomTransport({
proxyUrl,
debug: pluginOptions.debug,
});
return {
dispose: async () => {
uninstallTransport();
},
tool: {
headroom_retrieve: tool({
description: retrieveTool.description,
args: {
hash: z
.string()
.regex(/^[a-f0-9]{24}$/i, "Expected 24-character hex hash"),
query: z.string().optional(),
},
async execute(args) {
return retrieveTool.execute(args);
},
}),
},
"shell.env": async (_input, output) => {
output.env.HEADROOM_ACTIVE = "1";
output.env.HEADROOM_PROXY_URL = proxyUrl;
output.env.HEADROOM_PROJECT =
pluginOptions.project ??
(input.project as { id?: string }).id ??
input.directory;
if (pluginOptions.backend) {
output.env.HEADROOM_BACKEND = pluginOptions.backend;
}
},
};
};
export default HeadroomPlugin;

View file

@ -0,0 +1,92 @@
export interface HeadroomModelMapping {
name: string;
limit: {
context: number;
output: number;
};
}
export interface HeadroomProviderOptions {
proxyBaseUrl?: string;
proxyPort?: number;
defaultModel?: string;
models?: Record<string, HeadroomModelMapping>;
}
export const DEFAULT_MODELS: Record<string, HeadroomModelMapping> = {
"claude-sonnet-4-6": {
name: "Claude Sonnet 4.6",
limit: { context: 200000, output: 16384 },
},
"claude-opus-4-6": {
name: "Claude Opus 4.6",
limit: { context: 200000, output: 16384 },
},
"claude-haiku-4-5-20251001": {
name: "Claude Haiku 4.5",
limit: { context: 200000, output: 8192 },
},
"gpt-4o": {
name: "GPT-4o",
limit: { context: 128000, output: 16384 },
},
"gpt-4.1": {
name: "GPT-4.1",
limit: { context: 1048576, output: 32768 },
},
};
export const DEFAULT_MODEL = "claude-sonnet-4-6";
function resolveBaseUrl(options: HeadroomProviderOptions): string {
if (options.proxyBaseUrl) return options.proxyBaseUrl.replace(/\/+$/, "");
const port = options.proxyPort ?? 8787;
return `http://127.0.0.1:${port}`;
}
export interface HeadroomProvider {
npm: string;
name: string;
options: {
baseURL: string;
apiKey?: string;
};
models: Record<string, HeadroomModelMapping>;
}
export function createHeadroomProvider(
options: HeadroomProviderOptions = {},
): HeadroomProvider {
const baseUrl = resolveBaseUrl(options);
const models = options.models ?? DEFAULT_MODELS;
return {
npm: "@ai-sdk/openai-compatible",
name: "Headroom Proxy",
options: { baseURL: `${baseUrl}/v1` },
models: Object.fromEntries(
Object.entries(models).map(([id, mapping]) => [
`headroom/${id}`,
mapping,
]),
),
};
}
export function buildOpencodeConfigContent(
options: HeadroomProviderOptions = {},
): Record<string, unknown> {
const defaultModel = options.defaultModel ?? DEFAULT_MODEL;
const provider = createHeadroomProvider(options);
return {
provider: { headroom: provider },
model: `headroom/${defaultModel}`,
};
}
export function buildOpencodeConfigContentJson(
options: HeadroomProviderOptions = {},
): string {
return JSON.stringify(buildOpencodeConfigContent(options));
}

View file

@ -0,0 +1,94 @@
import type { CompressResult } from "headroom-ai";
import { compress } from "headroom-ai";
let _proxyUrlCache: string | null = null;
export function setDefaultProxyUrl(url: string): void {
_proxyUrlCache = url;
}
export function getDefaultProxyUrl(): string {
return _proxyUrlCache ?? process.env.HEADROOM_BASE_URL ?? "http://localhost:8787";
}
export interface RetrieveToolConfig {
proxyBaseUrl: string;
}
export function createHeadroomRetrieveTool(config: RetrieveToolConfig) {
const origin = config.proxyBaseUrl.replace(/\/+$/, "");
return {
name: "headroom_retrieve",
description:
"Retrieve original uncompressed content from Headroom's compression store. " +
"Use when compressed context mentions a hash and you need the full details. " +
"Pass the hash from the compression marker (24 hex characters). " +
"Optionally pass a query to search within the original content.",
parameters: {
type: "object" as const,
properties: {
hash: {
type: "string",
description: "The 24-character hex hash from the compression marker",
},
query: {
type: "string",
description: "Optional search query to filter results within the original content",
},
},
required: ["hash"],
},
execute: async (args: { hash: string; query?: string }): Promise<string> => {
const { hash, query } = args;
if (!/^[a-f0-9]{24}$/i.test(hash)) {
return JSON.stringify({
error: "Invalid hash format. Expected 24 hex characters.",
});
}
try {
const url = query
? `${origin}/v1/retrieve/${hash}?query=${encodeURIComponent(query)}`
: `${origin}/v1/retrieve/${hash}`;
const resp = await fetch(url, {
signal: AbortSignal.timeout(10_000),
});
if (!resp.ok) {
const body = await resp.text().catch(() => "");
return JSON.stringify({
error: `Retrieval failed: HTTP ${resp.status}`,
details: body,
});
}
const data = await resp.json();
return typeof data === "string" ? data : JSON.stringify(data);
} catch (error) {
return JSON.stringify({
error: `Retrieval failed: ${error}`,
hint: "The compressed content may have expired (default TTL: 5 minutes)",
});
}
},
};
}
export async function compressWithHeadroom(
messages: unknown[],
options: {
model?: string;
tokenBudget?: number;
proxyUrl?: string;
} = {},
): Promise<CompressResult> {
return compress(messages, {
baseUrl: options.proxyUrl ?? getDefaultProxyUrl(),
model: options.model ?? "gpt-4o",
tokenBudget: options.tokenBudget,
stack: "opencode",
});
}

View file

@ -0,0 +1,215 @@
import childProcess from "node:child_process";
import http from "node:http";
import http2 from "node:http2";
import https from "node:https";
import { afterEach, describe, expect, it, vi } from "vitest";
import { installHeadroomTransport, uninstallHeadroomTransport } from "./transport.js";
afterEach(() => {
uninstallHeadroomTransport();
vi.restoreAllMocks();
});
type FetchCall = [RequestInfo | URL, RequestInit?];
type SeenRequest = {
method: string | undefined;
url: string | undefined;
headers: http.IncomingHttpHeaders;
body: string;
};
function proxyServer(): Promise<{ url: string; seen: SeenRequest[]; close: () => Promise<void> }> {
const seen: SeenRequest[] = [];
const server = http.createServer((req, res) => {
let body = "";
req.setEncoding("utf8");
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
seen.push({ method: req.method, url: req.url, headers: req.headers, body });
res.writeHead(200, { "content-type": "application/json" });
res.end("{\"ok\":true}");
});
});
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
reject(new Error("Expected TCP server address"));
return;
}
resolve({
url: `http://127.0.0.1:${address.port}/v1`,
seen,
close: () => new Promise((done) => server.close(() => done())),
});
});
});
}
describe("Headroom OpenCode transport", () => {
it("routes external fetch calls through the proxy without pre-registering providers", async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (..._args: FetchCall) => new Response("ok"));
globalThis.fetch = fetchMock as unknown as typeof fetch;
installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });
await fetch("https://api.deepseek.com/v1/chat/completions?x=1", {
method: "POST",
headers: { authorization: "Bearer test" },
});
await fetch("https://new-provider.example/base/v1/messages", { method: "POST" });
expect(fetchMock).toHaveBeenNthCalledWith(
1,
new URL("http://127.0.0.1:8787/v1/chat/completions?x=1"),
expect.objectContaining({ method: "POST" }),
);
expect(new Headers(fetchMock.mock.calls[0][1]?.headers).get("x-headroom-base-url")).toBe(
"https://api.deepseek.com",
);
expect(fetchMock.mock.calls[1][0]).toEqual(new URL("http://127.0.0.1:8787/base/v1/messages"));
expect(new Headers(fetchMock.mock.calls[1][1]?.headers).get("x-headroom-base-url")).toBe(
"https://new-provider.example",
);
globalThis.fetch = originalFetch;
});
it("bypasses local, OpenCode, and Headroom proxy fetch URLs", async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (..._args: FetchCall) => new Response("ok"));
globalThis.fetch = fetchMock as unknown as typeof fetch;
installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });
await fetch("http://127.0.0.1:8787/v1/retrieve");
await fetch("http://localhost:4096/config");
expect(fetchMock.mock.calls[0][0]).toBe("http://127.0.0.1:8787/v1/retrieve");
expect(fetchMock.mock.calls[1][0]).toBe("http://localhost:4096/config");
globalThis.fetch = originalFetch;
});
it("routes external https.request calls through the proxy", async () => {
const proxy = await proxyServer();
installHeadroomTransport({ proxyUrl: proxy.url });
await new Promise<void>((resolve, reject) => {
const req = https.request(
"https://api.anthropic.com/v1/messages?beta=1",
{ method: "POST", headers: { authorization: "Bearer test" } },
(res) => {
res.resume();
res.on("end", resolve);
},
);
req.on("error", reject);
req.end("{\"model\":\"claude\"}");
});
expect(proxy.seen).toHaveLength(1);
expect(proxy.seen[0]).toMatchObject({ method: "POST", url: "/v1/messages?beta=1" });
expect(proxy.seen[0].headers["x-headroom-base-url"]).toBe("https://api.anthropic.com");
expect(proxy.seen[0].headers.host).toMatch(/^127\.0\.0\.1:/);
expect(proxy.seen[0].body).toBe("{\"model\":\"claude\"}");
await proxy.close();
});
it("blocks external http2 connections instead of leaking them", () => {
installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });
expect(() => http2.connect("https://api.openai.com")).toThrow(
/blocked direct HTTP\/2 connection to https:\/\/api\.openai\.com/,
);
});
it("preloads the Headroom shim into child Node processes", () => {
const originalNodeOptions = process.env.NODE_OPTIONS;
const originalProxyUrl = process.env.HEADROOM_OPENCODE_TRANSPORT_PROXY_URL;
try {
process.env.NODE_OPTIONS = "--trace-warnings";
delete process.env.HEADROOM_OPENCODE_TRANSPORT_PROXY_URL;
installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });
expect(process.env.HEADROOM_OPENCODE_TRANSPORT_PROXY_URL).toBe("http://127.0.0.1:8787/v1");
expect(process.env.NODE_OPTIONS).toContain("--trace-warnings");
expect(process.env.NODE_OPTIONS).toContain("--import=file:");
expect(process.env.NODE_OPTIONS).toContain("/hook-shim/handler.js");
installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });
expect(process.env.NODE_OPTIONS?.match(/hook-shim\/handler\.js/g)).toHaveLength(1);
} finally {
if (originalNodeOptions === undefined) {
delete process.env.NODE_OPTIONS;
} else {
process.env.NODE_OPTIONS = originalNodeOptions;
}
if (originalProxyUrl === undefined) {
delete process.env.HEADROOM_OPENCODE_TRANSPORT_PROXY_URL;
} else {
process.env.HEADROOM_OPENCODE_TRANSPORT_PROXY_URL = originalProxyUrl;
}
uninstallHeadroomTransport();
}
});
it("injects the Headroom shim into child processes with custom env", () => {
const originalSpawn = childProcess.spawn;
const spawnMock = vi.fn(() => ({
on: vi.fn(),
once: vi.fn(),
emit: vi.fn(),
kill: vi.fn(),
killed: false,
pid: 123,
}));
childProcess.spawn = spawnMock as unknown as typeof childProcess.spawn;
try {
installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });
childProcess.spawn("node", ["agent.js"], { env: { PATH: "/bin", NODE_OPTIONS: "--trace-warnings" } });
const options = (spawnMock.mock.calls[0] as unknown[])[2] as { env: NodeJS.ProcessEnv };
expect(options.env.PATH).toBe("/bin");
expect(options.env.HEADROOM_OPENCODE_TRANSPORT_PROXY_URL).toBe("http://127.0.0.1:8787/v1");
expect(options.env.NODE_OPTIONS).toContain("--trace-warnings");
expect(options.env.NODE_OPTIONS).toContain("--import=file:");
expect(options.env.NODE_OPTIONS).toContain("/hook-shim/handler.js");
} finally {
uninstallHeadroomTransport();
childProcess.spawn = originalSpawn;
}
});
it("restores patched transports only after the final disposer", () => {
const originalFetch = globalThis.fetch;
const originalHttpRequest = http.request;
const originalHttpsRequest = https.request;
const firstDispose = installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" });
const secondDispose = installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8788/v1" });
expect(globalThis.fetch).not.toBe(originalFetch);
expect(http.request).not.toBe(originalHttpRequest);
expect(https.request).not.toBe(originalHttpsRequest);
firstDispose();
expect(globalThis.fetch).not.toBe(originalFetch);
expect(http.request).not.toBe(originalHttpRequest);
secondDispose();
expect(globalThis.fetch).toBe(originalFetch);
expect(http.request).toBe(originalHttpRequest);
expect(https.request).toBe(originalHttpsRequest);
});
});

View file

@ -0,0 +1,438 @@
import { createRequire, syncBuiltinESMExports } from "node:module";
const nodeRequire = createRequire(import.meta.url);
const http = nodeRequire("node:http") as typeof import("node:http");
const https = nodeRequire("node:https") as typeof import("node:https");
const http2 = nodeRequire("node:http2") as typeof import("node:http2");
const childProcess = nodeRequire("node:child_process") as typeof import("node:child_process");
const BASE_URL_HEADER = "x-headroom-base-url";
const PROXY_ENV = "HEADROOM_OPENCODE_TRANSPORT_PROXY_URL";
const STATE_KEY = Symbol.for("headroom.opencode.transport");
type FetchArgs = Parameters<typeof fetch>;
type HttpRequest = typeof http.request;
type HttpGet = typeof http.get;
type HttpsRequest = typeof https.request;
type HttpsGet = typeof https.get;
type Http2Connect = typeof http2.connect;
type ChildSpawn = typeof childProcess.spawn;
type ChildExec = typeof childProcess.exec;
type ChildExecFile = typeof childProcess.execFile;
type ChildFork = typeof childProcess.fork;
interface InstallOptions {
proxyUrl: string;
debug?: boolean;
}
interface TransportState {
refs: number;
proxyUrl: string;
debug: boolean;
originalFetch: typeof fetch;
originalHttpRequest: HttpRequest;
originalHttpGet: HttpGet;
originalHttpsRequest: HttpsRequest;
originalHttpsGet: HttpsGet;
originalHttp2Connect: Http2Connect;
originalChildSpawn: ChildSpawn;
originalChildExec: ChildExec;
originalChildExecFile: ChildExecFile;
originalChildFork: ChildFork;
}
interface GlobalWithHeadroomTransport {
[STATE_KEY]?: TransportState;
}
interface NodeRequestParts {
url?: URL;
options: Record<string, unknown>;
callback?: (...args: unknown[]) => unknown;
}
function getState(): TransportState | undefined {
return (globalThis as GlobalWithHeadroomTransport)[STATE_KEY];
}
function setState(state: TransportState | undefined): void {
(globalThis as GlobalWithHeadroomTransport)[STATE_KEY] = state;
}
function shimImportSpecifier(): string {
return new URL("../hook-shim/handler.js", import.meta.url).href;
}
function withNodeImportOption(existing: string | undefined, shim: string): string {
const parts = existing?.trim() ? existing.trim().split(/\s+/) : [];
const alreadyPresent = parts.some((part, index) => {
return part === `--import=${shim}` || (part === "--import" && parts[index + 1] === shim);
});
if (!alreadyPresent) {
parts.push(`--import=${shim}`);
}
return parts.join(" ");
}
function withShimEnv(env: NodeJS.ProcessEnv | Record<string, unknown> | undefined, proxyUrl: string): NodeJS.ProcessEnv {
const nextEnv = { ...(env ?? process.env) } as NodeJS.ProcessEnv;
nextEnv[PROXY_ENV] = proxyUrl;
nextEnv.NODE_OPTIONS = withNodeImportOption(nextEnv.NODE_OPTIONS, shimImportSpecifier());
return nextEnv;
}
function installProcessEnv(proxyUrl: string): void {
process.env[PROXY_ENV] = proxyUrl;
process.env.NODE_OPTIONS = withNodeImportOption(process.env.NODE_OPTIONS, shimImportSpecifier());
}
function isOptions(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && !(value instanceof URL);
}
function injectOptionsEnv(args: unknown[], optionIndex: number, proxyUrl: string): unknown[] {
const nextArgs = [...args];
const callback = typeof nextArgs.at(-1) === "function" ? nextArgs.pop() : undefined;
const existing = isOptions(nextArgs[optionIndex]) ? { ...(nextArgs[optionIndex] as Record<string, unknown>) } : {};
existing.env = withShimEnv(existing.env as NodeJS.ProcessEnv | undefined, proxyUrl);
if (isOptions(nextArgs[optionIndex])) {
nextArgs[optionIndex] = existing;
} else {
nextArgs.splice(optionIndex, 0, existing);
}
if (callback) {
nextArgs.push(callback);
}
return nextArgs;
}
function wrapSpawn(originalSpawn: ChildSpawn): ChildSpawn {
return function headroomSpawn(this: unknown, ...args: unknown[]) {
const state = getState();
if (!state) {
return Reflect.apply(originalSpawn, this, args);
}
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
return Reflect.apply(originalSpawn, this, injectOptionsEnv(args, optionIndex, state.proxyUrl));
} as ChildSpawn;
}
function wrapExec(originalExec: ChildExec): ChildExec {
return function headroomExec(this: unknown, ...args: unknown[]) {
const state = getState();
if (!state) {
return Reflect.apply(originalExec, this, args);
}
return Reflect.apply(originalExec, this, injectOptionsEnv(args, 1, state.proxyUrl));
} as ChildExec;
}
function wrapExecFile(originalExecFile: ChildExecFile): ChildExecFile {
return function headroomExecFile(this: unknown, ...args: unknown[]) {
const state = getState();
if (!state) {
return Reflect.apply(originalExecFile, this, args);
}
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
return Reflect.apply(originalExecFile, this, injectOptionsEnv(args, optionIndex, state.proxyUrl));
} as ChildExecFile;
}
function wrapFork(originalFork: ChildFork): ChildFork {
return function headroomFork(this: unknown, ...args: unknown[]) {
const state = getState();
if (!state) {
return Reflect.apply(originalFork, this, args);
}
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
return Reflect.apply(originalFork, this, injectOptionsEnv(args, optionIndex, state.proxyUrl));
} as ChildFork;
}
function normalizeProxyUrl(proxyUrl: string): URL {
return new URL(proxyUrl);
}
function isLoopback(hostname: string): boolean {
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
}
function shouldRoute(url: URL, proxy: URL): boolean {
if (url.protocol !== "http:" && url.protocol !== "https:") {
return false;
}
if (isLoopback(url.hostname)) {
return false;
}
if (url.origin === proxy.origin) {
return false;
}
return true;
}
function routedUrl(upstream: URL, proxy: URL): URL {
return new URL(`${upstream.pathname}${upstream.search}`, proxy.origin);
}
function requestUrl(input: RequestInfo | URL): URL {
if (input instanceof Request) {
return new URL(input.url);
}
if (input instanceof URL) {
return input;
}
return new URL(String(input));
}
function mergeFetchHeaders(input: RequestInfo | URL, init?: RequestInit, upstream?: URL): Headers {
const headers = new Headers(input instanceof Request ? input.headers : undefined);
if (init?.headers) {
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
}
if (upstream) {
headers.set(BASE_URL_HEADER, upstream.origin);
headers.delete("host");
}
return headers;
}
function withRoutedFetchInput(input: RequestInfo | URL, init: RequestInit | undefined, proxy: URL): FetchArgs {
const upstream = requestUrl(input);
if (!shouldRoute(upstream, proxy)) {
return [input, init];
}
const nextInit = {
...init,
headers: mergeFetchHeaders(input, init, upstream),
};
const nextUrl = routedUrl(upstream, proxy);
if (input instanceof Request) {
return [new Request(nextUrl, input), nextInit];
}
return [nextUrl, nextInit];
}
function splitNodeArgs(args: unknown[]): NodeRequestParts {
const callback = typeof args.at(-1) === "function" ? (args.at(-1) as (...args: unknown[]) => unknown) : undefined;
const withoutCallback = callback ? args.slice(0, -1) : args;
const [first, second] = withoutCallback;
const options = typeof second === "object" && second !== null ? { ...(second as Record<string, unknown>) } : {};
if (first instanceof URL) {
return { url: first, options, callback };
}
if (typeof first === "string") {
try {
return { url: new URL(first), options, callback };
} catch {
return { options, callback };
}
}
if (typeof first === "object" && first !== null) {
const requestOptions = { ...(first as Record<string, unknown>), ...options };
return { url: urlFromRequestOptions(requestOptions), options: requestOptions, callback };
}
return { options, callback };
}
function urlFromRequestOptions(options: Record<string, unknown>): URL | undefined {
const protocol = String(options.protocol ?? "http:");
if (protocol !== "http:" && protocol !== "https:") {
return undefined;
}
const hostValue = options.hostname ?? options.host;
if (!hostValue) {
return undefined;
}
const hostname = String(hostValue).replace(/:\d+$/, "");
const port = options.port ? `:${String(options.port)}` : "";
const path = String(options.path ?? "/");
try {
return new URL(`${protocol}//${hostname}${port}${path}`);
} catch {
return undefined;
}
}
function headersForNodeRequest(options: Record<string, unknown>, upstream: URL): Record<string, string> {
const headers = new Headers(options.headers as HeadersInit | undefined);
headers.set(BASE_URL_HEADER, upstream.origin);
headers.delete("host");
const result: Record<string, string> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
function routedNodeOptions(parts: NodeRequestParts, proxy: URL): Record<string, unknown> | undefined {
if (!parts.url || !shouldRoute(parts.url, proxy)) {
return undefined;
}
const nextUrl = routedUrl(parts.url, proxy);
const {
agent: _agent,
auth: _auth,
createConnection: _createConnection,
defaultPort: _defaultPort,
family: _family,
headers: _headers,
host: _host,
hostname: _hostname,
href: _href,
lookup: _lookup,
path: _path,
pathname: _pathname,
port: _port,
protocol: _protocol,
search: _search,
servername: _servername,
setHost: _setHost,
...rest
} = parts.options;
return {
...rest,
protocol: nextUrl.protocol,
hostname: nextUrl.hostname,
port: nextUrl.port || undefined,
path: `${nextUrl.pathname}${nextUrl.search}`,
headers: headersForNodeRequest(parts.options, parts.url),
};
}
function wrapRequest(
originalHttpRequest: HttpRequest,
originalHttpsRequest: HttpsRequest,
originalRequest: HttpRequest | HttpsRequest,
): HttpRequest | HttpsRequest {
return function headroomRequest(this: unknown, ...args: unknown[]) {
const state = getState();
if (!state) {
return Reflect.apply(originalRequest, this, args);
}
const proxy = normalizeProxyUrl(state.proxyUrl);
const parts = splitNodeArgs(args);
const nextOptions = routedNodeOptions(parts, proxy);
if (!nextOptions) {
return Reflect.apply(originalRequest, this, args);
}
const targetRequest = proxy.protocol === "https:" ? originalHttpsRequest : originalHttpRequest;
const nextArgs = parts.callback ? [nextOptions, parts.callback] : [nextOptions];
return Reflect.apply(targetRequest, this, nextArgs);
} as HttpRequest | HttpsRequest;
}
function wrapGet(request: HttpRequest | HttpsRequest): HttpGet | HttpsGet {
return function headroomGet(this: unknown, ...args: unknown[]) {
const req = Reflect.apply(request, this, args);
req.end();
return req;
} as HttpGet | HttpsGet;
}
function wrapHttp2Connect(originalConnect: Http2Connect): Http2Connect {
return function headroomHttp2Connect(this: unknown, authority: string | URL, ...args: unknown[]) {
const state = getState();
if (state) {
const proxy = normalizeProxyUrl(state.proxyUrl);
const upstream = authority instanceof URL ? authority : new URL(String(authority));
if (shouldRoute(upstream, proxy)) {
throw new Error(
`Headroom OpenCode wrap blocked direct HTTP/2 connection to ${upstream.origin}. ` +
"Use fetch, http, or https so traffic can be routed through Headroom.",
);
}
}
return Reflect.apply(originalConnect, this, [authority, ...args]);
} as Http2Connect;
}
export function installHeadroomTransport(options: InstallOptions): () => void {
const existing = getState();
if (existing) {
existing.refs += 1;
existing.proxyUrl = options.proxyUrl;
existing.debug = Boolean(options.debug);
installProcessEnv(options.proxyUrl);
return () => uninstallHeadroomTransport();
}
const state: TransportState = {
refs: 1,
proxyUrl: options.proxyUrl,
debug: Boolean(options.debug),
originalFetch: globalThis.fetch,
originalHttpRequest: http.request,
originalHttpGet: http.get,
originalHttpsRequest: https.request,
originalHttpsGet: https.get,
originalHttp2Connect: http2.connect,
originalChildSpawn: childProcess.spawn,
originalChildExec: childProcess.exec,
originalChildExecFile: childProcess.execFile,
originalChildFork: childProcess.fork,
};
setState(state);
installProcessEnv(options.proxyUrl);
globalThis.fetch = async (...args: FetchArgs) => {
const current = getState();
if (!current) {
return state.originalFetch(...args);
}
const proxy = normalizeProxyUrl(current.proxyUrl);
const [nextInput, nextInit] = withRoutedFetchInput(args[0], args[1], proxy);
return state.originalFetch(nextInput, nextInit);
};
http.request = wrapRequest(state.originalHttpRequest, state.originalHttpsRequest, state.originalHttpRequest) as HttpRequest;
https.request = wrapRequest(state.originalHttpRequest, state.originalHttpsRequest, state.originalHttpsRequest) as HttpsRequest;
http.get = wrapGet(http.request) as HttpGet;
https.get = wrapGet(https.request) as HttpsGet;
http2.connect = wrapHttp2Connect(state.originalHttp2Connect);
childProcess.spawn = wrapSpawn(state.originalChildSpawn);
childProcess.exec = wrapExec(state.originalChildExec);
childProcess.execFile = wrapExecFile(state.originalChildExecFile);
childProcess.fork = wrapFork(state.originalChildFork);
syncBuiltinESMExports();
return () => uninstallHeadroomTransport();
}
export function uninstallHeadroomTransport(): void {
const state = getState();
if (!state) {
return;
}
state.refs -= 1;
if (state.refs > 0) {
return;
}
globalThis.fetch = state.originalFetch;
http.request = state.originalHttpRequest;
http.get = state.originalHttpGet;
https.request = state.originalHttpsRequest;
https.get = state.originalHttpsGet;
http2.connect = state.originalHttp2Connect;
childProcess.spawn = state.originalChildSpawn;
childProcess.exec = state.originalChildExec;
childProcess.execFile = state.originalChildExecFile;
childProcess.fork = state.originalChildFork;
syncBuiltinESMExports();
setState(undefined);
}

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"types": ["node"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
"isolatedModules": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "test"]
}

View file

@ -0,0 +1,10 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: { index: "src/index.ts" },
format: ["esm"],
dts: true,
sourcemap: true,
clean: true,
external: ["headroom-ai"],
});

View file

@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
globals: true,
},
});