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:
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user