diff --git a/README.md b/README.md index f5640d9..19697e7 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ Agent Control cat plus OpenClaw lobster -

- +
npm version @@ -20,91 +19,155 @@ Codecov +

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: {