@goplus/agentguard 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GoPlus AgentGuard — SessionStart Auto-Scan Hook
5
+ *
6
+ * Runs on session startup to discover and scan newly installed skills.
7
+ * For each skill in ~/.claude/skills/:
8
+ * 1. Calculate artifact hash
9
+ * 2. Check trust registry — skip if already registered with same hash
10
+ * 3. Run quickScan for new/updated skills
11
+ * 4. Report results to stderr (scan-only, does NOT modify trust registry)
12
+ *
13
+ * OPT-IN: This script only runs when AGENTGUARD_AUTO_SCAN=1.
14
+ * Without this env var, the script exits immediately.
15
+ *
16
+ * To register scanned skills, use: /agentguard trust attest
17
+ *
18
+ * Exits 0 always (informational only, never blocks session startup).
19
+ */
20
+
21
+ import { readdirSync, existsSync, appendFileSync, mkdirSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { homedir } from 'node:os';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Opt-in gate: only run when explicitly enabled
27
+ // ---------------------------------------------------------------------------
28
+
29
+ if (process.env.AGENTGUARD_AUTO_SCAN !== '1') {
30
+ process.exit(0);
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Load AgentGuard engine
35
+ // ---------------------------------------------------------------------------
36
+
37
+ const agentguardPath = join(
38
+ import.meta.url.replace('file://', ''),
39
+ '..', '..', '..', '..', 'dist', 'index.js'
40
+ );
41
+
42
+ let createAgentGuard;
43
+ try {
44
+ const gs = await import(agentguardPath);
45
+ createAgentGuard = gs.createAgentGuard || gs.default;
46
+ } catch {
47
+ try {
48
+ const gs = await import('@goplus/agentguard');
49
+ createAgentGuard = gs.createAgentGuard || gs.default;
50
+ } catch {
51
+ // Can't load engine — exit silently
52
+ process.exit(0);
53
+ }
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Config
58
+ // ---------------------------------------------------------------------------
59
+
60
+ const SKILLS_DIRS = [
61
+ join(homedir(), '.claude', 'skills'),
62
+ join(homedir(), '.openclaw', 'skills'),
63
+ ];
64
+ const AGENTGUARD_DIR = join(homedir(), '.agentguard');
65
+ const AUDIT_PATH = join(AGENTGUARD_DIR, 'audit.jsonl');
66
+
67
+ function ensureDir() {
68
+ if (!existsSync(AGENTGUARD_DIR)) {
69
+ mkdirSync(AGENTGUARD_DIR, { recursive: true });
70
+ }
71
+ }
72
+
73
+ function writeAuditLog(entry) {
74
+ try {
75
+ ensureDir();
76
+ appendFileSync(AUDIT_PATH, JSON.stringify(entry) + '\n');
77
+ } catch {
78
+ // Non-critical
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Discover skills
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Find all skill directories under ~/.claude/skills/ and ~/.openclaw/skills/
88
+ * A skill directory is one that contains a SKILL.md file.
89
+ */
90
+ function discoverSkills() {
91
+ const skills = [];
92
+ for (const skillsDir of SKILLS_DIRS) {
93
+ if (!existsSync(skillsDir)) continue;
94
+ try {
95
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
96
+ for (const entry of entries) {
97
+ if (!entry.isDirectory()) continue;
98
+ const skillDir = join(skillsDir, entry.name);
99
+ const skillMd = join(skillDir, 'SKILL.md');
100
+ if (existsSync(skillMd)) {
101
+ skills.push({ name: entry.name, path: skillDir });
102
+ }
103
+ }
104
+ } catch {
105
+ // Can't read skills dir
106
+ }
107
+ }
108
+ return skills;
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Main — scan-only mode (no trust registry mutations)
113
+ // ---------------------------------------------------------------------------
114
+
115
+ async function main() {
116
+ const skills = discoverSkills();
117
+ if (skills.length === 0) {
118
+ process.exit(0);
119
+ }
120
+
121
+ const { scanner } = createAgentGuard();
122
+
123
+ let scanned = 0;
124
+ const results = [];
125
+
126
+ for (const skill of skills) {
127
+ // Skip self (agentguard)
128
+ if (skill.name === 'agentguard') continue;
129
+
130
+ try {
131
+ const result = await scanner.quickScan(skill.path);
132
+ scanned++;
133
+
134
+ results.push({
135
+ name: skill.name,
136
+ risk_level: result.risk_level,
137
+ risk_tags: result.risk_tags,
138
+ });
139
+
140
+ // Audit log — only record skill name, risk level, and tag names (no code/evidence)
141
+ writeAuditLog({
142
+ timestamp: new Date().toISOString(),
143
+ event: 'auto_scan',
144
+ skill_name: skill.name,
145
+ risk_level: result.risk_level,
146
+ risk_tags: result.risk_tags,
147
+ });
148
+ } catch {
149
+ // Skip skills that fail to scan
150
+ }
151
+ }
152
+
153
+ // Output summary to stderr (shown as status message)
154
+ if (scanned > 0) {
155
+ const lines = results.map(r =>
156
+ ` ${r.name}: ${r.risk_level}${r.risk_tags.length ? ` [${r.risk_tags.join(', ')}]` : ''}`
157
+ );
158
+ process.stderr.write(
159
+ `GoPlus AgentGuard: scanned ${scanned} skill(s)\n${lines.join('\n')}\n` +
160
+ `Use /agentguard trust attest to register trusted skills.\n`
161
+ );
162
+ }
163
+
164
+ process.exit(0);
165
+ }
166
+
167
+ main();
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GoPlus AgentGuard PreToolUse / PostToolUse Hook (Claude Code)
5
+ *
6
+ * Uses the common adapter + engine architecture.
7
+ * Reads Claude Code hook input from stdin, delegates to evaluateHook(),
8
+ * and outputs allow / deny / ask via Claude Code protocol.
9
+ *
10
+ * PreToolUse exit codes:
11
+ * 0 = allow (or JSON with permissionDecision)
12
+ * 2 = deny (stderr = reason shown to Claude)
13
+ *
14
+ * PostToolUse: appends audit log entry (async, always exits 0)
15
+ */
16
+
17
+ import { join } from 'node:path';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Load AgentGuard engine + adapters
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const agentguardPath = join(import.meta.url.replace('file://', ''), '..', '..', '..', '..', 'dist', 'index.js');
24
+
25
+ let createAgentGuard, ClaudeCodeAdapter, evaluateHook, loadConfig;
26
+ try {
27
+ const gs = await import(agentguardPath);
28
+ createAgentGuard = gs.createAgentGuard || gs.default;
29
+ ClaudeCodeAdapter = gs.ClaudeCodeAdapter;
30
+ evaluateHook = gs.evaluateHook;
31
+ loadConfig = gs.loadConfig;
32
+ } catch {
33
+ try {
34
+ const gs = await import('@goplus/agentguard');
35
+ createAgentGuard = gs.createAgentGuard || gs.default;
36
+ ClaudeCodeAdapter = gs.ClaudeCodeAdapter;
37
+ evaluateHook = gs.evaluateHook;
38
+ loadConfig = gs.loadConfig;
39
+ } catch {
40
+ process.stderr.write('GoPlus AgentGuard: unable to load engine, allowing action\n');
41
+ process.exit(0);
42
+ }
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Read stdin
47
+ // ---------------------------------------------------------------------------
48
+
49
+ function readStdin() {
50
+ return new Promise((resolve) => {
51
+ let data = '';
52
+ process.stdin.setEncoding('utf-8');
53
+ process.stdin.on('data', (chunk) => (data += chunk));
54
+ process.stdin.on('end', () => {
55
+ try {
56
+ resolve(JSON.parse(data));
57
+ } catch {
58
+ resolve(null);
59
+ }
60
+ });
61
+ setTimeout(() => resolve(null), 5000);
62
+ });
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Claude Code output helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function outputDeny(reason) {
70
+ process.stderr.write(reason + '\n');
71
+ process.exit(2);
72
+ }
73
+
74
+ function outputAsk(reason) {
75
+ console.log(JSON.stringify({
76
+ hookSpecificOutput: {
77
+ hookEventName: 'PreToolUse',
78
+ permissionDecision: 'ask',
79
+ permissionDecisionReason: reason,
80
+ },
81
+ }));
82
+ process.exit(0);
83
+ }
84
+
85
+ function outputAllow() {
86
+ process.exit(0);
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Main
91
+ // ---------------------------------------------------------------------------
92
+
93
+ async function main() {
94
+ const input = await readStdin();
95
+ if (!input) {
96
+ process.exit(0);
97
+ }
98
+
99
+ const adapter = new ClaudeCodeAdapter();
100
+ const config = loadConfig();
101
+ const agentguard = createAgentGuard();
102
+
103
+ const result = await evaluateHook(adapter, input, { config, agentguard });
104
+
105
+ if (result.decision === 'deny') outputDeny(result.reason || 'Action blocked');
106
+ else if (result.decision === 'ask') outputAsk(result.reason || 'Action requires confirmation');
107
+ else outputAllow();
108
+ }
109
+
110
+ main();
@@ -0,0 +1,7 @@
1
+ {
2
+ "private": true,
3
+ "type": "module",
4
+ "dependencies": {
5
+ "@goplus/agentguard": "file:../../.."
6
+ }
7
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GoPlus AgentGuard Trust CLI — lightweight wrapper for SkillRegistry operations.
5
+ *
6
+ * Usage:
7
+ * node trust-cli.ts lookup --id <id> --source <source> --version <version> --hash <hash>
8
+ * node trust-cli.ts attest --id <id> --source <source> --version <version> --hash <hash> --trust-level <level> [--preset <preset>] [--capabilities <json>] [--reviewed-by <name>] [--notes <text>] [--expires <iso>] [--force]
9
+ * node trust-cli.ts revoke [--source <source>] [--key <record_key>] --reason <reason>
10
+ * node trust-cli.ts list [--trust-level <level>] [--status <status>] [--source-pattern <pattern>]
11
+ * node trust-cli.ts hash --path <dir>
12
+ */
13
+
14
+ import { createAgentGuard, CAPABILITY_PRESETS, SkillScanner } from '@goplus/agentguard';
15
+
16
+ const args = process.argv.slice(2);
17
+ const command = args[0];
18
+
19
+ function getArg(name: string): string | undefined {
20
+ const idx = args.indexOf(`--${name}`);
21
+ if (idx === -1 || idx + 1 >= args.length) return undefined;
22
+ return args[idx + 1];
23
+ }
24
+
25
+ function hasFlag(name: string): boolean {
26
+ return args.includes(`--${name}`);
27
+ }
28
+
29
+ async function main() {
30
+ const registryPath = getArg('registry-path');
31
+ const { registry } = createAgentGuard({ registryPath });
32
+
33
+ switch (command) {
34
+ case 'lookup': {
35
+ const skill = {
36
+ id: getArg('id') || '',
37
+ source: getArg('source') || '',
38
+ version_ref: getArg('version') || '',
39
+ artifact_hash: getArg('hash') || '',
40
+ };
41
+ const record = await registry.lookup(skill);
42
+ console.log(JSON.stringify(record, null, 2));
43
+ break;
44
+ }
45
+
46
+ case 'attest': {
47
+ const skill = {
48
+ id: getArg('id') || '',
49
+ source: getArg('source') || '',
50
+ version_ref: getArg('version') || '',
51
+ artifact_hash: getArg('hash') || '',
52
+ };
53
+ const trustLevel = (getArg('trust-level') || 'restricted') as
54
+ | 'untrusted'
55
+ | 'restricted'
56
+ | 'trusted';
57
+
58
+ let capabilities;
59
+ const preset = getArg('preset');
60
+ if (preset && preset in CAPABILITY_PRESETS) {
61
+ capabilities =
62
+ CAPABILITY_PRESETS[preset as keyof typeof CAPABILITY_PRESETS];
63
+ } else if (getArg('capabilities')) {
64
+ capabilities = JSON.parse(getArg('capabilities')!);
65
+ }
66
+
67
+ const force = hasFlag('force');
68
+ const attestFn = force ? registry.forceAttest : registry.attest;
69
+ const result = await attestFn.call(registry, {
70
+ skill,
71
+ trust_level: trustLevel,
72
+ capabilities,
73
+ review: {
74
+ reviewed_by: getArg('reviewed-by') || 'cli',
75
+ reviewed_at: new Date().toISOString(),
76
+ notes: getArg('notes') || '',
77
+ },
78
+ expires_at: getArg('expires'),
79
+ });
80
+ console.log(JSON.stringify(result, null, 2));
81
+ break;
82
+ }
83
+
84
+ case 'revoke': {
85
+ const source = getArg('source');
86
+ const key = getArg('key');
87
+ const reason = getArg('reason') || 'Revoked via CLI';
88
+ const result = await registry.revoke(
89
+ { source, record_key: key },
90
+ reason
91
+ );
92
+ console.log(JSON.stringify(result, null, 2));
93
+ break;
94
+ }
95
+
96
+ case 'list': {
97
+ const filters: Record<string, string> = {};
98
+ const trustLevel = getArg('trust-level');
99
+ const status = getArg('status');
100
+ const sourcePattern = getArg('source-pattern');
101
+ if (trustLevel) filters.trust_level = trustLevel;
102
+ if (status) filters.status = status;
103
+ if (sourcePattern) filters.source_pattern = sourcePattern;
104
+
105
+ const records = await registry.list(filters);
106
+ console.log(JSON.stringify(records, null, 2));
107
+ break;
108
+ }
109
+
110
+ case 'hash': {
111
+ const dirPath = getArg('path');
112
+ if (!dirPath) {
113
+ console.error('Error: --path is required for hash');
114
+ process.exit(1);
115
+ }
116
+ const scanner = new SkillScanner({ useExternalScanner: false });
117
+ const hash = await scanner.calculateArtifactHash(dirPath);
118
+ console.log(JSON.stringify({ hash }));
119
+ break;
120
+ }
121
+
122
+ default:
123
+ console.error(
124
+ 'Usage: trust-cli.ts <lookup|attest|revoke|list|hash> [options]'
125
+ );
126
+ console.error('Run with --help for details.');
127
+ process.exit(1);
128
+ }
129
+ }
130
+
131
+ main().catch((err) => {
132
+ console.error(JSON.stringify({ error: err.message }));
133
+ process.exit(1);
134
+ });
@@ -0,0 +1,161 @@
1
+ # Web3 Vulnerability Patterns Reference
2
+
3
+ Detailed vulnerability patterns for the `web3` subcommand. Use this when performing Web3/smart contract security audits.
4
+
5
+ ## Solidity Vulnerability Categories
6
+
7
+ ### 1. Reentrancy (HIGH)
8
+
9
+ **Pattern**: External call before state update in the same function.
10
+
11
+ ```solidity
12
+ // VULNERABLE: state updated after external call
13
+ function withdraw(uint amount) public {
14
+ require(balances[msg.sender] >= amount);
15
+ (bool success, ) = msg.sender.call{value: amount}(""); // external call
16
+ balances[msg.sender] -= amount; // state change AFTER call
17
+ }
18
+ ```
19
+
20
+ **Detection**: Look for `.call{`, `.transfer(`, `.send(` followed by storage variable assignment (`var =`, `var +=`, `var -=`) within the same function body.
21
+
22
+ **Fix**: Use Checks-Effects-Interactions pattern or ReentrancyGuard.
23
+
24
+ ### 2. Wallet Draining (CRITICAL)
25
+
26
+ **Patterns**:
27
+ - `approve()` with max uint256 value followed by `transferFrom()`
28
+ - `permit()` with far-future deadline enabling gasless approvals
29
+ - Approval granted to attacker-controlled address
30
+
31
+ **Detection**:
32
+ - `approve(address, type(uint256).max)` or `MaxUint256` or `2**256-1`
33
+ - `transferFrom` in same contract/flow as `approve`
34
+ - `permit(` with `deadline` parameter
35
+
36
+ ### 3. Unlimited Approval (HIGH)
37
+
38
+ **Pattern**: Token approval for maximum amount, giving spender unlimited access.
39
+
40
+ ```solidity
41
+ token.approve(spender, type(uint256).max);
42
+ token.setApprovalForAll(operator, true);
43
+ ```
44
+
45
+ **Risk**: If the spender contract is compromised, all approved tokens can be drained.
46
+
47
+ ### 4. Self-Destruct (HIGH)
48
+
49
+ **Pattern**: `selfdestruct(address)` or deprecated `suicide(address)`.
50
+
51
+ **Risk**: Permanently destroys the contract, sending remaining ETH to the specified address. Can be exploited if access control is weak.
52
+
53
+ ### 5. Signature Replay (HIGH)
54
+
55
+ **Pattern**: Using `ecrecover()` without nonce or chain ID protection.
56
+
57
+ ```solidity
58
+ // VULNERABLE: no nonce, signature can be replayed
59
+ function execute(bytes memory signature, uint amount) public {
60
+ bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
61
+ address signer = ecrecover(hash, v, r, s);
62
+ // Missing: nonce check, chain ID, contract address in hash
63
+ }
64
+ ```
65
+
66
+ **Fix**: Include nonce, chainId, and contract address in signed message. Use EIP-712 typed data.
67
+
68
+ ### 6. Flash Loan Attacks (MEDIUM)
69
+
70
+ **Patterns**:
71
+ - `flashLoan()`, `IFlashLoan`, `executeOperation()`
72
+ - AAVE/dYdX/Uniswap flash loan interfaces
73
+
74
+ **Risk**: Attacker borrows large amounts without collateral within a single transaction to manipulate prices, governance, or vulnerable protocols.
75
+
76
+ **Check**: Ensure price oracles use TWAP, governance has timelock, and lending markets have proper collateral checks.
77
+
78
+ ### 7. Proxy Upgrade Risks (MEDIUM)
79
+
80
+ **Patterns**:
81
+ - `upgradeTo(address)`, `upgradeToAndCall(address, bytes)`
82
+ - `_setImplementation()`, `IMPLEMENTATION_SLOT`
83
+ - Transparent proxy / UUPS patterns
84
+
85
+ **Check**:
86
+ - Is upgrade function access-controlled?
87
+ - Is there a timelock on upgrades?
88
+ - Can the proxy be upgraded to a malicious implementation?
89
+
90
+ ### 8. Hidden Transfers (MEDIUM)
91
+
92
+ **Pattern**: ETH or token transfers hidden in functions not named transfer/send.
93
+
94
+ ```solidity
95
+ // Transfer hidden in an innocuous-looking function
96
+ function updateConfig(address _new) public {
97
+ payable(_new).transfer(address(this).balance); // hidden drain
98
+ }
99
+ ```
100
+
101
+ **Detection**: `.transfer(`, `.call{value:}` in functions not named `transfer`, `_transfer`, `send`, `withdraw`.
102
+
103
+ ### 9. Price Oracle Manipulation
104
+
105
+ **Patterns**:
106
+ - Single-block spot price from DEX (e.g., `getReserves()` used directly for pricing)
107
+ - No TWAP (Time-Weighted Average Price) implementation
108
+ - Price feed without staleness check
109
+
110
+ **Check**: Look for Uniswap `getReserves()`, Chainlink `latestRoundData()` without `updatedAt` staleness check.
111
+
112
+ ### 10. Access Control Issues
113
+
114
+ **Patterns**:
115
+ - Public/external functions that modify critical state without access modifiers
116
+ - Missing `onlyOwner`, `onlyRole`, or similar guards
117
+ - `tx.origin` used for authentication (vulnerable to phishing)
118
+
119
+ **Check**:
120
+ - All state-changing functions should have appropriate access control
121
+ - `tx.origin` should never be used for authorization (use `msg.sender`)
122
+ - Owner/admin functions should be behind multisig or timelock
123
+
124
+ ## JavaScript/TypeScript Web3 Patterns
125
+
126
+ ### Private Key Exposure
127
+
128
+ - Hardcoded private keys: `0x` followed by 64 hex characters in quotes
129
+ - `PRIVATE_KEY` in environment variable assignments
130
+ - Private key in config files, .env files committed to repo
131
+
132
+ ### Mnemonic Exposure
133
+
134
+ - BIP-39 seed phrases (12-24 words) in source code
135
+ - `seed_phrase`, `mnemonic`, `recovery_phrase` variable assignments
136
+
137
+ ### Unsafe RPC Usage
138
+
139
+ - Sending signed transactions without user confirmation
140
+ - Using `eth_sendTransaction` with hardcoded parameters
141
+ - Missing gas limit estimation (risk of out-of-gas)
142
+
143
+ ### Front-End Risks
144
+
145
+ - Phishing patterns: UI mimicking wallet approval screens
146
+ - Hidden iframe/overlay for approval harvesting
147
+ - Approval transactions disguised as other operations
148
+
149
+ ## GoPlus Security Checks (Optional)
150
+
151
+ If GoPlus API is available, these additional checks can be performed:
152
+
153
+ | Check | API | Description |
154
+ |-------|-----|-------------|
155
+ | Token Security | `tokenSecurity(chainId, address)` | Honeypot, tax, owner control |
156
+ | Address Security | `addressSecurity(chainId, address)` | Blacklisted, phishing, mixer |
157
+ | Approval Risk | `approvalSecurity(chainId, address)` | Risky approvals |
158
+ | Phishing Site | `phishingSite(url)` | Known phishing URL |
159
+ | Transaction Simulation | `simulateTransaction(...)` | Pre-execution risk analysis |
160
+
161
+ Requires `GOPLUS_API_KEY` and `GOPLUS_API_SECRET` environment variables.