Answer agent requests in a plug
Answer permission requests on the runner, and forward the rest to a person in Blaise.
The agent asks for permission before it runs a command or edits a file. A plug can answer some of those requests itself, and forward the rest to Blaise, where a person decides.
1. Write the plug
import type { ToolKind } from "@agentclientprotocol/sdk";
import { z } from "zod";
import { definePlug } from "@blaiseagent/runner";
const toolKind = z.enum([
"read",
"edit",
"delete",
"move",
"search",
"execute",
"think",
"fetch",
"switch_mode",
"other",
]) satisfies z.ZodType<ToolKind>;
/** Answer permission requests locally by tool kind; ask a human otherwise. */
export default definePlug({
name: "acme/auto-approve",
inputs: z.object({
allow: z.array(toolKind).default(["read", "search", "think"]),
deny: z.array(toolKind).default([]),
}),
async onMessage(ctx, message, next) {
if (message.method !== "session/request_permission") return next(message);
const { toolCall, options } = message.params;
const kind = toolCall.kind ?? "other";
const wanted = ctx.inputs.deny.includes(kind)
? "reject_once"
: ctx.inputs.allow.includes(kind)
? "allow_once"
: null;
const option = options.find((candidate) => candidate.kind === wanted);
if (!option) return next(message); // forward to Blaise: a human decides
ctx.log.info("Answered permission locally", {
kind,
decision: option.kind,
});
return { outcome: { outcome: "selected", optionId: option.optionId } };
},
});onMessage receives every message in both directions. The plug narrows on message.method, and
leaves other messages alone by passing them on. Returning a result without calling next answers
the request: the agent gets the answer, and Blaise stores the name of the plug that gave it with the
message.
2. Configure it
setup:
- use: ./plugs/auto-approve.ts
with:
allow: [read, search, think]
deny: [delete]A request of a kind in neither list is forwarded to Blaise. A request of a kind in deny is
rejected without asking.
3. Test it
import { describe, expect, test } from "vitest";
import type { Message } from "@blaiseagent/runner";
import { createPlugHarness } from "@blaiseagent/runner/testing";
import autoApprove from "./auto-approve";
function request(kind: "read" | "execute"): Message {
return {
direction: "from_agent",
method: "session/request_permission",
params: {
sessionId: "s",
toolCall: { toolCallId: "t", kind },
options: [
{ optionId: "allow", name: "Allow", kind: "allow_once" },
{ optionId: "reject", name: "Reject", kind: "reject_once" },
],
},
};
}
describe("auto-approve", () => {
test("answers reads itself", async () => {
const { result, forwarded } = await createPlugHarness(autoApprove).message(request("read"));
expect(result).toStrictEqual({
outcome: { outcome: "selected", optionId: "allow" },
});
expect(forwarded).toStrictEqual([]);
});
test("asks a human about commands", async () => {
const { forwarded } = await createPlugHarness(autoApprove).message(request("execute"));
expect(forwarded).toHaveLength(1);
});
});Answer with an error
To refuse a request with a JSON-RPC error, throw a RequestError from @agentclientprotocol/sdk.
Anything else you throw fails the run.