BlaiseBlaise
Documentation
Runner and PlugsHow-to guides

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

plugs/auto-approve.ts
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.

On this page