avorelo 0.1.0 → 0.2.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.
Files changed (260) hide show
  1. package/LICENSE +23 -16
  2. package/README.md +91 -51
  3. package/bin/avorelo.mjs +7 -0
  4. package/dist/avorelo.mjs +14337 -0
  5. package/package.json +106 -120
  6. package/bin/avorelo +0 -9
  7. package/scripts/README.md +0 -40
  8. package/scripts/cco-dashboard.js +0 -252
  9. package/scripts/cco-status.js +0 -430
  10. package/scripts/lib/activation/account-state.js +0 -37
  11. package/scripts/lib/activation/activation-runner.js +0 -546
  12. package/scripts/lib/activation/activation-self-healing.js +0 -480
  13. package/scripts/lib/activation/activation-state.js +0 -83
  14. package/scripts/lib/activation/activation-summary.js +0 -191
  15. package/scripts/lib/activation/adapters/claude-code.js +0 -77
  16. package/scripts/lib/activation/adapters/codex-cli.js +0 -52
  17. package/scripts/lib/activation/adapters/cursor.js +0 -37
  18. package/scripts/lib/activation/adapters/github-agent.js +0 -39
  19. package/scripts/lib/activation/adapters/terminal.js +0 -42
  20. package/scripts/lib/activation/adapters/vscode.js +0 -39
  21. package/scripts/lib/activation/adapters/windsurf.js +0 -37
  22. package/scripts/lib/activation/ai-surface-detector.js +0 -151
  23. package/scripts/lib/activation/connect-account.js +0 -145
  24. package/scripts/lib/activation/detect-environment.js +0 -75
  25. package/scripts/lib/activation/detect-hosts.js +0 -62
  26. package/scripts/lib/activation/format-activation-output.js +0 -109
  27. package/scripts/lib/activation/next-action.js +0 -43
  28. package/scripts/lib/activation/repair-engine.js +0 -219
  29. package/scripts/lib/activation-distribution-readiness.js +0 -507
  30. package/scripts/lib/adapter-conformance.js +0 -176
  31. package/scripts/lib/adapter-readiness.js +0 -417
  32. package/scripts/lib/adapter-safety-boundaries.js +0 -335
  33. package/scripts/lib/adapter-technical-readiness-gate.js +0 -205
  34. package/scripts/lib/agent-access-governance.js +0 -455
  35. package/scripts/lib/agent-enforcement.js +0 -765
  36. package/scripts/lib/agent-policy-profile.js +0 -210
  37. package/scripts/lib/agent-security/action-evaluator.js +0 -507
  38. package/scripts/lib/agent-security/adapter-registry.js +0 -98
  39. package/scripts/lib/agent-security/auto-policy.js +0 -139
  40. package/scripts/lib/agent-security/bounded-scan.js +0 -93
  41. package/scripts/lib/agent-security/enforcement-adapter.js +0 -174
  42. package/scripts/lib/agent-security/enforcement-engine.js +0 -1129
  43. package/scripts/lib/agent-security/file-write-adapter.js +0 -183
  44. package/scripts/lib/agent-security/file-write-rules.js +0 -178
  45. package/scripts/lib/agent-security/index.js +0 -3342
  46. package/scripts/lib/agent-security/instruction-risk.js +0 -181
  47. package/scripts/lib/agent-security/mcp-action-adapter.js +0 -185
  48. package/scripts/lib/agent-security/mcp-action-rules.js +0 -184
  49. package/scripts/lib/agent-security/package-action-adapter.js +0 -175
  50. package/scripts/lib/agent-security/package-action-rules.js +0 -233
  51. package/scripts/lib/agent-security/performance.js +0 -148
  52. package/scripts/lib/agent-security/permission-minimizer.js +0 -403
  53. package/scripts/lib/agent-security/scan-cache.js +0 -74
  54. package/scripts/lib/agent-security/source-trust.js +0 -146
  55. package/scripts/lib/ai-install-prompt.js +0 -288
  56. package/scripts/lib/ai-workspace-hygiene.js +0 -1499
  57. package/scripts/lib/alpha-activation.js +0 -520
  58. package/scripts/lib/alpha-feedback.js +0 -263
  59. package/scripts/lib/alpha-readiness-gate.js +0 -332
  60. package/scripts/lib/anti-gaming.js +0 -169
  61. package/scripts/lib/artifact-health.js +0 -431
  62. package/scripts/lib/attribution.js +0 -180
  63. package/scripts/lib/audit.js +0 -289
  64. package/scripts/lib/avorelo-skill-registry.js +0 -810
  65. package/scripts/lib/batch-jobs.js +0 -71
  66. package/scripts/lib/brain-pack.js +0 -578
  67. package/scripts/lib/brand-boundary.js +0 -424
  68. package/scripts/lib/brand.js +0 -74
  69. package/scripts/lib/browser-capability.js +0 -1048
  70. package/scripts/lib/browser-proof-preflight.js +0 -321
  71. package/scripts/lib/cache-readiness.js +0 -187
  72. package/scripts/lib/canonical-reentry.js +0 -162
  73. package/scripts/lib/capability-packs.js +0 -314
  74. package/scripts/lib/capability-recommender.js +0 -512
  75. package/scripts/lib/capability-registry.js +0 -1059
  76. package/scripts/lib/carry-forward-surfacing.js +0 -194
  77. package/scripts/lib/ccusage-adapter.js +0 -188
  78. package/scripts/lib/company-loop.js +0 -1149
  79. package/scripts/lib/config.js +0 -637
  80. package/scripts/lib/context-acquisition-plan.js +0 -287
  81. package/scripts/lib/context-budget-guard.js +0 -170
  82. package/scripts/lib/context-budget-scanner.js +0 -257
  83. package/scripts/lib/context-optimizer.js +0 -715
  84. package/scripts/lib/context-reduction-plan.js +0 -178
  85. package/scripts/lib/context-safety.js +0 -88
  86. package/scripts/lib/context-savings-engine.js +0 -158
  87. package/scripts/lib/cost-evidence.js +0 -254
  88. package/scripts/lib/cross-host-install-plan.js +0 -308
  89. package/scripts/lib/cross-host-install-readiness.js +0 -237
  90. package/scripts/lib/cross-host-value-flow.js +0 -268
  91. package/scripts/lib/dashboard.js +0 -900
  92. package/scripts/lib/design-partner-feedback.js +0 -346
  93. package/scripts/lib/entitlements.js +0 -100
  94. package/scripts/lib/execution-packet.js +0 -559
  95. package/scripts/lib/experimentation-events.js +0 -547
  96. package/scripts/lib/external-capability-compliance.js +0 -107
  97. package/scripts/lib/external-user-simulation.js +0 -166
  98. package/scripts/lib/failure-recovery-readiness.js +0 -81
  99. package/scripts/lib/failure-recovery.js +0 -419
  100. package/scripts/lib/feedback-intelligence.js +0 -537
  101. package/scripts/lib/feedback-signals.js +0 -205
  102. package/scripts/lib/file-integrity.js +0 -68
  103. package/scripts/lib/fsx.js +0 -127
  104. package/scripts/lib/full-readiness-gate.js +0 -451
  105. package/scripts/lib/guidance-builder.js +0 -174
  106. package/scripts/lib/hook-apply.js +0 -1019
  107. package/scripts/lib/hook-baseline.js +0 -310
  108. package/scripts/lib/hook-config-preview.js +0 -275
  109. package/scripts/lib/hook-contracts.js +0 -290
  110. package/scripts/lib/hook-safety-boundary-readiness.js +0 -80
  111. package/scripts/lib/host-capability-matrix.js +0 -351
  112. package/scripts/lib/host-support-context.js +0 -254
  113. package/scripts/lib/http-hook-action.js +0 -538
  114. package/scripts/lib/install-ai-readiness.js +0 -84
  115. package/scripts/lib/install-intake-risk.js +0 -1037
  116. package/scripts/lib/install-journey-intelligence.js +0 -329
  117. package/scripts/lib/intervention-guidance.js +0 -57
  118. package/scripts/lib/known-limitations.js +0 -115
  119. package/scripts/lib/l8-path-truth.js +0 -146
  120. package/scripts/lib/launch-hardening-gate.js +0 -436
  121. package/scripts/lib/launch-readiness.js +0 -628
  122. package/scripts/lib/learning-memory.js +0 -686
  123. package/scripts/lib/lifecycle-hooks.js +0 -802
  124. package/scripts/lib/local-package-smoke.js +0 -423
  125. package/scripts/lib/local-pricing.js +0 -299
  126. package/scripts/lib/mcp-enforcement.js +0 -311
  127. package/scripts/lib/mcp-least-privilege-policy.js +0 -303
  128. package/scripts/lib/mcp-tool-inventory.js +0 -388
  129. package/scripts/lib/mcp-tool-risk.js +0 -0
  130. package/scripts/lib/memory.js +0 -335
  131. package/scripts/lib/metrics.js +0 -699
  132. package/scripts/lib/micro-proof.js +0 -133
  133. package/scripts/lib/next-run-context.js +0 -436
  134. package/scripts/lib/operating-value.js +0 -1648
  135. package/scripts/lib/optimization-v3.js +0 -122
  136. package/scripts/lib/orchestration/adapters/_shared.js +0 -49
  137. package/scripts/lib/orchestration/adapters/aider.js +0 -18
  138. package/scripts/lib/orchestration/adapters/claude-code.js +0 -35
  139. package/scripts/lib/orchestration/adapters/codex.js +0 -35
  140. package/scripts/lib/orchestration/adapters/gemini-cli.js +0 -18
  141. package/scripts/lib/orchestration/adapters/git.js +0 -25
  142. package/scripts/lib/orchestration/adapters/index.js +0 -31
  143. package/scripts/lib/orchestration/adapters/lm-studio.js +0 -18
  144. package/scripts/lib/orchestration/adapters/ollama.js +0 -18
  145. package/scripts/lib/orchestration/adapters/opencode.js +0 -18
  146. package/scripts/lib/orchestration/adapters/openrouter.js +0 -18
  147. package/scripts/lib/orchestration/adapters/test-runner.js +0 -25
  148. package/scripts/lib/orchestration/cli.js +0 -438
  149. package/scripts/lib/orchestration/execution-manager.js +0 -279
  150. package/scripts/lib/orchestration/handoff.js +0 -314
  151. package/scripts/lib/orchestration/index.js +0 -456
  152. package/scripts/lib/orchestration/inventory.js +0 -47
  153. package/scripts/lib/orchestration/model-discovery.js +0 -498
  154. package/scripts/lib/orchestration/model-profiler.js +0 -170
  155. package/scripts/lib/orchestration/model-profiles.js +0 -252
  156. package/scripts/lib/orchestration/model-refresh-policy.js +0 -72
  157. package/scripts/lib/orchestration/proof-writer.js +0 -349
  158. package/scripts/lib/orchestration/provider-discovery/aider.js +0 -49
  159. package/scripts/lib/orchestration/provider-discovery/claude-code.js +0 -56
  160. package/scripts/lib/orchestration/provider-discovery/codex.js +0 -49
  161. package/scripts/lib/orchestration/provider-discovery/common.js +0 -186
  162. package/scripts/lib/orchestration/provider-discovery/gemini.js +0 -106
  163. package/scripts/lib/orchestration/provider-discovery/lm-studio.js +0 -118
  164. package/scripts/lib/orchestration/provider-discovery/models-dev.js +0 -12
  165. package/scripts/lib/orchestration/provider-discovery/ollama.js +0 -100
  166. package/scripts/lib/orchestration/provider-discovery/opencode.js +0 -47
  167. package/scripts/lib/orchestration/provider-discovery/openrouter.js +0 -44
  168. package/scripts/lib/orchestration/risk-classifier.js +0 -130
  169. package/scripts/lib/orchestration/routing-policy.js +0 -486
  170. package/scripts/lib/orchestration/settings.js +0 -112
  171. package/scripts/lib/orchestration/state.js +0 -165
  172. package/scripts/lib/orchestration/verification-manager.js +0 -138
  173. package/scripts/lib/output-profiles.js +0 -146
  174. package/scripts/lib/package-content-audit.js +0 -368
  175. package/scripts/lib/package-runtime.js +0 -278
  176. package/scripts/lib/plan-surface.js +0 -53
  177. package/scripts/lib/plans.js +0 -2318
  178. package/scripts/lib/policy-provider.js +0 -27
  179. package/scripts/lib/prelaunch-activation-readiness.js +0 -409
  180. package/scripts/lib/prelaunch-evidence-store.js +0 -816
  181. package/scripts/lib/prelaunch-intelligence.js +0 -869
  182. package/scripts/lib/pricing-experiment.js +0 -118
  183. package/scripts/lib/pro-moment-events.js +0 -77
  184. package/scripts/lib/pro-moment-state.js +0 -227
  185. package/scripts/lib/pro-moments.js +0 -1216
  186. package/scripts/lib/product-learning-events.js +0 -629
  187. package/scripts/lib/project-profile.js +0 -555
  188. package/scripts/lib/prompt-compiler.js +0 -280
  189. package/scripts/lib/prompt-lint.js +0 -32
  190. package/scripts/lib/prompt-suggestions.js +0 -52
  191. package/scripts/lib/proof-canonical.js +0 -398
  192. package/scripts/lib/proof-drilldown.js +0 -383
  193. package/scripts/lib/proof-events.js +0 -342
  194. package/scripts/lib/proof-history.js +0 -243
  195. package/scripts/lib/proof-metrics.js +0 -296
  196. package/scripts/lib/proof-outcome-evidence.js +0 -134
  197. package/scripts/lib/proof-receipt.js +0 -335
  198. package/scripts/lib/proof-record.js +0 -461
  199. package/scripts/lib/public-activation-distribution-gate.js +0 -258
  200. package/scripts/lib/public-cli.js +0 -3891
  201. package/scripts/lib/public-distribution-truth.js +0 -211
  202. package/scripts/lib/public-install-claim-checker.js +0 -294
  203. package/scripts/lib/publish-provenance-readiness.js +0 -283
  204. package/scripts/lib/readiness-delta.js +0 -218
  205. package/scripts/lib/readiness-evidence-closure.js +0 -196
  206. package/scripts/lib/reentry-memory-capture.js +0 -241
  207. package/scripts/lib/reentry-memory-retrieval.js +0 -302
  208. package/scripts/lib/reentry-memory-status.js +0 -146
  209. package/scripts/lib/reentry-memory-store.js +0 -178
  210. package/scripts/lib/reentry-state.js +0 -66
  211. package/scripts/lib/release-candidate-bundle.js +0 -166
  212. package/scripts/lib/remediation.js +0 -81
  213. package/scripts/lib/repo-map.js +0 -391
  214. package/scripts/lib/run-improvements-lifecycle.js +0 -330
  215. package/scripts/lib/run-improvements.js +0 -789
  216. package/scripts/lib/runtime-decision-policy.js +0 -387
  217. package/scripts/lib/safe-path-engine.js +0 -705
  218. package/scripts/lib/safe-run-controller.js +0 -887
  219. package/scripts/lib/score.js +0 -262
  220. package/scripts/lib/seamless-enforcement.js +0 -329
  221. package/scripts/lib/seamless-outcome.js +0 -689
  222. package/scripts/lib/seamless-reality-gate.js +0 -5043
  223. package/scripts/lib/security-risk-classifier.js +0 -511
  224. package/scripts/lib/security-scan.js +0 -384
  225. package/scripts/lib/session-context-optimizer.js +0 -1211
  226. package/scripts/lib/session-timing.js +0 -315
  227. package/scripts/lib/skill-hygiene.js +0 -805
  228. package/scripts/lib/skill-packs.js +0 -161
  229. package/scripts/lib/skills-operating-layer.js +0 -580
  230. package/scripts/lib/smart-work-routing.js +0 -768
  231. package/scripts/lib/source-catalog.js +0 -700
  232. package/scripts/lib/status-value-summary.js +0 -32
  233. package/scripts/lib/support-bundle.js +0 -578
  234. package/scripts/lib/task-continuation.js +0 -440
  235. package/scripts/lib/test-helpers.js +0 -15
  236. package/scripts/lib/tier.js +0 -38
  237. package/scripts/lib/token-context-quality-gate.js +0 -370
  238. package/scripts/lib/token-cost-capture.js +0 -187
  239. package/scripts/lib/token-cost-intelligence.js +0 -358
  240. package/scripts/lib/token-efficiency-evidence.js +0 -213
  241. package/scripts/lib/token-evidence.js +0 -699
  242. package/scripts/lib/tokenish.js +0 -17
  243. package/scripts/lib/tool-output-sandbox.js +0 -304
  244. package/scripts/lib/trust-audit.js +0 -136
  245. package/scripts/lib/unified-events.js +0 -396
  246. package/scripts/lib/upgrade-interruption-recovery.js +0 -407
  247. package/scripts/lib/usage-ledger.js +0 -201
  248. package/scripts/lib/value-ledger.js +0 -130
  249. package/scripts/lib/value-proof-calibration.js +0 -531
  250. package/scripts/lib/visual-qa.js +0 -231
  251. package/scripts/lib/voice-alpha.js +0 -29
  252. package/scripts/lib/work-aware-orchestration.js +0 -976
  253. package/scripts/lib/work-control-receipts.js +0 -577
  254. package/scripts/lib/work-ledger.js +0 -1123
  255. package/scripts/lib/work-panel-preview.js +0 -352
  256. package/scripts/lib/workflow-discipline.js +0 -280
  257. package/scripts/lib/workflow-signals.js +0 -419
  258. package/scripts/lib/workspace-map.js +0 -281
  259. package/scripts/lib/workspace-registry.js +0 -1367
  260. package/scripts/lib/workspace-resolver.js +0 -480
@@ -1,3342 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
- const crypto = require("crypto");
6
- const { safeReadJson, safeWriteJson, nowIso } = require("../fsx");
7
- const { buildActionId, normalizeAgentSecurityAction, evaluateAgentSecurityAction } = require("./action-evaluator");
8
- const { buildPermissionLimitPlan } = require("./permission-minimizer");
9
- const {
10
- ENFORCEMENT_SUMMARY_REL_PATH,
11
- ENFORCEMENT_CAPABILITIES_REL_PATH,
12
- JIT_GRANTS_REL_PATH,
13
- ONE_TIME_GRANTS_REL_PATH,
14
- ENFORCEMENT_AUDIT_LOG_REL_PATH,
15
- loadJitGrantStore,
16
- loadEnforcementSummary,
17
- recordEnforcementOutcome,
18
- enforceAgentSecurityAction,
19
- } = require("./enforcement-engine");
20
- const { resolveAgentSecurityAdapter, writeEnforcementCapabilityMatrix } = require("./adapter-registry");
21
- const { decideAutoAction } = require("./auto-policy");
22
- const { scanInstructionRisk, sanitizeInstructionLikeContent } = require("./instruction-risk");
23
- const { buildSourceRecord, sourceTypeForComponentType } = require("./source-trust");
24
- const {
25
- PERFORMANCE_SUMMARY_REL_PATH,
26
- PERFORMANCE_GUARDRAILS,
27
- createPerformanceTracker,
28
- recordPerformanceSummary,
29
- buildCompactPerformanceSummary,
30
- } = require("./performance");
31
- const { SCAN_CACHE_REL_PATH, cacheLookup, updateScanCache, buildCacheKey } = require("./scan-cache");
32
- const { createBoundedScanSession } = require("./bounded-scan");
33
-
34
- const AGENT_SECURITY_MODES = ["off", "basic", "visibility", "warn", "auto"];
35
- const SNAPSHOT_REL_PATH = ".claude/cco/security/agent-surface-snapshot.json";
36
- const SUMMARY_REL_PATH = ".claude/cco/security/agent-basic-check-summary.json";
37
- const VISIBILITY_SUMMARY_REL_PATH = ".claude/cco/security/agent-visibility-summary.json";
38
- const TRUST_CARDS_REL_PATH = ".claude/cco/security/agent-trust-cards.json";
39
- const RISK_DIFF_REL_PATH = ".claude/cco/security/agent-risk-diff.json";
40
- const PERMISSION_PREVIEW_REL_PATH = ".claude/cco/security/agent-permission-preview.json";
41
- const INSTRUCTION_RISK_REL_PATH = ".claude/cco/security/agent-instruction-risk.json";
42
- const SOURCE_TRUST_REL_PATH = ".claude/cco/security/agent-source-trust.json";
43
- const INSTRUCTION_REMEDIATIONS_REL_PATH = ".claude/cco/security/agent-instruction-remediations.json";
44
- const SANITIZED_CONTENT_REL_PATH = ".claude/cco/security/agent-sanitized-content.json";
45
- const AUDIT_LOG_REL_PATH = ".claude/cco/audit/agent-security.jsonl";
46
- const INSTRUCTION_AUDIT_LOG_REL_PATH = ".claude/cco/audit/agent-instruction-risk.jsonl";
47
- const ACTION_PREFLIGHT_REL_PATH = ".claude/cco/security/agent-action-preflight.json";
48
- const DECISION_SUMMARY_REL_PATH = ".claude/cco/security/agent-decision-summary.json";
49
- const DECISION_AUDIT_LOG_REL_PATH = ".claude/cco/audit/agent-security-decisions.jsonl";
50
-
51
- const TEXT_EXTENSIONS = new Set([".md", ".txt", ".json", ".js", ".ts", ".yaml", ".yml", ".sh", ".ps1", ".toml", ".mjs", ".cjs"]);
52
- const EXCLUDED_DIR_NAMES = new Set([".git", "node_modules", ".claude/cco", "coverage", "dist", "build", ".next", ".turbo", ".cache", ".wasp/out"]);
53
- const EXCLUDED_DIR_BASENAMES = new Set([".git", "node_modules", "coverage", "dist", "build", ".next", ".turbo", ".cache"]);
54
- const CAPABILITY_KEYS = [
55
- "filesystemRead",
56
- "filesystemWrite",
57
- "shell",
58
- "network",
59
- "mcpRead",
60
- "mcpWrite",
61
- "externalMutation",
62
- "sensitiveAccess",
63
- ];
64
- const AGENT_SECURITY_SCANNER_VERSION = "slice-8-6";
65
-
66
- function buildAgentSecurityModeControl(config) {
67
- const currentMode = config?.agentSecurity?.mode || "off";
68
- return {
69
- title: "Agent Security",
70
- currentMode,
71
- options: [
72
- {
73
- value: "off",
74
- label: "Off",
75
- description: "Wuz stays out of the way. No Agent Security scans, warnings, or dashboard section.",
76
- available: true,
77
- },
78
- {
79
- value: "basic",
80
- label: "Basic Check",
81
- description: "Check new Skills and MCP entries. Included in the basic/free layer.",
82
- available: true,
83
- },
84
- {
85
- value: "visibility",
86
- label: "Visibility",
87
- description: "Map the full AI agent surface and show risk diff + permission preview.",
88
- available: true,
89
- },
90
- {
91
- value: "warn",
92
- label: "Warn & Approve",
93
- description: "Warn before sensitive actions. Approve once, deny, or let Wuz limit.",
94
- available: true,
95
- },
96
- {
97
- value: "auto",
98
- label: "Auto-Minimize",
99
- description: "Wuz automatically applies least-privilege limits only for safe, enforceable actions. Risky or unknown actions still require approval.",
100
- available: true,
101
- },
102
- ],
103
- };
104
- }
105
-
106
- function normalizePath(value) {
107
- return String(value || "").replace(/\\/g, "/");
108
- }
109
-
110
- function relPath(cwd, absPath) {
111
- return normalizePath(path.relative(cwd, absPath));
112
- }
113
-
114
- function stableStringify(value) {
115
- if (Array.isArray(value)) {
116
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
117
- }
118
- if (value && typeof value === "object") {
119
- return `{${Object.keys(value)
120
- .sort((left, right) => left.localeCompare(right))
121
- .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
122
- .join(",")}}`;
123
- }
124
- return JSON.stringify(value);
125
- }
126
-
127
- function sha256(value) {
128
- return crypto.createHash("sha256").update(value).digest("hex");
129
- }
130
-
131
- function shortHash(value) {
132
- return sha256(String(value || "")).slice(0, 12);
133
- }
134
-
135
- function ensureDir(absPath) {
136
- fs.mkdirSync(absPath, { recursive: true });
137
- }
138
-
139
- function readJsonAbs(absPath, fallback = null) {
140
- try {
141
- return JSON.parse(fs.readFileSync(absPath, "utf8").replace(/^\uFEFF/, ""));
142
- } catch {
143
- return fallback;
144
- }
145
- }
146
-
147
- function readTextAbs(absPath) {
148
- try {
149
- return fs.readFileSync(absPath, "utf8").replace(/^\uFEFF/, "");
150
- } catch {
151
- return "";
152
- }
153
- }
154
-
155
- function appendJsonl(cwd, relPathValue, entry) {
156
- const absPath = path.join(cwd, relPathValue);
157
- ensureDir(path.dirname(absPath));
158
- fs.appendFileSync(absPath, `${JSON.stringify(entry)}\n`, "utf8");
159
- }
160
-
161
- function boundedPreview(value, max = 120) {
162
- const text = String(value || "").replace(/\s+/g, " ").trim();
163
- return text.length > max ? `${text.slice(0, max)}...` : text;
164
- }
165
-
166
- function shouldExcludeDir(cwd, absDir) {
167
- const rel = normalizePath(path.relative(cwd, absDir));
168
- if (rel === ".claude/cco") return true;
169
- if (EXCLUDED_DIR_NAMES.has(rel)) return true;
170
- return EXCLUDED_DIR_BASENAMES.has(path.basename(absDir));
171
- }
172
-
173
- function walkFiles(absDir, out, cwd, options = {}, depth = 0) {
174
- if (!fs.existsSync(absDir)) return;
175
- const scanSession = options.scanSession || null;
176
- const entries = fs.readdirSync(absDir, { withFileTypes: true });
177
- entries.forEach((entry) => {
178
- const entryAbs = path.join(absDir, entry.name);
179
- if (entry.isDirectory()) {
180
- if (shouldExcludeDir(cwd, entryAbs)) {
181
- if (scanSession?.tracker) scanSession.tracker.noteIgnoredDirectory(relPath(cwd, entryAbs));
182
- return;
183
- }
184
- const maxDepth = Number.isFinite(Number(options.maxDepth)) ? Number(options.maxDepth) : Infinity;
185
- if (depth >= maxDepth) return;
186
- if (scanSession && !scanSession.canDescend(depth, entryAbs)) return;
187
- walkFiles(entryAbs, out, cwd, options, depth + 1);
188
- return;
189
- }
190
- if (entry.isFile()) {
191
- const stat = fs.statSync(entryAbs);
192
- if (scanSession && !scanSession.canReadFile(entryAbs, stat)) return;
193
- if (scanSession) scanSession.noteFile(entryAbs, stat);
194
- out.push(entryAbs);
195
- }
196
- });
197
- }
198
-
199
- function listFiles(absDir, cwd, options = {}) {
200
- const out = [];
201
- walkFiles(absDir, out, cwd, options, 0);
202
- return out.sort((left, right) => left.localeCompare(right));
203
- }
204
-
205
- function listTextFiles(absDir, cwd, options = {}) {
206
- return listFiles(absDir, cwd, options).filter((absPath) => TEXT_EXTENSIONS.has(path.extname(absPath).toLowerCase()));
207
- }
208
-
209
- function hashDirectory(cwd, absDir, options = {}) {
210
- const pieces = listFiles(absDir, cwd, options).map((absPath) => {
211
- const fileRel = relPath(cwd, absPath);
212
- const content = fs.readFileSync(absPath);
213
- return `${fileRel}\n${sha256(content)}\n`;
214
- });
215
- return sha256(pieces.join(""));
216
- }
217
-
218
- function hashFile(cwd, absPath) {
219
- const buffer = fs.readFileSync(absPath);
220
- return sha256(`${relPath(cwd, absPath)}\n${buffer.toString("utf8")}`);
221
- }
222
-
223
- function hashPathStat(cwd, absPath) {
224
- try {
225
- const stat = fs.statSync(absPath);
226
- return sha256(`${relPath(cwd, absPath)}\n${stat.isDirectory() ? "dir" : "file"}\n${Number(stat.size || 0)}`);
227
- } catch {
228
- return sha256(relPath(cwd, absPath));
229
- }
230
- }
231
-
232
- function hashNormalizedObject(value) {
233
- return sha256(stableStringify(value));
234
- }
235
-
236
- function createComponentId(type, name, sourcePath) {
237
- const safeName = String(name || type)
238
- .toLowerCase()
239
- .replace(/[^a-z0-9]+/g, "-")
240
- .replace(/^-+|-+$/g, "") || type;
241
- return `${type}-${safeName}-${shortHash(`${type}:${sourcePath}`)}`;
242
- }
243
-
244
- function maybePush(list, message, seen) {
245
- if (!message || seen.has(message)) return;
246
- seen.add(message);
247
- list.push(message);
248
- }
249
-
250
- function emptyCapabilities() {
251
- return {
252
- filesystemRead: false,
253
- filesystemWrite: false,
254
- shell: false,
255
- network: false,
256
- mcpRead: false,
257
- mcpWrite: false,
258
- externalMutation: false,
259
- sensitiveAccess: false,
260
- };
261
- }
262
-
263
- function scrubSecretValues(value) {
264
- if (Array.isArray(value)) {
265
- return value.map((item) => scrubSecretValues(item));
266
- }
267
- if (!value || typeof value !== "object") return value;
268
-
269
- const out = {};
270
- Object.keys(value).forEach((key) => {
271
- const lowered = String(key || "").toLowerCase();
272
- if (lowered === "env" && value[key] && typeof value[key] === "object" && !Array.isArray(value[key])) {
273
- out[key] = Object.keys(value[key]).sort();
274
- return;
275
- }
276
- out[key] = scrubSecretValues(value[key]);
277
- });
278
- return out;
279
- }
280
-
281
- function commandTextFromEntry(entry) {
282
- if (!entry || typeof entry !== "object") return "";
283
- const command = String(entry.command || "");
284
- const args = Array.isArray(entry.args) ? entry.args.map((item) => String(item || "")).join(" ") : "";
285
- return `${command} ${args}`.trim();
286
- }
287
-
288
- function envKeyNames(entry) {
289
- const env = entry && entry.env && typeof entry.env === "object" && !Array.isArray(entry.env) ? entry.env : null;
290
- return env ? Object.keys(env).sort() : [];
291
- }
292
-
293
- function collectSkillDirectories(absDir, cwd, out, options = {}, depth = 0) {
294
- if (!fs.existsSync(absDir)) return;
295
- const skillRoots = new Set([path.join(cwd, "skills"), path.join(cwd, ".claude/skills")]);
296
- if (options.scanSession && !skillRoots.has(absDir) && !options.scanSession.canDescend(depth, absDir)) {
297
- return;
298
- }
299
- const entries = fs.readdirSync(absDir, { withFileTypes: true });
300
- const hasSkillFile = entries.some((entry) => entry.isFile() && entry.name.toLowerCase() === "skill.md");
301
- if (hasSkillFile) {
302
- const textFiles = listTextFiles(absDir, cwd, { scanSession: options.scanSession });
303
- out.push({
304
- name: path.basename(absDir),
305
- type: "skill",
306
- sourcePath: relPath(cwd, absDir),
307
- sourceKind: "local",
308
- contentHash: hashDirectory(cwd, absDir, { scanSession: options.scanSession }),
309
- contentText: textFiles.map((filePath) => `# ${relPath(cwd, filePath)}\n${readTextAbs(filePath)}`).join("\n"),
310
- mtimeMs: textFiles.reduce((max, filePath) => Math.max(max, statMeta(filePath).mtimeMs), 0),
311
- });
312
- }
313
-
314
- entries
315
- .filter((entry) => entry.isDirectory())
316
- .forEach((entry) => {
317
- const nextDir = path.join(absDir, entry.name);
318
- collectSkillDirectories(nextDir, cwd, out, options, depth + 1);
319
- });
320
- }
321
-
322
- function detectSkillComponents(cwd, options = {}) {
323
- const out = [];
324
- ["skills", ".claude/skills"].forEach((rootRel) => {
325
- collectSkillDirectories(path.join(cwd, rootRel), cwd, out, options);
326
- });
327
- return out;
328
- }
329
-
330
- function detectCommandComponents(cwd, options = {}) {
331
- const commandsRoot = path.join(cwd, ".claude", "commands");
332
- if (!fs.existsSync(commandsRoot)) return [];
333
-
334
- return listFiles(commandsRoot, cwd, { scanSession: options.scanSession })
335
- .filter((absPath) => fs.statSync(absPath).isFile())
336
- .map((absPath) => ({
337
- mtimeMs: statMeta(absPath).mtimeMs,
338
- name: path.parse(absPath).name,
339
- type: "command",
340
- sourcePath: relPath(cwd, absPath),
341
- sourceKind: "local",
342
- contentHash: hashFile(cwd, absPath),
343
- contentText: readTextAbs(absPath),
344
- }));
345
- }
346
-
347
- function extractNamedEntries(container) {
348
- if (!container) return [];
349
-
350
- if (Array.isArray(container)) {
351
- return container
352
- .map((item, index) => {
353
- const name = item && typeof item === "object" ? item.name || item.id || item.server || `server-${index + 1}` : `server-${index + 1}`;
354
- return { name: String(name), entry: item };
355
- })
356
- .filter((item) => item.name);
357
- }
358
-
359
- if (typeof container === "object") {
360
- return Object.keys(container).map((name) => ({ name, entry: container[name] }));
361
- }
362
-
363
- return [];
364
- }
365
-
366
- function extractMcpEntriesFromConfig(config) {
367
- const candidates = [
368
- config?.mcpServers,
369
- config?.servers,
370
- config?.mcp?.servers,
371
- config?.mcp?.mcpServers,
372
- ];
373
-
374
- for (const candidate of candidates) {
375
- const entries = extractNamedEntries(candidate);
376
- if (entries.length > 0) return entries;
377
- }
378
-
379
- return [];
380
- }
381
-
382
- function detectMcpComponents(cwd) {
383
- const configFiles = [
384
- ".mcp.json",
385
- ".cursor/mcp.json",
386
- ".claude/settings.json",
387
- ".claude/settings.local.json",
388
- ];
389
-
390
- const out = [];
391
- configFiles.forEach((configRel) => {
392
- const absPath = path.join(cwd, configRel);
393
- if (!fs.existsSync(absPath)) return;
394
-
395
- const parsed = readJsonAbs(absPath, null);
396
- if (!parsed || typeof parsed !== "object") return;
397
-
398
- extractMcpEntriesFromConfig(parsed).forEach(({ name, entry }) => {
399
- const normalizedEntry = entry && typeof entry === "object" ? scrubSecretValues(entry) : { value: entry };
400
- out.push({
401
- name: String(name),
402
- type: "mcp",
403
- sourcePath: normalizePath(`${configRel}#${name}`),
404
- sourceKind: "config",
405
- contentHash: hashNormalizedObject(normalizedEntry),
406
- contentText: stableStringify(normalizedEntry),
407
- rawConfigPath: configRel,
408
- rawEntry: normalizedEntry,
409
- envKeys: envKeyNames(entry),
410
- commandText: commandTextFromEntry(entry),
411
- mtimeMs: statMeta(absPath).mtimeMs,
412
- });
413
- });
414
- });
415
-
416
- return out;
417
- }
418
-
419
- function detectConfigComponents(cwd) {
420
- const configFiles = [
421
- ".claude/settings.json",
422
- ".claude/settings.local.json",
423
- ".mcp.json",
424
- ".cursor/mcp.json",
425
- ];
426
- const out = [];
427
-
428
- configFiles.forEach((configRel) => {
429
- const absPath = path.join(cwd, configRel);
430
- if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) return;
431
- out.push({
432
- mtimeMs: statMeta(absPath).mtimeMs,
433
- name: configRel.replace(/[\\/]/g, "-").replace(/^-+|-+$/g, ""),
434
- type: "config",
435
- sourcePath: configRel,
436
- sourceKind: "config",
437
- contentHash: hashFile(cwd, absPath),
438
- contentText: readTextAbs(absPath),
439
- });
440
- });
441
-
442
- const packageJsonAbs = path.join(cwd, "package.json");
443
- if (fs.existsSync(packageJsonAbs) && fs.statSync(packageJsonAbs).isFile()) {
444
- out.push({
445
- mtimeMs: statMeta(packageJsonAbs).mtimeMs,
446
- name: "package-json",
447
- type: "config",
448
- sourcePath: "package.json",
449
- sourceKind: "config",
450
- contentHash: hashFile(cwd, packageJsonAbs),
451
- contentText: readTextAbs(packageJsonAbs),
452
- });
453
- }
454
-
455
- const lockfiles = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
456
- lockfiles.forEach((name) => {
457
- const absPath = path.join(cwd, name);
458
- if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) return;
459
- out.push({
460
- mtimeMs: statMeta(absPath).mtimeMs,
461
- name: name.replace(/\W+/g, "-"),
462
- type: "config",
463
- sourcePath: name,
464
- sourceKind: "local",
465
- contentHash: hashFile(cwd, absPath),
466
- contentText: `Lockfile present: ${name}`,
467
- });
468
- });
469
-
470
- const dockerCandidates = fs
471
- .readdirSync(cwd, { withFileTypes: true })
472
- .filter((entry) => entry.isFile())
473
- .map((entry) => entry.name)
474
- .filter((name) => /^dockerfile(?:\..+)?$/i.test(name) || /^(?:docker-)?compose(?:\..+)?\.(?:ya?ml)$/i.test(name));
475
-
476
- dockerCandidates.forEach((name) => {
477
- const absPath = path.join(cwd, name);
478
- out.push({
479
- mtimeMs: statMeta(absPath).mtimeMs,
480
- name: name.replace(/\W+/g, "-").toLowerCase(),
481
- type: "config",
482
- sourcePath: name,
483
- sourceKind: "local",
484
- contentHash: hashFile(cwd, absPath),
485
- contentText: readTextAbs(absPath),
486
- });
487
- });
488
-
489
- return out;
490
- }
491
-
492
- function detectCursorRuleComponents(cwd, options = {}) {
493
- const rulesRoot = path.join(cwd, ".cursor", "rules");
494
- if (!fs.existsSync(rulesRoot)) return [];
495
-
496
- return listFiles(rulesRoot, cwd, { scanSession: options.scanSession })
497
- .filter((absPath) => fs.statSync(absPath).isFile())
498
- .map((absPath) => ({
499
- mtimeMs: statMeta(absPath).mtimeMs,
500
- name: path.parse(absPath).name,
501
- type: "cursor-rule",
502
- sourcePath: relPath(cwd, absPath),
503
- sourceKind: "local",
504
- contentHash: hashFile(cwd, absPath),
505
- contentText: readTextAbs(absPath),
506
- }));
507
- }
508
-
509
- function detectPackageScriptComponents(cwd) {
510
- const packageJsonAbs = path.join(cwd, "package.json");
511
- if (!fs.existsSync(packageJsonAbs)) return [];
512
-
513
- const pkg = readJsonAbs(packageJsonAbs, null);
514
- const scripts = pkg && pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : null;
515
- if (!scripts) return [];
516
-
517
- return Object.keys(scripts)
518
- .sort((left, right) => left.localeCompare(right))
519
- .map((name) => {
520
- const command = String(scripts[name] || "");
521
- return {
522
- mtimeMs: statMeta(packageJsonAbs).mtimeMs,
523
- name,
524
- type: "package-script",
525
- sourcePath: `package.json#scripts.${name}`,
526
- sourceKind: "config",
527
- contentHash: hashNormalizedObject({ name, command }),
528
- contentText: command,
529
- };
530
- });
531
- }
532
-
533
- function detectWorkflowComponents(cwd, options = {}) {
534
- const workflowsRoot = path.join(cwd, ".github", "workflows");
535
- if (!fs.existsSync(workflowsRoot)) return [];
536
-
537
- return listFiles(workflowsRoot, cwd, { scanSession: options.scanSession })
538
- .filter((absPath) => fs.statSync(absPath).isFile())
539
- .map((absPath) => ({
540
- mtimeMs: statMeta(absPath).mtimeMs,
541
- name: path.parse(absPath).name,
542
- type: "workflow",
543
- sourcePath: relPath(cwd, absPath),
544
- sourceKind: "local",
545
- contentHash: hashFile(cwd, absPath),
546
- contentText: readTextAbs(absPath),
547
- }));
548
- }
549
-
550
- function detectSensitiveSurfaceComponents(cwd, options = {}) {
551
- const out = [];
552
- const seen = new Set();
553
-
554
- function pushComponent(component) {
555
- if (seen.has(component.sourcePath)) return;
556
- seen.add(component.sourcePath);
557
- out.push(component);
558
- }
559
-
560
- try {
561
- fs.readdirSync(cwd, { withFileTypes: true }).forEach((entry) => {
562
- if (!entry.isFile()) return;
563
- if (!/^\.env(?:\..+)?$/i.test(entry.name)) return;
564
- const absPath = path.join(cwd, entry.name);
565
- pushComponent({
566
- mtimeMs: statMeta(absPath).mtimeMs,
567
- name: entry.name,
568
- type: "sensitive-surface",
569
- sourcePath: entry.name,
570
- sourceKind: "local",
571
- contentHash: hashPathStat(cwd, absPath),
572
- contentText: `Sensitive surface present: ${entry.name}`,
573
- });
574
- });
575
- } catch {
576
- // Ignore missing root read access and keep scan local-first.
577
- }
578
-
579
- [".ssh", ".aws", ".gcp", ".azure"].forEach((dirName) => {
580
- const absPath = path.join(cwd, dirName);
581
- if (!fs.existsSync(absPath)) return;
582
- pushComponent({
583
- mtimeMs: statMeta(absPath).mtimeMs,
584
- name: dirName.replace(/^\./, ""),
585
- type: "sensitive-surface",
586
- sourcePath: normalizePath(dirName),
587
- sourceKind: "local",
588
- contentHash: hashPathStat(cwd, absPath),
589
- contentText: `Sensitive directory present: ${dirName}`,
590
- });
591
- });
592
-
593
- listFiles(cwd, cwd, { maxDepth: 3, scanSession: options.scanSession }).forEach((absPath) => {
594
- const fileRel = relPath(cwd, absPath);
595
- if (fileRel.startsWith(".claude/cco/")) return;
596
- const baseName = path.basename(absPath).toLowerCase();
597
- if (!/(secret|token|private.?key|id_rsa|id_dsa|id_ed25519|credential)/i.test(baseName)) return;
598
- pushComponent({
599
- mtimeMs: statMeta(absPath).mtimeMs,
600
- name: path.basename(absPath),
601
- type: "sensitive-surface",
602
- sourcePath: fileRel,
603
- sourceKind: "local",
604
- contentHash: hashPathStat(cwd, absPath),
605
- contentText: `Sensitive file naming pattern detected: ${path.basename(absPath)}`,
606
- });
607
- });
608
-
609
- return out.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
610
- }
611
-
612
- function hasPattern(text, pattern) {
613
- return pattern.test(String(text || ""));
614
- }
615
-
616
- function inferCapabilities(component) {
617
- const capabilities = emptyCapabilities();
618
- const entry = component.rawEntry || {};
619
- const text = `${component.name}\n${component.sourcePath}\n${component.contentText}\n${component.commandText || ""}`;
620
- const lowerText = text.toLowerCase();
621
-
622
- if (component.type === "sensitive-surface") {
623
- capabilities.filesystemRead = true;
624
- capabilities.sensitiveAccess = true;
625
- }
626
-
627
- if (component.type === "mcp") {
628
- capabilities.mcpRead = true;
629
- capabilities.filesystemRead = true;
630
- }
631
-
632
- if (component.type === "package-script") {
633
- capabilities.shell = true;
634
- }
635
-
636
- if (component.type === "command") {
637
- capabilities.shell = true;
638
- }
639
-
640
- if (/\b(read|cat|open|grep|search|inspect|review|list files|find files|docs)\b/i.test(text)) {
641
- capabilities.filesystemRead = true;
642
- }
643
-
644
- if (/\b(write|modify|edit|delete|remove-item|rm\s+-rf|touch|truncate|mkdir|copy-item|move-item|tee\b|git commit|git push|workflow file|update workflow)\b/i.test(text)) {
645
- capabilities.filesystemWrite = true;
646
- }
647
-
648
- if (/\b(shell|terminal|bash|sh\b|pwsh|powershell|cmd(?:\.exe)?\b|subprocess|spawn|exec|npm run|pnpm run|yarn run|make\b)\b/i.test(text)) {
649
- capabilities.shell = true;
650
- }
651
-
652
- if (/\b(curl|wget|invoke-webrequest|invoke-restmethod|downloadstring|https?:\/\/|gh api|npm install|pnpm add|yarn add|docker pull|npx\b|pnpm dlx|yarn dlx|uvx)\b/i.test(text)) {
653
- capabilities.network = true;
654
- }
655
-
656
- if (/\b(secret|secrets|token|tokens|private key|private keys|ssh key|ssh keys|\.env\b|credentials?|aws|gcp|azure)\b/i.test(text)) {
657
- capabilities.sensitiveAccess = true;
658
- }
659
-
660
- if (component.type === "mcp") {
661
- const nameAndCommand = `${component.name}\n${component.commandText || ""}\n${stableStringify(entry)}`;
662
- if (/\b(write|delete|admin|workflow|workflows|secret|secrets|token|tokens|push|merge|deploy|prod|production|create pr|pull request)\b/i.test(nameAndCommand)) {
663
- capabilities.mcpWrite = true;
664
- capabilities.externalMutation = true;
665
- }
666
- if (Array.isArray(entry.capabilities)) {
667
- const joined = entry.capabilities.join(" ").toLowerCase();
668
- if (/\bwrite|mutate|admin|delete\b/.test(joined)) {
669
- capabilities.mcpWrite = true;
670
- capabilities.externalMutation = true;
671
- }
672
- }
673
- if (entry.transport && /https?|sse|stream/i.test(String(entry.transport))) {
674
- capabilities.network = true;
675
- }
676
- }
677
-
678
- if (component.type === "workflow" && /\b(deploy|publish|release|workflow_dispatch|secrets?|gh api|actions\/checkout|permissions: write-all)\b/i.test(text)) {
679
- capabilities.externalMutation = true;
680
- }
681
-
682
- if (component.type === "config" && /\bmcpservers\b/i.test(lowerText)) {
683
- capabilities.mcpRead = true;
684
- }
685
-
686
- return capabilities;
687
- }
688
-
689
- function deriveTrustLevel(component) {
690
- if (component.type !== "mcp") {
691
- return component.sourceKind === "local" || component.sourceKind === "config" ? "local" : "unknown";
692
- }
693
-
694
- const entry = component.rawEntry || {};
695
- const command = component.commandText || "";
696
- const image = String(entry.image || "");
697
- const text = `${component.name}\n${component.contentText}\n${command}\n${image}`;
698
-
699
- const localScriptPath = String(entry.command || "");
700
- if (localScriptPath && /^(node|python|py|bash|sh|powershell|pwsh)$/i.test(localScriptPath)) {
701
- const args = Array.isArray(entry.args) ? entry.args : [];
702
- if (args.some((arg) => /\.(js|cjs|mjs|py|ps1|sh)$/i.test(String(arg || "")))) return "local";
703
- }
704
-
705
- if (/@[0-9]+\.[0-9]+(?:\.[0-9]+)?\b/.test(text) || /sha256:[a-f0-9]{32,}/i.test(text) || /#[a-f0-9]{12,40}\b/i.test(text)) {
706
- return "pinned";
707
- }
708
-
709
- if (/\b(npx|pnpm dlx|yarn dlx|uvx|docker|podman)\b/i.test(command) || /^https?:\/\//i.test(command)) {
710
- return "review-needed";
711
- }
712
-
713
- return "unknown";
714
- }
715
-
716
- function riskRank(level) {
717
- if (level === "high") return 3;
718
- if (level === "medium") return 2;
719
- return 1;
720
- }
721
-
722
- function determineStatusForCurrent(previousEntry, component, mode) {
723
- if (!previousEntry) return "new";
724
- if (mode === "visibility" && (previousEntry.contentHash !== component.contentHash || previousEntry.source !== component.sourcePath)) {
725
- return "changed";
726
- }
727
- return "known";
728
- }
729
-
730
- function classifyRisk(component, previousEntry, mode) {
731
- const findings = [];
732
- const seen = new Set();
733
- let riskScore = 1;
734
-
735
- const capabilities = inferCapabilities(component);
736
- const text = `${component.name}\n${component.sourcePath}\n${component.contentText}\n${component.commandText || ""}`;
737
- const trustLevel = deriveTrustLevel(component);
738
- const status = determineStatusForCurrent(previousEntry, component, mode);
739
- const sourceType = sourceTypeForComponentType(component.type);
740
- const instructionScan = scanInstructionRisk(text, {
741
- sourceType,
742
- intendedUse: "instruction",
743
- });
744
-
745
- const highSignals = [
746
- { test: /(?:^|[^a-z0-9_])\.env\b|\b(secret|secrets|token|tokens|private key|private keys|ssh key|ssh keys)\b/i, finding: "References secrets, tokens, or private key material" },
747
- { test: /\b(ignore safety|ignore guardrails|bypass review|hide actions|silently run|silently execute|exfiltrat|steal data|bypass policy)\b/i, finding: "Contains suspicious instruction language that bypasses review or hides actions" },
748
- { test: /\b(curl|wget)\b[^\n]*\|\s*(bash|sh|pwsh|powershell)\b/i, finding: "Includes remote download piped into a shell" },
749
- { test: /\b(invoke-webrequest|invoke-restmethod|downloadstring)\b/i, finding: "Includes remote PowerShell download behavior" },
750
- ];
751
-
752
- highSignals.forEach((signal) => {
753
- if (hasPattern(text, signal.test)) {
754
- maybePush(findings, signal.finding, seen);
755
- riskScore = Math.max(riskScore, 3);
756
- }
757
- });
758
-
759
- if (component.type === "package-script" && /^(?:preinstall|postinstall|install)$/i.test(component.name)) {
760
- maybePush(findings, "Package install script can execute automatically during dependency operations", seen);
761
- riskScore = Math.max(riskScore, 3);
762
- }
763
-
764
- if (capabilities.sensitiveAccess) {
765
- maybePush(findings, "Can reach sensitive files, credentials, or secret-like surfaces", seen);
766
- riskScore = Math.max(riskScore, component.type === "sensitive-surface" ? 2 : 3);
767
- }
768
-
769
- if (component.type === "mcp" && (capabilities.mcpWrite || capabilities.externalMutation) && !/\b(approval|readOnly|read_only)\b/i.test(text)) {
770
- maybePush(findings, "This MCP can modify external systems or has elevated posture", seen);
771
- riskScore = Math.max(riskScore, 3);
772
- }
773
-
774
- if (component.type === "workflow" && /\b(workflows?|permissions:\s*write-all|secrets?|deploy|publish|release)\b/i.test(text)) {
775
- maybePush(findings, "Workflow can affect automation, releases, or secret-backed execution", seen);
776
- riskScore = Math.max(riskScore, 3);
777
- }
778
-
779
- if (status === "new") {
780
- maybePush(findings, "New AI component detected", seen);
781
- riskScore = Math.max(riskScore, 2);
782
- }
783
- if (status === "changed" && mode === "visibility") {
784
- maybePush(findings, "Component changed since the previous recorded scan", seen);
785
- riskScore = Math.max(riskScore, 2);
786
- }
787
-
788
- if (component.sourceKind === "unknown") {
789
- maybePush(findings, "Source could not be verified locally", seen);
790
- riskScore = Math.max(riskScore, 2);
791
- }
792
- if (trustLevel === "unknown") {
793
- maybePush(findings, "Trust level is still unknown", seen);
794
- riskScore = Math.max(riskScore, 2);
795
- }
796
- if (trustLevel === "review-needed") {
797
- maybePush(findings, "Source should be reviewed before use", seen);
798
- riskScore = Math.max(riskScore, 2);
799
- }
800
-
801
- if (capabilities.filesystemWrite) {
802
- maybePush(findings, "Can modify local files or project automation", seen);
803
- riskScore = Math.max(riskScore, 2);
804
- }
805
- if (capabilities.shell) {
806
- maybePush(findings, "Can run shell commands or subprocesses", seen);
807
- riskScore = Math.max(riskScore, 2);
808
- }
809
- if (capabilities.network) {
810
- maybePush(findings, "Can reach remote network resources or pull remote tooling", seen);
811
- riskScore = Math.max(riskScore, 2);
812
- }
813
- if (component.type === "package-script" && /\b(build|prepare|compile|bundle)\b/i.test(component.name)) {
814
- maybePush(findings, "Package script can mutate generated outputs or build artifacts", seen);
815
- riskScore = Math.max(riskScore, 2);
816
- }
817
-
818
- instructionScan.findings.forEach((finding) => {
819
- maybePush(findings, finding.message, seen);
820
- });
821
- riskScore = Math.max(riskScore, riskRank(instructionScan.instructionRisk));
822
-
823
- const riskLevel = riskScore >= 3 ? "high" : riskScore >= 2 ? "medium" : "low";
824
- if (findings.length === 0) {
825
- findings.push("No obvious risky indicators found");
826
- }
827
-
828
- let recommendation = "No high-risk findings";
829
- if (riskLevel === "high") recommendation = "Review before use";
830
- else if (riskLevel === "medium" || status === "new" || status === "changed") recommendation = "Review details before use";
831
-
832
- return {
833
- status,
834
- trustLevel,
835
- riskLevel,
836
- findings: findings.slice(0, 6),
837
- recommendation,
838
- requiresCheck: status === "new" || status === "changed" || riskLevel !== "low",
839
- capabilities,
840
- instructionRisk: instructionScan.instructionRisk,
841
- instructionSignals: instructionScan.signals,
842
- instructionFindings: instructionScan.findings.slice(0, 8),
843
- sourceType,
844
- };
845
- }
846
-
847
- function statMeta(absPath) {
848
- try {
849
- const stat = fs.statSync(absPath);
850
- return {
851
- mtimeMs: Number(stat.mtimeMs || 0),
852
- size: Number(stat.size || 0),
853
- };
854
- } catch {
855
- return {
856
- mtimeMs: 0,
857
- size: 0,
858
- };
859
- }
860
- }
861
-
862
- function classifyRiskCached(cwd, component, previousEntry, mode, tracker) {
863
- const size = Buffer.byteLength(String(component.contentText || ""), "utf8");
864
- const expectedStatus = determineStatusForCurrent(previousEntry, component, mode);
865
- const cacheKey = buildCacheKey({
866
- scannerVersion: AGENT_SECURITY_SCANNER_VERSION,
867
- type: component.type,
868
- path: component.sourcePath,
869
- mode,
870
- contentHash: component.contentHash,
871
- });
872
- const cached = cacheLookup(cwd, {
873
- cacheKey,
874
- path: component.sourcePath,
875
- mtimeMs: Number(component.mtimeMs || 0),
876
- size,
877
- contentHash: component.contentHash,
878
- scannerVersion: AGENT_SECURITY_SCANNER_VERSION,
879
- });
880
- if (cached && cached.result) {
881
- if (cached.result.status === expectedStatus) {
882
- if (tracker) tracker.noteCacheHit();
883
- return cached.result;
884
- }
885
- }
886
-
887
- if (tracker) tracker.noteCacheMiss();
888
- const result = classifyRisk(component, previousEntry, mode);
889
- updateScanCache(cwd, AGENT_SECURITY_SCANNER_VERSION, {
890
- cacheKey,
891
- path: component.sourcePath,
892
- mtimeMs: Number(component.mtimeMs || 0),
893
- size,
894
- contentHash: component.contentHash,
895
- scannerVersion: AGENT_SECURITY_SCANNER_VERSION,
896
- lastScannedAt: nowIso(),
897
- result,
898
- });
899
- return result;
900
- }
901
-
902
- function toSnapshotComponent(component) {
903
- return {
904
- id: component.id,
905
- type: component.type,
906
- name: component.name,
907
- source: component.source,
908
- sourceKind: component.sourceKind,
909
- status: component.status,
910
- riskLevel: component.riskLevel,
911
- trustLevel: component.trustLevel,
912
- sourceType: component.sourceType || null,
913
- sourceTrust: component.sourceTrust || null,
914
- instructionRisk: component.instructionRisk || "low",
915
- instructionSignals: component.instructionSignals || [],
916
- instructionFindings: component.instructionFindings || [],
917
- contentHash: component.contentHash,
918
- capabilities: component.capabilities,
919
- findings: component.findings,
920
- recommendation: component.recommendation,
921
- firstSeenAt: component.firstSeenAt,
922
- lastSeenAt: component.lastSeenAt,
923
- lastCheckedAt: component.lastCheckedAt,
924
- };
925
- }
926
-
927
- function normalizeSnapshotItems(snapshot, mode) {
928
- const scoped = snapshot && snapshot.scopedComponents && typeof snapshot.scopedComponents === "object"
929
- ? snapshot.scopedComponents
930
- : {};
931
- const scopedItems = mode && Array.isArray(scoped[mode]) ? scoped[mode] : null;
932
- const fallbackItems = Array.isArray(snapshot?.components)
933
- ? snapshot.components
934
- : snapshot?.components && typeof snapshot.components === "object"
935
- ? Object.values(snapshot.components)
936
- : [];
937
- return scopedItems || fallbackItems;
938
- }
939
-
940
- function loadPreviousSnapshot(cwd, mode = "basic") {
941
- const snapshot = safeReadJson(cwd, SNAPSHOT_REL_PATH, null);
942
- if (!snapshot || typeof snapshot !== "object") {
943
- return {
944
- snapshotVersion: 2,
945
- generatedAt: null,
946
- scope: mode,
947
- scanMode: null,
948
- scopedComponents: {},
949
- components: [],
950
- componentsById: {},
951
- raw: null,
952
- };
953
- }
954
-
955
- const componentItems = normalizeSnapshotItems(snapshot, mode);
956
- const componentsById = {};
957
- componentItems.forEach((item) => {
958
- if (item && item.id) componentsById[item.id] = item;
959
- });
960
-
961
- return {
962
- snapshotVersion: Number(snapshot.snapshotVersion || 1),
963
- generatedAt: snapshot.generatedAt || null,
964
- scope: mode,
965
- scanMode: snapshot.scanMode || null,
966
- scopedComponents: snapshot.scopedComponents || {},
967
- components: componentItems,
968
- componentsById,
969
- raw: snapshot,
970
- };
971
- }
972
-
973
- function buildMergedSnapshot(previousSnapshot, mode, components, generatedAt) {
974
- const snapshotComponents = components.map((component) => toSnapshotComponent(component));
975
- const priorScoped = previousSnapshot?.raw?.scopedComponents && typeof previousSnapshot.raw.scopedComponents === "object"
976
- ? previousSnapshot.raw.scopedComponents
977
- : previousSnapshot?.scopedComponents && typeof previousSnapshot.scopedComponents === "object"
978
- ? previousSnapshot.scopedComponents
979
- : {};
980
- const priorGeneratedAts = previousSnapshot?.raw?.scopedGeneratedAts && typeof previousSnapshot.raw.scopedGeneratedAts === "object"
981
- ? previousSnapshot.raw.scopedGeneratedAts
982
- : {};
983
-
984
- return {
985
- snapshotVersion: 2,
986
- generatedAt,
987
- scanMode: mode,
988
- componentCount: snapshotComponents.length,
989
- components: snapshotComponents,
990
- scopedComponents: {
991
- ...priorScoped,
992
- [mode]: snapshotComponents,
993
- },
994
- scopedGeneratedAts: {
995
- ...priorGeneratedAts,
996
- [mode]: generatedAt,
997
- },
998
- };
999
- }
1000
-
1001
- function sortTrustCards(cards) {
1002
- return cards.sort((left, right) => {
1003
- const severityRank = { high: 3, medium: 2, low: 1 };
1004
- if (severityRank[right.riskLevel] !== severityRank[left.riskLevel]) {
1005
- return severityRank[right.riskLevel] - severityRank[left.riskLevel];
1006
- }
1007
- const statusRank = { new: 0, changed: 1, removed: 2, known: 3 };
1008
- if ((statusRank[left.status] ?? 99) !== (statusRank[right.status] ?? 99)) {
1009
- return (statusRank[left.status] ?? 99) - (statusRank[right.status] ?? 99);
1010
- }
1011
- if (left.type !== right.type) return left.type.localeCompare(right.type);
1012
- return left.name.localeCompare(right.name);
1013
- });
1014
- }
1015
-
1016
- function buildRemovedTrustCard(previousEntry, generatedAt) {
1017
- return {
1018
- id: previousEntry.id,
1019
- type: previousEntry.type,
1020
- name: previousEntry.name,
1021
- source: previousEntry.source,
1022
- sourceKind: previousEntry.sourceKind || "local",
1023
- status: "removed",
1024
- requiresCheck: false,
1025
- riskLevel: previousEntry.riskLevel || "low",
1026
- trustLevel: previousEntry.trustLevel || "unknown",
1027
- sourceType: previousEntry.sourceType || sourceTypeForComponentType(previousEntry.type),
1028
- sourceTrust: previousEntry.sourceTrust || null,
1029
- instructionRisk: previousEntry.instructionRisk || "low",
1030
- instructionSignals: previousEntry.instructionSignals || [],
1031
- instructionFindings: previousEntry.instructionFindings || [],
1032
- findings: ["Component no longer detected locally"],
1033
- recommendation: "Confirm removal was intentional",
1034
- contentHash: previousEntry.contentHash || null,
1035
- capabilities: previousEntry.capabilities || emptyCapabilities(),
1036
- firstSeenAt: previousEntry.firstSeenAt || generatedAt,
1037
- lastSeenAt: previousEntry.lastSeenAt || generatedAt,
1038
- lastCheckedAt: generatedAt,
1039
- };
1040
- }
1041
-
1042
- function detectComponentsForMode(cwd, mode, options = {}) {
1043
- const base = [...detectSkillComponents(cwd, options), ...detectCommandComponents(cwd, options), ...detectMcpComponents(cwd)];
1044
- if (mode !== "visibility") return base;
1045
- return [
1046
- ...base,
1047
- ...detectConfigComponents(cwd),
1048
- ...detectCursorRuleComponents(cwd, options),
1049
- ...detectPackageScriptComponents(cwd),
1050
- ...detectWorkflowComponents(cwd, options),
1051
- ...detectSensitiveSurfaceComponents(cwd, options),
1052
- ];
1053
- }
1054
-
1055
- function buildTrustCardsForMode(cwd, previousSnapshot, generatedAt, mode, options = {}) {
1056
- const tracker = options.tracker || null;
1057
- const currentCards = detectComponentsForMode(cwd, mode, options).map((component) => {
1058
- const id = createComponentId(component.type, component.name, component.sourcePath);
1059
- const previousEntry = previousSnapshot.componentsById[id] || null;
1060
- const classification = classifyRiskCached(cwd, component, previousEntry, mode, tracker);
1061
- const sourceTrust = buildSourceRecord({
1062
- sourceId: id,
1063
- sourceType: classification.sourceType,
1064
- origin: component.type === "mcp" ? component.rawConfigPath || component.sourcePath : component.sourcePath,
1065
- path: component.sourcePath,
1066
- content: component.contentText,
1067
- instructionRisk: classification.instructionRisk,
1068
- findings: classification.instructionFindings,
1069
- intendedUse: ["skill_instruction", "claude_command", "cursor_rule", "mcp_config"].includes(classification.sourceType)
1070
- ? "instruction"
1071
- : "evidence",
1072
- createdAt: previousEntry?.sourceTrust?.createdAt || previousEntry?.firstSeenAt || generatedAt,
1073
- updatedAt: generatedAt,
1074
- remediationActions: previousEntry?.sourceTrust?.remediationActions || [],
1075
- });
1076
- const firstSeenAt = previousEntry?.firstSeenAt || generatedAt;
1077
-
1078
- return {
1079
- id,
1080
- type: component.type,
1081
- name: component.name,
1082
- source: component.sourcePath,
1083
- sourceKind: component.sourceKind,
1084
- status: classification.status,
1085
- requiresCheck: classification.requiresCheck,
1086
- riskLevel: classification.riskLevel,
1087
- trustLevel: classification.trustLevel,
1088
- sourceType: classification.sourceType,
1089
- sourceTrust,
1090
- instructionRisk: classification.instructionRisk,
1091
- instructionSignals: classification.instructionSignals,
1092
- instructionFindings: classification.instructionFindings,
1093
- findings: classification.findings,
1094
- recommendation: classification.recommendation,
1095
- contentHash: component.contentHash,
1096
- capabilities: classification.capabilities,
1097
- firstSeenAt,
1098
- lastSeenAt: generatedAt,
1099
- lastCheckedAt: generatedAt,
1100
- };
1101
- });
1102
-
1103
- if (mode !== "visibility") {
1104
- return sortTrustCards(currentCards);
1105
- }
1106
-
1107
- const currentIds = new Set(currentCards.map((card) => card.id));
1108
- const removedCards = previousSnapshot.components
1109
- .filter((item) => item && item.id && !currentIds.has(item.id))
1110
- .map((item) => buildRemovedTrustCard(item, generatedAt));
1111
-
1112
- return sortTrustCards([...currentCards, ...removedCards]);
1113
- }
1114
-
1115
- function summarizeTrustCards(mode, trustCards, generatedAt, persistedSummary = null, extras = {}) {
1116
- const presentCards = trustCards.filter((card) => card.status !== "removed");
1117
- const counts = {
1118
- componentsScanned: presentCards.length,
1119
- newComponents: trustCards.filter((card) => card.status === "new").length,
1120
- changedComponents: trustCards.filter((card) => card.status === "changed").length,
1121
- removedComponents: trustCards.filter((card) => card.status === "removed").length,
1122
- highRisk: presentCards.filter((card) => card.riskLevel === "high").length,
1123
- mediumRisk: presentCards.filter((card) => card.riskLevel === "medium").length,
1124
- lowRisk: presentCards.filter((card) => card.riskLevel === "low").length,
1125
- suspiciousInstructionSources: presentCards.filter((card) => card.instructionRisk === "medium" || card.instructionRisk === "high").length,
1126
- highInstructionFindings: presentCards.reduce((sum, card) => sum + (Array.isArray(card.instructionFindings)
1127
- ? card.instructionFindings.filter((item) => item.severity === "high").length
1128
- : 0), 0),
1129
- sourcesTreatedAsData: presentCards.filter((card) => card.sourceTrust && card.sourceTrust.allowedToInfluenceTools === false).length,
1130
- };
1131
-
1132
- const newComponents = trustCards
1133
- .filter((card) => card.status === "new")
1134
- .slice(0, 5)
1135
- .map((card) => ({
1136
- id: card.id,
1137
- name: card.name,
1138
- type: card.type,
1139
- }));
1140
-
1141
- const topFindings = trustCards
1142
- .filter((card) => card.riskLevel !== "low" || card.status !== "known")
1143
- .slice(0, 5)
1144
- .map((card) => ({
1145
- id: card.id,
1146
- name: card.name,
1147
- type: card.type,
1148
- riskLevel: card.riskLevel,
1149
- status: card.status,
1150
- recommendation: card.recommendation,
1151
- }));
1152
-
1153
- if (mode === "visibility") {
1154
- return {
1155
- summaryVersion: 2,
1156
- what: "Agent Security Visibility summary.",
1157
- mode,
1158
- generatedAt,
1159
- lastCompletedCheckAt: persistedSummary?.generatedAt || null,
1160
- componentsScanned: counts.componentsScanned,
1161
- newComponents: counts.newComponents,
1162
- changedComponents: counts.changedComponents,
1163
- removedComponents: counts.removedComponents,
1164
- highRisk: counts.highRisk,
1165
- mediumRisk: counts.mediumRisk,
1166
- lowRisk: counts.lowRisk,
1167
- suspiciousInstructionSources: counts.suspiciousInstructionSources,
1168
- highInstructionFindings: counts.highInstructionFindings,
1169
- sourcesTreatedAsData: counts.sourcesTreatedAsData,
1170
- riskIncreases: Number(extras.riskIncreases || 0),
1171
- permissionExpansions: Number(extras.permissionExpansions || 0),
1172
- permissionPreviews: Number(extras.permissionPreviews || 0),
1173
- newComponentNames: newComponents.map((item) => item.name),
1174
- newComponentsList: newComponents,
1175
- topFindings,
1176
- externalScannersRequired: false,
1177
- };
1178
- }
1179
-
1180
- return {
1181
- summaryVersion: 1,
1182
- what: "Agent Security Basic Check summary.",
1183
- mode,
1184
- generatedAt,
1185
- lastCompletedCheckAt: persistedSummary?.generatedAt || null,
1186
- componentsScanned: counts.componentsScanned,
1187
- newComponents: counts.newComponents,
1188
- highRisk: counts.highRisk,
1189
- mediumRisk: counts.mediumRisk,
1190
- lowRisk: counts.lowRisk,
1191
- suspiciousInstructionSources: counts.suspiciousInstructionSources,
1192
- highInstructionFindings: counts.highInstructionFindings,
1193
- sourcesTreatedAsData: counts.sourcesTreatedAsData,
1194
- newComponentNames: newComponents.map((item) => item.name),
1195
- newComponentsList: newComponents,
1196
- topFindings,
1197
- externalScannersRequired: false,
1198
- };
1199
- }
1200
-
1201
- function capabilityExpansions(previousCapabilities, currentCapabilities) {
1202
- const expansions = [];
1203
- CAPABILITY_KEYS.forEach((key) => {
1204
- if (!previousCapabilities?.[key] && currentCapabilities?.[key]) expansions.push(key);
1205
- });
1206
- return expansions;
1207
- }
1208
-
1209
- function buildRiskDiff(previousSnapshot, trustCards, generatedAt) {
1210
- const currentPresent = trustCards.filter((card) => card.status !== "removed");
1211
- const currentById = {};
1212
- currentPresent.forEach((card) => {
1213
- currentById[card.id] = card;
1214
- });
1215
-
1216
- const changes = [];
1217
- let added = 0;
1218
- let changed = 0;
1219
- let removed = 0;
1220
- let riskIncreases = 0;
1221
- let permissionExpansions = 0;
1222
-
1223
- currentPresent.forEach((card) => {
1224
- const previousEntry = previousSnapshot.componentsById[card.id] || null;
1225
- if (!previousEntry) {
1226
- added += 1;
1227
- changes.push({
1228
- id: card.id,
1229
- name: card.name,
1230
- type: card.type,
1231
- changeType: "added",
1232
- previousRisk: null,
1233
- currentRisk: card.riskLevel,
1234
- reasons: ["New component added to the local agent surface"],
1235
- });
1236
- return;
1237
- }
1238
-
1239
- const reasons = [];
1240
- const permissionExpansionKeys = capabilityExpansions(previousEntry.capabilities, card.capabilities);
1241
- if (previousEntry.contentHash !== card.contentHash) reasons.push("Component content hash changed");
1242
- if (previousEntry.source !== card.source) reasons.push("Component source path changed");
1243
- if (permissionExpansionKeys.length > 0) {
1244
- permissionExpansions += 1;
1245
- reasons.push(`Permission expansion: ${permissionExpansionKeys.join(", ")}`);
1246
- }
1247
- if (riskRank(card.riskLevel) > riskRank(previousEntry.riskLevel)) {
1248
- riskIncreases += 1;
1249
- reasons.push(`Risk increased from ${previousEntry.riskLevel || "low"} to ${card.riskLevel}`);
1250
- }
1251
- if (!reasons.length) return;
1252
-
1253
- changed += 1;
1254
- changes.push({
1255
- id: card.id,
1256
- name: card.name,
1257
- type: card.type,
1258
- changeType: "changed",
1259
- previousRisk: previousEntry.riskLevel || "low",
1260
- currentRisk: card.riskLevel,
1261
- reasons,
1262
- });
1263
- });
1264
-
1265
- previousSnapshot.components.forEach((previousEntry) => {
1266
- if (currentById[previousEntry.id]) return;
1267
- removed += 1;
1268
- changes.push({
1269
- id: previousEntry.id,
1270
- name: previousEntry.name,
1271
- type: previousEntry.type,
1272
- changeType: "removed",
1273
- previousRisk: previousEntry.riskLevel || "low",
1274
- currentRisk: null,
1275
- reasons: ["Component was present in the previous recorded scan but is no longer detected"],
1276
- });
1277
- });
1278
-
1279
- return {
1280
- generatedAt,
1281
- summary: {
1282
- added,
1283
- changed,
1284
- removed,
1285
- riskIncreases,
1286
- permissionExpansions,
1287
- },
1288
- changes,
1289
- };
1290
- }
1291
-
1292
- function buildPermissionPreview(trustCards, generatedAt) {
1293
- const previews = trustCards
1294
- .filter((card) => card.status !== "removed")
1295
- .filter((card) => {
1296
- const caps = card.capabilities || emptyCapabilities();
1297
- return (
1298
- card.type === "mcp" ||
1299
- card.type === "package-script" ||
1300
- card.type === "workflow" ||
1301
- card.type === "sensitive-surface" ||
1302
- caps.filesystemWrite ||
1303
- caps.shell ||
1304
- caps.network ||
1305
- caps.externalMutation ||
1306
- caps.sensitiveAccess
1307
- );
1308
- })
1309
- .map((card) => {
1310
- const currentCapabilities = CAPABILITY_KEYS.filter((key) => card.capabilities?.[key]);
1311
- let recommendedMode = "read-only";
1312
- let ttlRecommendation = "No TTL needed for read-only components.";
1313
- const wouldAllow = [];
1314
- const wouldLimit = [];
1315
- let reason = "Local read-only components can stay visible without tighter controls.";
1316
-
1317
- if (card.capabilities?.externalMutation || card.capabilities?.mcpWrite || card.capabilities?.sensitiveAccess) {
1318
- recommendedMode = "approval-required";
1319
- ttlRecommendation = "30 minutes for write actions";
1320
- reason = "External mutation or sensitive access should be scoped and time-bound.";
1321
- } else if (card.capabilities?.filesystemWrite || card.capabilities?.shell || card.capabilities?.network) {
1322
- recommendedMode = "scoped-write";
1323
- ttlRecommendation = "15 minutes for shell or write-capable actions";
1324
- reason = "Write-capable or shell-capable components should run with a bounded scope.";
1325
- }
1326
-
1327
- if (card.type === "mcp") {
1328
- wouldAllow.push("read repository metadata");
1329
- if (card.capabilities?.mcpWrite || card.capabilities?.externalMutation) {
1330
- wouldAllow.push("create pull request or issue updates with review");
1331
- wouldLimit.push("no delete actions");
1332
- wouldLimit.push("no workflow or secrets modification");
1333
- wouldLimit.push("no push to main without approval");
1334
- } else {
1335
- wouldAllow.push("read external system state");
1336
- wouldLimit.push("no external write actions");
1337
- }
1338
- } else if (card.type === "package-script" || card.capabilities?.shell) {
1339
- wouldAllow.push("run declared local commands");
1340
- wouldLimit.push("no remote script execution without review");
1341
- wouldLimit.push("no background shell escalation");
1342
- } else if (card.type === "sensitive-surface" || card.capabilities?.sensitiveAccess) {
1343
- wouldAllow.push("inspect presence of sensitive surfaces");
1344
- wouldLimit.push("no secret value reads");
1345
- wouldLimit.push("no credential export or copying");
1346
- } else if (card.capabilities?.filesystemWrite) {
1347
- wouldAllow.push("modify project files within the working tree");
1348
- wouldLimit.push("no destructive delete actions");
1349
- } else {
1350
- wouldAllow.push("read local configuration");
1351
- wouldLimit.push("no write actions by default");
1352
- }
1353
-
1354
- return {
1355
- componentId: card.id,
1356
- name: card.name,
1357
- type: card.type,
1358
- currentCapabilities,
1359
- recommendedMode,
1360
- wouldAllow,
1361
- wouldLimit,
1362
- ttlRecommendation,
1363
- reason,
1364
- };
1365
- });
1366
-
1367
- return {
1368
- generatedAt,
1369
- previews,
1370
- };
1371
- }
1372
-
1373
- function summaryPathForMode(mode) {
1374
- return mode === "visibility" ? VISIBILITY_SUMMARY_REL_PATH : SUMMARY_REL_PATH;
1375
- }
1376
-
1377
- function loadLatestAgentSecuritySummary(cwd, mode = "basic") {
1378
- return safeReadJson(cwd, summaryPathForMode(mode), null);
1379
- }
1380
-
1381
- function loadTrustCardsEvidence(cwd) {
1382
- const doc = safeReadJson(cwd, TRUST_CARDS_REL_PATH, null);
1383
- return Array.isArray(doc?.items) ? doc.items : [];
1384
- }
1385
-
1386
- function loadAgentSecurityEvidenceBundle(cwd, mode) {
1387
- return {
1388
- summary: loadLatestAgentSecuritySummary(cwd, mode),
1389
- trustCards: loadTrustCardsEvidence(cwd),
1390
- riskDiff: mode === "visibility" ? safeReadJson(cwd, RISK_DIFF_REL_PATH, null) : null,
1391
- permissionPreview: mode === "visibility" ? safeReadJson(cwd, PERMISSION_PREVIEW_REL_PATH, null) : null,
1392
- };
1393
- }
1394
-
1395
- function loadPersistedInventoryForActions(cwd) {
1396
- return {
1397
- trustCards: loadTrustCardsEvidence(cwd),
1398
- summary: loadLatestAgentSecuritySummary(cwd, "visibility") || loadLatestAgentSecuritySummary(cwd, "basic"),
1399
- };
1400
- }
1401
-
1402
- function buildSourceTrustDocument(generatedAt, trustCards) {
1403
- return {
1404
- sourceTrustVersion: 1,
1405
- what: "Agent Security source trust classifications.",
1406
- generatedAt,
1407
- items: trustCards
1408
- .filter((card) => card.status !== "removed")
1409
- .map((card) => ({
1410
- ...card.sourceTrust,
1411
- sourceId: card.id,
1412
- })),
1413
- };
1414
- }
1415
-
1416
- function buildInstructionRiskDocument(generatedAt, trustCards) {
1417
- const items = trustCards
1418
- .filter((card) => card.status !== "removed")
1419
- .filter((card) => card.instructionRisk !== "low" || (Array.isArray(card.instructionFindings) && card.instructionFindings.length > 0))
1420
- .map((card) => ({
1421
- sourceId: card.id,
1422
- sourceType: card.sourceType,
1423
- name: card.name,
1424
- source: card.source,
1425
- instructionRisk: card.instructionRisk,
1426
- signals: card.instructionSignals || [],
1427
- findings: (card.instructionFindings || []).map((finding) => ({
1428
- category: finding.category,
1429
- severity: finding.severity,
1430
- message: finding.message,
1431
- evidencePreview: boundedPreview(finding.evidencePreview || "", 120),
1432
- location: finding.location || null,
1433
- })),
1434
- recommendation: card.sourceTrust?.recommendation || card.recommendation,
1435
- }));
1436
-
1437
- return {
1438
- instructionRiskVersion: 1,
1439
- what: "Agent Security instruction risk findings.",
1440
- generatedAt,
1441
- counts: {
1442
- suspiciousSources: items.length,
1443
- highRiskSources: items.filter((item) => item.instructionRisk === "high").length,
1444
- highRiskFindings: items.reduce((sum, item) => sum + item.findings.filter((finding) => finding.severity === "high").length, 0),
1445
- },
1446
- items,
1447
- };
1448
- }
1449
-
1450
- function writeInstructionRiskEvidence(cwd, generatedAt, trustCards, mode, options = {}) {
1451
- const sourceTrustDoc = buildSourceTrustDocument(generatedAt, trustCards);
1452
- const instructionRiskDoc = buildInstructionRiskDocument(generatedAt, trustCards);
1453
- safeWriteJson(cwd, SOURCE_TRUST_REL_PATH, sourceTrustDoc);
1454
- safeWriteJson(cwd, INSTRUCTION_RISK_REL_PATH, instructionRiskDoc);
1455
-
1456
- if (options.writeAudit !== false) {
1457
- sourceTrustDoc.items.forEach((item) => {
1458
- appendJsonl(cwd, INSTRUCTION_AUDIT_LOG_REL_PATH, {
1459
- eventType: "agent_security_source_trust_classified",
1460
- mode,
1461
- sourceId: item.sourceId,
1462
- sourceType: item.sourceType,
1463
- trustLevel: item.trustLevel,
1464
- instructionRisk: item.instructionRisk,
1465
- signals: [],
1466
- reason: item.recommendation,
1467
- timestamp: generatedAt,
1468
- });
1469
- });
1470
- instructionRiskDoc.items.forEach((item) => {
1471
- appendJsonl(cwd, INSTRUCTION_AUDIT_LOG_REL_PATH, {
1472
- eventType: "agent_security_instruction_risk_detected",
1473
- mode,
1474
- sourceId: item.sourceId,
1475
- sourceType: item.sourceType,
1476
- trustLevel: sourceTrustDoc.items.find((source) => source.sourceId === item.sourceId)?.trustLevel || "unknown",
1477
- instructionRisk: item.instructionRisk,
1478
- signals: item.signals,
1479
- reason: item.recommendation,
1480
- timestamp: generatedAt,
1481
- });
1482
- });
1483
- }
1484
-
1485
- return {
1486
- sourceTrustPath: SOURCE_TRUST_REL_PATH,
1487
- instructionRiskPath: INSTRUCTION_RISK_REL_PATH,
1488
- auditPath: INSTRUCTION_AUDIT_LOG_REL_PATH,
1489
- };
1490
- }
1491
-
1492
- function loadInstructionRemediations(cwd) {
1493
- const doc = safeReadJson(cwd, INSTRUCTION_REMEDIATIONS_REL_PATH, null);
1494
- if (!doc || typeof doc !== "object" || !Array.isArray(doc.items)) {
1495
- return {
1496
- remediationVersion: 1,
1497
- items: [],
1498
- };
1499
- }
1500
- return doc;
1501
- }
1502
-
1503
- function activeRemediationActionsBySource(cwd) {
1504
- const doc = loadInstructionRemediations(cwd);
1505
- const bySource = new Map();
1506
- doc.items.forEach((item) => {
1507
- if (!item || !item.sourceId) return;
1508
- const actions = bySource.get(item.sourceId) || [];
1509
- actions.push(String(item.action || ""));
1510
- bySource.set(item.sourceId, actions);
1511
- });
1512
- return bySource;
1513
- }
1514
-
1515
- function loadSourceTrustRecords(cwd) {
1516
- const doc = safeReadJson(cwd, SOURCE_TRUST_REL_PATH, null);
1517
- if (!doc || typeof doc !== "object" || !Array.isArray(doc.items)) return [];
1518
- const remediationMap = activeRemediationActionsBySource(cwd);
1519
- return doc.items.map((item) => ({
1520
- ...item,
1521
- remediationActions: remediationMap.get(item.sourceId) || item.remediationActions || [],
1522
- allowedToInfluenceTools: (remediationMap.get(item.sourceId) || []).includes("trust_source")
1523
- ? item.allowedToInfluenceTools
1524
- : item.allowedToInfluenceTools && !(remediationMap.get(item.sourceId) || []).some((action) => action === "treat_as_data_only" || action === "quarantine_source"),
1525
- }));
1526
- }
1527
-
1528
- function resolveRelatedSourcesForAction(cwd, action, componentCard) {
1529
- const sourceRecords = loadSourceTrustRecords(cwd);
1530
- const requestedIds = Array.isArray(action?.relatedSourceIds)
1531
- ? action.relatedSourceIds
1532
- : Array.isArray(action?.context?.relatedSourceIds)
1533
- ? action.context.relatedSourceIds
1534
- : [];
1535
- const ids = requestedIds.length > 0
1536
- ? requestedIds.map((item) => String(item || "")).filter(Boolean)
1537
- : componentCard?.id
1538
- ? [componentCard.id]
1539
- : [];
1540
- if (!ids.length) return [];
1541
- const byId = new Map(sourceRecords.map((item) => [item.sourceId, item]));
1542
- const resolved = ids.map((id) => byId.get(id)).filter(Boolean);
1543
- if (resolved.length === 0 && componentCard?.sourceTrust && ids.includes(componentCard.id)) {
1544
- resolved.push({
1545
- ...componentCard.sourceTrust,
1546
- sourceId: componentCard.id,
1547
- sourceType: componentCard.sourceType || componentCard.sourceTrust.sourceType,
1548
- instructionRisk: componentCard.instructionRisk || componentCard.sourceTrust.instructionRisk || "low",
1549
- remediationActions: componentCard.sourceTrust.remediationActions || [],
1550
- });
1551
- }
1552
- return resolved;
1553
- }
1554
-
1555
- function recordAgentSecurityOperation(cwd, operation, tracker, details, writeEvidence) {
1556
- if (writeEvidence === false || !tracker) return null;
1557
- const entry = tracker.finish({
1558
- operation,
1559
- ...details,
1560
- });
1561
- recordPerformanceSummary(cwd, entry);
1562
- return entry;
1563
- }
1564
-
1565
- function writeBasicEvidence(cwd, generatedAt, previousSnapshot, trustCards) {
1566
- const summary = summarizeTrustCards("basic", trustCards, generatedAt, null);
1567
- const nextSnapshot = buildMergedSnapshot(previousSnapshot, "basic", trustCards.filter((card) => card.status !== "removed"), generatedAt);
1568
- const instructionEvidence = writeInstructionRiskEvidence(cwd, generatedAt, trustCards, "basic");
1569
-
1570
- safeWriteJson(cwd, SNAPSHOT_REL_PATH, nextSnapshot);
1571
- safeWriteJson(cwd, SUMMARY_REL_PATH, summary);
1572
- safeWriteJson(cwd, TRUST_CARDS_REL_PATH, {
1573
- trustCardsVersion: 1,
1574
- what: "Agent Security trust cards.",
1575
- generatedAt,
1576
- items: trustCards,
1577
- });
1578
-
1579
- appendJsonl(cwd, AUDIT_LOG_REL_PATH, {
1580
- eventType: "agent_security_basic_check",
1581
- mode: "basic",
1582
- componentsScanned: summary.componentsScanned,
1583
- newComponents: summary.newComponents,
1584
- highRisk: summary.highRisk,
1585
- mediumRisk: summary.mediumRisk,
1586
- lowRisk: summary.lowRisk,
1587
- timestamp: generatedAt,
1588
- });
1589
-
1590
- return {
1591
- snapshotPath: SNAPSHOT_REL_PATH,
1592
- summaryPath: SUMMARY_REL_PATH,
1593
- trustCardsPath: TRUST_CARDS_REL_PATH,
1594
- sourceTrustPath: instructionEvidence.sourceTrustPath,
1595
- instructionRiskPath: instructionEvidence.instructionRiskPath,
1596
- performanceSummaryPath: PERFORMANCE_SUMMARY_REL_PATH,
1597
- scanCachePath: SCAN_CACHE_REL_PATH,
1598
- auditPath: AUDIT_LOG_REL_PATH,
1599
- summary,
1600
- };
1601
- }
1602
-
1603
- function writeVisibilityEvidence(cwd, generatedAt, previousSnapshot, trustCards) {
1604
- const riskDiff = buildRiskDiff(previousSnapshot, trustCards, generatedAt);
1605
- const permissionPreview = buildPermissionPreview(trustCards, generatedAt);
1606
- const summary = summarizeTrustCards("visibility", trustCards, generatedAt, null, {
1607
- riskIncreases: riskDiff.summary.riskIncreases,
1608
- permissionExpansions: riskDiff.summary.permissionExpansions,
1609
- permissionPreviews: permissionPreview.previews.length,
1610
- });
1611
- const nextSnapshot = buildMergedSnapshot(previousSnapshot, "visibility", trustCards.filter((card) => card.status !== "removed"), generatedAt);
1612
- const instructionEvidence = writeInstructionRiskEvidence(cwd, generatedAt, trustCards, "visibility");
1613
-
1614
- safeWriteJson(cwd, SNAPSHOT_REL_PATH, nextSnapshot);
1615
- safeWriteJson(cwd, VISIBILITY_SUMMARY_REL_PATH, summary);
1616
- safeWriteJson(cwd, TRUST_CARDS_REL_PATH, {
1617
- trustCardsVersion: 2,
1618
- what: "Agent Security trust cards.",
1619
- generatedAt,
1620
- items: trustCards,
1621
- });
1622
- safeWriteJson(cwd, RISK_DIFF_REL_PATH, riskDiff);
1623
- safeWriteJson(cwd, PERMISSION_PREVIEW_REL_PATH, permissionPreview);
1624
-
1625
- appendJsonl(cwd, AUDIT_LOG_REL_PATH, {
1626
- eventType: "agent_security_visibility_scan",
1627
- mode: "visibility",
1628
- componentsScanned: summary.componentsScanned,
1629
- newComponents: summary.newComponents,
1630
- changedComponents: summary.changedComponents,
1631
- riskIncreases: summary.riskIncreases,
1632
- permissionExpansions: summary.permissionExpansions,
1633
- highRisk: summary.highRisk,
1634
- mediumRisk: summary.mediumRisk,
1635
- timestamp: generatedAt,
1636
- });
1637
-
1638
- return {
1639
- snapshotPath: SNAPSHOT_REL_PATH,
1640
- summaryPath: VISIBILITY_SUMMARY_REL_PATH,
1641
- trustCardsPath: TRUST_CARDS_REL_PATH,
1642
- sourceTrustPath: instructionEvidence.sourceTrustPath,
1643
- instructionRiskPath: instructionEvidence.instructionRiskPath,
1644
- performanceSummaryPath: PERFORMANCE_SUMMARY_REL_PATH,
1645
- scanCachePath: SCAN_CACHE_REL_PATH,
1646
- riskDiffPath: RISK_DIFF_REL_PATH,
1647
- permissionPreviewPath: PERMISSION_PREVIEW_REL_PATH,
1648
- auditPath: AUDIT_LOG_REL_PATH,
1649
- summary,
1650
- riskDiff,
1651
- permissionPreview,
1652
- };
1653
- }
1654
-
1655
- function loadDecisionSummary(cwd) {
1656
- return safeReadJson(cwd, DECISION_SUMMARY_REL_PATH, null);
1657
- }
1658
-
1659
- function saveDecisionSummary(cwd, summary) {
1660
- safeWriteJson(cwd, DECISION_SUMMARY_REL_PATH, summary);
1661
- return summary;
1662
- }
1663
-
1664
- function updateDecisionSummary(cwd, payload) {
1665
- const previous = loadDecisionSummary(cwd) || {
1666
- decisionSummaryVersion: 1,
1667
- what: "Agent Security warn-mode decision summary.",
1668
- generatedAt: null,
1669
- mode: "warn",
1670
- counts: {
1671
- approveOnce: 0,
1672
- denied: 0,
1673
- limited: 0,
1674
- },
1675
- latestDecision: null,
1676
- recentDecisions: [],
1677
- };
1678
-
1679
- const counts = {
1680
- approveOnce: Number(previous.counts?.approveOnce || 0),
1681
- denied: Number(previous.counts?.denied || 0),
1682
- limited: Number(previous.counts?.limited || 0),
1683
- };
1684
-
1685
- if (payload.decision === "approve_once") counts.approveOnce += 1;
1686
- if (payload.decision === "deny") counts.denied += 1;
1687
- if (payload.decision === "let_wuz_limit") counts.limited += 1;
1688
-
1689
- const nextDecision = {
1690
- decision: payload.decision,
1691
- effectiveBehavior: payload.effectiveBehavior,
1692
- actionId: payload.actionId,
1693
- componentId: payload.componentId,
1694
- componentType: payload.componentType,
1695
- actionType: payload.actionType,
1696
- riskLevel: payload.riskLevel,
1697
- timestamp: payload.timestamp,
1698
- };
1699
-
1700
- const recentDecisions = [nextDecision, ...(Array.isArray(previous.recentDecisions) ? previous.recentDecisions : [])]
1701
- .slice(0, 5);
1702
-
1703
- return saveDecisionSummary(cwd, {
1704
- decisionSummaryVersion: 1,
1705
- what: "Agent Security warn-mode decision summary.",
1706
- generatedAt: payload.timestamp,
1707
- mode: "warn",
1708
- counts,
1709
- latestDecision: nextDecision,
1710
- recentDecisions,
1711
- });
1712
- }
1713
-
1714
- function resolveComponentCard(trustCards, action) {
1715
- if (!Array.isArray(trustCards)) return null;
1716
- if (action?.componentId) {
1717
- const exact = trustCards.find((card) => card.id === action.componentId);
1718
- if (exact) return exact;
1719
- }
1720
- if (action?.componentType && action?.source) {
1721
- const exactSource = trustCards.find((card) => card.type === action.componentType && card.source === action.source);
1722
- if (exactSource) return exactSource;
1723
- }
1724
- if (action?.componentType && action?.target) {
1725
- const byTarget = trustCards.find((card) => card.type === action.componentType && String(action.target).includes(card.name));
1726
- if (byTarget) return byTarget;
1727
- }
1728
- return null;
1729
- }
1730
-
1731
- function effectiveBehaviorForDecision(decision) {
1732
- if (decision === "deny") return "deny_recommended";
1733
- if (decision === "let_wuz_limit") return "limit_plan_only";
1734
- if (decision === "auto_allow_limited") return "limited";
1735
- if (decision === "auto_deny") return "blocked";
1736
- return "allowed";
1737
- }
1738
-
1739
- function ttlSecondsFromRecommendation(value) {
1740
- const normalized = String(value || "").toLowerCase();
1741
- if (normalized.includes("30 minute")) return 1800;
1742
- if (normalized.includes("15 minute")) return 900;
1743
- if (normalized.includes("this session")) return null;
1744
- if (normalized.includes("once")) return null;
1745
- return null;
1746
- }
1747
-
1748
- function buildDecisionRecord(input) {
1749
- return {
1750
- decisionId: `decision-${shortHash(`${input.timestamp}:${input.actionId}:${input.decision}`)}`,
1751
- actionId: input.actionId,
1752
- componentId: input.componentId || null,
1753
- decision: input.decision,
1754
- mode: input.mode || "warn",
1755
- effectiveBehavior: effectiveBehaviorForDecision(input.decision),
1756
- ttlSeconds: ttlSecondsFromRecommendation(input.limitPlan?.ttlRecommendation || input.ttlRecommendation),
1757
- scope: input.limitPlan?.scope || {
1758
- actionType: input.actionType || null,
1759
- target: input.target || null,
1760
- },
1761
- denied: input.limitPlan?.wouldDeny || [],
1762
- reason: input.limitPlan?.reason || input.reason || "",
1763
- timestamp: input.timestamp,
1764
- };
1765
- }
1766
-
1767
- function writeDecisionAuditEvents(cwd, payload) {
1768
- const baseEvent = {
1769
- mode: "warn",
1770
- actionId: payload.actionId,
1771
- componentId: payload.componentId || null,
1772
- componentType: payload.componentType || null,
1773
- actionType: payload.actionType,
1774
- riskLevel: payload.riskLevel,
1775
- recommendedDecision: payload.recommendedDecision,
1776
- userDecision: payload.userDecision,
1777
- effectiveBehavior: payload.effectiveBehavior,
1778
- timestamp: payload.timestamp,
1779
- };
1780
-
1781
- appendJsonl(cwd, DECISION_AUDIT_LOG_REL_PATH, {
1782
- eventType: "agent_security_action_preflight",
1783
- ...baseEvent,
1784
- });
1785
- appendJsonl(cwd, DECISION_AUDIT_LOG_REL_PATH, {
1786
- eventType: "agent_security_decision_recorded",
1787
- ...baseEvent,
1788
- });
1789
-
1790
- if (payload.userDecision === "let_wuz_limit" && payload.limitPlan) {
1791
- appendJsonl(cwd, DECISION_AUDIT_LOG_REL_PATH, {
1792
- eventType: "agent_security_limit_plan_created",
1793
- ...baseEvent,
1794
- ttlRecommendation: payload.limitPlan.ttlRecommendation || null,
1795
- denied: payload.limitPlan.wouldDeny || [],
1796
- });
1797
- }
1798
- }
1799
-
1800
- function buildBasicStatusLine(summary, options = {}) {
1801
- if (!summary) return null;
1802
- const needsBaselineRecording = options.needsBaselineRecording === true;
1803
- if (summary.newComponents > 0) {
1804
- if (needsBaselineRecording) {
1805
- return "Agent security: New component detected. Run Basic Check to record it as reviewed.";
1806
- }
1807
- return `Agent security: ${summary.newComponents} new component${summary.newComponents === 1 ? "" : "s"} need review`;
1808
- }
1809
- if (summary.highRisk > 0) {
1810
- return `Agent security: ${summary.highRisk} high risk component${summary.highRisk === 1 ? "" : "s"} need review`;
1811
- }
1812
- if (summary.suspiciousInstructionSources > 0) {
1813
- return `Agent security: Basic check passed - ${pluralize(summary.suspiciousInstructionSources, "suspicious instruction source")}`;
1814
- }
1815
- return `Agent security: Basic check passed - ${summary.componentsScanned} components - 0 high risk`;
1816
- }
1817
-
1818
- function buildBasicDashboardBlurb(summary, options = {}) {
1819
- if (!summary) return null;
1820
- const needsBaselineRecording = options.needsBaselineRecording === true;
1821
- const blurb = {
1822
- title: "Basic Skill/MCP Check",
1823
- headline: "Basic Check active",
1824
- summary: summary.suspiciousInstructionSources > 0
1825
- ? `${summary.componentsScanned} components - ${pluralize(summary.suspiciousInstructionSources, "suspicious instruction source")}`
1826
- : `${summary.componentsScanned} components - ${summary.highRisk} high risk`,
1827
- nextBestAction: needsBaselineRecording && summary.newComponents > 0
1828
- ? "Run Basic Check to record new components as reviewed."
1829
- : null,
1830
- detailsAvailable: true,
1831
- checkedLabel: `${summary.componentsScanned} components checked`,
1832
- newLabel: `${summary.newComponents} new component${summary.newComponents === 1 ? "" : "s"}`,
1833
- riskLabel: `${summary.highRisk} high risk`,
1834
- };
1835
- if (needsBaselineRecording && summary.newComponents > 0) {
1836
- blurb.note = "New AI component detected. Run Basic Check to record it as reviewed.";
1837
- }
1838
- return blurb;
1839
- }
1840
-
1841
- function pluralize(count, singular, plural) {
1842
- return `${count} ${count === 1 ? singular : (plural || `${singular}s`)}`;
1843
- }
1844
-
1845
- function buildProductSummary(mode, fields = {}) {
1846
- return {
1847
- mode,
1848
- headline: fields.headline || "",
1849
- summary: fields.summary || "",
1850
- safeActionsLimited: Number(fields.safeActionsLimited || 0),
1851
- actionsBlocked: Number(fields.actionsBlocked || 0),
1852
- approvalRequired: Number(fields.approvalRequired || 0),
1853
- jitGrantsActive: Number(fields.jitGrantsActive || 0),
1854
- instructionRiskSources: Number(fields.instructionRiskSources || 0),
1855
- sourcesTreatedAsData: Number(fields.sourcesTreatedAsData || 0),
1856
- remediationsActive: Number(fields.remediationsActive || 0),
1857
- performance: fields.performance || { lastOperationMs: null, slow: false },
1858
- nextBestAction: fields.nextBestAction || null,
1859
- detailsAvailable: fields.detailsAvailable !== false,
1860
- };
1861
- }
1862
-
1863
- function buildBasicProductSummary(summary, options = {}) {
1864
- const needsBaselineRecording = options.needsBaselineRecording === true;
1865
- return buildProductSummary("basic", {
1866
- headline: "Basic Check active",
1867
- summary: needsBaselineRecording && summary.newComponents > 0
1868
- ? `${pluralize(summary.newComponents, "new component")} needs review`
1869
- : summary.suspiciousInstructionSources > 0
1870
- ? `${pluralize(summary.componentsScanned, "component")} - ${pluralize(summary.suspiciousInstructionSources, "suspicious instruction source")}`
1871
- : `${pluralize(summary.componentsScanned, "component")} - ${summary.highRisk} high risk`,
1872
- nextBestAction: needsBaselineRecording && summary.newComponents > 0
1873
- ? "Run Basic Check to record new components as reviewed."
1874
- : summary.suspiciousInstructionSources > 0
1875
- ? `Review ${pluralize(summary.suspiciousInstructionSources, "suspicious instruction source")}.`
1876
- : null,
1877
- performance: options.performance || { lastOperationMs: null, slow: false },
1878
- });
1879
- }
1880
-
1881
- function visibilityRiskLabel(summary) {
1882
- if (summary.highRisk > 0) return "High risk";
1883
- if (summary.mediumRisk > 0) return "Medium risk";
1884
- return "Low risk";
1885
- }
1886
-
1887
- function buildVisibilityStatusLine(summary, options = {}) {
1888
- if (!summary) return null;
1889
- const needsBaselineRecording = options.needsBaselineRecording === true;
1890
- if (needsBaselineRecording && summary.newComponents > 0) {
1891
- return "Agent security: New component detected. Run Visibility Scan to record it as reviewed.";
1892
- }
1893
- if (needsBaselineRecording && (summary.changedComponents > 0 || summary.removedComponents > 0)) {
1894
- return "Agent security: Surface change detected. Run Visibility Scan to record it as reviewed.";
1895
- }
1896
- if (summary.highRisk === 0 && summary.permissionExpansions === 0 && summary.newComponents === 0 && summary.changedComponents === 0) {
1897
- return `Agent security: Visibility clean - ${summary.componentsScanned} components - 0 high risk`;
1898
- }
1899
- if (summary.suspiciousInstructionSources > 0) {
1900
- return `Agent security: Visibility - ${pluralize(summary.suspiciousInstructionSources, "suspicious instruction source")} - ${summary.componentsScanned} components`;
1901
- }
1902
- return `Agent security: Visibility - ${summary.componentsScanned} components - ${summary.highRisk} high risk - ${summary.permissionExpansions} permission expansion${summary.permissionExpansions === 1 ? "" : "s"}`;
1903
- }
1904
-
1905
- function buildVisibilityDashboardCard(summary, options = {}) {
1906
- if (!summary) return null;
1907
- const needsBaselineRecording = options.needsBaselineRecording === true;
1908
- const topFinding = (summary.topFindings || []).find((item) => item.riskLevel === "high") || summary.topFindings?.[0] || null;
1909
- const card = {
1910
- title: "Agent Security",
1911
- subtitle: `Full visibility - ${visibilityRiskLabel(summary)}`,
1912
- headline: "Visibility active",
1913
- summary: `${pluralize(summary.componentsScanned, "component")} - ${summary.highRisk} high risk - ${pluralize(summary.permissionExpansions, "permission expansion")}`,
1914
- nextBestAction: needsBaselineRecording && summary.newComponents > 0
1915
- ? "Run Visibility Scan to record the new baseline."
1916
- : null,
1917
- detailsAvailable: true,
1918
- surfaceLabel: `${summary.componentsScanned} components mapped`,
1919
- changeLabel: `${summary.newComponents} new - ${summary.changedComponents} changed - ${summary.permissionExpansions} permission expansion${summary.permissionExpansions === 1 ? "" : "s"}`,
1920
- topRiskLabel: topFinding
1921
- ? `Top risk: ${topFinding.name} ${topFinding.type === "mcp" ? "MCP" : topFinding.type} ${topFinding.riskLevel === "high" ? "needs review" : "changed"}`
1922
- : "Top risk: No high-risk findings",
1923
- previewLabel: `${summary.permissionPreviews || 0} permission preview${summary.permissionPreviews === 1 ? "" : "s"}`,
1924
- };
1925
- if (needsBaselineRecording && summary.newComponents > 0) {
1926
- card.note = "New AI component detected. Run Visibility Scan to record it as reviewed.";
1927
- } else if (needsBaselineRecording && (summary.changedComponents > 0 || summary.removedComponents > 0)) {
1928
- card.note = "Agent surface changed. Run Visibility Scan to record the new baseline.";
1929
- }
1930
- return card;
1931
- }
1932
-
1933
- function buildWarnEnforcementLabel(enforcementSummary) {
1934
- const counts = enforcementSummary?.counts || {};
1935
- const blocked = Number(counts.blocked || 0);
1936
- const approvedOnce = Number(counts.approvedOnce || 0);
1937
- const limited = Number(counts.limited || 0);
1938
- const unavailable = Number(counts.unavailable || 0);
1939
- const fileWritesBlocked = Number(counts.fileWritesBlocked || 0);
1940
- const fileWritesLimited = Number(counts.fileWritesLimited || 0);
1941
- const fileWritesApprovedOnce = Number(counts.fileWritesApprovedOnce || 0);
1942
- const packageActionsBlocked = Number(counts.packageActionsBlocked || 0);
1943
- const packageActionsLimited = Number(counts.packageActionsLimited || 0);
1944
- const packageActionsApprovedOnce = Number(counts.packageActionsApprovedOnce || 0);
1945
- const mcpActionsBlocked = Number(counts.mcpActionsBlocked || 0);
1946
- const mcpActionsLimited = Number(counts.mcpActionsLimited || 0);
1947
- const mcpActionsApprovedOnce = Number(counts.mcpActionsApprovedOnce || 0);
1948
- const specificBlockedKinds = [
1949
- mcpActionsBlocked > 0,
1950
- packageActionsBlocked > 0,
1951
- fileWritesBlocked > 0,
1952
- ].filter(Boolean).length;
1953
- const specificLimitedKinds = [
1954
- mcpActionsLimited > 0,
1955
- packageActionsLimited > 0,
1956
- fileWritesLimited > 0,
1957
- ].filter(Boolean).length;
1958
-
1959
- if (specificBlockedKinds > 1) return `${blocked} actions blocked`;
1960
- if (specificLimitedKinds > 1 && blocked === 0) return `${limited} actions limited`;
1961
- if (mcpActionsBlocked > 0) return `${mcpActionsBlocked} MCP action blocked`;
1962
- if (mcpActionsLimited > 0) return `${mcpActionsLimited} MCP action limited`;
1963
- if (mcpActionsApprovedOnce > 0) return `${mcpActionsApprovedOnce} MCP action approved once`;
1964
-
1965
- if (packageActionsBlocked > 0) return `${packageActionsBlocked} package action blocked`;
1966
- if (packageActionsLimited > 0) return `${packageActionsLimited} package action limited`;
1967
- if (packageActionsApprovedOnce > 0) return `${packageActionsApprovedOnce} package action approved once`;
1968
-
1969
- if (fileWritesBlocked > 0) return `${fileWritesBlocked} file write blocked`;
1970
- if (fileWritesLimited > 0) return `${fileWritesLimited} file write limited`;
1971
- if (fileWritesApprovedOnce > 0) return `${fileWritesApprovedOnce} file write approved once`;
1972
-
1973
- if (blocked > 0) return `${blocked} action blocked`;
1974
- if (approvedOnce > 0) return `${approvedOnce} approved once`;
1975
- if (limited > 0) return `${limited} action limited`;
1976
- if (unavailable > 0) return `${unavailable} unavailable`;
1977
- return "no enforced actions yet";
1978
- }
1979
-
1980
- function buildWarnProductSummary(summary, decisionSummary, enforcementSummary) {
1981
- const enforcementCounts = enforcementSummary?.counts || {};
1982
- const decisionCounts = decisionSummary?.counts || {};
1983
- const blocked = Number(enforcementCounts.blocked || 0);
1984
- const limited = Number(enforcementCounts.limited || 0);
1985
- const approvedOnce = Number(enforcementCounts.approvedOnce || 0) + Number(decisionCounts.approveOnce || 0);
1986
- const nextBestAction = summary.highRisk > 0
1987
- ? `Review ${pluralize(summary.highRisk, "high-risk action")} before proceeding.`
1988
- : null;
1989
-
1990
- if (blocked > 0 || limited > 0 || approvedOnce > 0) {
1991
- const parts = [];
1992
- if (limited > 0) parts.push(`${pluralize(limited, "action")} limited`);
1993
- if (blocked > 0) parts.push(`${pluralize(blocked, "action")} blocked`);
1994
- if (approvedOnce > 0) parts.push(`${pluralize(approvedOnce, "action")} approved once`);
1995
- return buildProductSummary("warn", {
1996
- headline: "Warn & Approve active",
1997
- summary: parts.join(" - "),
1998
- safeActionsLimited: limited,
1999
- actionsBlocked: blocked,
2000
- approvalRequired: summary.highRisk,
2001
- nextBestAction,
2002
- performance: buildCompactPerformanceSummary(summary.cwd || "", ["agent_security_preflight", "agent_security_guarded_action"]),
2003
- });
2004
- }
2005
-
2006
- return buildProductSummary("warn", {
2007
- headline: "Warn & Approve active",
2008
- summary: "Approvals enabled",
2009
- approvalRequired: summary.highRisk,
2010
- nextBestAction,
2011
- performance: buildCompactPerformanceSummary(summary.cwd || "", ["agent_security_preflight", "agent_security_guarded_action"]),
2012
- });
2013
- }
2014
-
2015
- function buildWarnStatusLine(summary, decisionSummary, enforcementSummary) {
2016
- const enforcementCounts = enforcementSummary?.counts || {};
2017
- const hasEnforcementActivity =
2018
- Number(enforcementCounts.attempted || 0) > 0 ||
2019
- Number(enforcementCounts.blocked || 0) > 0 ||
2020
- Number(enforcementCounts.approvedOnce || 0) > 0 ||
2021
- Number(enforcementCounts.limited || 0) > 0 ||
2022
- Number(enforcementCounts.unavailable || 0) > 0;
2023
-
2024
- if (hasEnforcementActivity) {
2025
- return `Agent security: Warn mode - approvals enabled - ${buildWarnEnforcementLabel(enforcementSummary)}`;
2026
- }
2027
-
2028
- const counts = decisionSummary?.counts || {};
2029
- const limited = Number(counts.limited || 0);
2030
- const denied = Number(counts.denied || 0);
2031
- if (limited > 0 || denied > 0) {
2032
- return `Agent security: Warn mode - ${limited} action limited - ${denied} denied`;
2033
- }
2034
- if (Number(summary?.suspiciousInstructionSources || 0) > 0) {
2035
- return "Agent security: Warn mode - approval needed for suspicious source";
2036
- }
2037
- return "Agent security: Warn mode - approvals enabled";
2038
- }
2039
-
2040
- function buildWarnDashboardCard(summary, decisionSummary, enforcementSummary) {
2041
- const counts = decisionSummary?.counts || {};
2042
- const topFinding = (summary.topFindings || []).find((item) => item.riskLevel === "high") || summary.topFindings?.[0] || null;
2043
- const enforcementCounts = enforcementSummary?.counts || {};
2044
- const card = {
2045
- title: "Agent Security",
2046
- subtitle: `Warn mode active - ${summary.componentsScanned} components mapped - ${summary.highRisk} high risk`,
2047
- headline: "Warn & Approve active",
2048
- summary: Number(enforcementCounts.blocked || 0) > 0 || Number(enforcementCounts.limited || 0) > 0
2049
- ? buildWarnEnforcementLabel(enforcementSummary)
2050
- : "Approvals enabled",
2051
- nextBestAction: topFinding ? `Review ${topFinding.name} before continuing.` : null,
2052
- detailsAvailable: true,
2053
- surfaceLabel: `${summary.componentsScanned} components mapped`,
2054
- changeLabel: `${summary.newComponents} new - ${summary.changedComponents} changed - ${summary.permissionExpansions} permission expansion${summary.permissionExpansions === 1 ? "" : "s"}`,
2055
- decisionsLabel: `${Number(counts.approveOnce || 0)} action approved once - ${Number(counts.limited || 0)} action limited - ${Number(counts.denied || 0)} denied`,
2056
- pendingRiskLabel: topFinding
2057
- ? `Top pending risk: ${topFinding.name} ${topFinding.type === "mcp" ? "MCP" : topFinding.type} actions require approval`
2058
- : "Top pending risk: no high-risk findings",
2059
- };
2060
-
2061
- if (
2062
- Number(enforcementCounts.attempted || 0) > 0 ||
2063
- Number(enforcementCounts.blocked || 0) > 0 ||
2064
- Number(enforcementCounts.approvedOnce || 0) > 0 ||
2065
- Number(enforcementCounts.limited || 0) > 0 ||
2066
- Number(enforcementCounts.unavailable || 0) > 0
2067
- ) {
2068
- card.enforcementLabel = buildWarnEnforcementLabel(enforcementSummary);
2069
- }
2070
-
2071
- return card;
2072
- }
2073
-
2074
- function buildAutoStatusLine(summary, enforcementSummary) {
2075
- const counts = enforcementSummary?.counts || {};
2076
- const autoLimited = Number(counts.autoLimited || 0);
2077
- const autoDenied = Number(counts.autoDenied || 0) || Number(counts.blocked || 0);
2078
- const autoRequiresApproval = Number(counts.autoRequiresApproval || 0);
2079
- const autoRecommendedOnly = Number(counts.autoRecommendedOnly || 0);
2080
- const suspiciousSources = Number(summary?.suspiciousInstructionSources || 0);
2081
-
2082
- if (autoLimited > 0 || autoRequiresApproval > 0 || autoDenied > 0) {
2083
- const parts = [];
2084
- if (autoLimited > 0) parts.push(`${pluralize(autoLimited, "safe action")} limited`);
2085
- if (autoRequiresApproval > 0) {
2086
- parts.push(autoRequiresApproval === 1 ? "1 approval needed" : `${autoRequiresApproval} approvals needed`);
2087
- } else if (autoDenied > 0) {
2088
- parts.push(`${pluralize(autoDenied, "dangerous action")} blocked`);
2089
- }
2090
- if (suspiciousSources > 0) parts.push(`${pluralize(suspiciousSources, "suspicious source")} treated as data`);
2091
- return `Agent security: Auto-Minimize - ${parts.join(" - ")}`;
2092
- }
2093
- if (autoRecommendedOnly > 0) {
2094
- return `Agent security: Auto-Minimize - no safe enforcement available for ${autoRecommendedOnly} action${autoRecommendedOnly === 1 ? "" : "s"}`;
2095
- }
2096
- return "Agent security: Auto-Minimize - safe actions limited automatically - risky actions still require approval";
2097
- }
2098
-
2099
- function buildAutoProductSummary(summary, enforcementSummary, options = {}) {
2100
- const counts = enforcementSummary?.counts || {};
2101
- const autoLimited = Number(counts.autoLimited || 0);
2102
- const autoDenied = Number(counts.autoDenied || 0) || Number(counts.blocked || 0);
2103
- const autoRequiresApproval = Number(counts.autoRequiresApproval || 0);
2104
- const autoRecommendedOnly = Number(counts.autoRecommendedOnly || 0);
2105
- const activeJitGrants = Number(counts.activeJitGrants || 0);
2106
- const suspiciousSources = Number(summary?.suspiciousInstructionSources || 0);
2107
- const sourcesTreatedAsData = Number(summary?.sourcesTreatedAsData || 0);
2108
- const activeRemediations = Number(options.activeRemediations || 0);
2109
- const nextBestAction = autoRequiresApproval > 0
2110
- ? `Review ${autoRequiresApproval} risky action${autoRequiresApproval === 1 ? "" : "s"}.`
2111
- : suspiciousSources > 0
2112
- ? `Review ${pluralize(suspiciousSources, "suspicious instruction source")}.`
2113
- : autoDenied > 0
2114
- ? `Review ${autoDenied} blocked risky action${autoDenied === 1 ? "" : "s"}.`
2115
- : autoRecommendedOnly > 0
2116
- ? `Review ${autoRecommendedOnly} unsupported action path${autoRecommendedOnly === 1 ? "" : "s"}.`
2117
- : null;
2118
-
2119
- let compactSummary = "Safe actions limited automatically";
2120
- if (autoLimited > 0 || autoRequiresApproval > 0 || autoDenied > 0) {
2121
- const parts = [];
2122
- if (autoLimited > 0) parts.push(`${pluralize(autoLimited, "safe action")} limited`);
2123
- if (autoRequiresApproval > 0) {
2124
- parts.push(autoRequiresApproval === 1 ? "1 approval needed" : `${autoRequiresApproval} approvals needed`);
2125
- } else if (autoDenied > 0) {
2126
- parts.push(`${pluralize(autoDenied, "dangerous action")} blocked`);
2127
- }
2128
- compactSummary = parts.join(" - ");
2129
- } else if (autoRecommendedOnly > 0) {
2130
- compactSummary = `No safe enforcement available for ${pluralize(autoRecommendedOnly, "action")}`;
2131
- }
2132
-
2133
- return buildProductSummary("auto", {
2134
- headline: "Auto-Minimize active",
2135
- summary: compactSummary,
2136
- safeActionsLimited: autoLimited,
2137
- actionsBlocked: autoDenied,
2138
- approvalRequired: autoRequiresApproval,
2139
- jitGrantsActive: activeJitGrants,
2140
- instructionRiskSources: suspiciousSources,
2141
- sourcesTreatedAsData,
2142
- remediationsActive: activeRemediations,
2143
- performance: options.performance || { lastOperationMs: null, slow: false },
2144
- nextBestAction,
2145
- });
2146
- }
2147
-
2148
- function buildAutoDashboardCard(summary, enforcementSummary, productSummary) {
2149
- return {
2150
- title: "Agent Security",
2151
- subtitle: productSummary.headline || "Auto-Minimize active",
2152
- headline: productSummary.headline || "Auto-Minimize active",
2153
- summary: productSummary.summary || "Safe actions limited automatically",
2154
- nextBestAction: productSummary.nextBestAction || null,
2155
- detailsAvailable: true,
2156
- surfaceLabel: `${summary.componentsScanned} components mapped`,
2157
- changeLabel: `${summary.newComponents} new - ${summary.changedComponents} changed - ${summary.permissionExpansions} permission expansion${summary.permissionExpansions === 1 ? "" : "s"}`,
2158
- };
2159
- }
2160
-
2161
- function buildVisibilityProductSummary(summary, options = {}) {
2162
- const needsBaselineRecording = options.needsBaselineRecording === true;
2163
- return buildProductSummary("visibility", {
2164
- headline: "Visibility active",
2165
- summary: needsBaselineRecording && summary.newComponents > 0
2166
- ? `${pluralize(summary.newComponents, "new component")} needs review`
2167
- : summary.suspiciousInstructionSources > 0
2168
- ? `${pluralize(summary.suspiciousInstructionSources, "suspicious instruction source")} - ${summary.componentsScanned} components`
2169
- : `${pluralize(summary.componentsScanned, "component")} - ${summary.highRisk} high risk - ${pluralize(summary.permissionExpansions, "permission expansion")}`,
2170
- nextBestAction: needsBaselineRecording && summary.newComponents > 0
2171
- ? "Run Visibility Scan to record the new baseline."
2172
- : summary.suspiciousInstructionSources > 0
2173
- ? `Review ${pluralize(summary.suspiciousInstructionSources, "suspicious instruction source")}.`
2174
- : null,
2175
- performance: options.performance || { lastOperationMs: null, slow: false },
2176
- });
2177
- }
2178
-
2179
- function buildAgentSecuritySurface(cwd, config) {
2180
- const mode = config?.agentSecurity?.mode || "off";
2181
- const basicCheckEnabled = config?.agentSecurity?.basicCheckEnabled !== false;
2182
-
2183
- if (mode === "off" || !basicCheckEnabled) return null;
2184
- if (mode !== "basic" && mode !== "visibility" && mode !== "warn" && mode !== "auto") return null;
2185
-
2186
- const inventoryMode = mode === "basic" ? "basic" : "visibility";
2187
- const persisted = loadAgentSecurityEvidenceBundle(cwd, inventoryMode);
2188
- const persistedSummary = persisted.summary;
2189
- const trustCards = persisted.trustCards;
2190
- const generatedAt = persistedSummary?.generatedAt || null;
2191
- const summary = persistedSummary
2192
- ? { ...persistedSummary, cwd }
2193
- : {
2194
- mode: inventoryMode,
2195
- generatedAt: null,
2196
- cwd,
2197
- componentsScanned: 0,
2198
- newComponents: 0,
2199
- changedComponents: 0,
2200
- removedComponents: 0,
2201
- highRisk: 0,
2202
- mediumRisk: 0,
2203
- lowRisk: 0,
2204
- suspiciousInstructionSources: 0,
2205
- highInstructionFindings: 0,
2206
- sourcesTreatedAsData: 0,
2207
- permissionExpansions: 0,
2208
- permissionPreviews: 0,
2209
- topFindings: [],
2210
- };
2211
- const noRecentScanLine = mode === "basic"
2212
- ? "Agent security: Basic check active - Last checked: never"
2213
- : mode === "visibility"
2214
- ? "Agent security: Visibility active - No recent scan"
2215
- : mode === "warn"
2216
- ? "Agent security: Warn mode - no recent scan"
2217
- : "Agent security: Auto-Minimize - no recent scan";
2218
-
2219
- if (mode === "basic") {
2220
- const needsBaselineRecording = Boolean(persistedSummary) && summary.newComponents > 0;
2221
- const showInStatus = true;
2222
- const productSummary = buildBasicProductSummary(summary, {
2223
- needsBaselineRecording,
2224
- performance: buildCompactPerformanceSummary(cwd, "agent_security_basic_check"),
2225
- });
2226
-
2227
- return {
2228
- mode,
2229
- summary,
2230
- productSummary,
2231
- showInDashboard: true,
2232
- showInStatus,
2233
- hasRecordedCheck: Boolean(persistedSummary),
2234
- needsBaselineRecording,
2235
- statusLine: persistedSummary ? buildBasicStatusLine(summary, { needsBaselineRecording }) : noRecentScanLine,
2236
- dashboardCard: persistedSummary ? buildBasicDashboardBlurb(summary, { needsBaselineRecording }) : {
2237
- title: "Basic Skill/MCP Check",
2238
- headline: "Basic Check active",
2239
- summary: "No recent scan",
2240
- nextBestAction: "Run Basic Check.",
2241
- detailsAvailable: true,
2242
- checkedLabel: "Last checked: never",
2243
- newLabel: "0 new components",
2244
- riskLabel: "0 high risk",
2245
- },
2246
- detailsPath: persistedSummary ? SUMMARY_REL_PATH : null,
2247
- };
2248
- }
2249
-
2250
- const riskDiff = persisted.riskDiff;
2251
- const permissionPreview = persisted.permissionPreview;
2252
- const needsBaselineRecording = Boolean(persistedSummary) && (summary.newComponents > 0 || summary.changedComponents > 0 || summary.removedComponents > 0);
2253
-
2254
- if (mode === "warn") {
2255
- const decisionSummary = loadDecisionSummary(cwd);
2256
- const enforcementSummary = loadEnforcementSummary(cwd);
2257
- const jitGrantStore = loadJitGrantStore(cwd);
2258
- const productSummary = buildWarnProductSummary(summary, decisionSummary, enforcementSummary);
2259
- return {
2260
- mode,
2261
- summary: {
2262
- ...summary,
2263
- mode: "warn",
2264
- },
2265
- riskDiff,
2266
- permissionPreview,
2267
- decisionSummary,
2268
- enforcementSummary,
2269
- jitGrantStore,
2270
- productSummary,
2271
- showInDashboard: true,
2272
- showInStatus: true,
2273
- hasRecordedCheck: Boolean(persistedSummary),
2274
- needsBaselineRecording,
2275
- statusLine: persistedSummary ? buildWarnStatusLine(summary, decisionSummary, enforcementSummary) : noRecentScanLine,
2276
- dashboardCard: persistedSummary ? buildWarnDashboardCard(summary, decisionSummary, enforcementSummary) : {
2277
- title: "Agent Security",
2278
- subtitle: "Warn mode active",
2279
- headline: "Warn & Approve active",
2280
- summary: "No recent scan",
2281
- nextBestAction: "Run Visibility Scan.",
2282
- detailsAvailable: true,
2283
- },
2284
- detailsPath: persistedSummary ? VISIBILITY_SUMMARY_REL_PATH : null,
2285
- };
2286
- }
2287
-
2288
- if (mode === "auto") {
2289
- const enforcementSummary = loadEnforcementSummary(cwd);
2290
- const jitGrantStore = loadJitGrantStore(cwd);
2291
- const activeRemediations = loadInstructionRemediations(cwd).items.length;
2292
- const productSummary = buildAutoProductSummary(summary, enforcementSummary, {
2293
- activeRemediations,
2294
- performance: buildCompactPerformanceSummary(cwd, ["agent_security_guarded_action", "agent_security_auto_policy"]),
2295
- });
2296
- return {
2297
- mode,
2298
- summary: {
2299
- ...summary,
2300
- mode: "auto",
2301
- },
2302
- riskDiff,
2303
- permissionPreview,
2304
- enforcementSummary,
2305
- jitGrantStore,
2306
- productSummary,
2307
- showInDashboard: true,
2308
- showInStatus: true,
2309
- hasRecordedCheck: Boolean(persistedSummary),
2310
- needsBaselineRecording,
2311
- statusLine: persistedSummary ? buildAutoStatusLine(summary, enforcementSummary) : noRecentScanLine,
2312
- dashboardCard: persistedSummary ? buildAutoDashboardCard(summary, enforcementSummary, productSummary) : {
2313
- title: "Agent Security",
2314
- subtitle: "Auto-Minimize active",
2315
- headline: "Auto-Minimize active",
2316
- summary: "No recent scan",
2317
- nextBestAction: "Run Visibility Scan.",
2318
- detailsAvailable: true,
2319
- },
2320
- detailsPath: persistedSummary ? VISIBILITY_SUMMARY_REL_PATH : null,
2321
- };
2322
- }
2323
-
2324
- const productSummary = buildVisibilityProductSummary(summary, {
2325
- needsBaselineRecording,
2326
- performance: buildCompactPerformanceSummary(cwd, "agent_security_visibility_scan"),
2327
- });
2328
- return {
2329
- mode,
2330
- summary,
2331
- productSummary,
2332
- riskDiff,
2333
- permissionPreview,
2334
- showInDashboard: true,
2335
- showInStatus: true,
2336
- hasRecordedCheck: Boolean(persistedSummary),
2337
- needsBaselineRecording,
2338
- statusLine: persistedSummary ? buildVisibilityStatusLine(summary, { needsBaselineRecording }) : noRecentScanLine,
2339
- dashboardCard: persistedSummary ? buildVisibilityDashboardCard(summary, { needsBaselineRecording }) : {
2340
- title: "Agent Security",
2341
- subtitle: "Full visibility",
2342
- headline: "Visibility active",
2343
- summary: "No recent scan",
2344
- nextBestAction: "Run Visibility Scan.",
2345
- detailsAvailable: true,
2346
- surfaceLabel: "Last checked: never",
2347
- changeLabel: "0 new - 0 changed - 0 permission expansions",
2348
- topRiskLabel: "Top risk: No recent scan",
2349
- previewLabel: "0 permission previews",
2350
- },
2351
- detailsPath: persistedSummary ? VISIBILITY_SUMMARY_REL_PATH : null,
2352
- };
2353
- }
2354
-
2355
- function runAgentSecurityScan(cwd, options = {}) {
2356
- const configuredMode = AGENT_SECURITY_MODES.includes(options.mode) ? options.mode : "basic";
2357
- const explicitMode = options.scanMode === "visibility" || options.scanMode === "basic" ? options.scanMode : null;
2358
- const mode = explicitMode || (configuredMode === "visibility" || configuredMode === "warn" || configuredMode === "auto" ? "visibility" : "basic");
2359
- const generatedAt = nowIso();
2360
- const operation = mode === "visibility" ? "agent_security_visibility_scan" : "agent_security_basic_check";
2361
- const tracker = createPerformanceTracker(operation);
2362
- const scanSession = createBoundedScanSession(cwd, tracker, {
2363
- maxFiles: PERFORMANCE_GUARDRAILS.scanMaxFiles,
2364
- maxFileBytes: PERFORMANCE_GUARDRAILS.scanMaxFileBytes,
2365
- maxTotalBytes: PERFORMANCE_GUARDRAILS.scanMaxTotalBytes,
2366
- maxDepth: PERFORMANCE_GUARDRAILS.scanMaxDepth,
2367
- maxDurationMs: PERFORMANCE_GUARDRAILS.scanMaxDurationMs,
2368
- });
2369
- const previousSnapshot = loadPreviousSnapshot(cwd, mode);
2370
- const trustCards = buildTrustCardsForMode(cwd, previousSnapshot, generatedAt, mode, { tracker, scanSession });
2371
-
2372
- let summary;
2373
- let riskDiff = null;
2374
- let permissionPreview = null;
2375
- if (mode === "visibility") {
2376
- riskDiff = buildRiskDiff(previousSnapshot, trustCards, generatedAt);
2377
- permissionPreview = buildPermissionPreview(trustCards, generatedAt);
2378
- summary = summarizeTrustCards(mode, trustCards, generatedAt, null, {
2379
- riskIncreases: riskDiff.summary.riskIncreases,
2380
- permissionExpansions: riskDiff.summary.permissionExpansions,
2381
- permissionPreviews: permissionPreview.previews.length,
2382
- });
2383
- } else {
2384
- summary = summarizeTrustCards(mode, trustCards, generatedAt, null);
2385
- }
2386
-
2387
- let evidence = null;
2388
- if (options.writeEvidence !== false) {
2389
- evidence = mode === "visibility"
2390
- ? writeVisibilityEvidence(cwd, generatedAt, previousSnapshot, trustCards)
2391
- : writeBasicEvidence(cwd, generatedAt, previousSnapshot, trustCards);
2392
- recordAgentSecurityOperation(cwd, operation, tracker, {
2393
- operation,
2394
- mode,
2395
- componentsScanned: summary.componentsScanned,
2396
- cachePath: SCAN_CACHE_REL_PATH,
2397
- }, options.writeEvidence);
2398
- }
2399
-
2400
- return {
2401
- generatedAt,
2402
- mode,
2403
- configuredMode,
2404
- summary,
2405
- trustCards,
2406
- riskDiff,
2407
- permissionPreview,
2408
- evidence,
2409
- };
2410
- }
2411
-
2412
- function runAgentSecurityBasicCheck(cwd, options = {}) {
2413
- return runAgentSecurityScan(cwd, {
2414
- ...options,
2415
- scanMode: "basic",
2416
- });
2417
- }
2418
-
2419
- function runAgentSecurityVisibilityScan(cwd, options = {}) {
2420
- return runAgentSecurityScan(cwd, {
2421
- ...options,
2422
- scanMode: "visibility",
2423
- });
2424
- }
2425
-
2426
- function inspectAgentSecurityContent(cwd, input, options = {}) {
2427
- const configuredMode = AGENT_SECURITY_MODES.includes(options.mode) ? options.mode : "off";
2428
- const mode = configuredMode;
2429
- const generatedAt = nowIso();
2430
- const sourceType = input?.sourceType || "unknown_external";
2431
- const content = String(input?.content || "");
2432
- const inspection = scanInstructionRisk(content, {
2433
- sourceType,
2434
- intendedUse: input?.intendedUse || "unknown",
2435
- });
2436
- const sourceRecord = buildSourceRecord({
2437
- sourceId: input?.sourceId,
2438
- sourceType,
2439
- origin: input?.origin || null,
2440
- path: input?.path || null,
2441
- content,
2442
- instructionRisk: inspection.instructionRisk,
2443
- findings: inspection.findings,
2444
- intendedUse: input?.intendedUse || "unknown",
2445
- createdAt: generatedAt,
2446
- updatedAt: generatedAt,
2447
- });
2448
-
2449
- const previousSourceDoc = safeReadJson(cwd, SOURCE_TRUST_REL_PATH, {
2450
- sourceTrustVersion: 1,
2451
- what: "Agent Security source trust classifications.",
2452
- generatedAt,
2453
- items: [],
2454
- });
2455
- const previousRiskDoc = safeReadJson(cwd, INSTRUCTION_RISK_REL_PATH, {
2456
- instructionRiskVersion: 1,
2457
- what: "Agent Security instruction risk findings.",
2458
- generatedAt,
2459
- counts: {
2460
- suspiciousSources: 0,
2461
- highRiskSources: 0,
2462
- highRiskFindings: 0,
2463
- },
2464
- items: [],
2465
- });
2466
-
2467
- const nextSourceItems = previousSourceDoc.items.filter((item) => item.sourceId !== sourceRecord.sourceId);
2468
- nextSourceItems.push(sourceRecord);
2469
- const nextRiskItems = previousRiskDoc.items.filter((item) => item.sourceId !== sourceRecord.sourceId);
2470
- nextRiskItems.push({
2471
- sourceId: sourceRecord.sourceId,
2472
- sourceType: sourceRecord.sourceType,
2473
- name: input?.origin || sourceRecord.path || sourceRecord.sourceId,
2474
- source: sourceRecord.path || sourceRecord.origin || null,
2475
- instructionRisk: inspection.instructionRisk,
2476
- signals: inspection.signals,
2477
- findings: inspection.findings.map((finding) => ({
2478
- category: finding.category,
2479
- severity: finding.severity,
2480
- message: finding.message,
2481
- evidencePreview: boundedPreview(finding.evidencePreview, 120),
2482
- location: finding.location || null,
2483
- })),
2484
- recommendation: sourceRecord.recommendation,
2485
- });
2486
-
2487
- const nextSourceDoc = {
2488
- sourceTrustVersion: 1,
2489
- what: "Agent Security source trust classifications.",
2490
- generatedAt,
2491
- items: nextSourceItems,
2492
- };
2493
- const nextRiskDoc = {
2494
- instructionRiskVersion: 1,
2495
- what: "Agent Security instruction risk findings.",
2496
- generatedAt,
2497
- counts: {
2498
- suspiciousSources: nextRiskItems.filter((item) => item.instructionRisk !== "low").length,
2499
- highRiskSources: nextRiskItems.filter((item) => item.instructionRisk === "high").length,
2500
- highRiskFindings: nextRiskItems.reduce((sum, item) => sum + item.findings.filter((finding) => finding.severity === "high").length, 0),
2501
- },
2502
- items: nextRiskItems,
2503
- };
2504
-
2505
- if (options.writeEvidence !== false) {
2506
- safeWriteJson(cwd, SOURCE_TRUST_REL_PATH, nextSourceDoc);
2507
- safeWriteJson(cwd, INSTRUCTION_RISK_REL_PATH, nextRiskDoc);
2508
- appendJsonl(cwd, INSTRUCTION_AUDIT_LOG_REL_PATH, {
2509
- eventType: "agent_security_source_trust_classified",
2510
- mode,
2511
- sourceId: sourceRecord.sourceId,
2512
- sourceType: sourceRecord.sourceType,
2513
- trustLevel: sourceRecord.trustLevel,
2514
- instructionRisk: sourceRecord.instructionRisk,
2515
- signals: inspection.signals,
2516
- reason: sourceRecord.recommendation,
2517
- timestamp: generatedAt,
2518
- });
2519
- if (inspection.findings.length > 0) {
2520
- appendJsonl(cwd, INSTRUCTION_AUDIT_LOG_REL_PATH, {
2521
- eventType: "agent_security_instruction_risk_detected",
2522
- mode,
2523
- sourceId: sourceRecord.sourceId,
2524
- sourceType: sourceRecord.sourceType,
2525
- trustLevel: sourceRecord.trustLevel,
2526
- instructionRisk: sourceRecord.instructionRisk,
2527
- signals: inspection.signals,
2528
- reason: sourceRecord.recommendation,
2529
- timestamp: generatedAt,
2530
- });
2531
- }
2532
- }
2533
-
2534
- return {
2535
- generatedAt,
2536
- mode,
2537
- sourceRecord,
2538
- inspection,
2539
- remediation: {
2540
- defaultAction: sourceRecord.recommendation,
2541
- availableActions: ["treat_as_data_only", "use_sanitized_copy", "quarantine_source", "trust_source"],
2542
- },
2543
- evidence: options.writeEvidence === false ? null : {
2544
- sourceTrustPath: SOURCE_TRUST_REL_PATH,
2545
- instructionRiskPath: INSTRUCTION_RISK_REL_PATH,
2546
- auditPath: INSTRUCTION_AUDIT_LOG_REL_PATH,
2547
- },
2548
- };
2549
- }
2550
-
2551
- function remediateAgentSecurityInstruction(cwd, input, options = {}) {
2552
- const mode = AGENT_SECURITY_MODES.includes(options.mode) ? options.mode : "warn";
2553
- const generatedAt = nowIso();
2554
- const sourceId = String(input?.sourceId || "");
2555
- const action = String(input?.action || "");
2556
- const reason = String(input?.reason || "");
2557
- if (!sourceId) throw new Error("Instruction remediation requires sourceId.");
2558
- if (!["treat_as_data_only", "use_sanitized_copy", "quarantine_source", "trust_source"].includes(action)) {
2559
- throw new Error("Unsupported instruction remediation action.");
2560
- }
2561
-
2562
- const sourceDoc = safeReadJson(cwd, SOURCE_TRUST_REL_PATH, {
2563
- sourceTrustVersion: 1,
2564
- what: "Agent Security source trust classifications.",
2565
- generatedAt,
2566
- items: [],
2567
- });
2568
- const sourceRecord = sourceDoc.items.find((item) => item.sourceId === sourceId);
2569
- if (!sourceRecord) throw new Error(`Unknown sourceId: ${sourceId}`);
2570
-
2571
- const remediations = loadInstructionRemediations(cwd);
2572
- const remediationRecord = {
2573
- remediationId: `rem-${shortHash(`${sourceId}:${action}:${generatedAt}`)}`,
2574
- sourceId,
2575
- action,
2576
- mode,
2577
- reason,
2578
- createdAt: generatedAt,
2579
- expiresAt: null,
2580
- userDecision: action === "trust_source",
2581
- };
2582
-
2583
- safeWriteJson(cwd, INSTRUCTION_REMEDIATIONS_REL_PATH, {
2584
- remediationVersion: 1,
2585
- what: "Agent Security instruction remediations.",
2586
- generatedAt,
2587
- items: [...remediations.items, remediationRecord],
2588
- });
2589
-
2590
- let sanitizedCopy = null;
2591
- if (action === "use_sanitized_copy") {
2592
- const sanitizedDoc = safeReadJson(cwd, SANITIZED_CONTENT_REL_PATH, {
2593
- sanitizedContentVersion: 1,
2594
- what: "Agent Security sanitized content copies.",
2595
- generatedAt,
2596
- items: [],
2597
- });
2598
- sanitizedCopy = {
2599
- sourceId,
2600
- sourceType: sourceRecord.sourceType,
2601
- contentHash: sourceRecord.contentHash,
2602
- sanitizedContent: sanitizeInstructionLikeContent(sourceRecord.contentSample || ""),
2603
- createdAt: generatedAt,
2604
- };
2605
- safeWriteJson(cwd, SANITIZED_CONTENT_REL_PATH, {
2606
- sanitizedContentVersion: 1,
2607
- what: "Agent Security sanitized content copies.",
2608
- generatedAt,
2609
- items: [...sanitizedDoc.items.filter((item) => item.sourceId !== sourceId), sanitizedCopy],
2610
- });
2611
- appendJsonl(cwd, INSTRUCTION_AUDIT_LOG_REL_PATH, {
2612
- eventType: "agent_security_sanitized_copy_created",
2613
- mode,
2614
- sourceId,
2615
- sourceType: sourceRecord.sourceType,
2616
- trustLevel: sourceRecord.trustLevel,
2617
- instructionRisk: sourceRecord.instructionRisk,
2618
- signals: [],
2619
- remediationAction: action,
2620
- reason,
2621
- timestamp: generatedAt,
2622
- });
2623
- }
2624
-
2625
- appendJsonl(cwd, INSTRUCTION_AUDIT_LOG_REL_PATH, {
2626
- eventType: "agent_security_instruction_remediation_recorded",
2627
- mode,
2628
- sourceId,
2629
- sourceType: sourceRecord.sourceType,
2630
- trustLevel: sourceRecord.trustLevel,
2631
- instructionRisk: sourceRecord.instructionRisk,
2632
- signals: [],
2633
- remediationAction: action,
2634
- reason,
2635
- timestamp: generatedAt,
2636
- });
2637
-
2638
- return {
2639
- generatedAt,
2640
- mode,
2641
- remediationRecord,
2642
- sanitizedCopy,
2643
- evidence: {
2644
- remediationPath: INSTRUCTION_REMEDIATIONS_REL_PATH,
2645
- sanitizedContentPath: action === "use_sanitized_copy" ? SANITIZED_CONTENT_REL_PATH : null,
2646
- auditPath: INSTRUCTION_AUDIT_LOG_REL_PATH,
2647
- },
2648
- };
2649
- }
2650
-
2651
- function runAgentSecurityPreflight(cwd, actionInput, options = {}) {
2652
- const mode = "warn";
2653
- const generatedAt = nowIso();
2654
- const tracker = createPerformanceTracker("agent_security_preflight");
2655
- const inventory = loadPersistedInventoryForActions(cwd);
2656
-
2657
- const action = {
2658
- ...(actionInput && typeof actionInput === "object" ? actionInput : {}),
2659
- };
2660
- action.context = action.context && typeof action.context === "object" ? { ...action.context } : {};
2661
- if (!action.context.cwd) action.context.cwd = cwd;
2662
- if (!action.actionId) action.actionId = buildActionId(action);
2663
-
2664
- const componentCard = resolveComponentCard(inventory.trustCards, action);
2665
- const relatedSources = resolveRelatedSourcesForAction(cwd, action, componentCard);
2666
- const evaluation = evaluateAgentSecurityAction(action, { componentCard, relatedSources });
2667
- const requestedDecision = action.decision || options.decision || null;
2668
- const limitPlan = evaluation.decisionRequired || requestedDecision === "let_wuz_limit"
2669
- ? buildPermissionLimitPlan({ evaluation, action }, { componentCard })
2670
- : null;
2671
-
2672
- const decision = requestedDecision;
2673
- const decisionRecord = decision
2674
- ? buildDecisionRecord({
2675
- actionId: evaluation.actionId,
2676
- componentId: evaluation.componentId,
2677
- decision,
2678
- actionType: evaluation.actionType,
2679
- target: evaluation.target,
2680
- limitPlan,
2681
- reason: evaluation.reasons[0],
2682
- timestamp: generatedAt,
2683
- })
2684
- : null;
2685
-
2686
- let decisionSummary = null;
2687
- if (options.writeEvidence !== false) {
2688
- writeEnforcementCapabilityMatrix(cwd, generatedAt, mode);
2689
- safeWriteJson(cwd, ACTION_PREFLIGHT_REL_PATH, {
2690
- what: "Agent Security preflight result.",
2691
- generatedAt,
2692
- mode,
2693
- action: {
2694
- actionId: evaluation.actionId,
2695
- componentId: evaluation.componentId,
2696
- componentType: evaluation.componentType,
2697
- actionType: evaluation.actionType,
2698
- target: evaluation.target,
2699
- command: evaluation.command,
2700
- packageManager: evaluation.packageManager || null,
2701
- scriptName: evaluation.scriptName || null,
2702
- packageName: evaluation.packageName || null,
2703
- mcpServer: evaluation.mcpServer || null,
2704
- toolName: evaluation.toolName || null,
2705
- argsHash: evaluation.argsHash || null,
2706
- targetHash: evaluation.targetHash || null,
2707
- relatedSourceIds: evaluation.relatedSourceIds || [],
2708
- },
2709
- evaluation,
2710
- ...(limitPlan ? { limitPlan } : {}),
2711
- ...(decisionRecord ? { decisionRecord } : {}),
2712
- });
2713
- }
2714
-
2715
- if (decision && options.writeEvidence !== false) {
2716
- decisionSummary = updateDecisionSummary(cwd, {
2717
- decision,
2718
- effectiveBehavior: decisionRecord.effectiveBehavior,
2719
- actionId: evaluation.actionId,
2720
- componentId: evaluation.componentId,
2721
- componentType: evaluation.componentType,
2722
- actionType: evaluation.actionType,
2723
- riskLevel: evaluation.riskLevel,
2724
- timestamp: generatedAt,
2725
- });
2726
- writeDecisionAuditEvents(cwd, {
2727
- actionId: evaluation.actionId,
2728
- componentId: evaluation.componentId,
2729
- componentType: evaluation.componentType,
2730
- actionType: evaluation.actionType,
2731
- riskLevel: evaluation.riskLevel,
2732
- recommendedDecision: evaluation.recommendedDecision,
2733
- userDecision: decision,
2734
- effectiveBehavior: decisionRecord.effectiveBehavior,
2735
- limitPlan,
2736
- timestamp: generatedAt,
2737
- });
2738
- }
2739
-
2740
- recordAgentSecurityOperation(cwd, "agent_security_preflight", tracker, {
2741
- mode,
2742
- actionId: evaluation.actionId,
2743
- actionType: evaluation.actionType,
2744
- relatedSourceIds: evaluation.relatedSourceIds || [],
2745
- }, options.writeEvidence);
2746
-
2747
- return {
2748
- generatedAt,
2749
- mode,
2750
- evaluation,
2751
- limitPlan,
2752
- decision: decision || null,
2753
- decisionRecord,
2754
- decisionSummary,
2755
- evidence: options.writeEvidence === false ? null : {
2756
- preflightPath: ACTION_PREFLIGHT_REL_PATH,
2757
- decisionSummaryPath: decisionSummary ? DECISION_SUMMARY_REL_PATH : null,
2758
- decisionAuditPath: decision ? DECISION_AUDIT_LOG_REL_PATH : null,
2759
- enforcementCapabilitiesPath: ENFORCEMENT_CAPABILITIES_REL_PATH,
2760
- performanceSummaryPath: PERFORMANCE_SUMMARY_REL_PATH,
2761
- },
2762
- };
2763
- }
2764
-
2765
- function runAgentSecurityGuardedRun(cwd, actionInput, options = {}) {
2766
- const mode = "warn";
2767
- const generatedAt = nowIso();
2768
- const tracker = createPerformanceTracker("agent_security_guarded_action");
2769
- const inventory = loadPersistedInventoryForActions(cwd);
2770
-
2771
- const action = {
2772
- ...(actionInput && typeof actionInput === "object" ? actionInput : {}),
2773
- };
2774
- action.context = action.context && typeof action.context === "object" ? { ...action.context } : {};
2775
- if (!action.context.cwd) action.context.cwd = cwd;
2776
- if (!action.actionId) action.actionId = buildActionId(action);
2777
-
2778
- const componentCard = resolveComponentCard(inventory.trustCards, action);
2779
- const relatedSources = resolveRelatedSourcesForAction(cwd, action, componentCard);
2780
- const evaluation = evaluateAgentSecurityAction(action, { componentCard, relatedSources });
2781
- const requestedDecision = action.decision || options.decision || null;
2782
- const limitPlan = evaluation.decisionRequired || requestedDecision === "let_wuz_limit"
2783
- ? buildPermissionLimitPlan({ evaluation, action }, { componentCard })
2784
- : null;
2785
-
2786
- const decision = requestedDecision;
2787
- const decisionRecord = decision
2788
- ? buildDecisionRecord({
2789
- actionId: evaluation.actionId,
2790
- componentId: evaluation.componentId,
2791
- decision,
2792
- actionType: evaluation.actionType,
2793
- target: evaluation.target,
2794
- limitPlan,
2795
- reason: evaluation.reasons[0],
2796
- timestamp: generatedAt,
2797
- })
2798
- : null;
2799
-
2800
- if (options.writeEvidence !== false) {
2801
- safeWriteJson(cwd, ACTION_PREFLIGHT_REL_PATH, {
2802
- what: "Agent Security guarded action preflight result.",
2803
- generatedAt,
2804
- mode,
2805
- action: {
2806
- actionId: evaluation.actionId,
2807
- componentId: evaluation.componentId,
2808
- componentType: evaluation.componentType,
2809
- actionType: evaluation.actionType,
2810
- target: evaluation.target,
2811
- command: evaluation.command,
2812
- packageManager: evaluation.packageManager || null,
2813
- scriptName: evaluation.scriptName || null,
2814
- packageName: evaluation.packageName || null,
2815
- mcpServer: evaluation.mcpServer || null,
2816
- toolName: evaluation.toolName || null,
2817
- argsHash: evaluation.argsHash || null,
2818
- targetHash: evaluation.targetHash || null,
2819
- relatedSourceIds: evaluation.relatedSourceIds || [],
2820
- },
2821
- evaluation,
2822
- ...(limitPlan ? { limitPlan } : {}),
2823
- ...(decisionRecord ? { decisionRecord } : {}),
2824
- });
2825
- }
2826
-
2827
- let decisionSummary = null;
2828
- if (decision && options.writeEvidence !== false) {
2829
- decisionSummary = updateDecisionSummary(cwd, {
2830
- decision,
2831
- effectiveBehavior: decisionRecord.effectiveBehavior,
2832
- actionId: evaluation.actionId,
2833
- componentId: evaluation.componentId,
2834
- componentType: evaluation.componentType,
2835
- actionType: evaluation.actionType,
2836
- riskLevel: evaluation.riskLevel,
2837
- timestamp: generatedAt,
2838
- });
2839
- writeDecisionAuditEvents(cwd, {
2840
- actionId: evaluation.actionId,
2841
- componentId: evaluation.componentId,
2842
- componentType: evaluation.componentType,
2843
- actionType: evaluation.actionType,
2844
- riskLevel: evaluation.riskLevel,
2845
- recommendedDecision: evaluation.recommendedDecision,
2846
- userDecision: decision,
2847
- effectiveBehavior: decisionRecord.effectiveBehavior,
2848
- limitPlan,
2849
- timestamp: generatedAt,
2850
- });
2851
- }
2852
-
2853
- const enforcement = enforceAgentSecurityAction(cwd, {
2854
- action: {
2855
- ...action,
2856
- actionId: evaluation.actionId,
2857
- componentId: evaluation.componentId,
2858
- componentType: evaluation.componentType,
2859
- actionType: evaluation.actionType,
2860
- target: evaluation.target,
2861
- command: evaluation.command,
2862
- packageManager: evaluation.packageManager || action.packageManager || null,
2863
- scriptName: evaluation.scriptName || action.scriptName || null,
2864
- packageName: evaluation.packageName || action.packageName || null,
2865
- mcpServer: evaluation.mcpServer || action.mcpServer || null,
2866
- toolName: evaluation.toolName || action.toolName || null,
2867
- args: action.args && typeof action.args === "object" ? action.args : null,
2868
- resource: action.resource && typeof action.resource === "object" ? action.resource : null,
2869
- },
2870
- evaluation,
2871
- limitPlan,
2872
- decisionRecord,
2873
- }, {
2874
- timestamp: generatedAt,
2875
- execute: action.execute === true,
2876
- executor: options.executor,
2877
- writeEvidence: options.writeEvidence !== false,
2878
- });
2879
-
2880
- recordAgentSecurityOperation(cwd, "agent_security_guarded_action", tracker, {
2881
- mode,
2882
- actionId: evaluation.actionId,
2883
- actionType: evaluation.actionType,
2884
- relatedSourceIds: evaluation.relatedSourceIds || [],
2885
- enforcementAvailable: enforcement.enforcementAvailable === true,
2886
- }, options.writeEvidence);
2887
-
2888
- return {
2889
- generatedAt,
2890
- mode,
2891
- evaluation,
2892
- limitPlan,
2893
- decision: decision || null,
2894
- decisionRecord,
2895
- decisionSummary,
2896
- enforcement,
2897
- evidence: options.writeEvidence === false ? null : {
2898
- preflightPath: ACTION_PREFLIGHT_REL_PATH,
2899
- decisionSummaryPath: decisionSummary ? DECISION_SUMMARY_REL_PATH : null,
2900
- decisionAuditPath: decision ? DECISION_AUDIT_LOG_REL_PATH : null,
2901
- enforcementSummaryPath: ENFORCEMENT_SUMMARY_REL_PATH,
2902
- jitGrantsPath: JIT_GRANTS_REL_PATH,
2903
- grantsPath: ONE_TIME_GRANTS_REL_PATH,
2904
- enforcementAuditPath: ENFORCEMENT_AUDIT_LOG_REL_PATH,
2905
- enforcementCapabilitiesPath: ENFORCEMENT_CAPABILITIES_REL_PATH,
2906
- performanceSummaryPath: PERFORMANCE_SUMMARY_REL_PATH,
2907
- },
2908
- };
2909
- }
2910
-
2911
- function writeGuardedActionPreflight(cwd, generatedAt, mode, evaluation, limitPlan, decisionRecord, autoPolicy, writeEvidence) {
2912
- if (writeEvidence === false) return;
2913
- safeWriteJson(cwd, ACTION_PREFLIGHT_REL_PATH, {
2914
- what: "Agent Security guarded action preflight result.",
2915
- generatedAt,
2916
- mode,
2917
- action: {
2918
- actionId: evaluation.actionId,
2919
- componentId: evaluation.componentId,
2920
- componentType: evaluation.componentType,
2921
- actionType: evaluation.actionType,
2922
- target: evaluation.target,
2923
- command: evaluation.command,
2924
- packageManager: evaluation.packageManager || null,
2925
- scriptName: evaluation.scriptName || null,
2926
- packageName: evaluation.packageName || null,
2927
- mcpServer: evaluation.mcpServer || null,
2928
- toolName: evaluation.toolName || null,
2929
- argsHash: evaluation.argsHash || null,
2930
- targetHash: evaluation.targetHash || null,
2931
- contentHash: evaluation.contentHash || null,
2932
- scopeKey: evaluation.scopeKey || null,
2933
- },
2934
- evaluation,
2935
- ...(limitPlan ? { limitPlan } : {}),
2936
- ...(decisionRecord ? { decisionRecord } : {}),
2937
- ...(autoPolicy ? { autoPolicy } : {}),
2938
- });
2939
- }
2940
-
2941
- function buildAutoDecisionRecord(input) {
2942
- if (input.autoDecision === "auto_allow_limited") {
2943
- return buildDecisionRecord({
2944
- ...input,
2945
- decision: "let_wuz_limit",
2946
- mode: "auto",
2947
- });
2948
- }
2949
- if (input.autoDecision === "auto_allow_once") {
2950
- return buildDecisionRecord({
2951
- ...input,
2952
- decision: "approve_once",
2953
- mode: "auto",
2954
- });
2955
- }
2956
- if (input.autoDecision === "auto_deny") {
2957
- return buildDecisionRecord({
2958
- ...input,
2959
- decision: "deny",
2960
- mode: "auto",
2961
- });
2962
- }
2963
- return null;
2964
- }
2965
-
2966
- function buildImplicitGuardedComponentCard(action) {
2967
- const componentType = String(action?.componentType || "");
2968
- if (componentType === "file-write" || componentType === "command" || componentType === "package-manager") {
2969
- return {
2970
- id: String(action?.componentId || componentType),
2971
- type: componentType,
2972
- source: "guarded",
2973
- status: "known",
2974
- trustLevel: "local",
2975
- capabilities: {},
2976
- };
2977
- }
2978
- if (componentType === "mcp" && String(action?.mcpServer || "").toLowerCase() === "github") {
2979
- return {
2980
- id: String(action?.componentId || "mcp-github"),
2981
- type: "mcp",
2982
- source: "guarded",
2983
- status: "known",
2984
- trustLevel: "local",
2985
- capabilities: {},
2986
- };
2987
- }
2988
- return null;
2989
- }
2990
-
2991
- function runAgentSecurityAutoGuardedAction(cwd, actionInput, options = {}) {
2992
- const mode = "auto";
2993
- const generatedAt = nowIso();
2994
- const tracker = createPerformanceTracker("agent_security_guarded_action");
2995
- const inventory = loadPersistedInventoryForActions(cwd);
2996
-
2997
- const action = {
2998
- ...(actionInput && typeof actionInput === "object" ? actionInput : {}),
2999
- };
3000
- action.context = action.context && typeof action.context === "object" ? { ...action.context } : {};
3001
- if (!action.context.cwd) action.context.cwd = cwd;
3002
- if (!action.actionId) action.actionId = buildActionId(action);
3003
-
3004
- const componentCard = resolveComponentCard(inventory.trustCards, action) || buildImplicitGuardedComponentCard(action);
3005
- const relatedSources = resolveRelatedSourcesForAction(cwd, action, componentCard);
3006
- const evaluation = evaluateAgentSecurityAction(action, { componentCard, relatedSources });
3007
- const normalizedAction = normalizeAgentSecurityAction({
3008
- ...action,
3009
- actionId: evaluation.actionId,
3010
- componentId: evaluation.componentId,
3011
- componentType: evaluation.componentType,
3012
- actionType: evaluation.actionType,
3013
- target: evaluation.target,
3014
- command: evaluation.command,
3015
- packageManager: evaluation.packageManager || action.packageManager || null,
3016
- scriptName: evaluation.scriptName || action.scriptName || null,
3017
- packageName: evaluation.packageName || action.packageName || null,
3018
- mcpServer: evaluation.mcpServer || action.mcpServer || null,
3019
- toolName: evaluation.toolName || action.toolName || null,
3020
- relatedSourceIds: evaluation.relatedSourceIds || [],
3021
- });
3022
- const adapter = resolveAgentSecurityAdapter(evaluation.actionType);
3023
- const limitPlan = buildPermissionLimitPlan({ evaluation, action: normalizedAction }, { componentCard });
3024
- const explicitDecision = action.decision || options.decision || null;
3025
-
3026
- if (explicitDecision) {
3027
- const decisionRecord = buildDecisionRecord({
3028
- actionId: evaluation.actionId,
3029
- componentId: evaluation.componentId,
3030
- decision: explicitDecision,
3031
- actionType: evaluation.actionType,
3032
- target: evaluation.target,
3033
- limitPlan,
3034
- reason: evaluation.reasons[0],
3035
- timestamp: generatedAt,
3036
- mode,
3037
- });
3038
- writeGuardedActionPreflight(cwd, generatedAt, mode, evaluation, limitPlan, decisionRecord, null, options.writeEvidence);
3039
- const enforcement = enforceAgentSecurityAction(cwd, {
3040
- action: normalizedAction,
3041
- evaluation,
3042
- limitPlan,
3043
- decisionRecord,
3044
- }, {
3045
- timestamp: generatedAt,
3046
- execute: action.execute === true,
3047
- executor: options.executor,
3048
- writeEvidence: options.writeEvidence !== false,
3049
- mode,
3050
- autoDecision: explicitDecision === "deny"
3051
- ? "auto_deny"
3052
- : explicitDecision === "let_wuz_limit"
3053
- ? "auto_allow_limited"
3054
- : "auto_allow_once",
3055
- confidence: "high",
3056
- });
3057
-
3058
- recordAgentSecurityOperation(cwd, "agent_security_guarded_action", tracker, {
3059
- mode,
3060
- actionId: evaluation.actionId,
3061
- actionType: evaluation.actionType,
3062
- autoDecision: explicitDecision === "deny"
3063
- ? "auto_deny"
3064
- : explicitDecision === "let_wuz_limit"
3065
- ? "auto_allow_limited"
3066
- : "auto_allow_once",
3067
- relatedSourceIds: evaluation.relatedSourceIds || [],
3068
- enforcementAvailable: enforcement.enforcementAvailable === true,
3069
- }, options.writeEvidence);
3070
-
3071
- return {
3072
- generatedAt,
3073
- mode,
3074
- action: normalizedAction,
3075
- evaluation,
3076
- limitPlan,
3077
- autoDecision: explicitDecision === "deny"
3078
- ? "auto_deny"
3079
- : explicitDecision === "let_wuz_limit"
3080
- ? "auto_allow_limited"
3081
- : "auto_allow_once",
3082
- confidence: "high",
3083
- decision: explicitDecision,
3084
- decisionRecord,
3085
- enforcement,
3086
- evidence: options.writeEvidence === false ? null : {
3087
- preflightPath: ACTION_PREFLIGHT_REL_PATH,
3088
- enforcementSummaryPath: ENFORCEMENT_SUMMARY_REL_PATH,
3089
- jitGrantsPath: JIT_GRANTS_REL_PATH,
3090
- grantsPath: ONE_TIME_GRANTS_REL_PATH,
3091
- enforcementAuditPath: ENFORCEMENT_AUDIT_LOG_REL_PATH,
3092
- enforcementCapabilitiesPath: ENFORCEMENT_CAPABILITIES_REL_PATH,
3093
- performanceSummaryPath: PERFORMANCE_SUMMARY_REL_PATH,
3094
- },
3095
- };
3096
- }
3097
-
3098
- const autoPolicyTracker = createPerformanceTracker("agent_security_auto_policy");
3099
- const autoPolicy = decideAutoAction(normalizedAction, evaluation, limitPlan, adapter, {
3100
- mode,
3101
- cwd,
3102
- });
3103
- recordAgentSecurityOperation(cwd, "agent_security_auto_policy", autoPolicyTracker, {
3104
- mode,
3105
- actionId: evaluation.actionId,
3106
- actionType: evaluation.actionType,
3107
- autoDecision: autoPolicy.autoDecision,
3108
- relatedSourceIds: evaluation.relatedSourceIds || [],
3109
- }, options.writeEvidence);
3110
- writeGuardedActionPreflight(cwd, generatedAt, mode, evaluation, limitPlan, null, autoPolicy, options.writeEvidence);
3111
-
3112
- if (autoPolicy.autoDecision === "require_approval") {
3113
- const enforcement = recordEnforcementOutcome(cwd, {
3114
- actionId: normalizedAction.actionId,
3115
- componentId: normalizedAction.componentId,
3116
- componentType: normalizedAction.componentType,
3117
- actionType: normalizedAction.actionType,
3118
- target: normalizedAction.target || null,
3119
- command: normalizedAction.command || "",
3120
- packageManager: normalizedAction.packageManager || null,
3121
- scriptName: normalizedAction.scriptName || null,
3122
- packageName: normalizedAction.packageName || null,
3123
- mcpServer: normalizedAction.mcpServer || null,
3124
- toolName: normalizedAction.toolName || null,
3125
- decision: null,
3126
- adapterId: adapter?.adapterId || null,
3127
- enforcementAvailable: adapter?.canEnforce === true,
3128
- effectiveBehavior: "limit_plan_only",
3129
- executed: false,
3130
- exitCode: null,
3131
- stdoutPreview: "",
3132
- stderrPreview: "",
3133
- reason: autoPolicy.reason,
3134
- timestamp: generatedAt,
3135
- mode,
3136
- autoDecision: autoPolicy.autoDecision,
3137
- confidence: autoPolicy.confidence,
3138
- jitGrant: null,
3139
- jitGrantConsumed: null,
3140
- jitGrantExpired: null,
3141
- }, {
3142
- mode,
3143
- writeEvidence: options.writeEvidence !== false,
3144
- });
3145
-
3146
- recordAgentSecurityOperation(cwd, "agent_security_guarded_action", tracker, {
3147
- mode,
3148
- actionId: evaluation.actionId,
3149
- actionType: evaluation.actionType,
3150
- autoDecision: autoPolicy.autoDecision,
3151
- relatedSourceIds: evaluation.relatedSourceIds || [],
3152
- enforcementAvailable: enforcement.enforcementAvailable === true,
3153
- }, options.writeEvidence);
3154
-
3155
- return {
3156
- generatedAt,
3157
- mode,
3158
- action: normalizedAction,
3159
- evaluation,
3160
- limitPlan,
3161
- autoDecision: autoPolicy.autoDecision,
3162
- confidence: autoPolicy.confidence,
3163
- enforcement,
3164
- evidence: options.writeEvidence === false ? null : {
3165
- preflightPath: ACTION_PREFLIGHT_REL_PATH,
3166
- enforcementSummaryPath: ENFORCEMENT_SUMMARY_REL_PATH,
3167
- enforcementAuditPath: ENFORCEMENT_AUDIT_LOG_REL_PATH,
3168
- enforcementCapabilitiesPath: ENFORCEMENT_CAPABILITIES_REL_PATH,
3169
- performanceSummaryPath: PERFORMANCE_SUMMARY_REL_PATH,
3170
- },
3171
- };
3172
- }
3173
-
3174
- if (autoPolicy.autoDecision === "recommended_only" || autoPolicy.autoDecision === "limit_plan_only") {
3175
- const enforcement = recordEnforcementOutcome(cwd, {
3176
- actionId: normalizedAction.actionId,
3177
- componentId: normalizedAction.componentId,
3178
- componentType: normalizedAction.componentType,
3179
- actionType: normalizedAction.actionType,
3180
- target: normalizedAction.target || null,
3181
- command: normalizedAction.command || "",
3182
- packageManager: normalizedAction.packageManager || null,
3183
- scriptName: normalizedAction.scriptName || null,
3184
- packageName: normalizedAction.packageName || null,
3185
- mcpServer: normalizedAction.mcpServer || null,
3186
- toolName: normalizedAction.toolName || null,
3187
- decision: null,
3188
- adapterId: adapter?.adapterId || null,
3189
- enforcementAvailable: adapter?.canEnforce === true,
3190
- effectiveBehavior: autoPolicy.autoDecision === "recommended_only" ? "recommended_only" : "limit_plan_only",
3191
- executed: false,
3192
- exitCode: null,
3193
- stdoutPreview: "",
3194
- stderrPreview: "",
3195
- reason: autoPolicy.reason,
3196
- timestamp: generatedAt,
3197
- mode,
3198
- autoDecision: autoPolicy.autoDecision,
3199
- confidence: autoPolicy.confidence,
3200
- jitGrant: null,
3201
- jitGrantConsumed: null,
3202
- jitGrantExpired: null,
3203
- }, {
3204
- mode,
3205
- writeEvidence: options.writeEvidence !== false,
3206
- unsupportedActionTypes: adapter ? [] : [normalizedAction.actionType],
3207
- });
3208
-
3209
- recordAgentSecurityOperation(cwd, "agent_security_guarded_action", tracker, {
3210
- mode,
3211
- actionId: evaluation.actionId,
3212
- actionType: evaluation.actionType,
3213
- autoDecision: autoPolicy.autoDecision,
3214
- relatedSourceIds: evaluation.relatedSourceIds || [],
3215
- enforcementAvailable: enforcement.enforcementAvailable === true,
3216
- }, options.writeEvidence);
3217
-
3218
- return {
3219
- generatedAt,
3220
- mode,
3221
- action: normalizedAction,
3222
- evaluation,
3223
- limitPlan,
3224
- autoDecision: autoPolicy.autoDecision,
3225
- confidence: autoPolicy.confidence,
3226
- enforcement,
3227
- evidence: options.writeEvidence === false ? null : {
3228
- preflightPath: ACTION_PREFLIGHT_REL_PATH,
3229
- enforcementSummaryPath: ENFORCEMENT_SUMMARY_REL_PATH,
3230
- enforcementAuditPath: ENFORCEMENT_AUDIT_LOG_REL_PATH,
3231
- enforcementCapabilitiesPath: ENFORCEMENT_CAPABILITIES_REL_PATH,
3232
- performanceSummaryPath: PERFORMANCE_SUMMARY_REL_PATH,
3233
- },
3234
- };
3235
- }
3236
-
3237
- const decisionRecord = buildAutoDecisionRecord({
3238
- actionId: evaluation.actionId,
3239
- componentId: evaluation.componentId,
3240
- actionType: evaluation.actionType,
3241
- target: evaluation.target,
3242
- limitPlan,
3243
- reason: autoPolicy.reason,
3244
- timestamp: generatedAt,
3245
- autoDecision: autoPolicy.autoDecision,
3246
- });
3247
-
3248
- const enforcement = enforceAgentSecurityAction(cwd, {
3249
- action: normalizedAction,
3250
- evaluation,
3251
- limitPlan,
3252
- decisionRecord,
3253
- }, {
3254
- timestamp: generatedAt,
3255
- execute: action.execute === true,
3256
- executor: options.executor,
3257
- writeEvidence: options.writeEvidence !== false,
3258
- mode,
3259
- autoDecision: autoPolicy.autoDecision,
3260
- confidence: autoPolicy.confidence,
3261
- });
3262
-
3263
- recordAgentSecurityOperation(cwd, "agent_security_guarded_action", tracker, {
3264
- mode,
3265
- actionId: evaluation.actionId,
3266
- actionType: evaluation.actionType,
3267
- autoDecision: autoPolicy.autoDecision,
3268
- relatedSourceIds: evaluation.relatedSourceIds || [],
3269
- enforcementAvailable: enforcement.enforcementAvailable === true,
3270
- }, options.writeEvidence);
3271
-
3272
- return {
3273
- generatedAt,
3274
- mode,
3275
- action: normalizedAction,
3276
- evaluation,
3277
- limitPlan,
3278
- autoDecision: autoPolicy.autoDecision,
3279
- confidence: autoPolicy.confidence,
3280
- decisionRecord,
3281
- enforcement,
3282
- evidence: options.writeEvidence === false ? null : {
3283
- preflightPath: ACTION_PREFLIGHT_REL_PATH,
3284
- enforcementSummaryPath: ENFORCEMENT_SUMMARY_REL_PATH,
3285
- jitGrantsPath: JIT_GRANTS_REL_PATH,
3286
- grantsPath: ONE_TIME_GRANTS_REL_PATH,
3287
- enforcementAuditPath: ENFORCEMENT_AUDIT_LOG_REL_PATH,
3288
- enforcementCapabilitiesPath: ENFORCEMENT_CAPABILITIES_REL_PATH,
3289
- performanceSummaryPath: PERFORMANCE_SUMMARY_REL_PATH,
3290
- },
3291
- };
3292
- }
3293
-
3294
- function runGuardedAction(cwd, actionInput, options = {}) {
3295
- const requestedMode = AGENT_SECURITY_MODES.includes(options.mode) ? options.mode : "warn";
3296
- if (requestedMode === "auto") {
3297
- return runAgentSecurityAutoGuardedAction(cwd, actionInput, options);
3298
- }
3299
- return runAgentSecurityGuardedRun(cwd, actionInput, options);
3300
- }
3301
-
3302
- module.exports = {
3303
- AGENT_SECURITY_MODES,
3304
- SNAPSHOT_REL_PATH,
3305
- SUMMARY_REL_PATH,
3306
- VISIBILITY_SUMMARY_REL_PATH,
3307
- TRUST_CARDS_REL_PATH,
3308
- RISK_DIFF_REL_PATH,
3309
- PERMISSION_PREVIEW_REL_PATH,
3310
- INSTRUCTION_RISK_REL_PATH,
3311
- SOURCE_TRUST_REL_PATH,
3312
- INSTRUCTION_REMEDIATIONS_REL_PATH,
3313
- SANITIZED_CONTENT_REL_PATH,
3314
- AUDIT_LOG_REL_PATH,
3315
- INSTRUCTION_AUDIT_LOG_REL_PATH,
3316
- ACTION_PREFLIGHT_REL_PATH,
3317
- DECISION_SUMMARY_REL_PATH,
3318
- DECISION_AUDIT_LOG_REL_PATH,
3319
- ENFORCEMENT_CAPABILITIES_REL_PATH,
3320
- ENFORCEMENT_SUMMARY_REL_PATH,
3321
- JIT_GRANTS_REL_PATH,
3322
- ONE_TIME_GRANTS_REL_PATH,
3323
- ENFORCEMENT_AUDIT_LOG_REL_PATH,
3324
- stableStringify,
3325
- loadPreviousSnapshot,
3326
- loadLatestAgentSecuritySummary,
3327
- loadDecisionSummary,
3328
- loadJitGrantStore,
3329
- loadEnforcementSummary,
3330
- buildAgentSecurityModeControl,
3331
- buildAgentSecuritySurface,
3332
- buildRiskDiff,
3333
- buildPermissionPreview,
3334
- runAgentSecurityScan,
3335
- runAgentSecurityBasicCheck,
3336
- runAgentSecurityVisibilityScan,
3337
- inspectAgentSecurityContent,
3338
- remediateAgentSecurityInstruction,
3339
- runAgentSecurityPreflight,
3340
- runAgentSecurityGuardedRun,
3341
- runGuardedAction,
3342
- };