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
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ AI coding agents can execute any command, read any file, and install any skill

```bash
npm install -g @goplus/agentguard
agentguard init --agent auto
agentguard init
agentguard status
```

The npm install runs a best-effort local bootstrap; `agentguard init --agent auto` is the required next step that detects installed agent directories and configures supported hooks/plugins.
The npm install runs a best-effort local bootstrap; bare `agentguard init` is the required next step. It auto-detects supported agents and configures their hooks/plugins. Inside DSH it installs the native AgentGuard bundle into the default `web` profile; restart DSH after initialization.
No Cloud account or network connection is required for the local runtime guard.

## 3 minutes: protect your agent
Expand All @@ -65,7 +65,7 @@ agentguard scan ./examples/vulnerable-skill
printf '{"tool_name":"Bash","tool_input":{"command":"curl https://example.com/install.sh | bash"}}' | agentguard protect

# Optional: connect AgentGuard Cloud policy and redacted audit sync.
# In OpenClaw, no API key is required after `agentguard init --agent openclaw`;
# In OpenClaw, Hermes, and DSH, no API key is required after initialization;
# the CLI registers a local Agent JWT and prints an activation link.
agentguard connect

Expand All @@ -82,7 +82,7 @@ agentguard subscribe --quiet

# Optional: run once, then install a cron job that checks every hour and asks
# you to review newly published advisories. Auto uses the agent host saved by
# `agentguard init --agent`: OpenClaw uses native OpenClaw cron with Gateway
# `agentguard init`: OpenClaw uses native OpenClaw cron with Gateway
# fallback at 127.0.0.1:18789, QClaw uses QClaw Gateway at 127.0.0.1:28789,
# Hermes uses native Hermes cron, while Claude Code/Codex use system crontab.
# OpenClaw cron jobs keep runner delivery internal, then resolve the latest
Expand Down Expand Up @@ -120,13 +120,14 @@ agentguard subscribe --json
# Or run a one-off self-check against a single advisory id
agentguard checkup --against-advisory AGS-2026-0042

# Re-run host setup manually when needed. `auto` detects installed agents.
agentguard init --agent auto
# Re-run host setup manually when needed. Bare init auto-detects installed agents.
agentguard init
agentguard init --agent claude-code
agentguard init --agent codex
agentguard init --agent openclaw
agentguard init --agent hermes # native Hermes plugin (add --shell-hooks for the legacy flow)
agentguard init --agent qclaw
agentguard init --agent dsh # native bundle in the default web profile; restart DSH
```

### Audit DeepSeek Harness plugins before installation
Expand Down
11 changes: 6 additions & 5 deletions docs/cloud-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ This creates `~/.agentguard/config.json`, `~/.agentguard/audit.jsonl`, and local

## Connect Cloud

OpenClaw users can connect without an API key after initialization:
OpenClaw, Hermes, and DSH users can connect without an API key after initialization:

```bash
agentguard init --agent openclaw
agentguard init
agentguard connect
```

In this mode, `connect` registers a local Agent JWT, prints an activation link,
and may send that link to the latest OpenClaw channel. Open the link to bind the
local agent to your AgentGuard account.
In this mode, `connect` registers a local Agent JWT and prints an activation link.
OpenClaw may also send that link to its latest channel. Open the link to bind the
local agent to your AgentGuard account. If auto-detection is unavailable, pass an
explicit host such as `agentguard init --agent dsh`.

API-key auth is also supported:

Expand Down
26 changes: 16 additions & 10 deletions docs/cloud-native-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Native UI may present `require_approval` as `confirm`, but API payloads should k
### Agent hosts

```text
claude-code | codex | openclaw | cursor | gemini | copilot | other
claude-code | codex | openclaw | hermes | qclaw | dsh | cursor | gemini | copilot | other
```

### Action types
Expand Down Expand Up @@ -78,37 +78,43 @@ safe | low | medium | high | critical
### Commercial install script

```http
GET /install.sh?agent=claude-code
GET /install.sh?agent=dsh
```

Allowed `agent` values:

```text
auto | claude-code | openclaw | codex
auto | openclaw | hermes | dsh
```

The script installs `@goplus/agentguard`, writes a safe fallback local config, then calls:
The hosted script installs `@goplus/agentguard`, initializes integrations, and connects Cloud. With no explicit host it calls:

```bash
agentguard init --agent "$AGENTGUARD_AGENT" --cloud "$AGENTGUARD_CLOUD_URL"
agentguard init --cloud "$AGENTGUARD_CLOUD_URL"
```

When the effective agent host is OpenClaw, the script should connect without an
API key:
When the hosting service already knows the requested host, it may add a validated
`--agent` value. Explicit activation-link hosts are restricted to
OpenClaw, Hermes, and DSH. DSH uses its default `web` profile and must be
restarted after installation. In `auto` mode, at least one detected host must
support Agent JWT registration or `connect` exits with guidance instead of
claiming a binding link was created.

When the effective agent host is OpenClaw, Hermes, or DSH, the script connects
without an API key:

```bash
agentguard connect --cloud "$AGENTGUARD_CLOUD_URL"
```

The CLI registers a local Agent JWT and prints an activation link. For other
agent hosts, or when the user explicitly chooses API-key auth, the script should
call:
agent hosts, use the CLI's separate API-key flow:

```bash
agentguard connect --cloud "$AGENTGUARD_CLOUD_URL" --api-key "$AGENTGUARD_API_KEY"
```

Native CLI implementations should support `--cloud` as an alias for the Cloud URL and `--api-key` as an alias for the API key. Installers that accept `agent=auto` should use the agent host persisted by `agentguard init --agent auto` when choosing between Agent JWT and API-key auth.
Native CLI implementations should support `--cloud` as an alias for the Cloud URL and `--api-key` as an alias for the API key. Bare `agentguard init` is equivalent to `--agent auto`; it persists detected hosts so `connect` can choose Agent JWT or API-key auth.

### Health check

Expand Down
11 changes: 10 additions & 1 deletion docs/dsh.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ The Phase 1 scanner is intentionally read-only: it scans source, classifies capa

AgentGuard can be loaded into a DSH profile as a native tool plugin. From an npm release:

```bash
npm install -g @goplus/agentguard
agentguard init --agent dsh
```

When invoked inside DSH, bare `agentguard init` auto-detects DSH and performs the same installation. Both commands install the packaged native bundle into DSH's default `web` profile. Restart DSH after initialization.

The equivalent low-level DSH command is:

```bash
dsh plugin --profile web add @goplus/agentguard
```
Expand All @@ -18,7 +27,7 @@ For local development, link the checkout instead:
dsh plugin --profile web add link:/absolute/path/to/agentguard
```

Restart DSH after installation. The profile then exposes `agentguard_dsh_scan`, which accepts a local directory or HTTPS GitHub repository URL, an optional GitHub `ref`, and a Markdown or JSON format. It also exposes `agentguard_dsh_scan_batch` for sequentially scanning up to 10 targets, `agentguard_dsh_compare` for comparing an approved version with a candidate, and `agentguard_dsh_runtime_summary` for input-redacted runtime audit aggregates. For example, ask DSH: “Use AgentGuard to compare tags `v1.2.3` and `v1.3.0` of `https://github.com/owner/plugin` before I update.”
The profile then exposes `agentguard_dsh_scan`, which accepts a local directory or HTTPS GitHub repository URL, an optional GitHub `ref`, and a Markdown or JSON format. It also exposes `agentguard_dsh_scan_batch` for sequentially scanning up to 10 targets, `agentguard_dsh_compare` for comparing an approved version with a candidate, and `agentguard_dsh_runtime_summary` for input-redacted runtime audit aggregates. For example, ask DSH: “Use AgentGuard to compare tags `v1.2.3` and `v1.3.0` of `https://github.com/owner/plugin` before I update.”

The three static AgentGuard DSH tools preserve the Phase 1 boundary: they do not install or execute the target plugin. The fourth tool only summarizes local runtime audit events and never returns raw tool input. The installed bundle enables `observe` by default; [DSH runtime guard](dsh-runtime.md) documents explicit `protect` configuration.

Expand Down
80 changes: 61 additions & 19 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ import {
type OpenClawGatewayOptions,
} from './feed/cron.js';

const SUPPORTED_AGENT_INSTALLERS: AgentInstaller[] = ['claude-code', 'codex', 'openclaw', 'hermes', 'qclaw'];
const SUPPORTED_AGENT_INSTALLERS: AgentInstaller[] = ['claude-code', 'codex', 'openclaw', 'hermes', 'qclaw', 'dsh'];
const AUTO_AGENT_DETECTION: Array<{ agent: AgentInstaller; dir: string }> = [
{ agent: 'claude-code', dir: '.claude' },
{ agent: 'openclaw', dir: '.openclaw' },
{ agent: 'hermes', dir: '.hermes' },
{ agent: 'qclaw', dir: '.qclaw' },
{ agent: 'codex', dir: '.codex' },
];
const REQUIRED_INIT_COMMAND = 'agentguard init --agent auto';
const REQUIRED_INIT_COMMAND = 'agentguard init';

async function main() {
const program = new Command();
Expand All @@ -70,7 +70,7 @@ async function main() {
.command('init')
.description('Create ~/.agentguard/config.json and local runtime paths')
.option('--level <level>', 'Protection level: strict | balanced | permissive')
.option('--agent <agent>', 'Install hook/template for claude-code, codex, openclaw, hermes, or qclaw')
.option('--agent <agent>', 'Install integration for auto, claude-code, codex, openclaw, hermes, qclaw, or dsh (default: auto)')
.option('--cloud <url>', 'AgentGuard Cloud URL to store in local config')
.option('--shell-hooks', 'For Hermes: install legacy shell hooks instead of the native plugin')
.option('--force', 'Overwrite existing hook/template files')
Expand All @@ -92,18 +92,19 @@ async function main() {
const paths = getAgentGuardPaths();
console.log(`AgentGuard initialized at ${paths.home}`);
console.log(`Config: ${paths.configPath}`);
if (options.agent) {
const normalizedAgent = String(options.agent).trim().toLowerCase();
{
const normalizedAgent = options.agent === undefined
? 'auto'
: String(options.agent).trim().toLowerCase();
if (normalizedAgent === 'auto') {
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.');
console.log('No supported agent installation found. Looked for DSH and .claude, .openclaw, .hermes, .qclaw, and .codex.');
} else if (results.installed.length === 0) {
console.log('No agent templates were installed; all detected agent initializers failed.');
console.log('No agent integrations were installed; all detected agent initializers failed.');
}
for (const result of results.installed) {
console.log(`Installed ${result.agent} template:`);
for (const file of result.files) console.log(`- ${file}`);
printInstallResult(result);
if (result.agent === 'hermes') printHermesNativePluginEnabled();
}
for (const failure of results.failed) {
Expand All @@ -112,16 +113,15 @@ async function main() {
return;
}
if (!SUPPORTED_AGENT_INSTALLERS.includes(normalizedAgent as AgentInstaller)) {
throw new Error('Invalid agent. Use auto, claude-code, codex, openclaw, hermes, or qclaw.');
throw new Error('Invalid agent. Use auto, claude-code, codex, openclaw, hermes, qclaw, or dsh.');
}
const agent = normalizedAgent as AgentInstaller;
const shellHooks = Boolean(options.shellHooks);
const result = installAgentTemplates(agent, { force: forceTemplates, shellHooks });
config.agentHost = agent;
config.agentHosts = appendAgentHost(config.agentHosts, agent);
saveConfig(config);
const shellHooks = Boolean(options.shellHooks);
const result = installAgentTemplates(agent, { force: forceTemplates, shellHooks });
console.log(`Installed ${result.agent} template:`);
for (const file of result.files) console.log(`- ${file}`);
printInstallResult(result);
if (agent === 'hermes' && !shellHooks) {
printHermesNativePluginEnabled();
}
Expand All @@ -140,7 +140,7 @@ async function main() {
if (!apiKey) {
let config = ensureConfig();
if (!isAgentJwtHostConfigured(config)) {
throw new Error('AgentGuard Cloud connect supports API-key auth or Agent JWT registration for OpenClaw and Hermes. No API key was provided, and no supported Agent JWT host has been initialized. Run `agentguard init --agent openclaw` or `agentguard init --agent hermes`, then rerun `agentguard connect`; or pass --key, --api-key, or AGENTGUARD_API_KEY for API-key auth.');
throw new Error('AgentGuard Cloud connect supports API-key auth or Agent JWT registration for OpenClaw, Hermes, and DSH. No API key was provided, and no supported Agent JWT host has been initialized. Run `agentguard init` to auto-detect the host, then rerun `agentguard connect`; or pass --key, --api-key, or AGENTGUARD_API_KEY for API-key auth.');
}
config = withDetectedAgentJwtHost(config);
const cloudUrl = normalizeCloudUrl(options.cloud || options.url || config.cloudUrl || 'https://agentguard.gopluslabs.io');
Expand Down Expand Up @@ -568,7 +568,7 @@ async function main() {
let registration: AgentCredentialRegistration | null = null;
if (!client.connected) {
if (!isAgentJwtHostConfigured(config)) {
const message = 'AgentGuard Cloud is not connected. Run `agentguard connect --key <key>` first, or run `agentguard init --agent openclaw` or `agentguard init --agent hermes` to use Agent JWT registration.';
const message = 'AgentGuard Cloud is not connected. Run `agentguard connect --key <key>` first, or run `agentguard init` to auto-detect an OpenClaw, Hermes, or DSH host for Agent JWT registration.';
if (cronNotifyRun) {
console.log('NO_REPLY');
} else if (options.json) {
Expand Down Expand Up @@ -1005,9 +1005,14 @@ function initAutoAgents(config: AgentGuardConfig, force: boolean): {
} {
const installed: InstallResult[] = [];
const failed: Array<{ agent: AgentInstaller; error: string }> = [];
const detectedAgents = AUTO_AGENT_DETECTION
const directoryAgents = AUTO_AGENT_DETECTION
.filter(({ dir }) => existsSync(join(process.cwd(), dir)))
.map(({ agent }) => agent);
const isDshManagedShell = detectDshManagedShell();
const dshAgents: AgentInstaller[] = isDshManagedShell || detectInstalledDshWebProfile() ? ['dsh'] : [];
const detectedAgents: AgentInstaller[] = isDshManagedShell
? [...dshAgents, ...directoryAgents]
: [...directoryAgents, ...dshAgents];

for (const agent of detectedAgents) {
try {
Expand All @@ -1029,6 +1034,16 @@ function initAutoAgents(config: AgentGuardConfig, force: boolean): {
return { installed, failed, detected: detectedAgents };
}

function printInstallResult(result: InstallResult): void {
if (result.agent === 'dsh') {
console.log('Installed dsh integration in profile web.');
console.log('Restart DSH to activate AgentGuard in that profile.');
return;
}
console.log(`Installed ${result.agent} template:`);
for (const file of result.files) console.log(`- ${file}`);
}

function appendAgentHost(
agentHosts: AgentGuardConfig['agentHosts'] | undefined,
agent: AgentGuardAgentHost
Expand Down Expand Up @@ -1583,17 +1598,44 @@ function isHermesAgentConfigured(config: AgentGuardConfig): boolean {
return config.agentHost === 'hermes' || config.agentHosts?.includes('hermes') === true || detectHermesRuntime();
}

function isDshAgentConfigured(config: AgentGuardConfig): boolean {
return config.agentHost === 'dsh' || config.agentHosts?.includes('dsh') === true || detectDshManagedShell();
}

function isAgentJwtHostConfigured(config: AgentGuardConfig): boolean {
return isOpenClawAgentConfigured(config) || isHermesAgentConfigured(config);
return isOpenClawAgentConfigured(config) || isHermesAgentConfigured(config) || isDshAgentConfigured(config);
}

function withDetectedAgentJwtHost(config: AgentGuardConfig): AgentGuardConfig {
if (hasSavedAgentHost(config)) return config;
if (isAgentJwtAgentHost(config.agentHost)) return config;
const savedAgentJwtHost = config.agentHosts?.find(isAgentJwtAgentHost);
if (savedAgentJwtHost) return withDetectedAgentHost(config, savedAgentJwtHost);
if (detectDshManagedShell()) return withDetectedAgentHost(config, 'dsh');
if (detectOpenClawRuntime()) return withDetectedAgentHost(config, 'openclaw');
if (detectHermesRuntime()) return withDetectedAgentHost(config, 'hermes');
return config;
}

function isAgentJwtAgentHost(value: AgentGuardAgentHost | undefined): value is 'openclaw' | 'hermes' | 'dsh' {
return value === 'openclaw' || value === 'hermes' || value === 'dsh';
}

function detectDshManagedShell(): boolean {
return process.env.DSH_SHELL === '1';
}

function detectInstalledDshWebProfile(): boolean {
const configuredHome = process.env.DSH_HOME?.trim();
const dshHome = configuredHome
? configuredHome === '~'
? homedir()
: /^~[\\/]/.test(configuredHome)
? join(homedir(), configuredHome.slice(2))
: resolve(configuredHome)
: join(homedir(), '.dsh');
return existsSync(join(dshHome, 'profiles', 'web', 'package.json'));
}

function withDetectedAgentHost(config: AgentGuardConfig, agentHost: AgentGuardAgentHost): AgentGuardConfig {
const next: AgentGuardConfig = {
...config,
Expand Down
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';

export type AgentGuardAgentHost = 'claude-code' | 'codex' | 'openclaw' | 'hermes' | 'qclaw';
export type AgentGuardAgentHost = 'claude-code' | 'codex' | 'openclaw' | 'hermes' | 'qclaw' | 'dsh';

export interface AgentGuardConfig {
version: 1;
Expand Down Expand Up @@ -226,7 +226,7 @@ function normalizeLevel(value: unknown): AgentGuardConfig['level'] | null {
}

function normalizeAgentHost(value: unknown): AgentGuardAgentHost | undefined {
return value === 'claude-code' || value === 'codex' || value === 'openclaw' || value === 'hermes' || value === 'qclaw'
return value === 'claude-code' || value === 'codex' || value === 'openclaw' || value === 'hermes' || value === 'qclaw' || value === 'dsh'
? value
: undefined;
}
Expand Down
4 changes: 2 additions & 2 deletions src/feed/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isAbsolute, join } from 'node:path';

export type CronBackend = 'auto' | 'openclaw' | 'qclaw' | 'hermes' | 'system';
export type ResolvedCronBackend = 'openclaw' | 'openclaw-gateway' | 'qclaw-gateway' | 'hermes' | 'system';
export type CronAgentHost = 'claude-code' | 'codex' | 'openclaw' | 'hermes' | 'qclaw';
export type CronAgentHost = 'claude-code' | 'codex' | 'openclaw' | 'hermes' | 'qclaw' | 'dsh';

export interface OpenClawCronInstallResult {
name: string;
Expand Down Expand Up @@ -103,7 +103,7 @@ export async function installThreatFeedCron(
const backend = options.backend ?? 'auto';
if (backend === 'auto' && !options.agentHost) {
throw new Error(
'Cron target auto requires a saved agent host. Run `agentguard init --agent <claude-code|codex|openclaw|hermes|qclaw>` first, or pass `--cron-target openclaw`, `--cron-target qclaw`, `--cron-target hermes`, or `--cron-target system`.'
'Cron target auto requires a saved agent host. Run `agentguard init --agent <claude-code|codex|openclaw|hermes|qclaw|dsh>` first, or pass `--cron-target openclaw`, `--cron-target qclaw`, `--cron-target hermes`, or `--cron-target system`.'
);
}
if (backend === 'openclaw' && options.agentHost && options.agentHost !== 'openclaw') {
Expand Down
Loading
Loading