@elmoxbt/agentskillguard 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elmo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # AgentSkillGuard
2
+
3
+ **A static security scanner for AI agent tools before they get anywhere near a Solana wallet.**
4
+
5
+ Agents increasingly install tools (MCP servers, plugins, "skills") from third parties. Every wallet-security effort so far has assumed the *agent* is the thing to secure. AgentSkillGuard's thesis: **the tool supply chain is the attack surface.** A perfectly secure agent handing a perfectly signed transaction to a malicious swap-tool is still a drained wallet.
6
+
7
+ Think `npm audit` + ClamAV, scoped to the specific things an agent tool can do to a Solana wallet.
8
+
9
+ ```
10
+ skillguard scan ./my-agent-tool --policy policies/example-swap-agent.yaml
11
+ ```
12
+
13
+ ```
14
+ AGENTSKILLGUARD
15
+ target: /home/user/my-agent-tool
16
+ files scanned: 3
17
+
18
+ Capabilities detected:
19
+ [x] CRITICAL wallet.private_key_access — Tool references private key / secret / mnemonic material directly.
20
+ index.js:12 const secretKey = process.env.WALLET_PRIVATE_KEY;
21
+ [x] CRITICAL system.shell_exec — Tool can execute OS shell commands via child_process.
22
+ index.js:34 exec('curl -s ' + relayHost + '/beacon');
23
+ [x] CRITICAL code.dynamic_load — Tool loads or executes code dynamically at runtime.
24
+ index.js:30 eval(Buffer.from(patch, 'base64').toString());
25
+ ...
26
+
27
+ Policy violations:
28
+ [!] CRITICAL wallet.private_key_access is never permitted, regardless of policy
29
+ [!] CRITICAL system.shell_exec is never permitted, regardless of policy
30
+ [!] CRITICAL code.dynamic_load is never permitted, regardless of policy
31
+
32
+ Recommended action: BLOCK
33
+ 3 critical violation(s): wallet.private_key_access, system.shell_exec, code.dynamic_load.
34
+ ```
35
+
36
+ ---
37
+
38
+ ## How it works
39
+
40
+ ```
41
+ ┌────────────────────┐
42
+ tool code → │ static analyzer │ → findings (capability + severity + line)
43
+ │ (rule engine) │
44
+ └─────────┬──────────┘
45
+ │
46
+ ┌──────────▼──────────┐
47
+ policy.yml → │ policy enforcer │ → violations (findings checked against
48
+ │ (capability manifest│ the manifest + a hard floor of
49
+ │ + hard floor) │ never-allowed capabilities)
50
+ └─────────┬──────────┘
51
+ │
52
+ ┌──────────▼──────────┐
53
+ │ verdict engine │ → ALLOW / WARN / BLOCK
54
+ └──────────────────────┘
55
+ ```
56
+
57
+ 1. **Static analyzer** (`src/scanner/staticAnalyzer.ts`) walks every `.js`/`.ts` file in the target and runs a rule set (`src/rules/rules.ts`) against it line-by-line, tagging matches with a **capability** (e.g. `wallet.signing`, `system.shell_exec`) and severity.
58
+ 2. **Policy enforcer** (`src/policy/enforcer.ts`) loads a YAML **capability manifest** and checks detected capabilities against it. A small set of capabilities (private-key access, shell exec, dynamic code loading, obfuscated execution, unlimited approvals) are an **always-blocked floor** — no policy can grant them.
59
+ 3. **Verdict engine** (`src/report/verdict.ts`) turns violations into a single recommendation: `BLOCK` (critical violation), `WARN` (needs manual review), or `ALLOW`.
60
+ 4. Every scan is optionally persisted to a local SQLite database (`~/.agentskillguard/scans.db`) so you can build a `skillguard history` audit trail across every tool you've ever vetted.
61
+
62
+ ### Detected capabilities
63
+
64
+ | Capability | Checks for | Severity |
65
+ |---|---|---|
66
+ | `wallet.signing` | Can the tool sign transactions/messages? | CRITICAL |
67
+ | `wallet.private_key_access` | Does it touch raw private keys / mnemonics? | CRITICAL |
68
+ | `network.http` | Does it make outbound HTTP requests? | MEDIUM |
69
+ | `network.dynamic_url` | Are request URLs built dynamically (unverifiable)? | HIGH |
70
+ | `solana.tx_modification` | Does it mutate transaction instructions/destinations? | HIGH |
71
+ | `system.shell_exec` | Can it run OS shell commands? | CRITICAL |
72
+ | `code.dynamic_load` | Does it `eval`/`new Function`/dynamic `require`? | CRITICAL |
73
+ | `solana.unlimited_approval` | Does it request unlimited token approvals? | HIGH |
74
+ | `system.env_read` | Does it read `process.env`? | MEDIUM |
75
+ | `code.obfuscation` | Does it decode+execute base64/hex payloads? | CRITICAL |
76
+ | `system.filesystem_write` | Does it write/delete files? | MEDIUM |
77
+
78
+ This maps directly to the "tool permissions" checklist a human reviewer would ask about — the scanner just does it automatically, on every install.
79
+
80
+ ## The capability manifest
81
+
82
+ ```yaml
83
+ name: swap-agent
84
+ description: "Allows a swap tool to route trades through Jupiter and call its price API."
85
+
86
+ permissions:
87
+ solana:
88
+ programs:
89
+ - Jupiter
90
+ max_sol: 0.5
91
+ tokens:
92
+ - USDC
93
+
94
+ network:
95
+ domains:
96
+ - api.jup.ag
97
+ - quote-api.jup.ag
98
+
99
+ wallet:
100
+ signing: true
101
+ max_transactions_per_hour: 10
102
+ ```
103
+
104
+ The scanner statically enforces `wallet.signing`, `network.domains`, and `solana.programs`. `max_sol` and `max_transactions_per_hour` describe the intended **runtime** ceiling — static analysis can prove a tool is *capable* of signing, but it can't prove what amount it will sign at runtime. See Roadmap below.
105
+
106
+ ## Install & run
107
+
108
+ ```bash
109
+ npm install
110
+ npm run build
111
+
112
+ # scan the bundled "malicious" fixture — should BLOCK
113
+ npm run scan:demo:malicious
114
+
115
+ # scan the bundled "safe" fixture — should ALLOW
116
+ npm run scan:demo:safe
117
+
118
+ # scan any tool on your machine
119
+ node dist/cli.js scan ./path/to/tool --policy policies/example-swap-agent.yaml
120
+
121
+ # scan without a policy — only the hard-blocked floor applies
122
+ node dist/cli.js scan ./path/to/tool
123
+
124
+ # machine-readable output (for CI / pre-install hooks)
125
+ node dist/cli.js scan ./path/to/tool --json
126
+
127
+ # generate a starter deny-by-default policy
128
+ node dist/cli.js init-policy -o my-tool.policy.yaml
129
+
130
+ # see everything you've scanned so far
131
+ node dist/cli.js history
132
+ ```
133
+
134
+ Exit codes are CI-friendly: `0` = ALLOW, `1` = WARN, `2` = BLOCK — so you can wire `skillguard scan` into a pre-install hook and have it actually stop an install.
135
+
136
+ Run the smoke tests directly with `npm test` (no test framework, just assertions — see `test/scanner.test.ts`).
137
+
138
+ ## Project structure
139
+
140
+ ```
141
+ agentskillguard/
142
+ ├── src/
143
+ │ ├── cli.ts # commander-based CLI entry point
144
+ │ ├── types.ts # shared types (Finding, Violation, ScanResult...)
145
+ │ ├── rules/rules.ts # the rule set — add new detection rules here
146
+ │ ├── scanner/
147
+ │ │ ├── index.ts # orchestrates a scan end-to-end
148
+ │ │ ├── staticAnalyzer.ts # line-based rule matching + URL extraction
149
+ │ │ └── mcpMetadata.ts # reads package.json / mcp.json for tool identity
150
+ │ ├── policy/
151
+ │ │ ├── schema.ts # zod schema for the YAML capability manifest
152
+ │ │ ├── loader.ts # loads + validates a policy file
153
+ │ │ └── enforcer.ts # findings + policy -> violations
154
+ │ ├── report/
155
+ │ │ ├── verdict.ts # violations -> ALLOW / WARN / BLOCK
156
+ │ │ └── formatter.ts # colored CLI report
157
+ │ ├── db/store.ts # SQLite scan history (better-sqlite3)
158
+ │ └── utils/fileWalker.ts # recursive source-file discovery
159
+ ├── policies/example-swap-agent.yaml
160
+ ├── examples/
161
+ │ ├── malicious-tool/ # fixture that trips nearly every rule (inert — do not run)
162
+ │ └── safe-tool/ # fixture that scans clean
163
+ └── test/scanner.test.ts # smoke test asserting both fixtures verdict correctly
164
+ ```
165
+
166
+ ## Scope & honesty about limitations
167
+
168
+ This is a portfolio-grade MVP, not a production security product. Specifically:
169
+
170
+ - **Detection is regex/line-based, not a real AST.** It's fast and dependency-light, but it can be evaded by anyone who tries (multi-line obfuscation, string-splitting `"ev" + "al"`, etc). The natural upgrade path is a Tree-sitter-based AST pass (`tree-sitter-javascript`/`tree-sitter-typescript`) doing call-graph and data-flow analysis instead of pattern matching — the rule *interface* (`Rule.pattern` → `Rule.check(ast)`) is designed so that swap is additive, not a rewrite.
171
+ - **Static analysis can't enforce runtime limits.** `max_sol` and `max_transactions_per_hour` in the manifest are real intents but need a runtime wrapper (a proxy the agent calls through, which meters actual transaction amounts/frequency against the manifest) to actually enforce — the scanner can only flag that a tool *is capable* of exceeding them if it doesn't hard-code such limits itself.
172
+ - **No sandboxed dynamic analysis yet.** A tool that decrypts its payload only at runtime (fetched from a CDN post-install, key derived from an unpredictable seed) won't be caught by source scanning at all. That needs an actual sandboxed execution pass — out of scope for the MVP.
173
+
174
+ ## Roadmap
175
+
176
+ - [ ] Tree-sitter AST engine (real call-graph analysis instead of regex)
177
+ - [ ] Runtime enforcement proxy: wraps `@solana/web3.js` calls so `max_sol` / `max_transactions_per_hour` are enforced live, not just requested
178
+ - [ ] MCP-native scanning: intercept tool registration at the protocol level and scan before an agent is ever allowed to call it
179
+ - [ ] Signature/hash database of previously-scanned malicious tools (the "ClamAV" half of the pitch)
180
+ - [ ] `skillguard watch` — scan on every `npm install` in an agent's tool directory
package/dist/cli.js ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const commander_1 = require("commander");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const scanner_1 = require("./scanner");
45
+ const loader_1 = require("./policy/loader");
46
+ const formatter_1 = require("./report/formatter");
47
+ const store_1 = require("./db/store");
48
+ const program = new commander_1.Command();
49
+ program
50
+ .name('skillguard')
51
+ .description('AgentSkillGuard — static security scanner for AI agent tools that touch a Solana wallet.')
52
+ .version('0.1.0');
53
+ program
54
+ .command('scan <target>')
55
+ .description('Scan a tool directory or file for dangerous capabilities')
56
+ .option('-p, --policy <file>', 'Path to a YAML capability manifest to enforce')
57
+ .option('--json', 'Print machine-readable JSON instead of the formatted report')
58
+ .option('--no-save', 'Do not persist this scan to the local history database')
59
+ .action((target, opts) => {
60
+ const resolvedTarget = path.resolve(target);
61
+ if (!fs.existsSync(resolvedTarget)) {
62
+ console.error(chalk_1.default.red(`Target not found: ${resolvedTarget}`));
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const policy = opts.policy ? (0, loader_1.loadPolicy)(path.resolve(opts.policy)) : null;
67
+ const result = (0, scanner_1.scanTarget)(resolvedTarget, policy);
68
+ if (opts.save) {
69
+ try {
70
+ (0, store_1.saveScan)(result);
71
+ }
72
+ catch (err) {
73
+ console.error(chalk_1.default.yellow(`Warning: could not save scan history (${err.message})`));
74
+ }
75
+ }
76
+ if (opts.json) {
77
+ console.log(JSON.stringify(result, null, 2));
78
+ }
79
+ else {
80
+ (0, formatter_1.printReport)(result);
81
+ }
82
+ if (result.verdict === 'BLOCK') {
83
+ process.exitCode = 2;
84
+ }
85
+ else if (result.verdict === 'WARN') {
86
+ process.exitCode = 1;
87
+ }
88
+ });
89
+ program
90
+ .command('init-policy')
91
+ .description('Write a starter deny-by-default policy manifest')
92
+ .option('-o, --out <file>', 'Output path', 'skillguard.policy.yaml')
93
+ .action((opts) => {
94
+ const outPath = path.resolve(opts.out);
95
+ if (fs.existsSync(outPath)) {
96
+ console.error(chalk_1.default.red(`Refusing to overwrite existing file: ${outPath}`));
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ fs.writeFileSync(outPath, loader_1.DEFAULT_POLICY_YAML, 'utf-8');
101
+ console.log(chalk_1.default.green(`Wrote starter policy to ${outPath}`));
102
+ });
103
+ program
104
+ .command('history')
105
+ .description('Show recent scans from local history')
106
+ .option('-n, --limit <n>', 'Number of rows to show', '20')
107
+ .action((opts) => {
108
+ const rows = (0, store_1.listScans)(parseInt(opts.limit, 10));
109
+ if (rows.length === 0) {
110
+ console.log(chalk_1.default.dim('No scans recorded yet. Run `skillguard scan <target>` first.'));
111
+ return;
112
+ }
113
+ for (const r of rows) {
114
+ const label = String(r.verdict).padEnd(6);
115
+ const colored = r.verdict === 'BLOCK' ? chalk_1.default.red(label) : r.verdict === 'WARN' ? chalk_1.default.yellow(label) : chalk_1.default.green(label);
116
+ console.log(`#${r.id} ${r.scanned_at} ${colored} ${r.tool_name ?? '(unnamed)'} ${r.target} ` +
117
+ chalk_1.default.dim(`(${r.findings_count} findings, ${r.violations_count} violations)`));
118
+ }
119
+ });
120
+ program.parse(process.argv);
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.saveScan = saveScan;
40
+ exports.listScans = listScans;
41
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
42
+ const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ const DB_DIR = path.join(os.homedir(), '.agentskillguard');
46
+ const DB_PATH = path.join(DB_DIR, 'scans.db');
47
+ function getDb() {
48
+ if (!fs.existsSync(DB_DIR))
49
+ fs.mkdirSync(DB_DIR, { recursive: true });
50
+ const db = new better_sqlite3_1.default(DB_PATH);
51
+ db.exec(`
52
+ CREATE TABLE IF NOT EXISTS scans (
53
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
54
+ target TEXT NOT NULL,
55
+ tool_name TEXT,
56
+ verdict TEXT NOT NULL,
57
+ verdict_reason TEXT,
58
+ findings_count INTEGER,
59
+ violations_count INTEGER,
60
+ result_json TEXT NOT NULL,
61
+ scanned_at TEXT NOT NULL
62
+ );
63
+ `);
64
+ return db;
65
+ }
66
+ function saveScan(result) {
67
+ const db = getDb();
68
+ try {
69
+ const stmt = db.prepare(`
70
+ INSERT INTO scans (target, tool_name, verdict, verdict_reason, findings_count, violations_count, result_json, scanned_at)
71
+ VALUES (@target, @tool_name, @verdict, @verdict_reason, @findings_count, @violations_count, @result_json, @scanned_at)
72
+ `);
73
+ const info = stmt.run({
74
+ target: result.target,
75
+ tool_name: result.metadata?.name ?? null,
76
+ verdict: result.verdict,
77
+ verdict_reason: result.verdictReason,
78
+ findings_count: result.findings.length,
79
+ violations_count: result.violations.length,
80
+ result_json: JSON.stringify(result),
81
+ scanned_at: result.scannedAt,
82
+ });
83
+ return info.lastInsertRowid;
84
+ }
85
+ finally {
86
+ db.close();
87
+ }
88
+ }
89
+ function listScans(limit = 20) {
90
+ const db = getDb();
91
+ try {
92
+ return db
93
+ .prepare(`SELECT id, target, tool_name, verdict, findings_count, violations_count, scanned_at FROM scans ORDER BY id DESC LIMIT ?`)
94
+ .all(limit);
95
+ }
96
+ finally {
97
+ db.close();
98
+ }
99
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.evaluatePolicy = evaluatePolicy;
37
+ const fs = __importStar(require("fs"));
38
+ const staticAnalyzer_1 = require("../scanner/staticAnalyzer");
39
+ /**
40
+ * Capabilities that are never acceptable for an agent tool, regardless of
41
+ * what a policy manifest grants. A policy can only narrow permissions
42
+ * below this floor — it can never widen past it.
43
+ */
44
+ const ALWAYS_BLOCKED_CAPABILITIES = new Set([
45
+ 'wallet.private_key_access',
46
+ 'system.shell_exec',
47
+ 'code.dynamic_load',
48
+ 'code.obfuscation',
49
+ 'solana.unlimited_approval',
50
+ ]);
51
+ function evaluatePolicy(findings, policy, filesScanned) {
52
+ const violations = [];
53
+ const capabilitiesSeen = new Set(findings.map((f) => f.capability));
54
+ // 1. Hard floor — applies even with no policy supplied at all.
55
+ for (const cap of capabilitiesSeen) {
56
+ if (ALWAYS_BLOCKED_CAPABILITIES.has(cap)) {
57
+ const f = findings.find((x) => x.capability === cap);
58
+ violations.push({
59
+ capability: cap,
60
+ severity: 'CRITICAL',
61
+ message: `${cap} is never permitted, regardless of policy (${f.description})`,
62
+ });
63
+ }
64
+ }
65
+ if (!policy) {
66
+ return violations;
67
+ }
68
+ // 2. Wallet signing must be explicitly granted.
69
+ if (capabilitiesSeen.has('wallet.signing') && !policy.permissions.wallet.signing) {
70
+ violations.push({
71
+ capability: 'wallet.signing',
72
+ severity: 'CRITICAL',
73
+ message: 'Tool signs transactions but policy.permissions.wallet.signing is false.',
74
+ });
75
+ }
76
+ // 3. Dynamic URLs can never be verified against an allowlist.
77
+ if (capabilitiesSeen.has('network.dynamic_url')) {
78
+ violations.push({
79
+ capability: 'network.dynamic_url',
80
+ severity: 'HIGH',
81
+ message: 'Tool builds request URLs dynamically — the target domain cannot be statically verified against the allowlist.',
82
+ });
83
+ }
84
+ // 4. Any literal domain contacted must be in the allowlist.
85
+ if (capabilitiesSeen.has('network.http')) {
86
+ const allowlist = new Set(policy.permissions.network.domains);
87
+ const seenDomains = new Set();
88
+ for (const file of filesScanned) {
89
+ const content = fs.readFileSync(file, 'utf-8');
90
+ (0, staticAnalyzer_1.extractDomains)((0, staticAnalyzer_1.extractLiteralUrls)(content)).forEach((d) => seenDomains.add(d));
91
+ }
92
+ for (const domain of seenDomains) {
93
+ if (!allowlist.has(domain)) {
94
+ violations.push({
95
+ capability: 'network.http',
96
+ severity: 'HIGH',
97
+ message: `Tool contacts domain "${domain}", which is not in the policy's network.domains allowlist.`,
98
+ });
99
+ }
100
+ }
101
+ }
102
+ // 5. Transaction manipulation requires at least one granted Solana program.
103
+ if (capabilitiesSeen.has('solana.tx_modification') && policy.permissions.solana.programs.length === 0) {
104
+ violations.push({
105
+ capability: 'solana.tx_modification',
106
+ severity: 'HIGH',
107
+ message: 'Tool modifies transaction instructions but the policy grants no Solana program permissions.',
108
+ });
109
+ }
110
+ return violations;
111
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_POLICY_YAML = void 0;
37
+ exports.loadPolicy = loadPolicy;
38
+ const fs = __importStar(require("fs"));
39
+ const yaml = __importStar(require("js-yaml"));
40
+ const schema_1 = require("./schema");
41
+ function loadPolicy(policyPath) {
42
+ const raw = fs.readFileSync(policyPath, 'utf-8');
43
+ const parsed = yaml.load(raw);
44
+ const result = schema_1.PolicySchema.safeParse(parsed);
45
+ if (!result.success) {
46
+ const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
47
+ throw new Error(`Invalid policy manifest (${policyPath}): ${issues}`);
48
+ }
49
+ return result.data;
50
+ }
51
+ exports.DEFAULT_POLICY_YAML = `name: default-deny
52
+ description: "Starter policy — deny everything until explicitly granted."
53
+
54
+ permissions:
55
+ solana:
56
+ programs: []
57
+ max_sol: 0
58
+ tokens: []
59
+
60
+ network:
61
+ domains: []
62
+
63
+ wallet:
64
+ signing: false
65
+ max_transactions_per_hour: 0
66
+ `;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PolicySchema = void 0;
4
+ const zod_1 = require("zod");
5
+ /**
6
+ * The Solana-specific capability manifest schema.
7
+ *
8
+ * This is intentionally narrow for the MVP: it describes the *ceiling* of
9
+ * what a tool is allowed to do. The static scanner's findings are checked
10
+ * against it in src/policy/enforcer.ts. Fields like max_sol and
11
+ * max_transactions_per_hour are captured here and are enforced at *runtime*
12
+ * by a wrapping guard (see README "Roadmap") — they cannot be verified by
13
+ * static analysis alone.
14
+ */
15
+ exports.PolicySchema = zod_1.z.object({
16
+ name: zod_1.z.string(),
17
+ description: zod_1.z.string().optional(),
18
+ permissions: zod_1.z.object({
19
+ solana: zod_1.z
20
+ .object({
21
+ programs: zod_1.z.array(zod_1.z.string()).default([]),
22
+ max_sol: zod_1.z.number().default(0),
23
+ tokens: zod_1.z.array(zod_1.z.string()).default([]),
24
+ })
25
+ .default({ programs: [], max_sol: 0, tokens: [] }),
26
+ network: zod_1.z
27
+ .object({
28
+ domains: zod_1.z.array(zod_1.z.string()).default([]),
29
+ })
30
+ .default({ domains: [] }),
31
+ wallet: zod_1.z
32
+ .object({
33
+ signing: zod_1.z.boolean().default(false),
34
+ max_transactions_per_hour: zod_1.z.number().default(0),
35
+ })
36
+ .default({ signing: false, max_transactions_per_hour: 0 }),
37
+ }),
38
+ });