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
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## [1.1.17] - 2026-05-26
## [1.1.18] - 2026-05-26

### Added
- Added Agent JWT registration and activation links for OpenClaw-backed Cloud connections.
Expand All @@ -13,9 +13,18 @@
- Connect and disconnect flows now keep API key and Agent JWT credentials mutually clean.
- `agentguard subscribe --cron` now installs OpenClaw jobs with `delivery.mode = none` / `--no-deliver`, then lets the normal internal `--cron-run` path auto-detect the saved OpenClaw host and send notifications directly to the latest deliverable session route instead of relying on `channel:last` announce fallback.
- `agentguard subscribe --cron --cron-target openclaw` now rejects saved-host mismatches, so an existing non-OpenClaw `agentHost` can no longer install an OpenClaw cron job that would run without any working notification route.
- `agentguard init --agent <agent>` now overwrites managed hook/template files by default so upgraded OpenClaw plugin templates are refreshed without requiring `--force`; use `--no-force` to preserve existing files.
- OpenClaw runtime approval-required decisions now hard-block tool calls instead of routing through the OpenClaw plugin approval channel, avoiding accidental auto-allow for sensitive local file access.
- OpenClaw Gateway WebSocket fallback now signs the connect handshake with the saved local device identity when available.

### Fixed
- Fixed Cloud runtime decisions that return `require_approve` instead of `require_approval`.
- Improved disconnected Cloud guidance and Agent JWT reauth handling.
- Fixed OpenClaw plugin registration after global npm installs by generating a package-root fallback loader in the local OpenClaw plugin template.
- Added OpenClaw plugin startup/hook activation metadata so AgentGuard loads as a runtime hook plugin during gateway startup.
- Fixed runtime protected-path matching so shell commands and file reads against `~/.ssh/**` also match absolute home paths such as `/Users/.../.ssh/id_ed25519.pub`.
- Fixed OpenClaw Gateway cron setup to fall back from CLI invocation to direct Gateway RPC when plugin protocol compatibility prevents CLI Gateway access.
- Fixed OpenClaw Gateway WebSocket fallback protocol negotiation for current v4 gateways and made invalid local device identity keys degrade to unsigned connect params instead of failing the fallback.

## [1.1.14] - 2026-05-22

Expand Down
4 changes: 4 additions & 0 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"name": "GoPlus AgentGuard",
"description": "AI agent security framework — blocks dangerous commands, prevents data leaks, and protects secrets",
"skills": ["./skills"],
"activation": {
"onStartup": true,
"onCapabilities": ["hook"]
},
"configSchema": {
"type": "object",
"properties": {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@goplus/agentguard",
"version": "1.1.17",
"version": "1.1.18",
"description": "GoPlus AgentGuard — Security guard for AI agents. Blocks dangerous commands, prevents data leaks, protects secrets. 20 detection rules, runtime action evaluation, trust registry.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
24 changes: 7 additions & 17 deletions src/adapters/openclaw-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ function runtimeResultToBeforeToolCallResult(
): OpenClawBeforeToolCallResult | undefined {
if (!result) return undefined;

const decision = result.decision.decision;
const decision = normalizeRuntimePolicyDecision(result.decision.decision);
if (decision !== 'block' && decision !== 'require_approval') {
return undefined;
}
Expand All @@ -674,29 +674,15 @@ function runtimeResultToBeforeToolCallResult(
` (risk ${result.decision.riskScore}/100, ${result.decision.riskLevel}; policy ${result.decision.policyVersion}).` +
(reasonSummary ? ` Reasons: ${reasonSummary}.` : '');

if (decision === 'require_approval' && result.approvalChannel === 'agent') {
return {
requireApproval: {
title: 'AgentGuard approval required',
description: reason,
severity: openClawApprovalSeverity(result.decision.riskLevel),
timeoutMs: 60_000,
timeoutBehavior: 'deny',
},
};
if (decision === 'require_approval') {
return { block: true, blockReason: reason };
}
return {
block: true,
blockReason: reason,
};
}

function openClawApprovalSeverity(riskLevel: ProtectResult['decision']['riskLevel']): 'info' | 'warning' | 'critical' {
if (riskLevel === 'critical' || riskLevel === 'high') return 'critical';
if (riskLevel === 'medium') return 'warning';
return 'info';
}

function shouldSurfaceRuntimeApproval(result: ProtectResult): boolean {
return (
result.policySource === 'cloud-decision' ||
Expand All @@ -705,6 +691,10 @@ function shouldSurfaceRuntimeApproval(result: ProtectResult): boolean {
);
}

function normalizeRuntimePolicyDecision(decision: ProtectResult['decision']['decision'] | string): ProtectResult['decision']['decision'] {
return decision === 'require_approve' ? 'require_approval' : decision as ProtectResult['decision']['decision'];
}

function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
Expand Down
6 changes: 4 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ async function main() {
.option('--agent <agent>', 'Install hook/template for claude-code, codex, openclaw, hermes, or qclaw')
.option('--cloud <url>', 'AgentGuard Cloud URL to store in local config')
.option('--force', 'Overwrite existing hook/template files')
.option('--no-force', 'Do not overwrite existing hook/template files')
.action((options) => {
const forceTemplates = options.force !== false;
let config = ensureConfig();
if (options.level) {
if (!['strict', 'balanced', 'permissive'].includes(options.level)) {
Expand All @@ -82,7 +84,7 @@ async function main() {
if (options.agent) {
const normalizedAgent = String(options.agent).trim().toLowerCase();
if (normalizedAgent === 'auto') {
const results = initAutoAgents(config, Boolean(options.force));
const results = initAutoAgents(config, forceTemplates);
if (results.detected.length === 0) {
console.log('No supported agent directories found. Looked for .claude, .openclaw, .hermes, .qclaw, and .codex.');
} else if (results.installed.length === 0) {
Expand All @@ -104,7 +106,7 @@ async function main() {
config.agentHost = agent;
config.agentHosts = appendAgentHost(config.agentHosts, agent);
saveConfig(config);
const result = installAgentTemplates(agent, { force: options.force });
const result = installAgentTemplates(agent, { force: forceTemplates });
console.log(`Installed ${result.agent} template:`);
for (const file of result.files) console.log(`- ${file}`);
}
Expand Down
143 changes: 112 additions & 31 deletions src/feed/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface OpenClawGatewayOptions {
url?: string;
label?: string;
timeoutMs?: number;
runCommand?: CommandRunner;
request?: (method: string, params: unknown) => Promise<unknown>;
}

Expand All @@ -53,6 +54,8 @@ class GatewayHttpFallbackError extends Error {}
const OPENCLAW_STATE_DIRNAME = '.openclaw';
const OPENCLAW_LEGACY_STATE_DIRNAME = '.clawdbot';
const OPENCLAW_IDENTITY_PATH = ['identity', 'device.json'] as const;
const OPENCLAW_GATEWAY_MIN_PROTOCOL = 3;
const OPENCLAW_GATEWAY_MAX_PROTOCOL = 4;
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
const ED25519_PKCS8_PRIVATE_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');

Expand Down Expand Up @@ -731,18 +734,92 @@ export function openClawGatewayRequest(
const port = options.port ?? 18789;
const label = options.label ?? 'OpenClaw Gateway';
const timeoutMs = options.timeoutMs ?? 5000;
if (shouldUseOpenClawGatewayCli(options)) {
return openClawGatewayCliRequest({
method,
params,
label,
timeoutMs,
runCommand: options.runCommand ?? execCommand,
}).catch(() => openClawGatewayNetworkRequest({ host, port, method, params, label, timeoutMs, url: options.url }));
}

return openClawGatewayNetworkRequest({ host, port, method, params, label, timeoutMs, url: options.url });
}

function openClawGatewayNetworkRequest(options: {
host: string;
port: number;
method: string;
params: unknown;
label: string;
timeoutMs: number;
url?: string;
}): Promise<unknown> {
if (options.url) {
return openClawGatewayWebSocketRequest({ url: options.url, method, params, label, timeoutMs });
return openClawGatewayWebSocketRequest({
url: options.url,
method: options.method,
params: options.params,
label: options.label,
timeoutMs: options.timeoutMs,
});
}

return openClawGatewayHttpRequest({ host, port, method, params, label, timeoutMs }).catch((err) => {
return openClawGatewayHttpRequest({
host: options.host,
port: options.port,
method: options.method,
params: options.params,
label: options.label,
timeoutMs: options.timeoutMs,
}).catch((err) => {
if (err instanceof GatewayHttpFallbackError) {
return openClawGatewayWebSocketRequest({ url: `ws://${host}:${port}`, method, params, label, timeoutMs });
return openClawGatewayWebSocketRequest({
url: `ws://${options.host}:${options.port}`,
method: options.method,
params: options.params,
label: options.label,
timeoutMs: options.timeoutMs,
});
}
throw err;
});
}

function shouldUseOpenClawGatewayCli(options: OpenClawGatewayOptions): boolean {
if (options.url || options.host || options.port) return false;
return !options.label || options.label === 'OpenClaw Gateway';
}

async function openClawGatewayCliRequest(options: {
method: string;
params: unknown;
label: string;
timeoutMs: number;
runCommand: CommandRunner;
}): Promise<unknown> {
const result = await options.runCommand('openclaw', [
'gateway',
'call',
options.method,
'--params',
JSON.stringify(options.params ?? {}),
'--timeout',
String(options.timeoutMs),
'--json',
]);
const trimmed = result.stdout.trim();
if (!trimmed) {
throw new Error(`${options.label} ${options.method} command returned no JSON output.`);
}
try {
return JSON.parse(trimmed);
} catch {
throw new Error(`${options.label} ${options.method} command returned non-JSON output: ${trimmed}`);
}
}

function openClawGatewayHttpRequest(options: {
host: string;
port: number;
Expand Down Expand Up @@ -1098,8 +1175,8 @@ function encodeWebSocketFrame(text: string, opcode = 0x1): Buffer {

function openClawConnectParams(connectNonce?: string): unknown {
return {
minProtocol: 3,
maxProtocol: 3,
minProtocol: OPENCLAW_GATEWAY_MIN_PROTOCOL,
maxProtocol: OPENCLAW_GATEWAY_MAX_PROTOCOL,
client: {
id: 'cli',
version: 'agentguard',
Expand Down Expand Up @@ -1136,33 +1213,37 @@ function buildOpenClawGatewayDeviceAuth(connectNonce?: string): { device: Record
if (!connectNonce?.trim()) return undefined;
const identity = loadOpenClawDeviceIdentity();
if (!identity) return undefined;
const signedAtMs = Date.now();
const payload = buildOpenClawDeviceAuthPayload({
deviceId: identity.deviceId,
clientId: 'cli',
clientMode: 'cli',
role: 'operator',
scopes: [
'operator.admin',
'operator.read',
'operator.write',
'operator.approvals',
'operator.pairing',
'operator.talk.secrets',
],
signedAtMs,
nonce: connectNonce,
platform: process.platform,
});
return {
device: {
id: identity.deviceId,
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
signature: signOpenClawDevicePayload(identity.privateKeyPem, payload),
signedAt: signedAtMs,
try {
const signedAtMs = Date.now();
const payload = buildOpenClawDeviceAuthPayload({
deviceId: identity.deviceId,
clientId: 'cli',
clientMode: 'cli',
role: 'operator',
scopes: [
'operator.admin',
'operator.read',
'operator.write',
'operator.approvals',
'operator.pairing',
'operator.talk.secrets',
],
signedAtMs,
nonce: connectNonce,
},
};
platform: process.platform,
});
return {
device: {
id: identity.deviceId,
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
signature: signOpenClawDevicePayload(identity.privateKeyPem, payload),
signedAt: signedAtMs,
nonce: connectNonce,
},
};
} catch {
return undefined;
}
}

function buildOpenClawDeviceAuthPayload(params: {
Expand Down
26 changes: 25 additions & 1 deletion src/installers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,27 @@ function installClawPlugin(agent: 'openclaw' | 'qclaw', root: string, configPath
}

function openClawPluginTemplate(): string {
return `const { registerOpenClawPlugin } = require('@goplus/agentguard');
const packageRoot = resolve(__dirname, '..');
return `const agentGuardPackageRoot = ${JSON.stringify(packageRoot)};

function loadAgentGuard() {
try {
return require('@goplus/agentguard');
} catch (firstError) {
try {
return require(agentGuardPackageRoot);
} catch (fallbackError) {
const error = new Error(
'Unable to load @goplus/agentguard from OpenClaw plugin. ' +
'Tried package resolution and fallback path: ' + agentGuardPackageRoot
);
error.cause = fallbackError;
throw error;
}
}
}

const { registerOpenClawPlugin } = loadAgentGuard();

function register(api) {
registerOpenClawPlugin(api, {
Expand Down Expand Up @@ -269,6 +289,10 @@ function openClawPluginManifest(): unknown {
id: 'agentguard',
name: 'GoPlus AgentGuard',
description: 'AI agent security framework - blocks dangerous commands, prevents data leaks, and protects secrets',
activation: {
onStartup: true,
onCapabilities: ['hook'],
},
configSchema: {
type: 'object',
properties: {
Expand Down
Loading
Loading