fix(agents): guard session tool call metadata errors

This commit is contained in:
Vincent Koc
2026-06-05 11:16:54 +02:00
parent dc77ae2aef
commit 4d05f80897
2 changed files with 73 additions and 5 deletions

View File

@@ -508,10 +508,12 @@ export class AgentSession {
input: args as Record<string, unknown>,
});
} catch (err) {
if (err instanceof Error) {
throw err;
}
throw new Error(`Extension failed, blocking execution: ${String(err)}`, { cause: err });
throw new Error(
`Extension failed, blocking execution: ${describeSessionExtensionError(err)}`,
{
cause: err,
},
);
}
});
};

View File

@@ -1,7 +1,7 @@
// Agent session SDK tests cover default tool wiring, prompt preservation, and
// session write-lock behavior.
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import type { Model } from "../../llm/types.js";
import { AuthStorage } from "./auth-storage.js";
import { createExtensionRuntime } from "./extensions/loader.js";
@@ -395,6 +395,72 @@ describe("createAgentSession tool defaults", () => {
expect(events).toEqual(["lock:start", "hook", "lock:end"]);
});
it("blocks hostile tool call metadata without trusting thrown stringification", async () => {
const hostileError = new Error("metadata denied");
Object.defineProperty(hostileError, "message", {
get() {
throw new Error("message denied");
},
});
const handler = vi.fn(async () => undefined);
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>([
["tool_call", [handler]],
]);
const hostileToolCall = Object.defineProperties(
{
type: "toolCall",
id: "call_1",
arguments: {},
},
{
name: {
enumerable: true,
get() {
throw hostileError;
},
},
},
);
const { session } = await createAgentSession({
model: testModel,
resourceLoader: createResourceLoaderWithHandlers(handlers),
sessionManager: SessionManager.inMemory(),
settingsManager: SettingsManager.inMemory(),
modelRegistry: ModelRegistry.inMemory(AuthStorage.inMemory()),
});
await expect(
session.agent.beforeToolCall?.({
assistantMessage: {
role: "assistant",
content: [],
api: testModel.api,
provider: testModel.provider,
model: testModel.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
},
toolCall: hostileToolCall as never,
args: {},
context: {
systemPrompt: "",
messages: [],
tools: [],
},
}),
).rejects.toThrow("Extension failed, blocking execution: Unknown session extension error");
expect(handler).not.toHaveBeenCalled();
});
it("fences tool execution when no extension hook is registered", async () => {
// Write-capable tools still enter the lock even without hooks; the lock is
// about shared session state, not just extension execution.