feat(coding-agent): add before_provider_headers extension hook (#6350)

* feat(coding-agent): add before_provider_headers extension hook

Extensions can already rewrite the request payload through before_provider_request, but there is no way to adjust the outgoing HTTP
headers of a provider call. This hook fills that gap for cases like request tracing, session correlation, or tenant routing.

Handlers mutate the headers map in place - a null value deletes a header - and the return value is ignored, so a handler cannot accidentally drop auth or attribution headers by
forgetting to spread.

* docs(coding-agent): document before_provider_headers extension hook

Add the before_provider_headers section and lifecycle-diagram entry to
extensions.md; drop the now-superseded proposal file.
This commit is contained in:
Mat
2026-07-06 22:35:56 +02:00
committed by GitHub
parent cfaa52e1c2
commit 244f1deaf1
7 changed files with 136 additions and 8 deletions
@@ -906,4 +906,58 @@ describe("ExtensionRunner", () => {
expect(runner.hasHandlers("agent_end")).toBe(false);
});
});
describe("before_provider_headers", () => {
it("lets a handler mutate headers in place and preserves existing headers", async () => {
const extCode = `
export default function(pi) {
pi.on("before_provider_headers", (event) => {
event.headers["X-Turn-Index"] = "3";
});
}
`;
fs.writeFileSync(path.join(extensionsDir, "headers.ts"), extCode);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
expect(runner.hasHandlers("before_provider_headers")).toBe(true);
const headers = await runner.emitBeforeProviderHeaders({ "User-Agent": "kimchi/1.0" });
expect(headers["X-Turn-Index"]).toBe("3");
expect(headers["User-Agent"]).toBe("kimchi/1.0");
});
it("isolates a throwing handler and still applies the others", async () => {
const throwing = `
export default function(pi) {
pi.on("before_provider_headers", () => {
throw new Error("header handler boom");
});
}
`;
const good = `
export default function(pi) {
pi.on("before_provider_headers", (event) => {
event.headers["X-Good"] = "yes";
});
}
`;
fs.writeFileSync(path.join(extensionsDir, "a-throwing.ts"), throwing);
fs.writeFileSync(path.join(extensionsDir, "b-good.ts"), good);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const errors: Array<{ event: string; error: string }> = [];
runner.onError((err) => errors.push(err));
const headers = await runner.emitBeforeProviderHeaders({ "User-Agent": "x" });
expect(headers["X-Good"]).toBe("yes");
expect(headers["User-Agent"]).toBe("x");
expect(errors).toHaveLength(1);
expect(errors[0].event).toBe("before_provider_headers");
expect(errors[0].error).toContain("header handler boom");
});
});
});