From 3ffc362643e53e823162caa6b82d2c56000c0fec Mon Sep 17 00:00:00 2001 From: 0xJeff Date: Tue, 16 Jun 2026 16:13:28 +0800 Subject: [PATCH 1/3] fix review findings --- mcpb/manifest.json | 2 +- scripts/build-mcpb.sh | 20 +++++++++++++++++--- src/tests/mcpb-manifest.test.ts | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/mcpb/manifest.json b/mcpb/manifest.json index 756c1a7..fafb760 100644 --- a/mcpb/manifest.json +++ b/mcpb/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.3", "name": "agentguard", "display_name": "GoPlus AgentGuard", - "version": "1.1.27", + "version": "1.1.28-beta.2", "icon": "icon.png", "description": "Security guard for AI agents — blocks malicious skills, prevents data leaks, protects secrets.", "long_description": "GoPlus AgentGuard is an AI-agent security framework. It exposes MCP tools for scanning skills, looking up and managing a trust registry, evaluating runtime actions against policy, and simulating Web3 transactions. 20+ detection rules cover dangerous commands, secret exfiltration, and risky on-chain actions.", diff --git a/scripts/build-mcpb.sh b/scripts/build-mcpb.sh index f257c52..3b7e622 100755 --- a/scripts/build-mcpb.sh +++ b/scripts/build-mcpb.sh @@ -8,7 +8,7 @@ # manifest.json <- from mcpb/manifest.json, version stamped from package.json # icon.png <- from mcpb/icon.png (512x512) # server/ -# dist/ <- compiled TypeScript (tsc output) +# dist/ <- compiled TypeScript runtime output # node_modules/ <- production dependencies only # package.json # package-lock.json @@ -47,8 +47,9 @@ if [ -n "${EXPECTED_VERSION:-}" ] && [ "${EXPECTED_VERSION#v}" != "$VERSION" ]; exit 1 fi -# 1. Clean install + compile -npm ci +# 1. Clean install + compile. Ignore lifecycle scripts here so release builds +# cannot mutate user/runner AgentGuard config via postinstall. +npm ci --ignore-scripts npm run build # 2. Fresh staging tree @@ -63,6 +64,11 @@ cp mcpb/icon.png "$STAGE/icon.png" cp -R dist "$STAGE/server/dist" cp README.md LICENSE package.json package-lock.json "$STAGE/server/" +# Keep the desktop extension runtime-only: tests, type declarations, and source +# maps are useful in npm/source builds but should not ship in the installed MCPB. +rm -rf "$STAGE/server/dist/tests" +find "$STAGE/server/dist" -type f \( -name '*.map' -o -name '*.d.ts' \) -delete + # 5. Production dependencies only ( cd "$STAGE/server" && npm ci --omit=dev --ignore-scripts ) @@ -80,4 +86,12 @@ if [ -n "$UNEXPECTED" ]; then exit 1 fi +DISALLOWED='^server/dist/tests/|^server/dist/.*(\.map|\.d\.ts)$' +DISALLOWED_FILES="$(unzip -Z1 "$OUT" | grep -v '/$' | grep -E "$DISALLOWED" || true)" +if [ -n "$DISALLOWED_FILES" ]; then + echo "ERROR: non-runtime TypeScript artifacts in bundle:" >&2 + echo "$DISALLOWED_FILES" >&2 + exit 1 +fi + echo "==> Built $OUT (contents verified against allowlist)" diff --git a/src/tests/mcpb-manifest.test.ts b/src/tests/mcpb-manifest.test.ts index d1267ae..f254d04 100644 --- a/src/tests/mcpb-manifest.test.ts +++ b/src/tests/mcpb-manifest.test.ts @@ -10,9 +10,11 @@ const repoRoot = path.resolve(__dirname, '..', '..'); const manifestPath = path.join(repoRoot, 'mcpb', 'manifest.json'); const iconPath = path.join(repoRoot, 'mcpb', 'icon.png'); const packagePath = path.join(repoRoot, 'package.json'); +const buildScriptPath = path.join(repoRoot, 'scripts', 'build-mcpb.sh'); const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); +const buildScript = fs.readFileSync(buildScriptPath, 'utf8'); // Minimal PNG header reader: validates the signature and returns IHDR dimensions. function readPngSize(buf: Buffer): { width: number; height: number } { @@ -70,3 +72,18 @@ describe('mcpb/manifest.json — MCP Directory requirements', () => { } }); }); + +describe('scripts/build-mcpb.sh — bundle hygiene', () => { + it('does not run package lifecycle scripts during the release build install', () => { + assert.match(buildScript, /\bnpm ci --ignore-scripts\b/); + assert.doesNotMatch(buildScript, /\bnpm ci\n/); + }); + + it('removes non-runtime TypeScript artifacts from the packaged server dist', () => { + assert.match(buildScript, /rm -rf "\$STAGE\/server\/dist\/tests"/); + assert.match(buildScript, /-name '\*\.map'/); + assert.match(buildScript, /-name '\*\.d\.ts'/); + assert.match(buildScript, /\^server\/dist\/tests\//); + assert.match(buildScript, /\\\.map\|\\\.d\\\.ts/); + }); +}); From 4b75a64677df4a3642756acd1c999c0cf024a2d5 Mon Sep 17 00:00:00 2001 From: 0xJeff Date: Tue, 16 Jun 2026 16:39:11 +0800 Subject: [PATCH 2/3] fix pr-107 review findings --- README.md | 2 +- docs/hermes.md | 8 +-- plugins/hermes/README.md | 10 ++-- src/cli.ts | 14 +++-- src/installers.ts | 109 +++++++++++++++++++++++++++++++++++- src/tests/cli-init.test.ts | 6 +- src/tests/installer.test.ts | 32 ++++++++++- 7 files changed, 159 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 074ed73..a52eb34 100644 --- a/README.md +++ b/README.md @@ -341,7 +341,7 @@ GoPlus AgentGuard follows the [Agent Skills](https://agentskills.io) open standa |----------|---------|----------| | **Claude Code** | Full | Skill + hooks auto-guard, transcript-based skill tracking | | **OpenClaw** | Full | Plugin hooks + **auto-scan on load** + tool→plugin mapping + **daily patrol** | -| **Hermes Agent** | Plugin + Hooks | Native plugin (`hermes plugins enable agentguard`) for `pre_tool_call` / `post_tool_call`, or legacy shell hooks | +| **Hermes Agent** | Plugin + Hooks | Native plugin for `pre_tool_call` / `post_tool_call` (enabled by `agentguard init --agent hermes`), or legacy shell hooks | | **OpenAI Codex CLI** | Skill | Scan/action/trust commands | | **Gemini CLI** | Skill | Scan/action/trust commands | | **Cursor** | Skill | Scan/action/trust commands | diff --git a/docs/hermes.md b/docs/hermes.md index 36c60b4..ace8521 100644 --- a/docs/hermes.md +++ b/docs/hermes.md @@ -8,8 +8,9 @@ is identical; they differ only in how they are installed and managed. ## Native plugin (recommended) The plugin is managed the Hermes-native way (`hermes plugins -enable/disable/list`), runs before shell hooks, adds a `/agentguard` slash -command, and needs no manual edits to `~/.hermes/config.yaml`. +enable/disable/list`), runs before shell hooks, and adds a `/agentguard` +slash command. `agentguard init --agent hermes` installs the plugin and enables +it in `~/.hermes/config.yaml`. ```bash # Build the engine so the plugin can reach it @@ -18,8 +19,7 @@ npm run build # Install the plugin into ~/.hermes/plugins/agentguard/ agentguard init --agent hermes -# Hermes plugins are opt-in — enable it -hermes plugins enable agentguard +# Confirm it is enabled hermes plugins list ``` diff --git a/plugins/hermes/README.md b/plugins/hermes/README.md index d4442b0..59191fb 100644 --- a/plugins/hermes/README.md +++ b/plugins/hermes/README.md @@ -24,9 +24,8 @@ place. # Installs the plugin into ~/.hermes/plugins/agentguard/ agentguard init --agent hermes -# Hermes plugins are opt-in — enable it: -hermes plugins enable agentguard -hermes plugins list # confirm it is enabled +# Confirm it is enabled: +hermes plugins list ``` Or copy this directory to `~/.hermes/plugins/agentguard/` manually. @@ -58,8 +57,9 @@ Hermes `pre_tool_call` has no native "ask"/confirm decision, so AgentGuard's | `AGENTGUARD_HERMES_ALLOW_NPX` | `0` | `1` permits the `npx -y @goplus/agentguard` fallback when no local binary is found. Off by default — `npx` fetches an unpinned package over the network, which is unsafe for a security gate. | | `AGENTGUARD_HERMES_AUTOSCAN` | `1` | `0` disables the session-start skill scan. | -**Activation:** installing only copies files; the plugin is **inactive** until you -run `hermes plugins enable agentguard` (Hermes plugins are opt-in). +**Activation:** `agentguard init --agent hermes` installs the plugin and enables +it in `~/.hermes/config.yaml`. It takes effect on the next Hermes session. If you +copy this directory manually, run `hermes plugins enable agentguard`. **Fail policy:** for the security-sensitive tools above, the plugin fails **closed** (blocks) on `pre_tool_call` when the engine cannot be reached, and also diff --git a/src/cli.ts b/src/cli.ts index 1197ce7..277609c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -98,6 +98,7 @@ async function main() { for (const result of results.installed) { console.log(`Installed ${result.agent} template:`); for (const file of result.files) console.log(`- ${file}`); + if (result.agent === 'hermes') printHermesNativePluginEnabled(); } for (const failure of results.failed) { console.error(`! Failed to initialize ${failure.agent}: ${failure.error}`); @@ -116,11 +117,7 @@ async function main() { console.log(`Installed ${result.agent} template:`); for (const file of result.files) console.log(`- ${file}`); if (agent === 'hermes' && !shellHooks) { - console.log(''); - console.log('⚠ The AgentGuard plugin is INSTALLED but INACTIVE — no protection yet.'); - console.log(' Activate it (takes effect on the next Hermes session):'); - console.log(' hermes plugins enable agentguard'); - console.log(' Or re-run with --shell-hooks to wire the always-on legacy shell hooks instead.'); + printHermesNativePluginEnabled(); } } }); @@ -962,6 +959,13 @@ function printInstalledGuidance(): void { console.log('Run `agentguard --help` to see all commands.'); } +function printHermesNativePluginEnabled(): void { + console.log(''); + console.log('Hermes native plugin enabled in config.yaml.'); + console.log('It takes effect on the next Hermes session.'); + console.log('Use `hermes plugins list` to verify, or re-run with --shell-hooks for the legacy shell-hook flow.'); +} + function printInitGuidanceIfNeeded(config: AgentGuardConfig): void { if (hasSavedAgentHost(config)) return; console.log(''); diff --git a/src/installers.ts b/src/installers.ts index 5e3a110..24f729b 100644 --- a/src/installers.ts +++ b/src/installers.ts @@ -91,11 +91,12 @@ function installHermes(cwd: string | undefined, force: boolean, opts: { shellHoo } files.push(...configPaths); } else { - // Default path: install the native Hermes plugin (opt-in to enable via - // `hermes plugins enable agentguard`). No config.yaml edits. + // Default path: install and enable the native Hermes plugin. const pluginDir = join(hermesRoot, 'plugins', 'agentguard'); + const configPath = join(hermesRoot, 'config.yaml'); copyBundledHermesPlugin(pluginDir, force); - files.push(pluginDir); + enableHermesNativePlugin(configPath); + files.push(pluginDir, configPath); } return { agent: 'hermes', files }; @@ -130,6 +131,108 @@ function copyBundledHermesPlugin(targetDir: string, force: boolean): void { } } +function enableHermesNativePlugin(configPath: string): void { + const existing = existsSync(configPath) ? readFileSync(configPath, 'utf8') : ''; + const next = mergeHermesNativePluginEnabled(existing); + if (next === existing) return; + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, next); +} + +function mergeHermesNativePluginEnabled(existing: string): string { + const lines = existing.replace(/\s+$/g, '').split(/\r?\n/).filter((line, index, arr) => !(arr.length === 1 && index === 0 && line === '')); + const merged: string[] = []; + let sawPlugins = false; + + for (let index = 0; index < lines.length;) { + if (isTopLevelHermesPluginsLine(lines[index])) { + sawPlugins = true; + const pluginsEnd = findNextTopLevelIndex(lines, index + 1); + merged.push('plugins:'); + merged.push(...enableHermesPluginInPluginsBlock(lines.slice(index + 1, pluginsEnd))); + index = pluginsEnd; + continue; + } + merged.push(lines[index]); + index += 1; + } + + if (!sawPlugins) { + if (merged.length > 0) merged.push(''); + merged.push('plugins:', ' enabled:', ' - agentguard'); + } + + return `${merged.join('\n').replace(/\s+$/g, '')}\n`; +} + +function isTopLevelHermesPluginsLine(line: string): boolean { + return /^plugins:\s*(?:\{\}\s*)?(?:#.*)?$/.test(line); +} + +function enableHermesPluginInPluginsBlock(lines: string[]): string[] { + const enabledPlugins = uniqueStrings([...readHermesEnabledPlugins(lines), 'agentguard']); + const kept = removeHermesPluginEnabled(lines); + return [' enabled:', ...enabledPlugins.map((plugin) => ` - ${plugin}`), ...kept]; +} + +function removeHermesPluginEnabled(lines: string[]): string[] { + const kept: string[] = []; + for (let index = 0; index < lines.length;) { + const match = /^ enabled:\s*(?:#.*)?$/.exec(lines[index]); + if (match) { + index += 1; + while (index < lines.length && !/^ [A-Za-z0-9_-]+:\s*(?:#.*)?$/.test(lines[index]) && !/^\S/.test(lines[index])) { + index += 1; + } + continue; + } + + const inlineList = /^ enabled:\s*\[(.*)\]\s*(?:#.*)?$/.exec(lines[index]); + if (inlineList) { + index += 1; + continue; + } + + kept.push(lines[index]); + index += 1; + } + return kept; +} + +function readHermesEnabledPlugins(lines: string[]): string[] { + const names: string[] = []; + for (let index = 0; index < lines.length; index += 1) { + const inlineList = /^ enabled:\s*\[(.*)\]\s*(?:#.*)?$/.exec(lines[index]); + if (inlineList) { + for (const item of inlineList[1].split(',')) { + const name = parseHermesYamlScalar(item); + if (name) names.push(name); + } + continue; + } + + if (!/^ enabled:\s*(?:#.*)?$/.test(lines[index])) continue; + index += 1; + while (index < lines.length && !/^ [A-Za-z0-9_-]+:\s*(?:#.*)?$/.test(lines[index]) && !/^\S/.test(lines[index])) { + const item = /^ -\s*(.+?)\s*(?:#.*)?$/.exec(lines[index]); + const name = item ? parseHermesYamlScalar(item[1]) : ''; + if (name) names.push(name); + index += 1; + } + index -= 1; + } + return names; +} + +function parseHermesYamlScalar(value: string | undefined): string { + const trimmed = (value || '').trim(); + if (!trimmed) return ''; + if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { + return trimmed.slice(1, -1).trim(); + } + return trimmed; +} + function installQClaw(root: string, force: boolean): InstallResult { const qclawRoot = join(root, '.qclaw'); const configPath = join(qclawRoot, 'qclaw.json'); diff --git a/src/tests/cli-init.test.ts b/src/tests/cli-init.test.ts index c7730d7..70f7f59 100644 --- a/src/tests/cli-init.test.ts +++ b/src/tests/cli-init.test.ts @@ -245,6 +245,7 @@ describe('init CLI', () => { assert.equal(config.agentHost, agent); if (agent === 'hermes') { assert.ok(existsSync(join(hermesHome, 'plugins', 'agentguard', 'plugin.yaml'))); + assert.ok(readFileSync(join(hermesHome, 'config.yaml'), 'utf8').includes('- agentguard')); } } }); @@ -264,7 +265,8 @@ describe('init CLI', () => { assert.equal(config.agentHost, 'hermes'); assert.match(stdout, /Installed hermes template:/); assert.ok(stdout.includes(join(hermesHome, 'plugins', 'agentguard'))); - assert.match(stdout, /hermes plugins enable agentguard/); + assert.match(stdout, /Hermes native plugin enabled in config\.yaml/); + assert.ok(readFileSync(join(hermesHome, 'config.yaml'), 'utf8').includes('- agentguard')); }); it('auto-initializes detected agents in detection order', async () => { @@ -289,10 +291,12 @@ describe('init CLI', () => { assert.ok(existsSync(join(cwd, '.openclaw', 'plugins', 'agentguard', 'openclaw.plugin.json'))); assert.ok(existsSync(join(cwd, '.hermes', 'skills', 'agentguard'))); assert.ok(existsSync(join(cwd, '.hermes', 'plugins', 'agentguard', 'plugin.yaml'))); + assert.ok(readFileSync(join(cwd, '.hermes', 'config.yaml'), 'utf8').includes('- agentguard')); assert.ok(existsSync(join(cwd, '.codex', 'skills', 'agentguard', 'SKILL.md'))); assert.ok(existsSync(join(cwd, '.codex', 'agentguard-hook.json'))); assert.match(stdout, /Installed openclaw template:/); assert.match(stdout, /Installed hermes template:/); + assert.match(stdout, /Hermes native plugin enabled in config\.yaml/); assert.match(stdout, /Installed codex template:/); }); diff --git a/src/tests/installer.test.ts b/src/tests/installer.test.ts index 696f71c..d712316 100644 --- a/src/tests/installer.test.ts +++ b/src/tests/installer.test.ts @@ -23,10 +23,12 @@ describe('Agent template installers', () => { assert.ok(readFileSync(join(dir, '.codex', 'agentguard-hook.json'), 'utf8').includes('AGENTGUARD_AGENT_HOST=codex')); }); - it('installs the native Hermes plugin by default (no config.yaml edits)', () => { + it('installs and enables the native Hermes plugin by default', () => { const dir = mkdtempSync(join(tmpdir(), 'agentguard-hermes-plugin-')); const result = installAgentTemplates('hermes', { cwd: dir }); const pluginDir = join(dir, '.hermes', 'plugins', 'agentguard'); + const configPath = join(dir, '.hermes', 'config.yaml'); + const config = readFileSync(configPath, 'utf8'); assert.equal(result.agent, 'hermes'); assert.ok(existsSync(join(pluginDir, 'plugin.yaml'))); @@ -36,10 +38,34 @@ describe('Agent template installers', () => { // Tests are excluded from the bundled copy. assert.ok(!existsSync(join(pluginDir, 'tests'))); assert.ok(result.files.includes(pluginDir)); + assert.ok(result.files.includes(configPath)); // The bundled skill is still installed for the engine fallback / auto-scan. assert.ok(existsSync(join(dir, '.hermes', 'skills', 'agentguard', 'SKILL.md'))); - // Default mode must not write shell hooks into config.yaml. - assert.ok(!existsSync(join(dir, '.hermes', 'config.yaml'))); + assert.match(config, /^plugins:\n enabled:\n - agentguard\n$/); + assert.ok(!config.includes('pre_tool_call:')); + }); + + it('preserves existing Hermes native plugin config while enabling AgentGuard', () => { + const dir = mkdtempSync(join(tmpdir(), 'agentguard-hermes-plugin-existing-')); + const configPath = join(dir, '.hermes', 'config.yaml'); + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, [ + 'theme: dark', + 'plugins:', + ' enabled:', + ' - other-plugin', + ' disabled:', + ' - old-plugin', + '', + ].join('\n')); + + installAgentTemplates('hermes', { cwd: dir }); + + const config = readFileSync(configPath, 'utf8'); + assert.ok(config.includes('theme: dark')); + assert.ok(config.includes(' enabled:\n - other-plugin\n - agentguard')); + assert.ok(config.includes(' disabled:\n - old-plugin')); + assert.ok(!config.includes('pre_tool_call:')); }); it('writes Hermes skill and enables hook config with --shell-hooks', () => { From e18ab209893c80aeee7d4d7839ba901f993957cf Mon Sep 17 00:00:00 2001 From: 0xJeff Date: Tue, 16 Jun 2026 17:00:37 +0800 Subject: [PATCH 3/3] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db6a5b7..bb7651e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added +- Added MCPB bundle assets and a reproducible MCP Desktop Extension build path, including the bundle manifest, AgentGuard directory icon, privacy policy metadata, production dependency staging, release workflow, build docs, and manifest tests. (#106) +- Added a native Hermes plugin that `agentguard init --agent hermes` installs and enables, with pre-tool blocking, post-tool audit handling, session-start skill scans, a `/agentguard` slash command, Hermes plugin docs, and Python plugin tests. (#107) + ### Changed - Web search actions now use a dedicated `web_search` runtime action across Claude Code, Hermes, OpenClaw, MCP, and the skill CLI, so query-only searches are handled separately from URL fetches and no longer trigger invalid-URL network approval flows. - Direct web fetch and browser navigation GET requests keep the default `network.defaultOutbound: warn` behavior as audit-only, while mutating or high-risk network requests still require confirmation or blocking. @@ -18,6 +22,8 @@ - Runtime network policies now enforce `network.defaultOutbound` and `network.blockedDomains` for direct network/browser tool calls instead of only checking shell commands. - Runtime blocked-domain matching now compares structured URL hosts and paths instead of raw substrings, avoiding false positives such as `notexample.com` matching `example.com`; curl/wget download-and-execute commands are detected with real regex patterns. - Hermes hook templates now split `web_search` from URL-bearing web/browser tools and recognize open-style URL tools consistently. +- MCPB release builds now harden staging and packaging so the generated bundle stamps the package version, includes only production server dependencies, preserves required bundle metadata, and validates manifest expectations before publishing. (#106) +- Native Hermes plugin packaging now excludes test cache artifacts, validates required mapped fields, preserves command arguments consistently, and only allows the unsafe `npx` fallback when explicitly enabled. (#107) ## [1.1.27] - 2026-05-29