diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c3093..346bfc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog -## [1.1.6] - 2026-05-18 +## [1.1.7] - 2026-05-18 ### Fixed - Added the missing `agentguard policy pull` command used by AgentGuard Cloud policy refresh instructions. +- OpenClaw installs now enable the AgentGuard plugin when installing the skill through `setup.sh` or running `agentguard init --agent openclaw`. +- Added a dedicated OpenClaw package entry so OpenClaw loads the runtime plugin instead of the generic SDK entrypoint. ## [1.1.5] - 2026-05-18 diff --git a/README.md b/README.md index cbcfa77..371fbb2 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ agentguard subscribe --json # Or run a one-off self-check against a single advisory id agentguard checkup --against-advisory AGS-2026-0042 -# Optional: write host-specific hook templates +# Optional: write host-specific hook templates. +# OpenClaw also installs and enables the AgentGuard plugin. agentguard init --agent claude-code agentguard init --agent codex agentguard init --agent openclaw @@ -135,7 +136,13 @@ cp -r agentguard/skills/agentguard ~/.claude/skills/agentguard npm install @goplus/agentguard ``` -Register in your OpenClaw plugin config: +Then enable it: + +```bash +agentguard init --agent openclaw +``` + +Or register manually in your OpenClaw plugin config: ```typescript import register from '@goplus/agentguard/openclaw'; diff --git a/docs/openclaw.md b/docs/openclaw.md index f0b59b9..6860352 100644 --- a/docs/openclaw.md +++ b/docs/openclaw.md @@ -4,13 +4,13 @@ OpenClaw can use AgentGuard as a local runtime guard and optional Cloud-connecte ## Plugin usage -To write a starter plugin file in the current project: +To install and enable the AgentGuard OpenClaw plugin: ```bash agentguard init --agent openclaw ``` -This creates `openclaw.agentguard.plugin.ts`. +This creates a local plugin under `~/.openclaw/plugins/agentguard` and enables it in `~/.openclaw/openclaw.json`. ```ts import { registerOpenClawPlugin } from '@goplus/agentguard'; diff --git a/openclaw.d.ts b/openclaw.d.ts new file mode 100644 index 0000000..9a2a595 --- /dev/null +++ b/openclaw.d.ts @@ -0,0 +1,6 @@ +export { default } from './dist/openclaw.js'; +export { + getPluginIdFromTool, + getPluginScanResult, + registerOpenClawPlugin, +} from './dist/openclaw.js'; diff --git a/openclaw.js b/openclaw.js new file mode 100644 index 0000000..6958740 --- /dev/null +++ b/openclaw.js @@ -0,0 +1 @@ +module.exports = require('./dist/openclaw.js'); diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 4d602e8..d444cbb 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,6 +2,7 @@ "id": "agentguard", "name": "GoPlus AgentGuard", "description": "AI agent security framework — blocks dangerous commands, prevents data leaks, and protects secrets", + "skills": ["./skills"], "configSchema": { "type": "object", "properties": { diff --git a/package.json b/package.json index f538194..eb68f52 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "dist/index.d.ts", "openclaw": { "extensions": [ - "./dist/index.js" + "./dist/openclaw.js" ] }, "bin": { @@ -66,6 +66,8 @@ "examples/openclaw-docker", "README.md", "LICENSE", + "openclaw.d.ts", + "openclaw.js", "openclaw.plugin.json", "skills" ] diff --git a/setup.sh b/setup.sh index 755aa4a..9834657 100755 --- a/setup.sh +++ b/setup.sh @@ -9,6 +9,9 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SKILL_SRC="$SCRIPT_DIR/skills/agentguard" AGENTGUARD_DIR="$HOME/.agentguard" MIN_NODE_VERSION=18 +OPENCLAW_ROOT="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}" +OPENCLAW_PLUGIN_DIR="$OPENCLAW_ROOT/plugins/agentguard" +OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-$OPENCLAW_ROOT/openclaw.json}" echo "" echo " GoPlus AgentGuard — AI Agent Security Guard" @@ -76,6 +79,7 @@ if [ "${1:-}" = "--uninstall" ] || [ "${1:-}" = "uninstall" ]; then rm -rf "$HOME/.claude/skills/agentguard" 2>/dev/null || true rm -rf "$HOME/.openclaw/skills/agentguard" 2>/dev/null || true rm -rf "$HOME/.openclaw/workspace/skills/agentguard" 2>/dev/null || true + rm -rf "$OPENCLAW_PLUGIN_DIR" 2>/dev/null || true rm -rf "$AGENTGUARD_DIR" 2>/dev/null && echo " Removed config from $AGENTGUARD_DIR" || true echo "" echo " GoPlus AgentGuard has been uninstalled." @@ -145,6 +149,76 @@ else echo " OK: Config already exists (keeping current settings)" fi +if [ "$PLATFORM" = "openclaw-workspace" ] || [ "$PLATFORM" = "openclaw-managed" ]; then + echo " Enabling OpenClaw plugin..." + mkdir -p "$OPENCLAW_PLUGIN_DIR" + AGENTGUARD_DIST_INDEX="$SCRIPT_DIR/dist/index.js" node - "$OPENCLAW_PLUGIN_DIR/index.js" <<'NODE' +const { writeFileSync } = require('node:fs'); +const pluginPath = process.argv[2]; +const distIndex = process.env.AGENTGUARD_DIST_INDEX; +writeFileSync(pluginPath, `const { registerOpenClawPlugin } = require(${JSON.stringify(distIndex)}); + +module.exports = function setup(api) { + registerOpenClawPlugin(api, { + skipAutoScan: false, + }); +}; +module.exports.default = module.exports; +`); +NODE + cat > "$OPENCLAW_PLUGIN_DIR/openclaw.plugin.json" <<'JSON' +{ + "id": "agentguard", + "name": "GoPlus AgentGuard", + "description": "AI agent security framework — blocks dangerous commands, prevents data leaks, and protects secrets", + "configSchema": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": ["strict", "balanced", "permissive"], + "default": "balanced", + "description": "Protection level: strict (block all risky), balanced (block dangerous, confirm risky), permissive (only block critical)" + } + } + } +} +JSON + node - "$OPENCLAW_CONFIG_PATH" "$OPENCLAW_PLUGIN_DIR" <<'NODE' +const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs'); +const { dirname } = require('node:path'); +const [configPath, pluginDir] = process.argv.slice(2); +const ensureRecord = (parent, key) => { + const existing = parent[key]; + if (existing && typeof existing === 'object' && !Array.isArray(existing)) return existing; + const next = {}; + parent[key] = next; + return next; +}; +let config = {}; +if (existsSync(configPath)) { + const raw = readFileSync(configPath, 'utf8').trim(); + config = raw ? JSON.parse(raw) : {}; +} +const plugins = ensureRecord(config, 'plugins'); +const load = ensureRecord(plugins, 'load'); +const entries = ensureRecord(plugins, 'entries'); +const agentguard = ensureRecord(entries, 'agentguard'); +agentguard.enabled = true; +const paths = Array.isArray(load.paths) ? load.paths.filter((p) => typeof p === 'string') : []; +if (!paths.includes(pluginDir)) paths.push(pluginDir); +load.paths = paths; +if (Array.isArray(plugins.allow)) { + const allow = plugins.allow.filter((id) => typeof id === 'string'); + if (!allow.includes('agentguard')) allow.push('agentguard'); + plugins.allow = allow; +} +mkdirSync(dirname(configPath), { recursive: true }); +writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); +NODE + echo " OK: OpenClaw plugin enabled in $OPENCLAW_CONFIG_PATH" +fi + # ---- Done ---- echo "" echo " ✅ GoPlus AgentGuard is installed!" diff --git a/skills/agentguard/SKILL.md b/skills/agentguard/SKILL.md index c706d58..274746a 100644 --- a/skills/agentguard/SKILL.md +++ b/skills/agentguard/SKILL.md @@ -31,7 +31,7 @@ filesystem-access: reason: "Read/write audit log (audit.jsonl) and protection level config (config.json)" user-invocable: true allowed-tools: Read, Write, Grep, Glob, Bash(node *trust-cli.ts *) Bash(node *action-cli.ts *) Bash(*checkup-report.js) Bash(echo *checkup-report.js) Bash(cat *checkup-report.js) Bash(agentguard *) Bash(openclaw *) Bash(ss *) Bash(lsof *) Bash(ufw *) Bash(iptables *) Bash(crontab *) Bash(systemctl list-timers *) Bash(find *) Bash(stat *) Bash(env) Bash(sha256sum *) Bash(node *) Bash(cd *) -argument-hint: "[scan|action|patrol|subscribe|trust|report|config|checkup] [args...]" +argument-hint: "[scan|action|patrol|subscribe|trust|report|config|checkup|cli] [args...]" --- # GoPlus AgentGuard — AI Agent Security Framework @@ -64,9 +64,32 @@ Parse `$ARGUMENTS` to determine the subcommand: - **`config `** — Set protection level - **`checkup`** — Run a comprehensive agent health checkup and generate a visual HTML report - **`hermes-hooks`** — Show or install Hermes shell-hook configuration for runtime protection +- **`cli `** — Run the installed `agentguard` CLI directly for supported commands not otherwise routed by this skill If no subcommand is given, or the first argument is a path, default to **scan**. +### CLI Passthrough + +This skill is allowed to run `agentguard *`, so CLI commands and flags are available even when the skill has a higher-level workflow for the same area. + +Use CLI passthrough when the user explicitly asks for a concrete `agentguard ...` command, when the command is one of the CLI-only commands below, or when a CLI flag changes semantics that this skill's high-level workflow does not implement. + +Supported CLI commands and options: + +| CLI command | Options | Notes | +|---|---|---| +| `agentguard init` | `--level `, `--agent `, `--cloud `, `--force` | Creates local config and optionally installs agent templates | +| `agentguard connect` | `--key `, `--api-key `, `--url `, `--cloud ` | Prefer `AGENTGUARD_API_KEY` over passing secrets in flags | +| `agentguard status` | none | Shows local config, Cloud URL/API key status, policy cache, audit path | +| `agentguard policy pull` | `--json` | Pulls Cloud effective runtime policy into the local cache | +| `agentguard doctor` | none | Checks local setup and Cloud reachability when connected | +| `agentguard scan ` | `--json` | Runs the packaged scanner against a local path | +| `agentguard protect` | `--agent `, `--action-type `, `--tool-name `, `--session-id `, `--decision-mode `, `--json` | Evaluates one runtime action from stdin or hook environment | +| `agentguard subscribe` | `--since `, `--json`, `--no-report`, `--install-cron`, `--cron-name `, `--interval-minutes `, `--force`, `--cron-run` | Pulls Cloud threat advisories and self-checks local skills | +| `agentguard checkup` | `--against-advisory `, `--json` | CLI threat-feed self-check; without `--against-advisory`, it only prints a tip in the current CLI build | + +If the user writes `/agentguard cli `, execute `agentguard ` directly. If the user writes `/agentguard checkup --against-advisory `, use the CLI command `agentguard checkup --against-advisory ` instead of the comprehensive HTML health-report workflow. + ## Subcommand: hermes-hooks Help the user configure AgentGuard runtime protection for Hermes Agent. @@ -152,15 +175,20 @@ Examples: ```bash agentguard subscribe agentguard subscribe --json +agentguard subscribe --since 2026-05-01T00:00:00.000Z +agentguard subscribe --no-report agentguard subscribe --install-cron +agentguard subscribe --install-cron --cron-name agentguard-threat-feed agentguard subscribe --install-cron --interval-minutes 5 agentguard subscribe --install-cron --force ``` -When `--install-cron` is used, the CLI registers an OpenClaw isolated cron job through the local OpenClaw Gateway at `127.0.0.1:18789`. It runs every 15 minutes by default. Pass `--interval-minutes ` to override the cadence. If a job with the same name already exists, the CLI leaves it untouched unless `--force` is passed. The cron delivery is intentionally silent (`delivery.mode = "none"`); the isolated turn executes `agentguard subscribe --json --cron-run` and only sends the configured notification when `shouldNotify` is `true`. +When `--install-cron` is used, the CLI registers an OpenClaw isolated cron job through the local OpenClaw Gateway at `127.0.0.1:18789`. It runs every 15 minutes by default. Pass `--interval-minutes ` to override the cadence and `--cron-name ` to choose the job name. If a job with the same name already exists, the CLI leaves it untouched unless `--force` is passed. The cron delivery is intentionally silent (`delivery.mode = "none"`); the isolated turn executes `agentguard subscribe --json --cron-run` and only sends the configured notification when `shouldNotify` is `true`. `agentguard subscribe --json` always includes a stable `cron` object with `requested`, `installed`, and optional `result` fields. If cron installation fails, the command exits non-zero instead of printing a misleading success summary. +`--since ` overrides the persisted feed cursor for one run. `--no-report` skips uploading local matches back to Cloud. `--cron-run` is internal and should only be used by the OpenClaw cron prompt unless the user explicitly asks to reproduce cron behavior. + --- # Security Operations @@ -726,6 +754,15 @@ If the log file doesn't exist, inform the user that no security events have been Run a comprehensive agent health checkup across 6 security dimensions. Generates a visual HTML report with a lobster mascot and opens it in the browser. The lobster's appearance reflects the agent's health: muscular bodybuilder (score 90+), healthy with shield (70–89), tired with coffee (50–69), or sick with bandages (0–49). +If the arguments include `--against-advisory `, do not run this comprehensive HTML workflow. Instead execute the CLI threat-feed self-check: + +```bash +agentguard checkup --against-advisory +agentguard checkup --against-advisory --json +``` + +That CLI path fetches the current Cloud advisory feed and checks local skills against the single advisory. It is separate from the full health report below. + ### Step 1: Data Collection **IMPORTANT: You MUST run ALL 7 checks below — not just the skill scan. The checkup covers 5 security dimensions, not just code scanning. Do NOT skip checks 2–7.** diff --git a/src/installers.ts b/src/installers.ts index ebe11e7..9f7ade7 100644 --- a/src/installers.ts +++ b/src/installers.ts @@ -1,4 +1,5 @@ -import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; import { dirname, join } from 'node:path'; export type AgentInstaller = 'claude-code' | 'codex' | 'openclaw'; @@ -12,7 +13,7 @@ export function installAgentTemplates(agent: AgentInstaller, options: { cwd?: st const root = options.cwd || process.cwd(); if (agent === 'claude-code') return installClaudeCode(root, Boolean(options.force)); if (agent === 'codex') return installCodex(root, Boolean(options.force)); - if (agent === 'openclaw') return installOpenClaw(root, Boolean(options.force)); + if (agent === 'openclaw') return installOpenClaw(options.cwd, Boolean(options.force)); throw new Error(`Unsupported agent installer: ${agent}`); } @@ -36,10 +37,22 @@ function installCodex(root: string, force: boolean): InstallResult { return { agent: 'codex', files: [skillPath, hookPath] }; } -function installOpenClaw(root: string, force: boolean): InstallResult { - const pluginPath = join(root, 'openclaw.agentguard.plugin.ts'); +function installOpenClaw(cwd: string | undefined, force: boolean): InstallResult { + const openClawRoot = cwd + ? join(cwd, '.openclaw') + : process.env.OPENCLAW_STATE_DIR || join(homedir(), '.openclaw'); + const pluginDir = join(openClawRoot, 'plugins', 'agentguard'); + const pluginPath = join(pluginDir, 'index.ts'); + const manifestPath = join(pluginDir, 'openclaw.plugin.json'); + const configPath = cwd + ? join(openClawRoot, 'openclaw.json') + : process.env.OPENCLAW_CONFIG_PATH || join(openClawRoot, 'openclaw.json'); + writeIfAllowed(pluginPath, openClawPluginTemplate(), force); - return { agent: 'openclaw', files: [pluginPath] }; + writeIfAllowed(manifestPath, JSON.stringify(openClawPluginManifest(), null, 2) + '\n', force); + enableOpenClawPlugin(configPath, pluginDir); + + return { agent: 'openclaw', files: [pluginPath, manifestPath, configPath] }; } function writeIfAllowed(path: string, content: string, force: boolean): void { @@ -147,3 +160,63 @@ export default function setup(api) { } `; } + +function openClawPluginManifest(): unknown { + return { + id: 'agentguard', + name: 'GoPlus AgentGuard', + description: 'AI agent security framework - blocks dangerous commands, prevents data leaks, and protects secrets', + configSchema: { + type: 'object', + properties: { + level: { + type: 'string', + enum: ['strict', 'balanced', 'permissive'], + default: 'balanced', + description: 'Protection level: strict (block all risky), balanced (block dangerous, confirm risky), permissive (only block critical)', + }, + }, + }, + }; +} + +function enableOpenClawPlugin(configPath: string, pluginDir: string): void { + let config: Record = {}; + if (existsSync(configPath)) { + const raw = readFileSync(configPath, 'utf8').trim(); + config = raw ? JSON.parse(raw) as Record : {}; + } + + const plugins = ensureRecord(config, 'plugins'); + const load = ensureRecord(plugins, 'load'); + const entries = ensureRecord(plugins, 'entries'); + const agentguard = ensureRecord(entries, 'agentguard'); + agentguard.enabled = true; + + const paths = Array.isArray(load.paths) ? load.paths.filter((p): p is string => typeof p === 'string') : []; + if (!paths.includes(pluginDir)) { + paths.push(pluginDir); + } + load.paths = paths; + + if (Array.isArray(plugins.allow)) { + const allow = plugins.allow.filter((id): id is string => typeof id === 'string'); + if (!allow.includes('agentguard')) { + allow.push('agentguard'); + } + plugins.allow = allow; + } + + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); +} + +function ensureRecord(parent: Record, key: string): Record { + const existing = parent[key]; + if (existing && typeof existing === 'object' && !Array.isArray(existing)) { + return existing as Record; + } + const next: Record = {}; + parent[key] = next; + return next; +} diff --git a/src/openclaw.ts b/src/openclaw.ts new file mode 100644 index 0000000..88310c5 --- /dev/null +++ b/src/openclaw.ts @@ -0,0 +1,6 @@ +export { default } from './adapters/openclaw-plugin.js'; +export { + getPluginIdFromTool, + getPluginScanResult, + registerOpenClawPlugin, +} from './adapters/openclaw-plugin.js'; diff --git a/src/tests/installer.test.ts b/src/tests/installer.test.ts index e262325..c330d7a 100644 --- a/src/tests/installer.test.ts +++ b/src/tests/installer.test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { existsSync, readFileSync, mkdtempSync } from 'node:fs'; +import { existsSync, readFileSync, mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { installAgentTemplates } from '../installers.js'; @@ -23,12 +23,33 @@ describe('Agent template installers', () => { assert.ok(readFileSync(join(dir, '.codex', 'agentguard-hook.example.json'), 'utf8').includes('AGENTGUARD_AGENT_HOST=codex')); }); - it('writes OpenClaw plugin template', () => { + it('writes and enables OpenClaw plugin template', () => { const dir = mkdtempSync(join(tmpdir(), 'agentguard-openclaw-')); - installAgentTemplates('openclaw', { cwd: dir }); + const result = installAgentTemplates('openclaw', { cwd: dir }); + + const pluginDir = join(dir, '.openclaw', 'plugins', 'agentguard'); + const template = readFileSync(join(pluginDir, 'index.ts'), 'utf8'); + const manifest = readFileSync(join(pluginDir, 'openclaw.plugin.json'), 'utf8'); + const config = JSON.parse(readFileSync(join(dir, '.openclaw', 'openclaw.json'), 'utf8')); - const template = readFileSync(join(dir, 'openclaw.agentguard.plugin.ts'), 'utf8'); + assert.equal(result.files.length, 3); assert.ok(template.includes('registerOpenClawPlugin')); + assert.ok(manifest.includes('"id": "agentguard"')); + assert.equal(config.plugins.entries.agentguard.enabled, true); + assert.deepEqual(config.plugins.load.paths, [pluginDir]); assert.ok(!template.includes("level: 'balanced'")); }); + + it('adds AgentGuard to an existing OpenClaw plugin allowlist', () => { + const dir = mkdtempSync(join(tmpdir(), 'agentguard-openclaw-existing-')); + const configPath = join(dir, '.openclaw', 'openclaw.json'); + mkdirSync(join(dir, '.openclaw'), { recursive: true }); + writeFileSync(configPath, JSON.stringify({ plugins: { allow: ['existing'] } }, null, 2)); + + installAgentTemplates('openclaw', { cwd: dir }); + + const config = JSON.parse(readFileSync(configPath, 'utf8')); + assert.deepEqual(config.plugins.allow, ['existing', 'agentguard']); + assert.equal(config.plugins.entries.agentguard.enabled, true); + }); });