-
+
@@ -20,91 +19,155 @@
+
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.
-## Why use Agent Control with OpenClaw?
+> [!WARNING]
+> Experimental plugin: this may break across OpenClaw updates. Use in non-production or pinned environments.
+
+## Why use this?
-- Enforce policy before tool execution, so unsafe or disallowed actions can be blocked before they run.
-- Carry session and channel context into evaluations, which helps policies reason about where a request came from and how the agent is being used.
+- Enforce policy before tool execution, so unsafe or disallowed actions never run.
+- Carry session and channel context into evaluations, so policies can reason about where a request came from and how the agent is being used.
-> [!WARNING]
-> Experimental plugin: this may break across OpenClaw updates. Use in non-production or pinned environments.
+## 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.
+
+The plugin handles multiple agents, tracks tool catalog changes between calls, and re-syncs automatically when the catalog drifts.
-## Install from npm
+## Quick start
-Install the published plugin directly into OpenClaw:
+Install and configure with the minimum required settings:
```bash
openclaw plugins install agent-control-openclaw-plugin
+
+openclaw config set plugins.entries.agent-control-openclaw-plugin.enabled true
+openclaw config set plugins.entries.agent-control-openclaw-plugin.config.serverUrl "http://localhost:8000"
```
-Then restart the gateway.
+Restart the gateway. The plugin is now active with fail-open defaults and warn-level logging.
-## Local dev install
+For authenticated setups, also set:
-1. Clone this repo anywhere on disk.
-2. Install plugin deps in this repo:
+```bash
+openclaw config set plugins.entries.agent-control-openclaw-plugin.config.apiKey "ac_your_api_key"
+```
+
+## Configuration
+
+| Setting | Type | Default | Description |
+|---------|------|---------|-------------|
+| `serverUrl` | string | — | Base URL for the Agent Control server. **Required.** |
+| `apiKey` | string | — | API key for authenticating with Agent Control. |
+| `agentName` | string | `openclaw-agent` | Base name used when registering agents with Agent Control. |
+| `agentVersion` | string | — | Version string sent to Agent Control during agent sync. |
+| `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). |
+| `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:
```bash
-npm install
-npm run lint
-npm run typecheck
-npm test
-npm run coverage
+openclaw config set plugins.entries.agent-control-openclaw-plugin.config.
```
-Coverage reports are written to `coverage/`, including `coverage/lcov.info` for Codecov-compatible uploads. The GitHub Actions workflow will upload that report to Codecov automatically when a `CODECOV_TOKEN` secret is configured for the repository.
+### Environment variables
+
+`serverUrl` and `apiKey` can also be set through environment variables. This is useful in container or CI environments where you do not want secrets in the OpenClaw config file.
+
+| Variable | Equivalent config |
+|----------|-------------------|
+| `AGENT_CONTROL_SERVER_URL` | `serverUrl` |
+| `AGENT_CONTROL_API_KEY` | `apiKey` |
+
+Config values take precedence over environment variables when both are set.
-3. Link it into your OpenClaw config from your OpenClaw checkout:
+## Fail-open vs fail-closed
+
+By default, the plugin is **fail-open**: if Agent Control is unreachable or the evaluation request fails, tool calls are allowed through. This avoids breaking your gateway when Agent Control has a transient outage.
+
+Set `failClosed` to `true` if you need the guarantee that no tool call executes without a policy decision. In fail-closed mode, a sync failure or evaluation error will block the tool call.
```bash
-openclaw plugins install -l /absolute/path/to/openclaw-plugin
+openclaw config set plugins.entries.agent-control-openclaw-plugin.config.failClosed true
+```
+
+## Logging
+
+The plugin stays quiet by default and only emits warnings, errors, and tool block events.
+
+| Level | What it logs |
+|-------|-------------|
+| `warn` | Warnings, errors, and block events. This is the default. |
+| `info` | Adds lifecycle events: client init, gateway warmup, agent syncs. |
+| `debug` | Adds verbose diagnostics: phase timings, context building, evaluation details. |
+
+```bash
+openclaw config set plugins.entries.agent-control-openclaw-plugin.config.logLevel "debug"
```
-4. Restart gateway.
+## OpenClaw CLI reference
-## OpenClaw commands
+### Inspect plugin state
```bash
-# Inspect plugin state
openclaw plugins list
openclaw plugins info agent-control-openclaw-plugin
openclaw plugins doctor
+```
+
+### Enable or disable
-# Enable / disable
+```bash
openclaw plugins enable agent-control-openclaw-plugin
openclaw plugins disable agent-control-openclaw-plugin
+```
-# Configure plugin entry + settings
-openclaw config set plugins.entries.agent-control-openclaw-plugin.enabled true --strict-json
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.serverUrl "http://localhost:8000"
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.apiKey "ac_your_api_key"
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.agentName "openclaw-agent"
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.timeoutMs 15000 --strict-json
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.failClosed false --strict-json
-
-# Optional settings
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.logLevel "info"
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.logLevel "debug"
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.agentId "00000000-0000-4000-8000-000000000000"
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.agentVersion "2026.3.3"
-openclaw config set plugins.entries.agent-control-openclaw-plugin.config.userAgent "agent-control-plugin/0.1"
+### Remove optional config keys
-# Remove optional keys
+```bash
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.agentId
openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.agentVersion
openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.userAgent
+```
-# Uninstall plugin link/install record from OpenClaw config
+### Uninstall
+
+```bash
openclaw plugins uninstall agent-control-openclaw-plugin --force
```
-By default the plugin stays quiet and only emits warnings, errors, and tool block events.
+## Local development
+
+1. Clone this repo anywhere on disk.
+2. Install dependencies and run the verification stack:
+
+```bash
+npm install
+npm run lint
+npm run typecheck
+npm test
+```
+
+3. Link the plugin into your OpenClaw checkout:
+
+```bash
+openclaw plugins install -l /absolute/path/to/openclaw-plugin
+```
+
+4. Restart the gateway.
+
+`npm run coverage` generates a report under `coverage/` including `coverage/lcov.info` for Codecov uploads.
+
+## Contributing
+
+See [AGENTS.md](AGENTS.md) for project conventions, testing patterns, and the verification checklist.
-Set `config.logLevel` to:
+## License
-- `info` for one-line lifecycle logs such as client init, warmup, and agent syncs
-- `debug` for verbose startup, sync, and evaluation diagnostics
+[Apache 2.0](LICENSE)
diff --git a/openclaw.plugin.json b/openclaw.plugin.json
index adbfe0c..b36a66e 100644
--- a/openclaw.plugin.json
+++ b/openclaw.plugin.json
@@ -18,9 +18,6 @@
"agentName": {
"type": "string"
},
- "agentId": {
- "type": "string"
- },
"agentVersion": {
"type": "string"
},
@@ -53,10 +50,6 @@
"label": "Agent Name Prefix",
"help": "Base name used for OpenClaw agents when registering with Agent Control."
},
- "agentId": {
- "label": "Agent UUID",
- "help": "Optional fixed Agent Control UUID for single-agent deployments."
- },
"failClosed": {
"label": "Fail Closed",
"help": "If true, block tool invocations when Agent Control is unavailable."
diff --git a/src/agent-control-plugin.ts b/src/agent-control-plugin.ts
index 802f989..ad81163 100644
--- a/src/agent-control-plugin.ts
+++ b/src/agent-control-plugin.ts
@@ -10,7 +10,6 @@ import {
formatToolArgsForLog,
hashSteps,
isRecord,
- isUuid,
secondsSince,
trimToMax,
USER_BLOCK_MESSAGE,
@@ -74,12 +73,6 @@ export default function register(api: OpenClawPluginApi) {
return;
}
- const configuredAgentId = asString(cfg.agentId);
- if (configuredAgentId && !isUuid(configuredAgentId)) {
- logger.warn(`agent-control: configured agentId is not a UUID: ${configuredAgentId}`);
- }
- const hasConfiguredAgentId = configuredAgentId ? isUuid(configuredAgentId) : false;
-
const failClosed = cfg.failClosed === true;
const baseAgentName = asString(cfg.agentName) ?? "openclaw-agent";
const configuredAgentVersion = asString(cfg.agentVersion);
@@ -109,9 +102,7 @@ export default function register(api: OpenClawPluginApi) {
return existing;
}
- const agentName = hasConfiguredAgentId
- ? trimToMax(baseAgentName, 255)
- : trimToMax(`${baseAgentName}:${sourceAgentId}`, 255);
+ const agentName = trimToMax(`${baseAgentName}:${sourceAgentId}`, 255);
const created: AgentState = {
sourceAgentId,
@@ -176,7 +167,6 @@ export default function register(api: OpenClawPluginApi) {
agentMetadata: {
source: "openclaw",
openclawAgentId: state.sourceAgentId,
- ...(configuredAgentId ? { openclawConfiguredAgentId: configuredAgentId } : {}),
pluginId: api.id,
},
},
@@ -282,7 +272,6 @@ export default function register(api: OpenClawPluginApi) {
},
pluginVersion,
failClosed,
- configuredAgentId,
configuredAgentVersion,
});
logger.debug(
diff --git a/src/session-context.ts b/src/session-context.ts
index 0b8e9dc..ceb7333 100644
--- a/src/session-context.ts
+++ b/src/session-context.ts
@@ -80,7 +80,6 @@ export async function buildEvaluationContext(params: {
};
pluginVersion?: string;
failClosed: boolean;
- configuredAgentId?: string;
configuredAgentVersion?: string;
}): Promise> {
const channelFromSessionKey = deriveChannelContext(params.ctx.sessionKey);
@@ -122,7 +121,6 @@ export async function buildEvaluationContext(params: {
},
policy: {
failClosed: params.failClosed,
- configuredAgentId: params.configuredAgentId ?? null,
configuredAgentVersion: params.configuredAgentVersion ?? null,
},
sync: {
diff --git a/src/shared.ts b/src/shared.ts
index a3e79c8..d2095dc 100644
--- a/src/shared.ts
+++ b/src/shared.ts
@@ -1,7 +1,6 @@
import { createHash } from "node:crypto";
import type { AgentControlStep } from "./types.ts";
-export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export const USER_BLOCK_MESSAGE =
"This action is blocked by a security policy set by your operator. Do not attempt to circumvent, disable, or work around this control. Inform the user that this action is restricted and explain what was blocked.";
export const BOOT_WARMUP_AGENT_ID = "main";
@@ -58,10 +57,6 @@ export function sanitizeToolCatalogConfig(config: Record): Reco
};
}
-export function isUuid(value: string): boolean {
- return UUID_RE.test(value);
-}
-
export function trimToMax(value: string, maxLen: number): string {
return value.length <= maxLen ? value : value.slice(0, maxLen);
}
diff --git a/src/types.ts b/src/types.ts
index a7a173c..2da17e4 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -7,7 +7,6 @@ export type AgentControlPluginConfig = {
serverUrl?: string;
apiKey?: string;
agentName?: string;
- agentId?: string;
agentVersion?: string;
timeoutMs?: number;
userAgent?: string;
diff --git a/test/agent-control-plugin.test.ts b/test/agent-control-plugin.test.ts
index c5d4f12..6229bd4 100644
--- a/test/agent-control-plugin.test.ts
+++ b/test/agent-control-plugin.test.ts
@@ -42,8 +42,6 @@ vi.mock("../src/session-context.ts", () => ({
import register from "../src/agent-control-plugin.ts";
-const VALID_AGENT_ID = "00000000-0000-4000-8000-000000000000";
-
type MockApi = {
api: OpenClawPluginApi;
handlers: Map unknown>;
@@ -151,22 +149,6 @@ describe("agent-control plugin logging and blocking", () => {
);
});
- it("warns when the configured agent ID is not a UUID", () => {
- // Given plugin configuration with an invalid configured agent ID
- const api = createMockApi({
- serverUrl: "http://localhost:8000",
- agentId: "not-a-uuid",
- });
-
- // When the plugin is registered
- register(api.api);
-
- // Then a UUID validation warning is emitted
- expect(api.warn).toHaveBeenCalledWith(
- "agent-control: configured agentId is not a UUID: not-a-uuid",
- );
- });
-
it("only logs the block event in warn mode for unsafe evaluations", async () => {
// Given warn-level logging and an unsafe policy evaluation response
const api = createMockApi({
@@ -261,33 +243,8 @@ describe("agent-control plugin logging and blocking", () => {
);
});
- it("uses the base agent name when a fixed configured agent ID is present", async () => {
- // Given a fixed configured agent ID and a base agent name
- const api = createMockApi({
- serverUrl: "http://localhost:8000",
- agentId: VALID_AGENT_ID,
- agentName: "base-agent",
- });
-
- // When a source agent evaluates a tool call
- register(api.api);
- await runBeforeToolCall(api, {}, { agentId: "worker-1" });
-
- // Then Agent Control receives the base agent name without a source suffix
- expect(clientMocks.agentsInit).toHaveBeenCalledWith(
- expect.objectContaining({
- agent: expect.objectContaining({
- agentName: "base-agent",
- agentMetadata: expect.objectContaining({
- openclawConfiguredAgentId: VALID_AGENT_ID,
- }),
- }),
- }),
- );
- });
-
- it("appends the source agent ID when no configured agent ID is present", async () => {
- // Given a base agent name without a fixed configured agent ID
+ it("appends the source agent ID to the base agent name", async () => {
+ // Given a base agent name
const api = createMockApi({
serverUrl: "http://localhost:8000",
agentName: "base-agent",
diff --git a/test/session-context.test.ts b/test/session-context.test.ts
index 7bf485e..8485dad 100644
--- a/test/session-context.test.ts
+++ b/test/session-context.test.ts
@@ -112,7 +112,6 @@ describe("buildEvaluationContext", () => {
toolCallId: "ctx-call",
},
failClosed: true,
- configuredAgentId: "configured-agent",
configuredAgentVersion: "2026.03.20",
pluginVersion: "test-version",
};
@@ -129,7 +128,6 @@ describe("buildEvaluationContext", () => {
senderFrom: "alice@example.com",
policy: {
failClosed: true,
- configuredAgentId: "configured-agent",
configuredAgentVersion: "2026.03.20",
},
sync: {