Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

## [1.1.22] - 2026-05-28

### Added
- Added local one-time runtime approval grants: `agentguard approve --action-id <id> --once`, `agentguard approve --last --once`, and `agentguard approvals list` let agents retry a previously intercepted protected action after explicit user approval, with short-lived pending approvals and audited approved retries.

### Changed
- `agentguard subscribe` cron internals (`--cron-run` and `--cron-notify-run`) now only pull feed advisories instead of re-subscribing on every scheduled run, preserving Cloud-side unsubscribe choices.
- OpenClaw Cloud connect guidance now documents the Agent JWT flow explicitly: initialized OpenClaw installs can run `agentguard connect` without an API key, while API-key auth remains available for explicit API-key connections.
- `agentguard init --agent openclaw` now installs the AgentGuard skill alongside the runtime plugin so OpenClaw agents can learn the local approval/retry flow.

### Fixed
- Supported agent CLI commands such as `openclaw`, `qclaw`, `hermes`, `codex`, and `claude` are now treated like AgentGuard self-commands so normal agent management commands are not audited, reported, or blocked by AgentGuard hooks while compound shell commands remain protected.
Expand Down
12 changes: 12 additions & 0 deletions docs/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,15 @@ Use these mappings for Codex-style hooks or skills:
- MCP tool calls → `mcp_tool`

When Cloud is connected, Codex events are synced as redacted previews. Confirmation still happens through the local agent permission flow, not a Cloud approval page.

If a protected action returns `confirm`, AgentGuard stores a short-lived pending
approval and includes an approval command:

```bash
agentguard approve --action-id act_local_... --once
```

Run that command only after the user explicitly approves, then retry the
original action once. If the action id was not visible, inspect
`agentguard approvals list --json`; use `agentguard approve --last --once`
only when there is exactly one relevant unexpired pending approval.
21 changes: 20 additions & 1 deletion docs/openclaw.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ To install and enable the AgentGuard OpenClaw plugin:
agentguard init --agent openclaw
```

This creates a local plugin under `~/.openclaw/plugins/agentguard` and enables it in `~/.openclaw/openclaw.json`.
This creates a local plugin under `~/.openclaw/plugins/agentguard`, installs the AgentGuard skill under `~/.openclaw/skills/agentguard`, and enables the plugin in `~/.openclaw/openclaw.json`.

```ts
import { registerOpenClawPlugin } from '@goplus/agentguard';
Expand Down Expand Up @@ -48,6 +48,25 @@ agentguard protect

AgentGuard accepts OpenClaw-style JSON with `toolName` and `params`, plus Claude-style `tool_name` and `tool_input`.

OpenClaw cannot safely pause and resume a protected tool call, so AgentGuard
blocks `require_approval` actions locally and stores a short-lived pending
approval. The block reason includes:

```bash
agentguard approve --action-id act_local_... --once
```

Run that command only after the user explicitly approves, then retry the
original action once. If the action id was not visible in the OpenClaw message,
inspect pending approvals first:

```bash
agentguard approvals list --json
```

Use `agentguard approve --last --once` only when there is exactly one relevant
unexpired pending approval; otherwise approve the specific `actionId`.

## Docker demo

See `examples/openclaw-docker/` for a minimal Docker demo that installs `@goplus/agentguard`, runs `agentguard init --agent openclaw`, and provides a starter plugin.
4 changes: 4 additions & 0 deletions skills/agentguard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ Supported CLI commands and options:
| `agentguard status` | none | Shows local config, active Cloud auth method, policy cache, audit path |
| `agentguard policy pull` | `--json` | Pulls Cloud effective runtime policy into the local cache |
| `agentguard policy show` | `--json` | Shows the cached effective runtime policy, or the bundled default policy when no cache exists |
| `agentguard approve` | `--action-id <id>` or `--last`, `--once`, `--json` | Approves one existing pending runtime action; never approve without explicit user confirmation |
| `agentguard approvals list` | `--json` | Lists unexpired pending runtime approvals |
| `agentguard doctor` | none | Checks local setup and Cloud reachability when connected |
| `agentguard protect` | `--agent <agent>`, `--action-type <type>`, `--tool-name <name>`, `--session-id <id>`, `--decision-mode <local-first|cloud>`, `--json` | Evaluates one runtime action from stdin or hook environment |
| `agentguard subscribe` | `--since <iso>`, `--json`, `--quiet`, `--no-report`, `--cron <expr>`, `--cron-target <auto|openclaw|qclaw|hermes|system>`, `--cron-name <name>`, `--force`, `--cron-run`, `--cron-notify-run` | Pulls Cloud threat advisories and optionally self-checks local skills |
Expand All @@ -117,6 +119,8 @@ Connect behavior:

If the user writes `/agentguard cli <args...>`, execute `agentguard <args...>` directly.

When AgentGuard returns `confirm` or a block reason that includes `Approve once: agentguard approve --action-id ... --once`, do not retry the protected action until the user explicitly approves. Treat user replies such as "yes", "approve", "approved", "confirm", "confirmed", "continue", "go ahead", "execute", "run it", "同意", "确认", "批准", "继续", or "执行" as explicit approval for the most recent protected action. After approval, run exactly the provided `agentguard approve --action-id ... --once` command, then retry the original action once. If the action id is unavailable, use `agentguard approvals list --json`; only use `agentguard approve --last --once` when there is exactly one relevant unexpired pending approval. If multiple pending approvals exist, ask the user to choose a specific action id.

Do **not** route plain `/agentguard scan`, `/agentguard action`, `/agentguard patrol`, `/agentguard trust`, `/agentguard report`, `/agentguard config`, `/agentguard checkup`, `/agentguard checkup --json`, or natural-language requests like "run agentguard checkup" through the packaged CLI. Those are this skill's higher-level workflows. Only use the packaged CLI checkup path when the user includes `--against-advisory <id>` or explicitly writes `/agentguard cli checkup ...`.

If the user writes `/agentguard checkup --against-advisory <id>`, use the CLI command `agentguard checkup --against-advisory <id>` instead of the comprehensive HTML health-report workflow.
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/openclaw-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,8 @@ function runtimeResultToBeforeToolCallResult(
(decision === 'require_approval'
? ' OpenClaw cannot safely resume this call after an external approval, so AgentGuard blocked it locally.'
: '') +
(reasonSummary ? ` Reasons: ${reasonSummary}.` : '');
(reasonSummary ? ` Reasons: ${reasonSummary}.` : '') +
(result.pendingApproval ? ` Approve once: agentguard approve --action-id ${result.pendingApproval.actionId} --once` : '');

if (decision === 'require_approval') {
return { block: true, blockReason: reason };
Expand Down
50 changes: 50 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import type { AgentGuardAgentHost, AgentGuardConfig } from './config.js';
import { SkillScanner } from './scanner/index.js';
import { formatProtectResult, protectAction, exitCodeForDecision } from './runtime/protect.js';
import { approvePendingApproval, listPendingApprovals } from './runtime/approvals.js';
import { getDefaultEffectiveRuntimePolicy, loadCachedPolicy, saveCachedPolicy } from './runtime/policy.js';
import type { RuntimeActionType, RuntimeAgentHost } from './runtime/types.js';
import { installAgentTemplates, type AgentInstaller, type InstallResult } from './installers.js';
Expand Down Expand Up @@ -360,6 +361,55 @@ async function main() {
process.exitCode = result.risk_level === 'critical' ? 2 : 0;
});

program
.command('approve')
.description('Approve one pending runtime action')
.option('--action-id <id>', 'Pending action id returned by agentguard protect')
.option('--last', 'Approve the most recent unambiguous pending action')
.option('--once', 'Approve only the next matching retry')
.option('--json', 'Print JSON output')
.action((options) => {
if (!options.once) {
throw new Error('Approvals must be scoped with --once.');
}
const config = ensureConfig();
const approved = approvePendingApproval(config.approvalStorePath!, {
actionId: options.actionId,
last: Boolean(options.last),
once: true,
sessionId: process.env.AGENTGUARD_SESSION_ID,
});
if (options.json) {
console.log(JSON.stringify({ success: true, approval: approved }, null, 2));
} else {
console.log(`Approved once: ${approved.actionId}`);
console.log(`Expires: ${approved.expiresAt}`);
}
});

const approvals = program
.command('approvals')
.description('Inspect pending runtime approvals');

approvals
.command('list')
.description('List unexpired pending approvals')
.option('--json', 'Print JSON output')
.action((options) => {
const config = ensureConfig();
const pending = listPendingApprovals(config.approvalStorePath!);
if (options.json) {
console.log(JSON.stringify({ success: true, approvals: pending }, null, 2));
} else if (pending.length === 0) {
console.log('No pending approvals.');
} else {
for (const approval of pending) {
console.log(`${approval.actionId} ${approval.actionType} ${approval.toolName} expires=${approval.expiresAt}`);
console.log(` ${approval.inputPreview}`);
}
}
});

program
.command('protect')
.description('Evaluate one runtime action from stdin or hook environment')
Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface AgentGuardConfig {
policyCachePath: string;
auditPath: string;
eventSpoolPath: string;
approvalStorePath?: string;
}

export interface AgentGuardPaths {
Expand All @@ -29,6 +30,7 @@ export interface AgentGuardPaths {
policyCachePath: string;
auditPath: string;
eventSpoolPath: string;
approvalStorePath: string;
}

const DEFAULT_CLOUD_URL = 'https://agentguard.gopluslabs.io';
Expand All @@ -42,6 +44,7 @@ export function getAgentGuardPaths(): AgentGuardPaths {
policyCachePath: join(home, 'policy-cache.json'),
auditPath: join(home, 'audit.jsonl'),
eventSpoolPath: join(home, 'events-spool.jsonl'),
approvalStorePath: join(home, 'approvals.json'),
};
}

Expand All @@ -54,6 +57,7 @@ export function defaultConfig(): AgentGuardConfig {
policyCachePath: paths.policyCachePath,
auditPath: paths.auditPath,
eventSpoolPath: paths.eventSpoolPath,
approvalStorePath: paths.approvalStorePath,
};
}

Expand Down Expand Up @@ -90,6 +94,7 @@ export function loadConfig(): AgentGuardConfig {
policyCachePath: parsed.policyCachePath || fallback.policyCachePath,
auditPath: parsed.auditPath || fallback.auditPath,
eventSpoolPath: parsed.eventSpoolPath || fallback.eventSpoolPath,
approvalStorePath: parsed.approvalStorePath || fallback.approvalStorePath,
};
} catch {
return fallback;
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ export {
type ProtectOptions,
type ProtectResult,
} from './runtime/protect.js';
export {
approvePendingApproval,
cleanupExpiredApprovals,
consumeApprovedApproval,
listPendingApprovals,
writePendingApproval,
type ApprovalRecord,
} from './runtime/approvals.js';
export { redactText, redactPreview, redactReasons } from './runtime/redaction.js';
export {
getDefaultEffectiveRuntimePolicy,
Expand Down
19 changes: 15 additions & 4 deletions src/installers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,9 @@ function installHermes(root: string, force: boolean): InstallResult {

function installQClaw(root: string, force: boolean): InstallResult {
const qclawRoot = join(root, '.qclaw');
const skillDir = join(qclawRoot, 'skills', 'agentguard');
const configPath = join(qclawRoot, 'qclaw.json');
copyBundledSkill(skillDir, force);
const pluginResult = installClawPlugin('qclaw', qclawRoot, configPath, force);
return { agent: 'qclaw', files: [skillDir, ...pluginResult.files] };
return { agent: 'qclaw', files: pluginResult.files };
}

function writeIfAllowed(path: string, content: string, force: boolean): void {
Expand Down Expand Up @@ -179,6 +177,17 @@ Expected decisions:
- \`warn\`: show warning and continue
- \`confirm\`: ask for approval in the agent channel before continuing
- \`block\`: stop the action

When a response includes \`Approve once: agentguard approve --action-id ... --once\`,
ask the user before running that approval command. Treat replies such as
"yes", "approve", "confirm", "continue", "go ahead", "execute", "run it",
"同意", "确认", "批准", "继续", or "执行" as explicit approval for the most
recent protected action. After approval, run the exact
\`agentguard approve --action-id ... --once\` command and retry the original
action once. If the id is unavailable, inspect \`agentguard approvals list --json\`;
use \`agentguard approve --last --once\` only when there is exactly one relevant
unexpired pending approval. If multiple pending approvals exist, ask the user to
choose a specific action id.
`;
}

Expand Down Expand Up @@ -228,16 +237,18 @@ hooks_auto_accept: false

function installClawPlugin(agent: 'openclaw' | 'qclaw', root: string, configPath: string, force: boolean): InstallResult {
const pluginDir = join(root, 'plugins', 'agentguard');
const skillDir = join(root, 'skills', 'agentguard');
const packagePath = join(pluginDir, 'package.json');
const pluginPath = join(pluginDir, 'index.js');
const manifestPath = join(pluginDir, 'openclaw.plugin.json');

copyBundledSkill(skillDir, force);
writeIfAllowed(packagePath, JSON.stringify(openClawPackageManifest(agent), null, 2) + '\n', force);
writeIfAllowed(pluginPath, openClawPluginTemplate(), force);
writeIfAllowed(manifestPath, JSON.stringify(openClawPluginManifest(), null, 2) + '\n', force);
enableClawPlugin(configPath, pluginDir);

return { agent, files: [packagePath, pluginPath, manifestPath, configPath] };
return { agent, files: [skillDir, packagePath, pluginPath, manifestPath, configPath] };
}

function inferOpenClawCompanionInstallTargets(root: string, configPath: string): ClawInstallTarget[] {
Expand Down
Loading
Loading