diff --git a/README.md b/README.md
index 99f1403..5577710 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
-This plugin integrates OpenClaw with [Agent Control](https://github.com/agentcontrol/agent-control), a security and policy layer for agent tool use. It registers OpenClaw tools with Agent Control and can block unsafe tool invocations before they execute.
+This plugin integrates OpenClaw with [Agent Control](https://github.com/agentcontrol/agent-control), a security and policy layer for agent tool use. It registers OpenClaw tools with Agent Control and can block unsafe tool invocations or escalate them for operator approval before they execute.
> [!WARNING]
> Experimental plugin: this may break across OpenClaw updates. Use in non-production or pinned environments.
@@ -36,7 +36,7 @@ This plugin integrates OpenClaw with [Agent Control](https://github.com/agentcon
## How it works
-When the gateway starts, the plugin loads the OpenClaw tool catalog and syncs it to Agent Control. On every tool call, the plugin intercepts the invocation through a `before_tool_call` hook, builds an evaluation context (session, channel, provider, agent identity), and sends it to Agent Control for a policy decision. If the evaluation comes back safe the call proceeds normally. If it comes back denied the call is blocked and the user sees a rejection message.
+When the gateway starts, the plugin loads the OpenClaw tool catalog and syncs it to Agent Control. On every tool call, the plugin intercepts the invocation through a `before_tool_call` hook, builds an evaluation context (session, channel, provider, agent identity), and sends it to Agent Control for a policy decision. If the evaluation comes back safe the call proceeds normally. If it comes back denied the call is blocked and the user sees a rejection message. If it comes back with a `steer` action, the plugin can either require operator approval or block immediately, depending on `steerBehavior`. When `steerBehavior=requireApproval`, the `exec` tool is routed into OpenClaw's native exec approval flow by forcing `ask=always`, while other tools use plugin approval requests.
The plugin handles multiple agents, tracks tool catalog changes between calls, and re-syncs automatically when the catalog drifts.
@@ -70,6 +70,7 @@ openclaw config set plugins.entries.agent-control-openclaw-plugin.config.apiKey
| `timeoutMs` | integer | SDK default | Client timeout in milliseconds. |
| `failClosed` | boolean | `false` | Block tool calls when Agent Control is unreachable. See [Fail-open vs fail-closed](#fail-open-vs-fail-closed). |
| `logLevel` | string | `warn` | Logging verbosity. See [Logging](#logging). |
+| `steerBehavior` | string | `requireApproval` | How Agent Control `steer` results for tool calls are handled: `requireApproval` uses OpenClaw approvals, with native exec approval for `exec` and plugin approval requests for other tools; `block` rejects immediately. |
| `userAgent` | string | `openclaw-agent-control-plugin/0.1` | Custom User-Agent header for requests to Agent Control. |
All settings are configured through the OpenClaw CLI:
@@ -99,6 +100,20 @@ Set `failClosed` to `true` if you need the guarantee that no tool call executes
openclaw config set plugins.entries.agent-control-openclaw-plugin.config.failClosed true
```
+## Steering behavior
+
+By default, the plugin maps Agent Control `steer` results for tool calls to OpenClaw approvals. For `exec`, the plugin forces `ask=always` so the command goes through OpenClaw's native exec approval flow in the originating session. For other tools, the plugin uses OpenClaw plugin approval requests and waits up to 2 minutes for an operator decision before denying on timeout or when approval is unavailable.
+
+```bash
+openclaw config set plugins.entries.agent-control-openclaw-plugin.config.steerBehavior "requireApproval"
+```
+
+If you prefer to reject steered tool calls immediately, switch the behavior to `block`:
+
+```bash
+openclaw config set plugins.entries.agent-control-openclaw-plugin.config.steerBehavior "block"
+```
+
## Logging
The plugin stays quiet by default and only emits warnings, errors, and tool block events.
@@ -136,6 +151,7 @@ openclaw plugins disable agent-control-openclaw-plugin
openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.apiKey
openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.logLevel
openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.agentVersion
+openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.steerBehavior
openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.userAgent
```
diff --git a/openclaw.plugin.json b/openclaw.plugin.json
index b36a66e..f1700d5 100644
--- a/openclaw.plugin.json
+++ b/openclaw.plugin.json
@@ -1,7 +1,7 @@
{
"id": "agent-control-openclaw-plugin",
"name": "Agent Control",
- "description": "Registers OpenClaw tools with Agent Control and blocks unsafe tool invocations.",
+ "description": "Registers OpenClaw tools with Agent Control and blocks or escalates unsafe tool invocations.",
"configSchema": {
"type": "object",
"additionalProperties": false,
@@ -34,6 +34,10 @@
"logLevel": {
"type": "string",
"enum": ["warn", "info", "debug"]
+ },
+ "steerBehavior": {
+ "type": "string",
+ "enum": ["requireApproval", "block"]
}
}
},
@@ -57,6 +61,10 @@
"logLevel": {
"label": "Log Level",
"help": "Controls plugin verbosity: warn logs only warnings, errors, and block events; info adds high-level lifecycle logs; debug adds verbose diagnostics."
+ },
+ "steerBehavior": {
+ "label": "Steer Behavior",
+ "help": "Controls how Agent Control steer decisions are handled for tool calls. requireApproval uses OpenClaw approvals, routing exec through native exec approval and other tools through plugin approvals; block rejects immediately."
}
}
}
diff --git a/src/agent-control-plugin.ts b/src/agent-control-plugin.ts
index ad81163..4ba1d25 100644
--- a/src/agent-control-plugin.ts
+++ b/src/agent-control-plugin.ts
@@ -14,30 +14,69 @@ import {
trimToMax,
USER_BLOCK_MESSAGE,
} from "./shared.ts";
-import type { AgentControlPluginConfig, AgentState } from "./types.ts";
+import type { AgentControlPluginConfig, AgentState, SteerBehavior } from "./types.ts";
-function collectDenyControlNames(response: {
- matches?: Array<{ action?: string; controlName?: string }> | null;
- errors?: Array<{ action?: string; controlName?: string }> | null;
-}): string[] {
+const APPROVAL_TITLE = "Agent Control approval required";
+const APPROVAL_TIMEOUT_MS = 120_000;
+const DEFAULT_STEER_GUIDANCE = "Tool call requires operator approval due to steering policy.";
+const EXEC_TOOL_NAME = "exec";
+const EXEC_STEER_ASK_MODE = "always";
+
+type PolicyMatch = {
+ action?: string | null;
+ controlName?: string | null;
+ steeringContext?: {
+ message?: string | null;
+ } | null;
+};
+
+type PolicyResponse = {
+ reason?: string | null;
+ matches?: PolicyMatch[] | null;
+ errors?: PolicyMatch[] | null;
+};
+
+function hasPolicyAction(
+ response: PolicyResponse,
+ action: "deny" | "steer",
+ includeErrors: boolean,
+): boolean {
+ const entries = includeErrors
+ ? [...(response.matches ?? []), ...(response.errors ?? [])]
+ : [...(response.matches ?? [])];
+ return entries.some((entry) => entry.action === action);
+}
+
+function collectControlNames(
+ entries: PolicyMatch[],
+ action: "deny" | "steer",
+): string[] {
const names: string[] = [];
- for (const match of [...(response.matches ?? []), ...(response.errors ?? [])]) {
+ for (const entry of entries) {
if (
- match.action === "deny" &&
- typeof match.controlName === "string" &&
- match.controlName.trim()
+ entry.action === action &&
+ typeof entry.controlName === "string" &&
+ entry.controlName.trim()
) {
- names.push(match.controlName.trim());
+ names.push(entry.controlName.trim());
}
}
return [...new Set(names)];
}
-function buildBlockReason(response: {
- reason?: string | null;
- matches?: Array<{ action?: string; controlName?: string }> | null;
- errors?: Array<{ action?: string; controlName?: string }> | null;
-}): string {
+function collectDenyControlNames(response: PolicyResponse): string[] {
+ return collectControlNames([...(response.matches ?? []), ...(response.errors ?? [])], "deny");
+}
+
+function collectSteerMatches(response: PolicyResponse): PolicyMatch[] {
+ return (response.matches ?? []).filter((match) => match.action === "steer");
+}
+
+function collectSteerControlNames(response: PolicyResponse): string[] {
+ return collectControlNames(collectSteerMatches(response), "steer");
+}
+
+function buildBlockReason(response: PolicyResponse): string {
const denyControls = collectDenyControlNames(response);
if (denyControls.length > 0) {
return `[agent-control] blocked by deny control(s): ${denyControls.join(", ")}`;
@@ -48,6 +87,47 @@ function buildBlockReason(response: {
return "[agent-control] blocked by policy evaluation";
}
+function resolveSteerGuidance(response: PolicyResponse): string {
+ for (const match of collectSteerMatches(response)) {
+ const steeringMessage = asString(match.steeringContext?.message)?.trim();
+ if (steeringMessage) {
+ return steeringMessage;
+ }
+ }
+
+ const reason = asString(response.reason)?.trim();
+ if (reason) {
+ return reason;
+ }
+
+ return DEFAULT_STEER_GUIDANCE;
+}
+
+function buildSteerReason(response: PolicyResponse): string {
+ const steerControls = collectSteerControlNames(response);
+ const guidance = resolveSteerGuidance(response);
+ if (steerControls.length > 0) {
+ return `[agent-control] blocked by steer control(s): ${steerControls.join(", ")}; guidance: ${guidance}`;
+ }
+ return `[agent-control] ${guidance}`;
+}
+
+function buildApprovalDescription(toolName: string, response: PolicyResponse): string {
+ const steerControls = collectSteerControlNames(response);
+ const guidance = resolveSteerGuidance(response);
+ const controlSummary =
+ steerControls.length > 0
+ ? `matched steering control(s): ${steerControls.join(", ")}`
+ : "matched a steering policy";
+ return `Tool call "${toolName}" ${controlSummary}. Guidance: ${guidance}`;
+}
+
+function buildNativeExecApprovalParams(): Record {
+ return {
+ ask: EXEC_STEER_ASK_MODE,
+ };
+}
+
function resolveSourceAgentId(agentId: string | undefined): string {
const normalized = asString(agentId);
return normalized ?? "default";
@@ -78,6 +158,7 @@ export default function register(api: OpenClawPluginApi) {
const configuredAgentVersion = asString(cfg.agentVersion);
const pluginVersion = asString(api.version);
const clientTimeoutMs = asPositiveInt(cfg.timeoutMs);
+ const steerBehavior: SteerBehavior = cfg.steerBehavior === "block" ? "block" : "requireApproval";
const clientInitStartedAt = process.hrtime.bigint();
const client = new AgentControlClient();
@@ -303,6 +384,55 @@ export default function register(api: OpenClawPluginApi) {
return;
}
+ if (hasPolicyAction(evaluation, "deny", true)) {
+ logger.block(
+ `agent-control: blocked tool=${event.toolName} agent=${sourceAgentId} reason=${buildBlockReason(evaluation)}`,
+ );
+ return {
+ block: true,
+ blockReason: USER_BLOCK_MESSAGE,
+ };
+ }
+
+ if (hasPolicyAction(evaluation, "steer", false)) {
+ if (steerBehavior === "block") {
+ logger.block(
+ `agent-control: blocked tool=${event.toolName} agent=${sourceAgentId} reason=${buildSteerReason(evaluation)} policy_action=steer`,
+ );
+ return {
+ block: true,
+ blockReason: USER_BLOCK_MESSAGE,
+ };
+ }
+
+ const description = buildApprovalDescription(event.toolName, evaluation);
+ if (event.toolName === EXEC_TOOL_NAME) {
+ logger.warn(
+ `agent-control: exec_native_approval_required tool=${event.toolName} agent=${sourceAgentId} reason=${description} ask=${EXEC_STEER_ASK_MODE} policy_action=steer`,
+ );
+ return {
+ params: buildNativeExecApprovalParams(),
+ };
+ }
+ logger.warn(
+ `agent-control: approval_required tool=${event.toolName} agent=${sourceAgentId} reason=${description}`,
+ );
+ return {
+ requireApproval: {
+ title: APPROVAL_TITLE,
+ description,
+ severity: "warning",
+ timeoutMs: APPROVAL_TIMEOUT_MS,
+ timeoutBehavior: "deny",
+ onResolution(resolution) {
+ logger.warn(
+ `agent-control: approval_resolved tool=${event.toolName} agent=${sourceAgentId} decision=${resolution} policy_action=steer`,
+ );
+ },
+ },
+ };
+ }
+
logger.block(
`agent-control: blocked tool=${event.toolName} agent=${sourceAgentId} reason=${buildBlockReason(evaluation)}`,
);
diff --git a/src/types.ts b/src/types.ts
index 2da17e4..c6d4fe5 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,7 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
export type LogLevel = "warn" | "info" | "debug";
+export type SteerBehavior = "requireApproval" | "block";
export type AgentControlPluginConfig = {
enabled?: boolean;
@@ -12,6 +13,7 @@ export type AgentControlPluginConfig = {
userAgent?: string;
failClosed?: boolean;
logLevel?: LogLevel;
+ steerBehavior?: SteerBehavior;
};
export type AgentControlStep = {
diff --git a/test/agent-control-plugin.test.ts b/test/agent-control-plugin.test.ts
index 6229bd4..d37f92d 100644
--- a/test/agent-control-plugin.test.ts
+++ b/test/agent-control-plugin.test.ts
@@ -1,4 +1,5 @@
import type {
+ OpenClawApprovalRequest,
OpenClawBeforeToolCallContext,
OpenClawBeforeToolCallEvent,
OpenClawPluginApi,
@@ -176,6 +177,200 @@ describe("agent-control plugin logging and blocking", () => {
expect(clientMocks.evaluationEvaluate).toHaveBeenCalledOnce();
});
+ it("requires approval when a steer control matches", async () => {
+ // Given the default steer behavior and an unsafe steer evaluation response
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ reason: "top-level reason",
+ matches: [
+ {
+ action: "steer",
+ controlName: "shell-review",
+ steeringContext: {
+ message: "Review the command before running it.",
+ },
+ },
+ ],
+ errors: null,
+ });
+
+ // When the plugin evaluates the tool call
+ register(api.api);
+ const result = await runBeforeToolCall(api);
+
+ // Then the tool call requires operator approval with the steering guidance
+ expect(result).toEqual({
+ requireApproval: expect.objectContaining({
+ title: "Agent Control approval required",
+ description:
+ 'Tool call "shell" matched steering control(s): shell-review. Guidance: Review the command before running it.',
+ severity: "warning",
+ timeoutMs: 120_000,
+ timeoutBehavior: "deny",
+ onResolution: expect.any(Function),
+ }),
+ });
+ expect(api.warn).toHaveBeenCalledWith(
+ expect.stringContaining("approval_required tool=shell agent=default"),
+ );
+ });
+
+ it("forces native exec approval when a steer control matches exec", async () => {
+ // Given the default steer behavior and a steered exec tool call
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ matches: [
+ {
+ action: "steer",
+ controlName: "exec-review",
+ steeringContext: {
+ message: "Ask before running this command.",
+ },
+ },
+ ],
+ errors: null,
+ });
+
+ // When the plugin evaluates the exec tool call
+ register(api.api);
+ const result = await runBeforeToolCall(
+ api,
+ {
+ toolName: "exec",
+ params: { command: "ls -la /" },
+ },
+ {},
+ );
+
+ // Then the plugin forces OpenClaw's native exec approval flow through ask=always
+ expect(result).toEqual({
+ params: {
+ ask: "always",
+ },
+ });
+ expect(api.warn).toHaveBeenCalledWith(
+ expect.stringContaining(
+ "exec_native_approval_required tool=exec agent=default",
+ ),
+ );
+ });
+
+ it("logs the approval resolution when OpenClaw resolves a steer approval", async () => {
+ // Given a steer evaluation response that returns a plugin approval request
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ matches: [
+ {
+ action: "steer",
+ controlName: "shell-review",
+ steeringContext: {
+ message: "Review the command before running it.",
+ },
+ },
+ ],
+ errors: null,
+ });
+
+ // When the plugin evaluates the tool call and OpenClaw reports an allow-once resolution
+ register(api.api);
+ const result = (await runBeforeToolCall(api)) as
+ | { requireApproval?: OpenClawApprovalRequest }
+ | undefined;
+ await result?.requireApproval?.onResolution?.("allow-once");
+
+ // Then the plugin logs the final approval resolution for the steered tool call
+ expect(api.warn).toHaveBeenCalledWith(
+ expect.stringContaining(
+ "approval_resolved tool=shell agent=default decision=allow-once policy_action=steer",
+ ),
+ );
+ });
+
+ it("blocks steer results when steerBehavior is block", async () => {
+ // Given steer handling is configured to block immediately
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ steerBehavior: "block",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ matches: [
+ {
+ action: "steer",
+ controlName: "shell-review",
+ steeringContext: {
+ message: "Review the command before running it.",
+ },
+ },
+ ],
+ errors: null,
+ });
+
+ // When the plugin evaluates the tool call
+ register(api.api);
+ const result = await runBeforeToolCall(api);
+
+ // Then the tool call is blocked instead of requesting approval
+ expect(result).toEqual({
+ block: true,
+ blockReason: USER_BLOCK_MESSAGE,
+ });
+ expect(api.warn).toHaveBeenCalledWith(
+ expect.stringContaining("policy_action=steer"),
+ );
+ });
+
+ it("blocks deny matches even when steer matches are also present", async () => {
+ // Given an unsafe evaluation response containing both deny and steer results
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ matches: [
+ {
+ action: "steer",
+ controlName: "shell-review",
+ steeringContext: {
+ message: "Review the command before running it.",
+ },
+ },
+ {
+ action: "deny",
+ controlName: "shell-deny",
+ },
+ ],
+ errors: null,
+ });
+
+ // When the plugin evaluates the tool call
+ register(api.api);
+ const result = await runBeforeToolCall(api);
+
+ // Then the deny result wins and the tool call is blocked
+ expect(result).toEqual({
+ block: true,
+ blockReason: USER_BLOCK_MESSAGE,
+ });
+ expect(api.warn).toHaveBeenCalledWith(
+ expect.stringContaining("blocked by deny control(s): shell-deny"),
+ );
+ });
+
it("emits lifecycle logs without debug traces in info mode", async () => {
// Given info-level logging for a plugin that can warm up and evaluate tools
const api = createMockApi({
@@ -351,6 +546,151 @@ describe("agent-control plugin logging and blocking", () => {
expect(message).not.toContain("alpha, alpha");
});
+ it("deduplicates steer controls in the approval description", async () => {
+ // Given an unsafe steer response with duplicate control names
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ matches: [
+ {
+ action: "steer",
+ controlName: "alpha",
+ steeringContext: {
+ message: "Review the command before running it.",
+ },
+ },
+ {
+ action: "steer",
+ controlName: "alpha",
+ steeringContext: {
+ message: "Review the command before running it.",
+ },
+ },
+ {
+ action: "steer",
+ controlName: "beta",
+ steeringContext: {
+ message: "Review the command before running it.",
+ },
+ },
+ ],
+ errors: null,
+ });
+
+ // When the plugin evaluates the tool call
+ register(api.api);
+ const result = await runBeforeToolCall(api);
+
+ // Then the approval description lists each steer control name only once
+ expect(result).toEqual({
+ requireApproval: expect.objectContaining({
+ description:
+ 'Tool call "shell" matched steering control(s): alpha, beta. Guidance: Review the command before running it.',
+ }),
+ });
+ });
+
+ it("prefers the evaluation reason when steer guidance is missing", async () => {
+ // Given a steer response without per-control steering guidance
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ reason: "Operator review is required for this command.",
+ matches: [
+ {
+ action: "steer",
+ controlName: "shell-review",
+ steeringContext: null,
+ },
+ ],
+ errors: null,
+ });
+
+ // When the plugin evaluates the tool call
+ register(api.api);
+ const result = await runBeforeToolCall(api);
+
+ // Then the approval description falls back to the top-level evaluation reason
+ expect(result).toEqual({
+ requireApproval: expect.objectContaining({
+ description:
+ 'Tool call "shell" matched steering control(s): shell-review. Guidance: Operator review is required for this command.',
+ }),
+ });
+ });
+
+ it("falls back to generic guidance when steer details are missing", async () => {
+ // Given a steer response without control guidance or a top-level reason
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ reason: "",
+ matches: [
+ {
+ action: "steer",
+ controlName: "shell-review",
+ steeringContext: {
+ message: "",
+ },
+ },
+ ],
+ errors: null,
+ });
+
+ // When the plugin evaluates the tool call
+ register(api.api);
+ const result = await runBeforeToolCall(api);
+
+ // Then the generic steering guidance is used in the approval description
+ expect(result).toEqual({
+ requireApproval: expect.objectContaining({
+ description:
+ 'Tool call "shell" matched steering control(s): shell-review. Guidance: Tool call requires operator approval due to steering policy.',
+ }),
+ });
+ });
+
+ it("does not treat evaluator errors as steer triggers", async () => {
+ // Given an unsafe evaluation response with a steer action only in errors
+ const api = createMockApi({
+ serverUrl: "http://localhost:8000",
+ });
+
+ clientMocks.evaluationEvaluate.mockResolvedValueOnce({
+ isSafe: false,
+ reason: "",
+ matches: null,
+ errors: [
+ {
+ action: "steer",
+ controlName: "shell-review",
+ },
+ ],
+ });
+
+ // When the plugin evaluates the tool call
+ register(api.api);
+ const result = await runBeforeToolCall(api);
+
+ // Then the plugin falls back to a generic block instead of requesting approval
+ expect(result).toEqual({
+ block: true,
+ blockReason: USER_BLOCK_MESSAGE,
+ });
+ expect(api.warn).toHaveBeenCalledWith(
+ expect.stringContaining("reason=[agent-control] blocked by policy evaluation"),
+ );
+ });
+
it("logs the generic block reason when no policy details are returned", async () => {
// Given an unsafe evaluation response with no policy reason or deny controls
const api = createMockApi({
@@ -366,9 +706,13 @@ describe("agent-control plugin logging and blocking", () => {
// When the tool call is evaluated and blocked
register(api.api);
- await runBeforeToolCall(api);
+ const result = await runBeforeToolCall(api);
- // Then the generic policy block reason is logged
+ // Then the tool call is blocked with the generic policy block reason logged
+ expect(result).toEqual({
+ block: true,
+ blockReason: USER_BLOCK_MESSAGE,
+ });
expect(api.warn).toHaveBeenCalledWith(
expect.stringContaining("reason=[agent-control] blocked by policy evaluation"),
);
diff --git a/types/openclaw-plugin-sdk-core.d.ts b/types/openclaw-plugin-sdk-core.d.ts
index 7fd01fc..65682dd 100644
--- a/types/openclaw-plugin-sdk-core.d.ts
+++ b/types/openclaw-plugin-sdk-core.d.ts
@@ -1,4 +1,28 @@
declare module "openclaw/plugin-sdk/core" {
+ export type OpenClawApprovalResolution =
+ | "allow-once"
+ | "allow-always"
+ | "deny"
+ | "timeout"
+ | "cancelled";
+
+ export type OpenClawApprovalRequest = {
+ title: string;
+ description: string;
+ severity: "warning";
+ timeoutMs: number;
+ timeoutBehavior: "deny";
+ onResolution?: (
+ resolution: OpenClawApprovalResolution,
+ ) => void | Promise;
+ };
+
+ export type OpenClawBeforeToolCallResult = {
+ block?: boolean;
+ blockReason?: string;
+ requireApproval?: OpenClawApprovalRequest;
+ };
+
export type OpenClawBeforeToolCallEvent = {
toolName: string;
params?: unknown;
@@ -29,7 +53,7 @@ declare module "openclaw/plugin-sdk/core" {
handler: (
event: OpenClawBeforeToolCallEvent,
ctx: OpenClawBeforeToolCallContext,
- ) => unknown,
+ ) => OpenClawBeforeToolCallResult | void | Promise,
): void;
on(event: string, handler: (...args: any[]) => unknown): void;
}