From 9d6e174ac2dc6f88a825c23033736ed358bfcdd3 Mon Sep 17 00:00:00 2001 From: Mr-Lucky Date: Sat, 17 Oct 2026 13:12:58 +0800 Subject: [PATCH 1/2] fix: preserve AgentGuard activation output in native hooks --- CHANGELOG.md | 8 +- docs/claude-code.md | 2 +- docs/privacy-boundary.md | 6 ++ mcpb/manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/runtime/protect.ts | 42 +++++--- src/runtime/self-command.ts | 25 ++++- src/runtime/self-output.ts | 29 +++++ src/tests/claude-code-lifecycle.test.ts | 138 +++++++++++++++++++++++- src/tests/codex-hooks.test.ts | 28 ++++- 11 files changed, 256 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4edf24e..da54269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,6 @@ ## Unreleased -### Fixed -- Native Codex and Claude Code hooks no longer block standalone `agentguard` CLI results when output contains sensitive data. Codex replaces model-visible tool results with locally redacted feedback, Claude Code rewrites supported post-tool results, and the exact pending Agent JWT activation URL remains clickable. Compound commands and other tools remain protected. -- OpenClaw and QClaw integrations now rewrite correlated AgentGuard CLI text in supported OpenClaw-owned `tool_result_persist` transcripts; the native Hermes `transform_tool_result` hook and DSH `tools/post-execute` accept path provide equivalent non-blocking redaction where supported. An exact saved activation link stays clickable; other hooks and downstream policy decisions retain their existing boundaries. - -## [1.2.2] - 2026-10-16 - ### Changed - Clean runtime events stay local across DSH, Codex, and Claude Code. - Reportable DSH and native-hook events now send bounded, redacted Cloud summaries with useful command previews, action details, web destinations, or file/configuration/model/policy context. Raw responses, output, credentials, and other sensitive details stay local. @@ -16,6 +10,8 @@ ### Fixed - Blocked Claude Code `PostToolBatch` findings now reach Cloud with a redacted summary and batch counts. - Codex and Claude Code hooks no longer flag masked output from the exact standalone `agentguard status` command as PII or bulk egress; modified commands and unmasked secrets remain inspected. +- Native Codex and Claude Code hooks no longer block standalone `agentguard` CLI results when output contains sensitive data. Codex replaces model-visible tool results with locally redacted feedback, Claude Code rewrites supported post-tool results, and the exact pending Agent JWT activation URL remains clickable. Compound commands and other tools remain protected. +- OpenClaw and QClaw integrations now rewrite correlated AgentGuard CLI text in supported OpenClaw-owned `tool_result_persist` transcripts; the native Hermes `transform_tool_result` hook and DSH `tools/post-execute` accept path provide equivalent non-blocking redaction where supported. An exact saved activation link stays clickable; other hooks and downstream policy decisions retain their existing boundaries. ## [1.2.1] - 2026-09-29 diff --git a/docs/claude-code.md b/docs/claude-code.md index 8b43efb..32a63f5 100644 --- a/docs/claude-code.md +++ b/docs/claude-code.md @@ -44,7 +44,7 @@ Routine successful `PostToolUse` observations remain in the local audit and are On Claude Code 2.1.251+, `PreModelSwitch` can deny or request approval for an explicit switch using `from_model`, `to_model`, `source`, and `context_tokens`. Explicit user/manual/API switches require approval, and any switch carrying 100,000 or more context tokens independently requires approval because it represents a material retransmission. Model IDs are labels, not endpoints, and are never treated as final transport destinations. `PostModelSwitch` audits automatic/session changes only. Automatic or one-shot fallbacks that do not emit the event remain `unsupported`. -`MessageDisplay` reads the official `delta` input and may return a masked top-level `displayContent`; it is always recorded as `display_only` with `canBlockCurrentAction=false`. It does not change the model response, transcript, or later `Stop` content. `Stop`, `InstructionsLoaded`, `PreCompact`, and `PostCompact` record redacted bounded metadata only; AgentGuard does not parse transcript files to invent payload, endpoint, credential, retry, or fallback facts. +`MessageDisplay` reads the official `delta` input and may return a masked `hookSpecificOutput.displayContent`. The exact saved pending activation link stays visible; other recognized credentials are masked. The replacement is returned to the hook only and is not stored in audit metadata. This event is always recorded as `display_only` with `canBlockCurrentAction=false`. It does not change the model response, transcript, or later `Stop` content. `Stop`, `InstructionsLoaded`, `PreCompact`, and `PostCompact` record redacted bounded metadata only; AgentGuard does not parse transcript files to invent payload, endpoint, credential, retry, or fallback facts. ## `@file` boundary diff --git a/docs/privacy-boundary.md b/docs/privacy-boundary.md index 4742147..d211de6 100644 --- a/docs/privacy-boundary.md +++ b/docs/privacy-boundary.md @@ -31,6 +31,12 @@ remains unchanged when it appears as an exact value, including a structured links and other tokenized URLs are not exempt. Compound commands, unverified executable paths, and unrelated tools retain their ordinary protection; `agentguard approve` is not exempt from agent-self-approval checks. +Direct `npx @goplus/agentguard` calls with supported public subcommands receive +the same native output treatment. The exact documented +`curl -fsSL https://agentguard.one/install.sh | bash` command receives it only +when the result contains the complete saved activation link. Other installer +results follow ordinary privacy checks. Running the remote installer still +requires approval before execution. | Host | Own-command result handling | Limit | |---|---|---| diff --git a/mcpb/manifest.json b/mcpb/manifest.json index 51c8435..8201816 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.2.2", + "version": "1.2.4-beta.1", "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/package-lock.json b/package-lock.json index 8a73a72..856e1c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@goplus/agentguard", - "version": "1.2.2", + "version": "1.2.4-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@goplus/agentguard", - "version": "1.2.2", + "version": "1.2.4-beta.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3fa6178..ed97d4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@goplus/agentguard", - "version": "1.2.2", + "version": "1.2.4-beta.1", "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", diff --git a/src/runtime/protect.ts b/src/runtime/protect.ts index 862d695..1a215ef 100644 --- a/src/runtime/protect.ts +++ b/src/runtime/protect.ts @@ -6,9 +6,11 @@ import type { AgentGuardConfig } from '../config.js'; import { consumeApprovedApproval, writePendingApproval, type ApprovalRecord } from './approvals.js'; import { flushEventSpool, spoolEvent, writeAuditLog } from './audit.js'; import { evaluateRuntimeAction } from './decision.js'; -import { isAgentGuardCliCommand, isAgentGuardStatusCommand, isStandaloneAgentGuardCliCommand } from './self-command.js'; +import { isAgentGuardCliCommand, isAgentGuardOwnedOutputCommand, isAgentGuardStatusCommand, + isOfficialAgentGuardInstallerCommand, isStandaloneAgentGuardCliCommand } from './self-command.js'; import { redactText, summarizeSensitiveData } from './redaction.js'; -import { redactAgentGuardSelfToolOutput, redactAgentGuardSelfToolValue } from './self-output.js'; +import { hasExactSavedActivationUrl, redactAgentGuardSelfToolOutput, redactAgentGuardSelfToolValue, + redactTextPreservingSavedActivationUrl } from './self-output.js'; import { hasSensitivePublicSearchResult, isNativePublicSearchTool, isVisiblePublicSearchResult, inspectWithPublicSearchProvenance, @@ -53,6 +55,8 @@ export interface ProtectResult { policySource: 'cloud' | 'cache' | 'default' | 'cloud-decision'; /** Hook-only response, never written to the audit log or Cloud. */ selfToolOutput?: { text: string; value: unknown }; + /** Display-only replacement, never written to the audit log or Cloud. */ + displayContent?: string; } type CodexHookEvent = @@ -195,7 +199,13 @@ export async function protectAction(options: ProtectOptions): Promise 0 ? { suppressedPublicSearchValues } : {}), - ...(displayMessage && redactText(displayMessage) !== displayMessage - ? { claudeUpdatedMessage: redactText(displayMessage) } - : {}), }, }; } @@ -1544,10 +1553,15 @@ function projectClaudeBatchForInspection( if (!isPlainRecord(call)) return call; const toolName = firstString(call.tool_name, call.toolName, call.name); const toolInput = pickToolInput(call); + const command = pickNativeToolAuditInput('shell', toolInput); if (mapToolToRuntimeAction(toolName, call) !== 'shell' - || !isStandaloneAgentGuardCliCommand(pickNativeToolAuditInput('shell', toolInput))) return call; + || !isAgentGuardOwnedOutputCommand(command)) return call; const responseKey = Object.prototype.hasOwnProperty.call(call, 'tool_response') ? 'tool_response' : Object.prototype.hasOwnProperty.call(call, 'toolResponse') ? 'toolResponse' : null; + if (isOfficialAgentGuardInstallerCommand(command)) { + const output = responseKey ? call[responseKey] : call.error ?? call.error_message; + if (!hasExactSavedActivationUrl(output, config)) return call; + } if (!responseKey) { const error = firstString(call.error, call.error_message); if (redactText(error) !== error || hasUnmaskedSelfCredential(error) diff --git a/src/runtime/self-command.ts b/src/runtime/self-command.ts index 64bafbe..1919ddc 100644 --- a/src/runtime/self-command.ts +++ b/src/runtime/self-command.ts @@ -24,12 +24,22 @@ export function isAgentGuardCliCommand(command: string): boolean { } /** Accept a single AgentGuard invocation, including simple shell wrappers. - * An explicit executable path must resolve to this package's CLI entrypoint. + * An explicit executable path must resolve to this package's CLI entrypoint; + * npx must name the official scoped package and a supported public command. */ export function isStandaloneAgentGuardCliCommand(command: string): boolean { return isStandaloneAgentGuardCliCommandInner(command, 0); } +/** Recognize the documented installer only for post-tool output sanitization. */ +export function isAgentGuardOwnedOutputCommand(command: string): boolean { + return isStandaloneAgentGuardCliCommand(command) || isOfficialAgentGuardInstallerCommand(command); +} + +export function isOfficialAgentGuardInstallerCommand(command: string): boolean { + return /^curl[ \t]+-fsSL[ \t]+https:\/\/(?:www\.)?agentguard\.one\/install\.sh[ \t]*\|[ \t]*bash$/.test(command.trim()); +} + export function isAgentGuardStatusCommand(command: string): boolean { const trimmed = command.trim(); if (!trimmed || /[\r\n]/.test(trimmed)) return false; @@ -58,6 +68,7 @@ function isAgentGuardCliCommandInner(command: string, depth: number): boolean { if (SUPPORTED_AGENT_COMMANDS.some((command) => matchesCommand(tokens, index, command))) { return true; } + if (isVerifiedAgentGuardNpxInvocation(tokens, index)) return true; const wrappedCommand = shellWrapperCommand(tokens, index); return wrappedCommand ? isAgentGuardCliCommandInner(wrappedCommand, depth + 1) : false; @@ -78,11 +89,23 @@ function isStandaloneAgentGuardCliCommandInner(command: string, depth: number): } while (['command', 'builtin'].includes(basename(tokens[index] || ''))) index += 1; if (isVerifiedAgentGuardExecutable(tokens[index])) return true; + if (isVerifiedAgentGuardNpxInvocation(tokens, index)) return true; const wrappedCommand = shellWrapperCommand(tokens, index); return wrappedCommand ? isStandaloneAgentGuardCliCommandInner(wrappedCommand, depth + 1) : false; } +function isVerifiedAgentGuardNpxInvocation(tokens: string[], start: number): boolean { + if (tokens[start] !== 'npx' + || !tokens.slice(0, start).every((token) => token === 'command' || token === 'builtin')) return false; + let index = start + 1; + if (tokens[index] === '-y' || tokens[index] === '--yes') index += 1; + return /^@goplus\/agentguard(?:@(?:\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?|latest|beta))?$/.test(tokens[index] || '') + && ['init', 'connect', 'disconnect', 'status', 'policy', 'privacy', 'doctor', + 'scan', 'dsh-scan', 'dsh-scan-batch', 'dsh-compare', 'approvals', + 'subscribe', 'checkup'].includes(tokens[index + 1] || ''); +} + function isVerifiedAgentGuardExecutable(executable: string | undefined): boolean { if (executable === 'agentguard') return true; if (!executable || basename(executable) !== 'agentguard' || !isAbsolute(executable)) return false; diff --git a/src/runtime/self-output.ts b/src/runtime/self-output.ts index 34e6fb6..34db9a3 100644 --- a/src/runtime/self-output.ts +++ b/src/runtime/self-output.ts @@ -41,6 +41,11 @@ function redactSelfText(text: string, config: AgentGuardConfig): string { } } catch { /* Plain text still receives the bounded text redactor. */ } } + return redactTextPreservingSavedActivationUrl(text, config); +} + +/** Keep only this installation's pending activation link visible to the user. */ +export function redactTextPreservingSavedActivationUrl(text: string, config: AgentGuardConfig): string { const activationUrl = config.agentJwt && config.agentRegisterUrl; const marker = randomMarker('AGENTGUARDLINK'); const exampleMarker = randomMarker('AGENTGUARDEXAMPLE'); @@ -63,6 +68,30 @@ function redactSelfText(text: string, config: AgentGuardConfig): string { } } +/** Require a complete saved link before treating an installer result as owned output. */ +export function hasExactSavedActivationUrl(value: unknown, config: AgentGuardConfig): boolean { + const activationUrl = config.agentJwt && config.agentRegisterUrl; + if (!activationUrl || activationUrl.length > 2048 || /[\x00-\x1f\x7f]/.test(activationUrl)) return false; + try { + const url = new URL(activationUrl); + if ((url.protocol !== 'https:' && url.protocol !== 'http:') || url.username || url.password) return false; + const seen = new WeakSet(); + let visited = 0; + const contains = (item: unknown, depth: number): boolean => { + if (++visited > 1000 || depth > 16) return false; + if (typeof item === 'string') { + return protectExactActivationUrl(item, activationUrl, '[validated activation link]') !== item; + } + if (!item || typeof item !== 'object' || seen.has(item)) return false; + seen.add(item); + return Object.values(item).some((nested) => contains(nested, depth + 1)); + }; + return contains(value, 0); + } catch { + return false; + } +} + /** Letters cannot accidentally match numeric PII rules during redaction. */ function randomMarker(prefix: string): string { const letters = Array.from(randomBytes(16), (byte) => diff --git a/src/tests/claude-code-lifecycle.test.ts b/src/tests/claude-code-lifecycle.test.ts index d303236..2eb6fc7 100644 --- a/src/tests/claude-code-lifecycle.test.ts +++ b/src/tests/claude-code-lifecycle.test.ts @@ -526,8 +526,8 @@ describe('Claude Code native lifecycle hooks', () => { hook_event_name: 'MessageDisplay', session_id: 'sess-display', cwd: fixture.root, delta: `hello ${secret}`, }); assert.equal(display.result.event.enforcementStatus, 'display_only'); - assert.match(display.output.displayContent, /REDACTED/); - assert.equal(display.output.hookSpecificOutput, undefined); + assert.match(display.output.hookSpecificOutput.displayContent, /REDACTED/); + assert.equal(display.output.hookSpecificOutput.hookEventName, 'MessageDisplay'); assert.doesNotMatch(display.text, new RegExp(secret.replace('.', '\\.'))); for (const event of ['Stop', 'InstructionsLoaded', 'PreCompact', 'PostCompact']) { @@ -544,6 +544,138 @@ describe('Claude Code native lifecycle hooks', () => { assert.doesNotMatch(readFileSync(fixture.config.auditPath, 'utf8'), new RegExp(secret.replace('.', '\\.'))); }); + it('shows the saved activation link in Claude display while masking other credentials', async () => { + const fixture = lifecycleFixture(); + const activationUrl = 'https://agentguard.example/activate?token=pending-display-link-123456'; + fixture.config.agentJwt = 'test.jwt.pending'; + fixture.config.agentRegisterUrl = activationUrl; + const otherUrl = 'https://agentguard.example/activate?token=unrelated-display-link-123456'; + const otherSecret = 'sk-display-secret-1234567890'; + for (const command of ['agentguard connect', 'npx -y @goplus/agentguard@1.2.4-beta.1 connect']) { + const connectCall = { + tool_name: 'Bash', tool_input: { command }, + tool_response: { stdout: `Registered local AgentGuard agent.\nOpen this link to bind this agent to your account:\n${activationUrl}\n`, exit_code: 0 }, + }; + const connectResult = await protectAction({ + config: fixture.config, agentHost: 'claude-code', auditSafe: true, rawInput: { + hook_event_name: 'PostToolUse', session_id: 'sess-connect-link', cwd: fixture.root, + ...connectCall, + }, + }); + assert.notEqual(connectResult?.decision.decision, 'block', command); + const connectBatch = await protectAction({ + config: fixture.config, agentHost: 'claude-code', auditSafe: true, rawInput: { + hook_event_name: 'PostToolBatch', session_id: 'sess-connect-link', cwd: fixture.root, + tool_calls: [connectCall], + }, + }); + assert.notEqual(connectBatch?.decision.decision, 'block', command); + } + + const npxWithOtherSecret = await runHook(fixture.config, { + hook_event_name: 'PostToolUse', session_id: 'sess-npx-secret', cwd: fixture.root, + tool_name: 'Bash', tool_input: { command: 'npx -y @goplus/agentguard@1.2.4-beta.1 connect' }, + tool_response: { stdout: `Open ${activationUrl}\napi_key=${otherSecret}`, exit_code: 0 }, + }); + assert.equal(npxWithOtherSecret.result.decision.decision, 'allow'); + assert.ok(npxWithOtherSecret.output.hookSpecificOutput.updatedToolOutput.stdout.includes(activationUrl)); + assert.doesNotMatch(npxWithOtherSecret.text, /display-secret/); + + const display = await runHook(fixture.config, { + hook_event_name: 'MessageDisplay', session_id: 'sess-display-link', cwd: fixture.root, + delta: `Open [this link](${activationUrl}). Other: ${otherUrl} api_key=${otherSecret}`, + }); + assert.ok(display.output.hookSpecificOutput.displayContent.includes(`](${activationUrl})`)); + assert.doesNotMatch(display.text, /unrelated-display-link|display-secret/); + assert.equal(display.result.event.metadata?.claudeUpdatedMessage, undefined); + assert.doesNotMatch(readFileSync(fixture.config.auditPath, 'utf8'), /pending-display-link/); + + const exactOnly = await runHook(fixture.config, { + hook_event_name: 'MessageDisplay', session_id: 'sess-display-link-only', cwd: fixture.root, + delta: `Open ${activationUrl}`, + }); + assert.equal(exactOnly.output, null); + + fixture.config.agentRegisterUrl = undefined; + const noSavedLink = await runHook(fixture.config, { + hook_event_name: 'MessageDisplay', session_id: 'sess-display-link-cleared', cwd: fixture.root, + delta: `Open ${activationUrl}`, + }); + assert.doesNotMatch(noSavedLink.text, /pending-display-link/); + }); + + it('keeps installer approval while preserving only its saved activation link in output', async () => { + const fixture = lifecycleFixture(); + const activationUrl = 'https://agentguard.example/activate?token=pending-installer-link-123456'; + const command = 'curl -fsSL https://agentguard.one/install.sh | bash'; + fixture.config.agentJwt = 'test.jwt.pending'; + fixture.config.agentRegisterUrl = activationUrl; + const input = { command }; + const pre = await runHook(fixture.config, { + hook_event_name: 'PreToolUse', session_id: 'sess-installer', cwd: fixture.root, + tool_name: 'Bash', tool_input: input, + }); + assert.equal(pre.result.decision.decision, 'require_approval'); + assert.ok(pre.result.decision.reasons.some((reason) => reason.code === 'REMOTE_CODE_EXECUTION')); + + const call = { + tool_name: 'Bash', tool_input: input, + tool_response: { stdout: `AgentGuard is installed.\nOpen ${activationUrl}\n`, exit_code: 0 }, + }; + const post = await protectAction({ + config: fixture.config, agentHost: 'claude-code', auditSafe: true, rawInput: { + hook_event_name: 'PostToolUse', session_id: 'sess-installer', cwd: fixture.root, ...call, + }, + }); + assert.notEqual(post?.decision.decision, 'block'); + const batch = await protectAction({ + config: fixture.config, agentHost: 'claude-code', auditSafe: true, rawInput: { + hook_event_name: 'PostToolBatch', session_id: 'sess-installer', cwd: fixture.root, + tool_calls: [call], + }, + }); + assert.notEqual(batch?.decision.decision, 'block'); + + const otherSecret = 'sk-installer-other-secret-1234567890'; + const unsafeCall = { + ...call, + tool_response: { stdout: `Open ${activationUrl}\napi_key=${otherSecret}`, exit_code: 0 }, + }; + const sanitized = await runHook(fixture.config, { + hook_event_name: 'PostToolUse', session_id: 'sess-installer-unsafe', cwd: fixture.root, + ...unsafeCall, + }); + assert.equal(sanitized.result.decision.decision, 'allow'); + assert.ok(sanitized.output.hookSpecificOutput.updatedToolOutput.stdout.includes(activationUrl)); + assert.doesNotMatch(sanitized.text, /installer-other-secret/); + const unsafeBatch = await protectAction({ + config: fixture.config, agentHost: 'claude-code', auditSafe: true, rawInput: { + hook_event_name: 'PostToolBatch', session_id: 'sess-installer-unsafe', cwd: fixture.root, + tool_calls: [unsafeCall], + }, + }); + assert.equal(unsafeBatch?.decision.decision, 'block'); + + const unrelatedCall = { + ...call, + tool_response: { stdout: `api_key=${otherSecret}`, exit_code: 0 }, + }; + const unrelatedPost = await protectAction({ + config: fixture.config, agentHost: 'claude-code', auditSafe: true, rawInput: { + hook_event_name: 'PostToolUse', session_id: 'sess-installer-unrelated', cwd: fixture.root, + ...unrelatedCall, + }, + }); + assert.equal(unrelatedPost?.decision.decision, 'block'); + const unrelatedBatch = await protectAction({ + config: fixture.config, agentHost: 'claude-code', auditSafe: true, rawInput: { + hook_event_name: 'PostToolBatch', session_id: 'sess-installer-unrelated', cwd: fixture.root, + tool_calls: [unrelatedCall], + }, + }); + assert.equal(unrelatedBatch?.decision.decision, 'block'); + }); + it('blocks dangerous ConfigChange content without rolling the file back', async () => { const fixture = lifecycleFixture(); const cases = [ @@ -678,7 +810,7 @@ describe('Claude Code native lifecycle hooks', () => { env, }); assert.equal(display.status, 0); - assert.match(JSON.parse(display.stdout).displayContent, /REDACTED/); + assert.match(JSON.parse(display.stdout).hookSpecificOutput.displayContent, /REDACTED/); assert.doesNotMatch(`${display.stdout}\n${display.stderr}`, new RegExp(secret)); const invalidBlocking = spawnSync('/bin/sh', ['-c', command], { diff --git a/src/tests/codex-hooks.test.ts b/src/tests/codex-hooks.test.ts index 6e01078..450f3cb 100644 --- a/src/tests/codex-hooks.test.ts +++ b/src/tests/codex-hooks.test.ts @@ -5,7 +5,7 @@ import { dirname, join } from 'node:path'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; import { installAgentTemplates } from '../installers.js'; -import { isStandaloneAgentGuardCliCommand } from '../runtime/self-command.js'; +import { isAgentGuardOwnedOutputCommand, isStandaloneAgentGuardCliCommand } from '../runtime/self-command.js'; interface HookRun { status: number | null; @@ -24,6 +24,30 @@ describe('installed Codex native hooks', () => { assert.equal(isStandaloneAgentGuardCliCommand(`${installed} status; echo extra`), false); }); + it('recognizes only direct official npx CLI commands', () => { + assert.equal(isStandaloneAgentGuardCliCommand('npx -y @goplus/agentguard@1.2.4-beta.1 connect'), true); + assert.equal(isStandaloneAgentGuardCliCommand('npx @goplus/agentguard status'), true); + assert.equal(isStandaloneAgentGuardCliCommand('npx --yes @goplus/agentguard@latest subscribe'), true); + assert.equal(isStandaloneAgentGuardCliCommand('npx -y agentguard connect'), false); + assert.equal(isStandaloneAgentGuardCliCommand('npx -p @goplus/agentguard -c agentguard connect'), false); + assert.equal(isStandaloneAgentGuardCliCommand('npm_config_registry=https://evil.example npx @goplus/agentguard connect'), false); + assert.equal(isStandaloneAgentGuardCliCommand('NODE_OPTIONS=--require=./other.js npx @goplus/agentguard connect'), false); + assert.equal(isStandaloneAgentGuardCliCommand('/tmp/npx @goplus/agentguard connect'), false); + assert.equal(isStandaloneAgentGuardCliCommand('npx @goplus/agentguard@1.2.4-beta.1 scan'), true); + assert.equal(isStandaloneAgentGuardCliCommand('npx @goplus/agentguard doctor'), true); + assert.equal(isStandaloneAgentGuardCliCommand('npx -y @goplus/agentguard init --yes'), true); + assert.equal(isStandaloneAgentGuardCliCommand('npx @goplus/agentguard approve --action-id act_local_1'), false); + assert.equal(isStandaloneAgentGuardCliCommand('npx @goplus/agentguard@1.2.4-beta.1 connect; echo extra'), false); + }); + + it('recognizes only the documented installer pipeline for output sanitization', () => { + assert.equal(isAgentGuardOwnedOutputCommand('curl -fsSL https://agentguard.one/install.sh | bash'), true); + assert.equal(isAgentGuardOwnedOutputCommand('curl -fsSL https://www.agentguard.one/install.sh | bash'), true); + assert.equal(isAgentGuardOwnedOutputCommand('curl -fsSL https://agentguard.one/install.sh | bash; echo extra'), false); + assert.equal(isAgentGuardOwnedOutputCommand('curl -fsSL https://other.example/install.sh | bash'), false); + assert.equal(isAgentGuardOwnedOutputCommand('curl -fsSL http://agentguard.one/install.sh | bash'), false); + }); + it('blocks sensitive user prompts before the model request without echoing secrets', () => { const fixture = installFixture(); const sensitivePrompts = [ @@ -178,6 +202,7 @@ describe('installed Codex native hooks', () => { const fixture = installFixture(); const commands = [ 'agentguard approve --action-id act_local_1 --once', + 'npx -y @goplus/agentguard@1.2.4-beta.1 approve --action-id act_local_1 --once', 'a=agentguard; "$a" approve --action-id act_local_1 --once', "sh -c 'agentguard approve --action-id act_local_1 --once'", 'agent\\guard approve --action-id act_local_1 --once', @@ -203,6 +228,7 @@ describe('installed Codex native hooks', () => { } for (const command of ['agentguard status', "sh -c 'agentguard status'", + 'npx -y @goplus/agentguard@1.2.4-beta.1 connect', 'agentguard scan ./approve', 'agentguard scan ./approve.json']) { const run = fixture.run('agentguard-pre-tool.sh', { hook_event_name: 'PreToolUse', From e8a184e91db7464ed7eb2bc530eb4eb21fae8b63 Mon Sep 17 00:00:00 2001 From: Mr-Lucky Date: Sat, 17 Oct 2026 13:16:28 +0800 Subject: [PATCH 2/2] version patch --- mcpb/manifest.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mcpb/manifest.json b/mcpb/manifest.json index 8201816..0866155 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.2.4-beta.1", + "version": "1.2.4-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/package-lock.json b/package-lock.json index 856e1c1..3200942 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@goplus/agentguard", - "version": "1.2.4-beta.1", + "version": "1.2.4-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@goplus/agentguard", - "version": "1.2.4-beta.1", + "version": "1.2.4-beta.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ed97d4f..67cdad2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@goplus/agentguard", - "version": "1.2.4-beta.1", + "version": "1.2.4-beta.2", "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",