agentseal 0.8.1 → 0.9.1

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,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/guard-models.ts
4
+ var GuardVerdict = {
5
+ SAFE: "safe",
6
+ WARNING: "warning",
7
+ DANGER: "danger",
8
+ ERROR: "error"
9
+ };
10
+ var SEVERITY_ORDER = {
11
+ critical: 0,
12
+ high: 1,
13
+ medium: 2,
14
+ low: 3
15
+ };
16
+ function customFindingFromDict(d) {
17
+ return {
18
+ code: d.code ?? "",
19
+ title: d.title ?? "",
20
+ severity: d.severity ?? "medium",
21
+ verdict: d.verdict ?? "warning",
22
+ remediation: d.remediation ?? "",
23
+ rule_file: d.rule_file ?? "",
24
+ entity_type: d.entity_type ?? "",
25
+ entity_name: d.entity_name ?? ""
26
+ };
27
+ }
28
+ function countVerdict(skills, mcp, runtime, verdict) {
29
+ return skills.filter((s) => s.verdict === verdict).length + mcp.filter((m) => m.verdict === verdict).length + runtime.filter((r) => r.verdict === verdict).length;
30
+ }
31
+ function totalDangers(report) {
32
+ return countVerdict(report.skill_results, report.mcp_results, report.mcp_runtime_results, GuardVerdict.DANGER);
33
+ }
34
+ function totalWarnings(report) {
35
+ return countVerdict(report.skill_results, report.mcp_results, report.mcp_runtime_results, GuardVerdict.WARNING);
36
+ }
37
+ function totalSafe(report) {
38
+ return countVerdict(report.skill_results, report.mcp_results, report.mcp_runtime_results, GuardVerdict.SAFE);
39
+ }
40
+ function guardReportFromDict(d) {
41
+ return {
42
+ timestamp: d.timestamp ?? "",
43
+ duration_seconds: d.duration_seconds ?? 0,
44
+ agents_found: d.agents_found ?? [],
45
+ skill_results: d.skill_results ?? [],
46
+ mcp_results: (d.mcp_results ?? []).map((m) => ({
47
+ ...m,
48
+ registry_score: m.registry?.score ?? m.registry_score,
49
+ registry_level: m.registry?.level ?? m.registry_level,
50
+ registry_findings_count: m.registry?.findings_count ?? m.registry_findings_count
51
+ })),
52
+ mcp_runtime_results: d.mcp_runtime_results ?? [],
53
+ toxic_flows: d.toxic_flows ?? [],
54
+ baseline_changes: d.baseline_changes ?? [],
55
+ llm_tokens_used: d.llm_tokens_used ?? 0,
56
+ unlisted_findings: d.unlisted_findings ?? [],
57
+ custom_findings: (d.custom_findings ?? []).map(customFindingFromDict),
58
+ config_path: d.config_path ?? ""
59
+ };
60
+ }
61
+
62
+ export {
63
+ GuardVerdict,
64
+ SEVERITY_ORDER,
65
+ totalDangers,
66
+ totalWarnings,
67
+ totalSafe,
68
+ guardReportFromDict
69
+ };
@@ -564,6 +564,7 @@ function _scanProjectDir(dir, mcpServers, skillPaths, seenSkillPaths) {
564
564
  var MAX_SKILL_SIZE, PROJECT_MCP_CONFIGS, PROJECT_SKILL_FILES, PROJECT_SKILL_DIRS, SKILL_DIRS, SKILL_FILES;
565
565
  var init_machine_discovery = __esm({
566
566
  "src/machine-discovery.ts"() {
567
+ "use strict";
567
568
  MAX_SKILL_SIZE = 10 * 1024 * 1024;
568
569
  PROJECT_MCP_CONFIGS = [
569
570
  [".mcp.json", "mcpServers", null],
@@ -628,8 +629,6 @@ export {
628
629
  PROJECT_SKILL_DIRS,
629
630
  getWellKnownConfigs,
630
631
  stripJsonComments,
631
- scanMachine,
632
- scanDirectory,
633
632
  machine_discovery_exports,
634
633
  init_machine_discovery
635
634
  };
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ init_machine_discovery,
4
+ machine_discovery_exports
5
+ } from "./chunk-IO5DO7DS.js";
6
+ import {
7
+ __toCommonJS
8
+ } from "./chunk-ZLRN7Q7C.js";
9
+
10
+ // src/project-config.ts
11
+ import { existsSync, readFileSync, writeFileSync, statSync } from "fs";
12
+ import { homedir } from "os";
13
+ import { dirname, join, resolve } from "path";
14
+ import { parse } from "yaml";
15
+ var CONFIG_FILENAME = ".agentseal.yaml";
16
+ function generateConfigYaml(agents, mcpServers) {
17
+ const activeAgents = agents.filter(
18
+ (a) => a.status === "found" || a.status === "installed_no_config"
19
+ );
20
+ const agentTypes = activeAgents.map((a) => a.agent_type);
21
+ const serverNameSet = /* @__PURE__ */ new Set();
22
+ for (const s of mcpServers) {
23
+ serverNameSet.add(s.name);
24
+ }
25
+ const serverNames = Array.from(serverNameSet);
26
+ const agentLines = agentTypes.length > 0 ? agentTypes.map((t) => ` - ${t}`).join("\n") : " # - cursor\n # - claude-desktop";
27
+ const serverLines = serverNames.length > 0 ? serverNames.map((n) => ` - ${n}`).join("\n") : " # - filesystem\n # - sqlite";
28
+ return `# AgentSeal project configuration
29
+ # https://agentseal.org/docs/config
30
+
31
+ # Exit code behavior: "danger" (default), "warning", or "safe"
32
+ fail_on: danger
33
+
34
+ # Agents expected on this machine (unlisted agents trigger GUARD-001)
35
+ allowed_agents:
36
+ ${agentLines}
37
+
38
+ # MCP servers expected (unlisted servers trigger GUARD-002)
39
+ # Use "name" or "name@agent_type" for agent-specific allowlisting
40
+ allowed_mcp_servers:
41
+ ${serverLines}
42
+
43
+ # Paths to ignore during skill scanning (matched by path segment)
44
+ ignore_paths:
45
+ - node_modules
46
+ - .git
47
+ - __pycache__
48
+
49
+ # Findings to ignore (by code, or code:path for file-specific ignores)
50
+ ignore_findings: []
51
+ # - id: "SKILL-001"
52
+ # reason: "Known safe pattern"
53
+ # - id: "MCP-002:./configs/server.json"
54
+ # reason: "Accepted risk for this file"
55
+
56
+ # Additional rule directories
57
+ rules_paths: []
58
+ # - ./rules
59
+ # - ./custom-rules
60
+ `;
61
+ }
62
+ function runGuardInit(opts) {
63
+ const { targetDir, force = false, interactive = true } = opts ?? {};
64
+ const dir = targetDir ?? process.cwd();
65
+ const configFile = join(dir, CONFIG_FILENAME);
66
+ if (existsSync(configFile) && !force) {
67
+ return false;
68
+ }
69
+ let agents = [];
70
+ let allMcpServers = [];
71
+ try {
72
+ const { scanMachine, scanDirectory } = (init_machine_discovery(), __toCommonJS(machine_discovery_exports));
73
+ const machineResult = scanMachine();
74
+ agents = machineResult.agents;
75
+ allMcpServers = [...machineResult.mcpServers];
76
+ const dirResult = scanDirectory(dir);
77
+ const seen = new Set(allMcpServers.map((s) => `${s.name}::${s.agent_type}`));
78
+ for (const srv of dirResult.mcpServers) {
79
+ const key = `${srv.name}::${srv.agent_type}`;
80
+ if (!seen.has(key)) {
81
+ seen.add(key);
82
+ allMcpServers.push(srv);
83
+ }
84
+ }
85
+ } catch {
86
+ }
87
+ const yaml = generateConfigYaml(agents, allMcpServers);
88
+ writeFileSync(configFile, yaml, "utf-8");
89
+ return true;
90
+ }
91
+
92
+ // src/rules.ts
93
+ import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
94
+ import { join as join2 } from "path";
95
+ import { parse as parse2 } from "yaml";
96
+ function fnmatchCase(value, pattern) {
97
+ let re = "";
98
+ let i = 0;
99
+ while (i < pattern.length) {
100
+ const ch = pattern[i];
101
+ if (ch === "*") {
102
+ re += ".*";
103
+ } else if (ch === "?") {
104
+ re += ".";
105
+ } else if (ch === "[") {
106
+ let j = i + 1;
107
+ if (j < pattern.length && pattern[j] === "!") {
108
+ re += "[^";
109
+ j++;
110
+ } else {
111
+ re += "[";
112
+ }
113
+ while (j < pattern.length && pattern[j] !== "]") {
114
+ re += pattern[j];
115
+ j++;
116
+ }
117
+ if (j < pattern.length) {
118
+ re += "]";
119
+ i = j;
120
+ } else {
121
+ re += "\\[";
122
+ }
123
+ } else if (".$^+{}()|\\".includes(ch)) {
124
+ re += "\\" + ch;
125
+ } else {
126
+ re += ch;
127
+ }
128
+ i++;
129
+ }
130
+ return new RegExp(`^${re}$`, "i").test(value);
131
+ }
132
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
133
+ var VALID_VERDICTS = /* @__PURE__ */ new Set(["danger", "warning"]);
134
+ var VALID_MATCH_TYPES = /* @__PURE__ */ new Set(["mcp", "skill", "agent"]);
135
+ var REQUIRED_FIELDS = ["id", "title", "severity", "verdict", "match"];
136
+ var RuleEngine = class _RuleEngine {
137
+ rules;
138
+ constructor(rules) {
139
+ this.rules = rules;
140
+ }
141
+ /**
142
+ * Load rules from file paths and/or directory paths.
143
+ *
144
+ * - Files are loaded directly.
145
+ * - Directories are globbed for *.yaml and *.yml files.
146
+ * - Files without a top-level "rules" key are silently skipped.
147
+ * - Validates required fields, severity, verdict, match.type.
148
+ * - Throws on duplicate IDs across files.
149
+ */
150
+ static fromPaths(paths) {
151
+ const resolvedFiles = [];
152
+ for (const p of paths) {
153
+ const stat = statSync2(p);
154
+ if (stat.isDirectory()) {
155
+ const entries = readdirSync(p);
156
+ for (const entry of entries) {
157
+ if (entry.endsWith(".yaml") || entry.endsWith(".yml")) {
158
+ resolvedFiles.push(join2(p, entry));
159
+ }
160
+ }
161
+ } else {
162
+ resolvedFiles.push(p);
163
+ }
164
+ }
165
+ const allRules = [];
166
+ const seenIds = /* @__PURE__ */ new Map();
167
+ for (const filePath of resolvedFiles) {
168
+ const raw = readFileSync2(filePath, "utf-8");
169
+ const doc = parse2(raw);
170
+ if (!doc || !("rules" in doc)) {
171
+ continue;
172
+ }
173
+ const rulesList = doc.rules;
174
+ if (!Array.isArray(rulesList)) {
175
+ continue;
176
+ }
177
+ for (const r of rulesList) {
178
+ for (const field of REQUIRED_FIELDS) {
179
+ if (r[field] == null || r[field] === "") {
180
+ throw new Error(
181
+ `Rule in ${filePath} is missing required field: ${field}`
182
+ );
183
+ }
184
+ }
185
+ const sev = String(r.severity).toLowerCase();
186
+ if (!VALID_SEVERITIES.has(sev)) {
187
+ throw new Error(
188
+ `Rule "${r.id}" in ${filePath} has invalid severity: "${r.severity}" (must be one of: ${[...VALID_SEVERITIES].join(", ")})`
189
+ );
190
+ }
191
+ const verd = String(r.verdict).toLowerCase();
192
+ if (!VALID_VERDICTS.has(verd)) {
193
+ throw new Error(
194
+ `Rule "${r.id}" in ${filePath} has invalid verdict: "${r.verdict}" (must be one of: ${[...VALID_VERDICTS].join(", ")})`
195
+ );
196
+ }
197
+ const matchType = r.match?.type;
198
+ if (!matchType || !VALID_MATCH_TYPES.has(String(matchType).toLowerCase())) {
199
+ throw new Error(
200
+ `Rule "${r.id}" in ${filePath} has invalid match.type: "${matchType}" (must be one of: ${[...VALID_MATCH_TYPES].join(", ")})`
201
+ );
202
+ }
203
+ const id = String(r.id);
204
+ const existingFile = seenIds.get(id);
205
+ if (existingFile) {
206
+ throw new Error(
207
+ `Duplicate rule ID "${id}" found in ${filePath} (already defined in ${existingFile})`
208
+ );
209
+ }
210
+ seenIds.set(id, filePath);
211
+ const rule = {
212
+ id,
213
+ title: String(r.title),
214
+ description: r.description ? String(r.description) : "",
215
+ severity: sev,
216
+ verdict: verd,
217
+ remediation: r.remediation ? String(r.remediation) : "",
218
+ match: r.match,
219
+ tests: Array.isArray(r.tests) ? r.tests.map((t) => ({
220
+ name: String(t.name ?? ""),
221
+ input: t.input ?? {},
222
+ expect: String(t.expect ?? "no_match")
223
+ })) : [],
224
+ source_file: filePath
225
+ };
226
+ allRules.push(rule);
227
+ }
228
+ }
229
+ return new _RuleEngine(allRules);
230
+ }
231
+ // ─────────────────────────────────────────────────────────────────────
232
+ // Internal matching
233
+ // ─────────────────────────────────────────────────────────────────────
234
+ /**
235
+ * Check if a rule matches an entity's data.
236
+ *
237
+ * - AND logic across fields (all fields must match).
238
+ * - OR logic within a field (any pattern in the array matches).
239
+ * - The "type" field in match is skipped (used for routing only).
240
+ */
241
+ _matchEntity(rule, entityData) {
242
+ for (const [field, patterns] of Object.entries(rule.match)) {
243
+ if (field === "type") continue;
244
+ const patternList = typeof patterns === "string" ? [patterns] : patterns;
245
+ const entityValue = entityData[field] ?? "";
246
+ let fieldMatched = false;
247
+ for (const pattern of patternList) {
248
+ if (fnmatchCase(entityValue, String(pattern))) {
249
+ fieldMatched = true;
250
+ break;
251
+ }
252
+ }
253
+ if (!fieldMatched) return false;
254
+ }
255
+ return true;
256
+ }
257
+ // ─────────────────────────────────────────────────────────────────────
258
+ // Evaluate methods
259
+ // ─────────────────────────────────────────────────────────────────────
260
+ /**
261
+ * Evaluate MCP rules against a server result.
262
+ */
263
+ evaluateMcp(server, rawConfig) {
264
+ const mcpRules = this.rules.filter(
265
+ (r) => String(r.match.type).toLowerCase() === "mcp"
266
+ );
267
+ const args = rawConfig.args;
268
+ const argsStr = Array.isArray(args) ? args.join(" ") : String(args ?? "");
269
+ const env = rawConfig.env;
270
+ const envKeys = env ? Object.keys(env).join(" ") : "";
271
+ const envValues = env ? Object.values(env).join(" ") : "";
272
+ const entityData = {
273
+ name: String(server.name ?? ""),
274
+ command: String(server.command ?? ""),
275
+ args: argsStr,
276
+ env_keys: envKeys,
277
+ env_values: envValues,
278
+ source_file: String(server.source_file ?? "")
279
+ };
280
+ const findings = [];
281
+ for (const rule of mcpRules) {
282
+ if (this._matchEntity(rule, entityData)) {
283
+ findings.push({
284
+ code: rule.id,
285
+ title: rule.title,
286
+ severity: rule.severity,
287
+ verdict: rule.verdict,
288
+ remediation: rule.remediation,
289
+ rule_file: rule.source_file,
290
+ entity_type: "mcp",
291
+ entity_name: entityData.name ?? ""
292
+ });
293
+ }
294
+ }
295
+ return findings;
296
+ }
297
+ /**
298
+ * Evaluate skill rules against a skill result.
299
+ */
300
+ evaluateSkill(skill, content) {
301
+ const skillRules = this.rules.filter(
302
+ (r) => String(r.match.type).toLowerCase() === "skill"
303
+ );
304
+ const entityData = {
305
+ name: String(skill.name ?? ""),
306
+ path: String(skill.path ?? ""),
307
+ content: content.slice(0, 10240)
308
+ };
309
+ const findings = [];
310
+ for (const rule of skillRules) {
311
+ if (this._matchEntity(rule, entityData)) {
312
+ findings.push({
313
+ code: rule.id,
314
+ title: rule.title,
315
+ severity: rule.severity,
316
+ verdict: rule.verdict,
317
+ remediation: rule.remediation,
318
+ rule_file: rule.source_file,
319
+ entity_type: "skill",
320
+ entity_name: entityData.name ?? ""
321
+ });
322
+ }
323
+ }
324
+ return findings;
325
+ }
326
+ /**
327
+ * Evaluate agent rules against an agent config result.
328
+ */
329
+ evaluateAgent(agent) {
330
+ const agentRules = this.rules.filter(
331
+ (r) => String(r.match.type).toLowerCase() === "agent"
332
+ );
333
+ const entityData = {
334
+ agent_type: String(agent.agent_type ?? ""),
335
+ name: String(agent.name ?? ""),
336
+ config_path: String(agent.config_path ?? "")
337
+ };
338
+ const findings = [];
339
+ for (const rule of agentRules) {
340
+ if (this._matchEntity(rule, entityData)) {
341
+ findings.push({
342
+ code: rule.id,
343
+ title: rule.title,
344
+ severity: rule.severity,
345
+ verdict: rule.verdict,
346
+ remediation: rule.remediation,
347
+ rule_file: rule.source_file,
348
+ entity_type: "agent",
349
+ entity_name: entityData.name ?? ""
350
+ });
351
+ }
352
+ }
353
+ return findings;
354
+ }
355
+ // ─────────────────────────────────────────────────────────────────────
356
+ // Self-test
357
+ // ─────────────────────────────────────────────────────────────────────
358
+ /**
359
+ * Run embedded tests for all rules.
360
+ */
361
+ runTests() {
362
+ const results = [];
363
+ for (const rule of this.rules) {
364
+ for (const test of rule.tests) {
365
+ const matched = this._matchEntity(rule, test.input);
366
+ const actual = matched ? "match" : "no_match";
367
+ results.push({
368
+ rule_id: rule.id,
369
+ test_name: test.name,
370
+ passed: actual === test.expect,
371
+ expected: test.expect,
372
+ actual
373
+ });
374
+ }
375
+ }
376
+ return results;
377
+ }
378
+ };
379
+
380
+ // src/baselines.ts
381
+ import { createHash } from "crypto";
382
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
383
+ import { homedir as homedir2 } from "os";
384
+ import { dirname as dirname2, join as join3 } from "path";
385
+ function configFingerprint(server) {
386
+ const rawCmd = server.command ?? "";
387
+ const cmdStr = Array.isArray(rawCmd) ? rawCmd.join(" ") : String(rawCmd);
388
+ const parts = [
389
+ cmdStr,
390
+ JSON.stringify([...server.args ?? []].map(String).sort()),
391
+ JSON.stringify(Object.keys(server.env ?? {}).map(String).sort()),
392
+ server.url ?? "",
393
+ JSON.stringify(Object.keys(server.headers ?? {}).map(String).sort())
394
+ ];
395
+ return createHash("sha256").update(parts.join("|")).digest("hex");
396
+ }
397
+ function sanitizeName(name) {
398
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
399
+ }
400
+ function rglob(dir, ext) {
401
+ const results = [];
402
+ const walk = (d) => {
403
+ try {
404
+ for (const entry of readdirSync2(d, { withFileTypes: true })) {
405
+ const full = join3(d, entry.name);
406
+ if (entry.isDirectory()) walk(full);
407
+ else if (entry.isFile() && entry.name.endsWith(ext)) results.push(full);
408
+ }
409
+ } catch {
410
+ }
411
+ };
412
+ walk(dir);
413
+ return results;
414
+ }
415
+ var BaselineStore = class {
416
+ _dir;
417
+ constructor(baselinesDir) {
418
+ this._dir = baselinesDir ?? join3(homedir2(), ".agentseal", "baselines");
419
+ }
420
+ _entryPath(agentType, serverName) {
421
+ return join3(this._dir, sanitizeName(agentType), `${sanitizeName(serverName)}.json`);
422
+ }
423
+ /** Load a stored baseline entry. Returns null if not found. */
424
+ load(agentType, serverName) {
425
+ const path = this._entryPath(agentType, serverName);
426
+ if (!existsSync2(path)) return null;
427
+ try {
428
+ const data = JSON.parse(readFileSync3(path, "utf-8"));
429
+ return data;
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
434
+ /** Save a baseline entry to disk. */
435
+ save(entry) {
436
+ const path = this._entryPath(entry.agent_type, entry.server_name);
437
+ mkdirSync(dirname2(path), { recursive: true });
438
+ writeFileSync2(path, JSON.stringify(entry, null, 2), "utf-8");
439
+ }
440
+ /** Check a single MCP server against its stored baseline. */
441
+ checkServer(server) {
442
+ const name = server.name ?? "unknown";
443
+ const agentType = server.agent_type ?? "unknown";
444
+ const rawCmd = server.command ?? "";
445
+ const command = Array.isArray(rawCmd) ? rawCmd.join(" ") : String(rawCmd);
446
+ const args = (server.args ?? []).filter((a) => typeof a === "string");
447
+ const now = (/* @__PURE__ */ new Date()).toISOString();
448
+ const configHash = configFingerprint(server);
449
+ const existing = this.load(agentType, name);
450
+ if (existing === null) {
451
+ this.save({
452
+ server_name: name,
453
+ agent_type: agentType,
454
+ config_hash: configHash,
455
+ binary_hash: null,
456
+ binary_path: null,
457
+ command,
458
+ args,
459
+ first_seen: now,
460
+ last_verified: now
461
+ });
462
+ return {
463
+ server_name: name,
464
+ agent_type: agentType,
465
+ change_type: "new_server",
466
+ detail: `New MCP server '${name}' baselined.`
467
+ };
468
+ }
469
+ if (existing.config_hash !== configHash) {
470
+ const change = {
471
+ server_name: name,
472
+ agent_type: agentType,
473
+ change_type: "config_changed",
474
+ old_value: existing.config_hash.slice(0, 12),
475
+ new_value: configHash.slice(0, 12),
476
+ detail: `Config for '${name}' changed (command/args/env modified).`
477
+ };
478
+ existing.config_hash = configHash;
479
+ existing.command = command;
480
+ existing.args = args;
481
+ existing.last_verified = now;
482
+ this.save(existing);
483
+ return change;
484
+ }
485
+ existing.last_verified = now;
486
+ this.save(existing);
487
+ return null;
488
+ }
489
+ /** Check all servers. Returns list of changes (empty = no changes). */
490
+ checkAll(servers, includeNew = false) {
491
+ const changes = [];
492
+ for (const srv of servers) {
493
+ const change = this.checkServer(srv);
494
+ if (change === null) continue;
495
+ if (change.change_type === "new_server" && !includeNew) continue;
496
+ changes.push(change);
497
+ }
498
+ return changes;
499
+ }
500
+ /** Remove all baselines. Returns count of entries removed. */
501
+ reset() {
502
+ let count = 0;
503
+ for (const f of rglob(this._dir, ".json")) {
504
+ try {
505
+ unlinkSync(f);
506
+ count++;
507
+ } catch {
508
+ }
509
+ }
510
+ return count;
511
+ }
512
+ /** List all stored baseline entries. */
513
+ listEntries() {
514
+ const entries = [];
515
+ for (const f of rglob(this._dir, ".json")) {
516
+ try {
517
+ const data = JSON.parse(readFileSync3(f, "utf-8"));
518
+ entries.push(data);
519
+ } catch {
520
+ }
521
+ }
522
+ return entries;
523
+ }
524
+ };
525
+
526
+ export {
527
+ runGuardInit,
528
+ RuleEngine,
529
+ BaselineStore
530
+ };