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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
### Added
- Added Agent JWT registration and activation links for OpenClaw-backed Cloud connections.
- Added Cloud feed subscription support for the default advisory ecosystems.
- `agentguard status` now shows saved Agent JWT registration details.
- `agentguard status` now shows the active Cloud auth method, including API-key and Agent JWT connection details.

### Changed
- Cloud flows now prefer Agent JWT auth when available, with API key support preserved.
- Threat-feed notifications now include Cloud remediation guidance when available.
- Connect and disconnect flows now keep API key and Agent JWT credentials mutually clean.
- `agentguard disconnect` now removes the managed threat-feed subscribe cron job from the configured agent backend and clears saved cron metadata.
- `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.
Expand All @@ -19,6 +20,8 @@

### Fixed
- Fixed Cloud runtime decisions that return `require_approve` instead of `require_approval`.
- Fixed OpenClaw Agent JWT connect so OpenClaw runtime detection can start registration without requiring an API key or prior AgentGuard init.
- Fixed AgentGuard runtime self-handling so direct `agentguard` and `agentguard-mcp` CLI commands are not audited, reported, or blocked by AgentGuard's own hooks while compound shell commands remain protected.
- 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.
Expand Down
14 changes: 10 additions & 4 deletions skills/agentguard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ required post-install steps.
Parse `$ARGUMENTS` to determine the subcommand:

- **`init [args...]`** — Run `agentguard init`, especially `agentguard init --agent <agent>` after installation
- **`connect [args...]`** — Run `agentguard connect` to connect optional Cloud policy, audit, and approvals
- **`connect [args...]`** — Run `agentguard connect` to connect optional Cloud policy, audit, and approvals. AgentGuard supports either API-key auth or Agent JWT auth; only one Cloud auth method is required.
- **`scan <path>`** — Scan a skill or codebase for security risks
- **`action <description>`** — Evaluate whether a runtime action is safe
- **`patrol [run|setup|status]`** — Daily security patrol for OpenClaw environments
Expand All @@ -98,9 +98,9 @@ Supported CLI commands and options:
| CLI command | Options | Notes |
|---|---|---|
| `agentguard init` | `--level <level>`, `--agent <agent>`, `--cloud <url>`, `--force` | Creates local config, persists the selected agent host, and optionally installs templates for `claude-code`, `codex`, `openclaw`, `hermes`, or `qclaw` |
| `agentguard connect` | `--key <key>`, `--api-key <key>`, `--url <url>`, `--cloud <url>` | Prefer `AGENTGUARD_API_KEY` over passing secrets in flags |
| `agentguard disconnect` | none | Removes local Cloud API key, connection timestamp, pending event spool, and cached Cloud policy; keeps Cloud URL, audit log, and installed hooks/templates |
| `agentguard status` | none | Shows local config, Cloud URL/API key status, policy cache, audit path |
| `agentguard connect` | `--key <key>`, `--api-key <key>`, `--url <url>`, `--cloud <url>` | API-key auth and Agent JWT auth are alternatives; configure only one. Prefer `AGENTGUARD_API_KEY` over passing secrets in flags |
| `agentguard disconnect` | none | Removes local Cloud credentials, pending event spool, cached Cloud policy, and the managed `agentguard-threat-feed` subscribe cron job; keeps Cloud URL, audit log, and installed hooks/templates |
| `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 doctor` | none | Checks local setup and Cloud reachability when connected |
Expand All @@ -109,6 +109,12 @@ Supported CLI commands and options:
| `agentguard checkup` | `--json` | Runs the local agent health checkup |
| `agentguard checkup --against-advisory <id>` | `--json` | CLI threat-feed self-check for one advisory; this is a targeted mode, not the default health-check workflow |

Connect behavior:

- Always execute `agentguard connect ...` directly when the user asks for it. Do not answer that an API key must be obtained before running the command.
- `agentguard connect` with no `--key`, `--api-key`, or `AGENTGUARD_API_KEY` is valid in OpenClaw environments: the CLI uses Agent JWT registration, prints an activation link, and may notify the latest OpenClaw channel.
- Only suggest `agentguard connect --key <key>` when the user explicitly wants API-key auth or when the CLI itself reports that Agent JWT registration is unavailable. If the CLI says OpenClaw is not initialized, suggest `agentguard init --agent openclaw` and then rerun `agentguard connect`.

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

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 ...`.
Expand Down
10 changes: 10 additions & 0 deletions src/adapters/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getSkillTrustPolicy,
isActionAllowedByCapabilities,
} from './common.js';
import { isAgentGuardCliCommand } from '../runtime/self-command.js';

/**
* Evaluate a hook event using the common AgentGuard decision engine.
Expand All @@ -20,6 +21,9 @@ export async function evaluateHook(
options: EngineOptions
): Promise<HookOutput> {
const input = adapter.parseInput(rawInput);
if (isAgentGuardHookCommand(adapter, input)) {
return { decision: 'allow' };
}

// Post-tool events → audit only
if (input.eventType === 'post') {
Expand Down Expand Up @@ -116,3 +120,9 @@ export async function evaluateHook(
return { decision: 'allow' };
}
}

function isAgentGuardHookCommand(adapter: HookAdapter, input: HookInput): boolean {
if (adapter.mapToolToActionType(input.toolName) !== 'exec_command') return false;
const command = input.toolInput.command;
return typeof command === 'string' && isAgentGuardCliCommand(command);
}
88 changes: 81 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import { CloudRequestError } from './cloud/client.js';
import { notifyOpenClawMessage, notifyOpenClawRegistrationLink } from './cloud/openclaw-notify.js';
import {
installThreatFeedCron,
removeThreatFeedCron,
validateCronExpression,
type OpenClawCronInstallResult,
type CronBackend,
type ThreatFeedCronRemovalResult,
type OpenClawGatewayOptions,
} from './feed/cron.js';

Expand Down Expand Up @@ -122,10 +124,11 @@ async function main() {
.action(async (options) => {
const apiKey = options.key || options.apiKey || process.env.AGENTGUARD_API_KEY;
if (!apiKey) {
const config = ensureConfig();
let config = ensureConfig();
if (!isOpenClawAgentConfigured(config)) {
throw new Error('Missing API key. Pass --key, --api-key, set AGENTGUARD_API_KEY, or run `agentguard init --agent openclaw` before using Agent JWT registration.');
}
config = withDetectedOpenClawAgentHost(config);
const cloudUrl = normalizeCloudUrl(options.cloud || options.url || config.cloudUrl || 'https://agentguard.gopluslabs.io');
if (config.agentId && config.agentJwt) {
const existingConfig = { ...config, cloudUrl };
Expand Down Expand Up @@ -183,10 +186,18 @@ async function main() {
program
.command('disconnect')
.description('Disconnect local AgentGuard from AgentGuard Cloud')
.action(() => {
.action(async () => {
const currentConfig = ensureConfig();
const cronRemoval = await removeThreatFeedCron({
name: currentConfig.threatFeedCronName || 'agentguard-threat-feed',
backend: 'auto',
agentHost: resolveCronAgentHost(currentConfig),
agentGuardHome: getAgentGuardPaths().home,
});
const config = disconnectCloud();
console.log('Disconnected from AgentGuard Cloud.');
console.log('Removed local Cloud API key, Agent JWT, connection timestamp, pending event spool, and cached Cloud policy.');
printCronRemovalSummary(cronRemoval);
console.log(`Local protection remains active using the built-in policy. Audit log: ${config.auditPath}`);
});

Expand All @@ -199,10 +210,7 @@ async function main() {
console.log(`Config: ${paths.configPath}`);
console.log(`Protection level: ${config.level}`);
console.log(`Cloud URL: ${config.cloudUrl || 'not configured'}`);
console.log(`API key: ${maskApiKey(config.apiKey)}`);
console.log(`Agent ID: ${config.agentId || 'not configured'}`);
console.log(`Agent JWT: ${config.agentJwt ? 'configured' : 'not configured'}`);
console.log(`Agent activation URL: ${config.agentRegisterUrl || 'not configured'}`);
printCloudAuthStatus(config);
console.log(`Agent host: ${config.agentHost || 'not configured'}`);
console.log(`Agent hosts: ${config.agentHosts?.join(', ') || 'not configured'}`);
console.log(`Policy cache: ${config.policyCachePath}`);
Expand Down Expand Up @@ -628,6 +636,11 @@ async function main() {
agentHost: resolveCronAgentHost(config),
agentGuardHome: getAgentGuardPaths().home,
});
saveConfig({
...config,
threatFeedCronName: summary.cron.result.name,
threatFeedCronInstalledAt: new Date().toISOString(),
});
summary.cron.installed = true;
} catch (err) {
summary.cron.error = (err as Error).message;
Expand Down Expand Up @@ -875,6 +888,46 @@ function printInitGuidanceIfNeeded(config: AgentGuardConfig): void {
console.log(` ${REQUIRED_INIT_COMMAND}`);
}

function printCloudAuthStatus(config: AgentGuardConfig): void {
if (config.agentJwt) {
console.log('Cloud auth: connected via Agent JWT');
console.log('API key: not used for this connection');
console.log(`Agent ID: ${config.agentId || 'configured'}`);
console.log('Agent JWT: configured');
console.log(`Agent activation URL: ${config.agentRegisterUrl || 'not configured'}`);
return;
}
if (config.apiKey) {
console.log('Cloud auth: connected via API key');
console.log(`API key: ${maskApiKey(config.apiKey)}`);
console.log('Agent JWT: not used for this connection');
return;
}

console.log('Cloud auth: not connected');
console.log('API key: not configured');
console.log('Agent JWT: not configured');
}

function printCronRemovalSummary(results: ThreatFeedCronRemovalResult[]): void {
const removed = results.filter((result) => result.removed);
if (removed.length > 0) {
console.log(`Removed AgentGuard subscribe cron job "${removed[0]!.name}" from: ${removed.map((result) => result.backend).join(', ')}.`);
return;
}

const errors = results.filter((result) => result.error);
if (errors.length > 0) {
console.log('No AgentGuard subscribe cron job was removed; some cron backends were unavailable.');
for (const result of errors) {
console.error(`! ${result.backend}: ${result.error}`);
}
return;
}

console.log('No AgentGuard subscribe cron job was found.');
}

function resolveCronAgentHost(config: AgentGuardConfig): AgentGuardAgentHost | undefined {
return config.agentHost ?? config.agentHosts?.[0];
}
Expand Down Expand Up @@ -1296,7 +1349,28 @@ function printAgentActivationRequired(
}

function isOpenClawAgentConfigured(config: AgentGuardConfig): boolean {
return config.agentHost === 'openclaw' || config.agentHosts?.includes('openclaw') === true;
return config.agentHost === 'openclaw' || config.agentHosts?.includes('openclaw') === true || detectOpenClawRuntime();
}

function withDetectedOpenClawAgentHost(config: AgentGuardConfig): AgentGuardConfig {
if (hasSavedAgentHost(config) || !detectOpenClawRuntime()) return config;
const next: AgentGuardConfig = {
...config,
agentHost: 'openclaw',
agentHosts: appendAgentHost(config.agentHosts, 'openclaw'),
};
saveConfig(next);
return next;
}

function detectOpenClawRuntime(): boolean {
const configPath = process.env.OPENCLAW_CONFIG_PATH?.trim();
if (configPath && existsSync(configPath)) return true;

const stateDir = process.env.OPENCLAW_STATE_DIR?.trim();
if (stateDir && (existsSync(stateDir) || existsSync(join(stateDir, 'openclaw.json')))) return true;

return existsSync(join(homedir(), '.openclaw', 'openclaw.json'));
}

function resolveOpenClawGatewayOptionsFromEnv(): OpenClawGatewayOptions {
Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface AgentGuardConfig {
agentRegisterUrl?: string;
agentRegisteredAt?: string;
connectedAt?: string;
threatFeedCronName?: string;
threatFeedCronInstalledAt?: string;
policyCachePath: string;
auditPath: string;
eventSpoolPath: string;
Expand Down Expand Up @@ -142,6 +144,7 @@ export function connectAgentJwt(options: {
agentRegisteredAt: new Date().toISOString(),
connectedAt: new Date().toISOString(),
};
delete next.apiKey;
saveConfig(next);
return next;
}
Expand All @@ -165,6 +168,8 @@ export function disconnectCloud(): AgentGuardConfig {
delete next.agentRegisterUrl;
delete next.agentRegisteredAt;
delete next.connectedAt;
delete next.threatFeedCronName;
delete next.threatFeedCronInstalledAt;
rmSync(current.eventSpoolPath, { force: true });
rmSync(current.policyCachePath, { force: true });
saveConfig(next);
Expand Down
Loading
Loading