@vibecheckai/cli 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/bin/registry.js +154 -338
  2. package/bin/runners/context/generators/mcp.js +13 -15
  3. package/bin/runners/context/proof-context.js +1 -248
  4. package/bin/runners/lib/analysis-core.js +180 -198
  5. package/bin/runners/lib/analyzers.js +223 -1669
  6. package/bin/runners/lib/cli-output.js +210 -242
  7. package/bin/runners/lib/detectors-v2.js +785 -547
  8. package/bin/runners/lib/entitlements-v2.js +458 -96
  9. package/bin/runners/lib/error-handler.js +9 -16
  10. package/bin/runners/lib/global-flags.js +0 -37
  11. package/bin/runners/lib/route-truth.js +322 -1167
  12. package/bin/runners/lib/scan-output.js +469 -448
  13. package/bin/runners/lib/ship-output.js +27 -280
  14. package/bin/runners/lib/terminal-ui.js +733 -231
  15. package/bin/runners/lib/truth.js +321 -1004
  16. package/bin/runners/lib/unified-output.js +158 -162
  17. package/bin/runners/lib/upsell.js +204 -104
  18. package/bin/runners/runAllowlist.js +324 -0
  19. package/bin/runners/runAuth.js +95 -324
  20. package/bin/runners/runCheckpoint.js +21 -39
  21. package/bin/runners/runContext.js +24 -136
  22. package/bin/runners/runDoctor.js +67 -115
  23. package/bin/runners/runEvidencePack.js +219 -0
  24. package/bin/runners/runFix.js +5 -6
  25. package/bin/runners/runGuard.js +118 -212
  26. package/bin/runners/runInit.js +2 -14
  27. package/bin/runners/runInstall.js +281 -0
  28. package/bin/runners/runLabs.js +341 -0
  29. package/bin/runners/runMcp.js +52 -130
  30. package/bin/runners/runPolish.js +20 -43
  31. package/bin/runners/runProve.js +3 -13
  32. package/bin/runners/runReality.js +0 -14
  33. package/bin/runners/runReport.js +2 -3
  34. package/bin/runners/runScan.js +44 -511
  35. package/bin/runners/runShip.js +14 -28
  36. package/bin/runners/runValidate.js +2 -19
  37. package/bin/runners/runWatch.js +54 -118
  38. package/bin/vibecheck.js +41 -148
  39. package/mcp-server/ARCHITECTURE.md +339 -0
  40. package/mcp-server/__tests__/cache.test.ts +313 -0
  41. package/mcp-server/__tests__/executor.test.ts +239 -0
  42. package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +1 -0
  43. package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +3 -0
  44. package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +3 -0
  45. package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +3 -0
  46. package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +3 -0
  47. package/mcp-server/__tests__/fixtures/exclusion-test/package.json +5 -0
  48. package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +5 -0
  49. package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +4 -0
  50. package/mcp-server/__tests__/ids.test.ts +345 -0
  51. package/mcp-server/__tests__/integration/tools.test.ts +410 -0
  52. package/mcp-server/__tests__/registry.test.ts +365 -0
  53. package/mcp-server/__tests__/sandbox.test.ts +323 -0
  54. package/mcp-server/__tests__/schemas.test.ts +372 -0
  55. package/mcp-server/benchmarks/run-benchmarks.ts +304 -0
  56. package/mcp-server/examples/doctor.request.json +14 -0
  57. package/mcp-server/examples/doctor.response.json +53 -0
  58. package/mcp-server/examples/error.response.json +15 -0
  59. package/mcp-server/examples/scan.request.json +14 -0
  60. package/mcp-server/examples/scan.response.json +108 -0
  61. package/mcp-server/handlers/tool-handler.ts +671 -0
  62. package/mcp-server/index-v3.ts +293 -0
  63. package/mcp-server/index.js +1072 -1573
  64. package/mcp-server/index.old.js +4137 -0
  65. package/mcp-server/lib/cache.ts +341 -0
  66. package/mcp-server/lib/errors.ts +346 -0
  67. package/mcp-server/lib/executor.ts +792 -0
  68. package/mcp-server/lib/ids.ts +238 -0
  69. package/mcp-server/lib/logger.ts +368 -0
  70. package/mcp-server/lib/metrics.ts +365 -0
  71. package/mcp-server/lib/sandbox.ts +337 -0
  72. package/mcp-server/lib/validator.ts +229 -0
  73. package/mcp-server/package-lock.json +165 -0
  74. package/mcp-server/package.json +32 -7
  75. package/mcp-server/premium-tools.js +2 -2
  76. package/mcp-server/registry/tools.json +476 -0
  77. package/mcp-server/schemas/error-envelope.schema.json +125 -0
  78. package/mcp-server/schemas/finding.schema.json +167 -0
  79. package/mcp-server/schemas/report-artifact.schema.json +88 -0
  80. package/mcp-server/schemas/run-request.schema.json +75 -0
  81. package/mcp-server/schemas/verdict.schema.json +168 -0
  82. package/mcp-server/tier-auth.d.ts +71 -0
  83. package/mcp-server/tier-auth.js +371 -183
  84. package/mcp-server/truth-context.js +90 -131
  85. package/mcp-server/truth-firewall-tools.js +1000 -1611
  86. package/mcp-server/tsconfig.json +34 -0
  87. package/mcp-server/vibecheck-tools.js +2 -2
  88. package/mcp-server/vitest.config.ts +16 -0
  89. package/package.json +3 -4
  90. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +0 -474
  91. package/bin/runners/lib/agent-firewall/change-packet/builder.js +0 -488
  92. package/bin/runners/lib/agent-firewall/change-packet/schema.json +0 -228
  93. package/bin/runners/lib/agent-firewall/change-packet/store.js +0 -200
  94. package/bin/runners/lib/agent-firewall/claims/claim-types.js +0 -21
  95. package/bin/runners/lib/agent-firewall/claims/extractor.js +0 -303
  96. package/bin/runners/lib/agent-firewall/claims/patterns.js +0 -24
  97. package/bin/runners/lib/agent-firewall/critic/index.js +0 -151
  98. package/bin/runners/lib/agent-firewall/critic/judge.js +0 -432
  99. package/bin/runners/lib/agent-firewall/critic/prompts.js +0 -305
  100. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +0 -88
  101. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +0 -75
  102. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +0 -127
  103. package/bin/runners/lib/agent-firewall/evidence/resolver.js +0 -102
  104. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +0 -213
  105. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +0 -145
  106. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +0 -19
  107. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +0 -87
  108. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +0 -184
  109. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +0 -163
  110. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +0 -107
  111. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +0 -68
  112. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +0 -66
  113. package/bin/runners/lib/agent-firewall/interceptor/base.js +0 -304
  114. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +0 -35
  115. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +0 -35
  116. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +0 -34
  117. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +0 -465
  118. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +0 -604
  119. package/bin/runners/lib/agent-firewall/lawbook/index.js +0 -304
  120. package/bin/runners/lib/agent-firewall/lawbook/registry.js +0 -514
  121. package/bin/runners/lib/agent-firewall/lawbook/schema.js +0 -420
  122. package/bin/runners/lib/agent-firewall/logger.js +0 -141
  123. package/bin/runners/lib/agent-firewall/policy/default-policy.json +0 -90
  124. package/bin/runners/lib/agent-firewall/policy/engine.js +0 -103
  125. package/bin/runners/lib/agent-firewall/policy/loader.js +0 -451
  126. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +0 -50
  127. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +0 -50
  128. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +0 -86
  129. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +0 -162
  130. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +0 -189
  131. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +0 -93
  132. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +0 -57
  133. package/bin/runners/lib/agent-firewall/policy/schema.json +0 -183
  134. package/bin/runners/lib/agent-firewall/policy/verdict.js +0 -54
  135. package/bin/runners/lib/agent-firewall/proposal/extractor.js +0 -394
  136. package/bin/runners/lib/agent-firewall/proposal/index.js +0 -212
  137. package/bin/runners/lib/agent-firewall/proposal/schema.js +0 -251
  138. package/bin/runners/lib/agent-firewall/proposal/validator.js +0 -386
  139. package/bin/runners/lib/agent-firewall/reality/index.js +0 -332
  140. package/bin/runners/lib/agent-firewall/reality/state.js +0 -625
  141. package/bin/runners/lib/agent-firewall/reality/watcher.js +0 -322
  142. package/bin/runners/lib/agent-firewall/risk/index.js +0 -173
  143. package/bin/runners/lib/agent-firewall/risk/scorer.js +0 -328
  144. package/bin/runners/lib/agent-firewall/risk/thresholds.js +0 -321
  145. package/bin/runners/lib/agent-firewall/risk/vectors.js +0 -421
  146. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +0 -472
  147. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +0 -346
  148. package/bin/runners/lib/agent-firewall/simulator/index.js +0 -181
  149. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +0 -380
  150. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +0 -661
  151. package/bin/runners/lib/agent-firewall/time-machine/index.js +0 -267
  152. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +0 -436
  153. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +0 -490
  154. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +0 -530
  155. package/bin/runners/lib/agent-firewall/truthpack/index.js +0 -67
  156. package/bin/runners/lib/agent-firewall/truthpack/loader.js +0 -137
  157. package/bin/runners/lib/agent-firewall/unblock/planner.js +0 -337
  158. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +0 -118
  159. package/bin/runners/lib/api-client.js +0 -269
  160. package/bin/runners/lib/authority-badge.js +0 -425
  161. package/bin/runners/lib/engines/accessibility-engine.js +0 -190
  162. package/bin/runners/lib/engines/api-consistency-engine.js +0 -162
  163. package/bin/runners/lib/engines/ast-cache.js +0 -99
  164. package/bin/runners/lib/engines/code-quality-engine.js +0 -255
  165. package/bin/runners/lib/engines/console-logs-engine.js +0 -115
  166. package/bin/runners/lib/engines/cross-file-analysis-engine.js +0 -268
  167. package/bin/runners/lib/engines/dead-code-engine.js +0 -198
  168. package/bin/runners/lib/engines/deprecated-api-engine.js +0 -226
  169. package/bin/runners/lib/engines/empty-catch-engine.js +0 -150
  170. package/bin/runners/lib/engines/file-filter.js +0 -131
  171. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +0 -251
  172. package/bin/runners/lib/engines/mock-data-engine.js +0 -272
  173. package/bin/runners/lib/engines/parallel-processor.js +0 -71
  174. package/bin/runners/lib/engines/performance-issues-engine.js +0 -265
  175. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +0 -243
  176. package/bin/runners/lib/engines/todo-fixme-engine.js +0 -115
  177. package/bin/runners/lib/engines/type-aware-engine.js +0 -152
  178. package/bin/runners/lib/engines/unsafe-regex-engine.js +0 -225
  179. package/bin/runners/lib/engines/vibecheck-engines/README.md +0 -53
  180. package/bin/runners/lib/engines/vibecheck-engines/index.js +0 -15
  181. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
  182. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
  183. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
  184. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
  185. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
  186. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
  187. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
  188. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +0 -139
  189. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
  190. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
  191. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
  192. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
  193. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
  194. package/bin/runners/lib/engines/vibecheck-engines/package.json +0 -13
  195. package/bin/runners/lib/exit-codes.js +0 -275
  196. package/bin/runners/lib/fingerprint.js +0 -377
  197. package/bin/runners/lib/help-formatter.js +0 -413
  198. package/bin/runners/lib/logger.js +0 -38
  199. package/bin/runners/lib/ship-output-enterprise.js +0 -239
  200. package/bin/runners/lib/unified-cli-output.js +0 -604
  201. package/bin/runners/runAgent.d.ts +0 -5
  202. package/bin/runners/runAgent.js +0 -161
  203. package/bin/runners/runApprove.js +0 -1200
  204. package/bin/runners/runClassify.js +0 -859
  205. package/bin/runners/runContext.d.ts +0 -4
  206. package/bin/runners/runFirewall.d.ts +0 -5
  207. package/bin/runners/runFirewall.js +0 -134
  208. package/bin/runners/runFirewallHook.d.ts +0 -5
  209. package/bin/runners/runFirewallHook.js +0 -56
  210. package/bin/runners/runPolish.d.ts +0 -4
  211. package/bin/runners/runProof.zip +0 -0
  212. package/bin/runners/runTruth.d.ts +0 -5
  213. package/bin/runners/runTruth.js +0 -101
  214. package/mcp-server/HARDENING_SUMMARY.md +0 -299
  215. package/mcp-server/agent-firewall-interceptor.js +0 -500
  216. package/mcp-server/authority-tools.js +0 -569
  217. package/mcp-server/conductor/conflict-resolver.js +0 -588
  218. package/mcp-server/conductor/execution-planner.js +0 -544
  219. package/mcp-server/conductor/index.js +0 -377
  220. package/mcp-server/conductor/lock-manager.js +0 -615
  221. package/mcp-server/conductor/request-queue.js +0 -550
  222. package/mcp-server/conductor/session-manager.js +0 -500
  223. package/mcp-server/conductor/tools.js +0 -510
  224. package/mcp-server/lib/api-client.cjs +0 -13
  225. package/mcp-server/lib/logger.cjs +0 -30
  226. package/mcp-server/logger.js +0 -173
  227. package/mcp-server/tools-v3.js +0 -706
  228. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Tool Registry Tests
3
+ *
4
+ * Ensures the registry is valid and consistent.
5
+ *
6
+ * CRITICAL: CI must fail if:
7
+ * - Tool count exceeds 20 without VIBECHECK_ALLOW_TOOL_GROWTH=1
8
+ * - A tool is added without CLI mapping (unless helper)
9
+ */
10
+
11
+ import { describe, it, expect, beforeAll } from 'vitest';
12
+ import { readFileSync, existsSync } from 'fs';
13
+ import { join, dirname } from 'path';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+
18
+ // Maximum allowed tools without explicit override
19
+ const MAX_TOOLS = 20;
20
+ const ALLOW_TOOL_GROWTH = process.env.VIBECHECK_ALLOW_TOOL_GROWTH === '1';
21
+
22
+ interface Tool {
23
+ name: string;
24
+ cli: string | null;
25
+ tier: string;
26
+ description: string;
27
+ category: string;
28
+ cacheable?: boolean;
29
+ cacheMaxAge?: number;
30
+ timeout?: number;
31
+ inputSchema: Record<string, unknown>;
32
+ outputSchema?: Record<string, unknown>;
33
+ tierGated?: Record<string, string>;
34
+ }
35
+
36
+ interface Registry {
37
+ version: string;
38
+ tools: Tool[];
39
+ aliases: Record<string, string>;
40
+ deprecated: Array<{ name: string; reason: string; replacement: string | null; removeIn: string }>;
41
+ categories: Record<string, { description: string; order: number }>;
42
+ tiers: Record<string, { name: string; order: number; price?: number }>;
43
+ }
44
+
45
+ describe('Tool Registry', () => {
46
+ let registry: Registry;
47
+
48
+ beforeAll(() => {
49
+ const registryPath = join(__dirname, '..', 'registry', 'tools.json');
50
+ registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
51
+ });
52
+
53
+ describe('Registry Structure', () => {
54
+ it('should have a version', () => {
55
+ expect(registry.version).toBeDefined();
56
+ expect(registry.version).toMatch(/^\d+\.\d+\.\d+$/);
57
+ });
58
+
59
+ it('should have tools array', () => {
60
+ expect(Array.isArray(registry.tools)).toBe(true);
61
+ expect(registry.tools.length).toBeGreaterThan(0);
62
+ });
63
+
64
+ it('should have aliases object', () => {
65
+ expect(typeof registry.aliases).toBe('object');
66
+ });
67
+
68
+ it('should have categories', () => {
69
+ expect(typeof registry.categories).toBe('object');
70
+ expect(Object.keys(registry.categories).length).toBeGreaterThan(0);
71
+ });
72
+
73
+ it('should have tiers', () => {
74
+ expect(typeof registry.tiers).toBe('object');
75
+ expect(registry.tiers.free).toBeDefined();
76
+ expect(registry.tiers.starter).toBeDefined();
77
+ expect(registry.tiers.pro).toBeDefined();
78
+ });
79
+ });
80
+
81
+ describe('Tool Definitions', () => {
82
+ it('all tools should have required fields', () => {
83
+ for (const tool of registry.tools) {
84
+ expect(tool.name).toBeDefined();
85
+ expect(tool.name).toMatch(/^vibecheck\.[a-z_]+$/);
86
+ expect(tool.tier).toBeDefined();
87
+ expect(tool.description).toBeDefined();
88
+ expect(tool.description.length).toBeGreaterThan(10);
89
+ expect(tool.category).toBeDefined();
90
+ expect(tool.inputSchema).toBeDefined();
91
+ expect(typeof tool.inputSchema).toBe('object');
92
+ }
93
+ });
94
+
95
+ it('all tools should have valid tiers', () => {
96
+ const validTiers = Object.keys(registry.tiers);
97
+
98
+ for (const tool of registry.tools) {
99
+ expect(validTiers).toContain(tool.tier);
100
+ }
101
+ });
102
+
103
+ it('all tools should have valid categories', () => {
104
+ const validCategories = Object.keys(registry.categories);
105
+
106
+ for (const tool of registry.tools) {
107
+ expect(validCategories).toContain(tool.category);
108
+ }
109
+ });
110
+
111
+ it('tool names should be unique', () => {
112
+ const names = registry.tools.map(t => t.name);
113
+ const uniqueNames = new Set(names);
114
+ expect(names.length).toBe(uniqueNames.size);
115
+ });
116
+
117
+ it('cacheable tools should have cacheMaxAge', () => {
118
+ for (const tool of registry.tools) {
119
+ if (tool.cacheable) {
120
+ expect(tool.cacheMaxAge).toBeDefined();
121
+ expect(tool.cacheMaxAge).toBeGreaterThan(0);
122
+ }
123
+ }
124
+ });
125
+
126
+ it('all tools should have reasonable timeouts', () => {
127
+ for (const tool of registry.tools) {
128
+ if (tool.timeout !== undefined) {
129
+ expect(tool.timeout).toBeGreaterThanOrEqual(1000);
130
+ expect(tool.timeout).toBeLessThanOrEqual(600000);
131
+ }
132
+ }
133
+ });
134
+ });
135
+
136
+ describe('CLI Mapping', () => {
137
+ it('CLI-backed tools should have valid cli field', () => {
138
+ const cliTools = registry.tools.filter(t => t.cli !== null);
139
+
140
+ for (const tool of cliTools) {
141
+ expect(typeof tool.cli).toBe('string');
142
+ expect(tool.cli!.length).toBeGreaterThan(0);
143
+ }
144
+ });
145
+
146
+ it('query tools should have null cli', () => {
147
+ const queryTools = registry.tools.filter(t => t.category === 'query');
148
+
149
+ for (const tool of queryTools) {
150
+ expect(tool.cli).toBeNull();
151
+ }
152
+ });
153
+ });
154
+
155
+ describe('Aliases', () => {
156
+ it('all alias targets should exist', () => {
157
+ const toolNames = new Set(registry.tools.map(t => t.name));
158
+
159
+ for (const [alias, target] of Object.entries(registry.aliases)) {
160
+ expect(toolNames.has(target)).toBe(true);
161
+ }
162
+ });
163
+
164
+ it('aliases should not conflict with tool names', () => {
165
+ const toolNames = new Set(registry.tools.map(t => t.name));
166
+
167
+ for (const alias of Object.keys(registry.aliases)) {
168
+ expect(toolNames.has(alias)).toBe(false);
169
+ }
170
+ });
171
+ });
172
+
173
+ describe('Deprecated Tools', () => {
174
+ it('deprecated tools should have required fields', () => {
175
+ for (const dep of registry.deprecated) {
176
+ expect(dep.name).toBeDefined();
177
+ expect(dep.reason).toBeDefined();
178
+ expect(dep.removeIn).toBeDefined();
179
+ expect(dep.removeIn).toMatch(/^\d+\.\d+\.\d+$/);
180
+ }
181
+ });
182
+
183
+ it('replacement tools should exist if specified', () => {
184
+ const toolNames = new Set(registry.tools.map(t => t.name));
185
+
186
+ for (const dep of registry.deprecated) {
187
+ if (dep.replacement !== null) {
188
+ expect(toolNames.has(dep.replacement)).toBe(true);
189
+ }
190
+ }
191
+ });
192
+ });
193
+
194
+ describe('Input Schemas', () => {
195
+ it('all input schemas should be valid JSON Schema', () => {
196
+ for (const tool of registry.tools) {
197
+ expect(tool.inputSchema.type).toBe('object');
198
+ expect(tool.inputSchema.properties).toBeDefined();
199
+ }
200
+ });
201
+
202
+ it('projectPath should have default value', () => {
203
+ for (const tool of registry.tools) {
204
+ const props = tool.inputSchema.properties as Record<string, { default?: string }>;
205
+ if (props.projectPath) {
206
+ expect(props.projectPath.default).toBe('.');
207
+ }
208
+ }
209
+ });
210
+
211
+ it('required fields should be in properties', () => {
212
+ for (const tool of registry.tools) {
213
+ const required = tool.inputSchema.required as string[] | undefined;
214
+ const props = tool.inputSchema.properties as Record<string, unknown>;
215
+
216
+ if (required) {
217
+ for (const field of required) {
218
+ expect(props[field]).toBeDefined();
219
+ }
220
+ }
221
+ }
222
+ });
223
+ });
224
+
225
+ describe('Tier Gating', () => {
226
+ it('tierGated options should reference valid tiers', () => {
227
+ const validTiers = Object.keys(registry.tiers);
228
+
229
+ for (const tool of registry.tools) {
230
+ if (tool.tierGated) {
231
+ for (const tier of Object.values(tool.tierGated)) {
232
+ expect(validTiers).toContain(tier);
233
+ }
234
+ }
235
+ }
236
+ });
237
+
238
+ it('tierGated options should be in input schema', () => {
239
+ for (const tool of registry.tools) {
240
+ if (tool.tierGated) {
241
+ const props = tool.inputSchema.properties as Record<string, unknown>;
242
+ for (const option of Object.keys(tool.tierGated)) {
243
+ expect(props[option]).toBeDefined();
244
+ }
245
+ }
246
+ }
247
+ });
248
+ });
249
+
250
+ describe('Core Tools Present', () => {
251
+ const coreTools = [
252
+ 'vibecheck.doctor',
253
+ 'vibecheck.init',
254
+ 'vibecheck.scan',
255
+ 'vibecheck.ship',
256
+ 'vibecheck.prove',
257
+ 'vibecheck.reality',
258
+ 'vibecheck.fix',
259
+ 'vibecheck.guard',
260
+ 'vibecheck.ctx',
261
+ 'vibecheck.report',
262
+ 'vibecheck.status',
263
+ ];
264
+
265
+ for (const toolName of coreTools) {
266
+ it(`should have ${toolName}`, () => {
267
+ const tool = registry.tools.find(t => t.name === toolName);
268
+ expect(tool).toBeDefined();
269
+ });
270
+ }
271
+ });
272
+
273
+ describe('Category Order', () => {
274
+ it('categories should have sequential order', () => {
275
+ const orders = Object.values(registry.categories).map(c => c.order);
276
+ const sorted = [...orders].sort((a, b) => a - b);
277
+
278
+ for (let i = 0; i < sorted.length; i++) {
279
+ expect(sorted[i]).toBe(i + 1);
280
+ }
281
+ });
282
+ });
283
+
284
+ describe('Tier Order', () => {
285
+ it('tiers should have sequential order', () => {
286
+ const tierEntries = Object.entries(registry.tiers).sort((a, b) => a[1].order - b[1].order);
287
+
288
+ expect(tierEntries[0][0]).toBe('free');
289
+ expect(tierEntries[0][1].order).toBe(0);
290
+ });
291
+
292
+ it('paid tiers should have prices', () => {
293
+ expect(registry.tiers.starter.price).toBeDefined();
294
+ expect(registry.tiers.pro.price).toBeDefined();
295
+ expect(registry.tiers.starter.price).toBeLessThan(registry.tiers.pro.price!);
296
+ });
297
+ });
298
+
299
+ describe('Tool Count Enforcement', () => {
300
+ it(`should not exceed ${MAX_TOOLS} tools without override`, () => {
301
+ const toolCount = registry.tools.length;
302
+
303
+ if (!ALLOW_TOOL_GROWTH) {
304
+ expect(toolCount).toBeLessThanOrEqual(MAX_TOOLS);
305
+ }
306
+
307
+ // Always log warning if approaching limit
308
+ if (toolCount >= MAX_TOOLS - 2) {
309
+ console.warn(`Warning: Tool count (${toolCount}) is approaching limit (${MAX_TOOLS})`);
310
+ }
311
+ });
312
+
313
+ it('all CLI-backed tools should have valid CLI mapping', () => {
314
+ const helperCategories = ['query', 'utility'];
315
+
316
+ for (const tool of registry.tools) {
317
+ // Helper tools are allowed to have null CLI
318
+ const isHelper = helperCategories.includes(tool.category) && tool.cli === null;
319
+
320
+ if (!isHelper) {
321
+ // Non-helper tools must have CLI mapping or be explicitly a helper
322
+ if (tool.cli === null) {
323
+ // This is a query/utility helper - allowed
324
+ expect(tool.description.toLowerCase()).toMatch(/artifact|query|list|get|search/);
325
+ }
326
+ }
327
+ }
328
+ });
329
+
330
+ it('should not add tools without CLI mapping (unless helper)', () => {
331
+ const toolsWithoutCli = registry.tools.filter(t => t.cli === null);
332
+ const allowedHelpers = [
333
+ 'vibecheck.get_truthpack',
334
+ 'vibecheck.validate_claim',
335
+ 'vibecheck.search_evidence',
336
+ 'vibecheck.list_reports',
337
+ 'vibecheck.get_last_verdict',
338
+ 'vibecheck.get_finding',
339
+ ];
340
+
341
+ for (const tool of toolsWithoutCli) {
342
+ expect(allowedHelpers).toContain(tool.name);
343
+ }
344
+ });
345
+ });
346
+
347
+ describe('Deprecation Handling', () => {
348
+ it('deprecated tools should have valid replacement or null', () => {
349
+ const toolNames = new Set(registry.tools.map(t => t.name));
350
+
351
+ for (const dep of registry.deprecated) {
352
+ if (dep.replacement) {
353
+ expect(toolNames.has(dep.replacement)).toBe(true);
354
+ }
355
+ }
356
+ });
357
+
358
+ it('aliases should warn about deprecation', () => {
359
+ // All aliases should have a target that exists
360
+ for (const [alias, target] of Object.entries(registry.aliases)) {
361
+ expect(registry.tools.some(t => t.name === target)).toBe(true);
362
+ }
363
+ });
364
+ });
365
+ });
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Sandbox Security Tests
3
+ *
4
+ * Ensures path sandboxing is secure and prevents:
5
+ * - Path traversal attacks (../)
6
+ * - Absolute path escape
7
+ * - Symlink escape
8
+ * - Access to sensitive files
9
+ */
10
+
11
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
12
+ import { mkdirSync, writeFileSync, symlinkSync, rmSync, existsSync } from 'fs';
13
+ import { join, resolve } from 'path';
14
+ import { tmpdir } from 'os';
15
+ import { createSandbox, redactPath, redactSensitive } from '../lib/sandbox.js';
16
+
17
+ describe('Path Sandboxing', () => {
18
+ let testDir: string;
19
+ let workspace: string;
20
+
21
+ beforeAll(() => {
22
+ testDir = join(tmpdir(), `vibecheck-sandbox-test-${Date.now()}`);
23
+ workspace = join(testDir, 'workspace');
24
+ mkdirSync(workspace, { recursive: true });
25
+ mkdirSync(join(workspace, 'src'));
26
+ mkdirSync(join(workspace, 'node_modules', 'pkg'), { recursive: true });
27
+ mkdirSync(join(workspace, 'venv', 'lib'), { recursive: true });
28
+
29
+ writeFileSync(join(workspace, 'src', 'app.ts'), 'export const app = 1;');
30
+ writeFileSync(join(workspace, 'node_modules', 'pkg', 'index.js'), 'module.exports = {}');
31
+
32
+ // Create a directory outside workspace for escape tests
33
+ mkdirSync(join(testDir, 'outside'));
34
+ writeFileSync(join(testDir, 'outside', 'secret.txt'), 'secret');
35
+
36
+ // Create a symlink pointing outside (if platform supports)
37
+ try {
38
+ symlinkSync(join(testDir, 'outside'), join(workspace, 'escape-link'));
39
+ } catch {
40
+ // Symlinks may not be supported on all platforms
41
+ }
42
+ });
43
+
44
+ afterAll(() => {
45
+ if (existsSync(testDir)) {
46
+ rmSync(testDir, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ describe('validate()', () => {
51
+ it('should allow paths within workspace', () => {
52
+ const sandbox = createSandbox({ workspace });
53
+
54
+ const result = sandbox.validate(join(workspace, 'src', 'app.ts'));
55
+ expect(result.allowed).toBe(true);
56
+ expect(result.relativePath).toBe(join('src', 'app.ts'));
57
+ });
58
+
59
+ it('should allow relative paths', () => {
60
+ const sandbox = createSandbox({ workspace });
61
+
62
+ const result = sandbox.validate('src/app.ts');
63
+ expect(result.allowed).toBe(true);
64
+ });
65
+
66
+ it('should block path traversal with ../', () => {
67
+ const sandbox = createSandbox({ workspace });
68
+
69
+ const result = sandbox.validate('../outside/secret.txt');
70
+ expect(result.allowed).toBe(false);
71
+ expect(result.reason).toMatch(/traversal|outside/i);
72
+ });
73
+
74
+ it('should block path traversal with multiple ../', () => {
75
+ const sandbox = createSandbox({ workspace });
76
+
77
+ const result = sandbox.validate('src/../../outside/secret.txt');
78
+ expect(result.allowed).toBe(false);
79
+ });
80
+
81
+ it('should block absolute paths outside workspace', () => {
82
+ const sandbox = createSandbox({ workspace });
83
+
84
+ const result = sandbox.validate(join(testDir, 'outside', 'secret.txt'));
85
+ expect(result.allowed).toBe(false);
86
+ expect(result.reason).toMatch(/outside|sandbox/i);
87
+ });
88
+
89
+ it('should block access to /etc/passwd (Unix)', () => {
90
+ const sandbox = createSandbox({ workspace });
91
+
92
+ const result = sandbox.validate('/etc/passwd');
93
+ expect(result.allowed).toBe(false);
94
+ });
95
+
96
+ it('should block access to C:\\Windows (Windows)', () => {
97
+ const sandbox = createSandbox({ workspace });
98
+
99
+ const result = sandbox.validate('C:\\Windows\\System32\\config');
100
+ expect(result.allowed).toBe(false);
101
+ });
102
+
103
+ it('should block access to .ssh directory', () => {
104
+ const sandbox = createSandbox({ workspace });
105
+
106
+ const result = sandbox.validate('/home/user/.ssh/id_rsa');
107
+ expect(result.allowed).toBe(false);
108
+ });
109
+
110
+ it('should block access to .aws directory', () => {
111
+ const sandbox = createSandbox({ workspace });
112
+
113
+ const result = sandbox.validate('/home/user/.aws/credentials');
114
+ expect(result.allowed).toBe(false);
115
+ });
116
+
117
+ it('should handle path depth limit', () => {
118
+ const sandbox = createSandbox({ workspace, maxDepth: 5 });
119
+
120
+ // Deep path
121
+ const deepPath = 'a/b/c/d/e/f/g/h/i/j/file.ts';
122
+ const result = sandbox.validate(deepPath);
123
+ expect(result.allowed).toBe(false);
124
+ expect(result.reason).toMatch(/depth/i);
125
+ });
126
+ });
127
+
128
+ describe('shouldExclude()', () => {
129
+ it('should exclude node_modules by default', () => {
130
+ const sandbox = createSandbox({ workspace });
131
+
132
+ expect(sandbox.shouldExclude('node_modules/pkg/index.js')).toBe(true);
133
+ expect(sandbox.shouldExclude(join(workspace, 'node_modules', 'pkg', 'index.js'))).toBe(true);
134
+ });
135
+
136
+ it('should exclude venv by default', () => {
137
+ const sandbox = createSandbox({ workspace });
138
+
139
+ expect(sandbox.shouldExclude('venv/lib/config.py')).toBe(true);
140
+ expect(sandbox.shouldExclude('.venv/lib/config.py')).toBe(true);
141
+ });
142
+
143
+ it('should exclude .git by default', () => {
144
+ const sandbox = createSandbox({ workspace });
145
+
146
+ expect(sandbox.shouldExclude('.git/config')).toBe(true);
147
+ expect(sandbox.shouldExclude('.git/objects/abc')).toBe(true);
148
+ });
149
+
150
+ it('should exclude dist/build/out by default', () => {
151
+ const sandbox = createSandbox({ workspace });
152
+
153
+ expect(sandbox.shouldExclude('dist/bundle.js')).toBe(true);
154
+ expect(sandbox.shouldExclude('build/output.js')).toBe(true);
155
+ expect(sandbox.shouldExclude('out/compiled.js')).toBe(true);
156
+ });
157
+
158
+ it('should exclude .next by default', () => {
159
+ const sandbox = createSandbox({ workspace });
160
+
161
+ expect(sandbox.shouldExclude('.next/server/app.js')).toBe(true);
162
+ });
163
+
164
+ it('should exclude coverage by default', () => {
165
+ const sandbox = createSandbox({ workspace });
166
+
167
+ expect(sandbox.shouldExclude('coverage/lcov.info')).toBe(true);
168
+ });
169
+
170
+ it('should exclude .cache and .turbo by default', () => {
171
+ const sandbox = createSandbox({ workspace });
172
+
173
+ expect(sandbox.shouldExclude('.cache/webpack/chunk.js')).toBe(true);
174
+ expect(sandbox.shouldExclude('.turbo/cache.json')).toBe(true);
175
+ });
176
+
177
+ it('should NOT exclude src files', () => {
178
+ const sandbox = createSandbox({ workspace });
179
+
180
+ expect(sandbox.shouldExclude('src/app.ts')).toBe(false);
181
+ expect(sandbox.shouldExclude('lib/utils.ts')).toBe(false);
182
+ expect(sandbox.shouldExclude('app/page.tsx')).toBe(false);
183
+ });
184
+
185
+ it('should apply custom exclusion patterns', () => {
186
+ const sandbox = createSandbox({
187
+ workspace,
188
+ excludePatterns: [/custom_exclude\//],
189
+ });
190
+
191
+ expect(sandbox.shouldExclude('custom_exclude/file.ts')).toBe(true);
192
+ });
193
+ });
194
+
195
+ describe('requireValid()', () => {
196
+ it('should return normalized path for valid paths', () => {
197
+ const sandbox = createSandbox({ workspace });
198
+
199
+ const result = sandbox.requireValid('src/app.ts');
200
+ expect(result).toBe(resolve(workspace, 'src/app.ts'));
201
+ });
202
+
203
+ it('should throw for invalid paths', () => {
204
+ const sandbox = createSandbox({ workspace });
205
+
206
+ expect(() => sandbox.requireValid('../outside/secret.txt')).toThrow();
207
+ });
208
+ });
209
+
210
+ describe('exists()', () => {
211
+ it('should return true for existing files in sandbox', () => {
212
+ const sandbox = createSandbox({ workspace });
213
+
214
+ expect(sandbox.exists('src/app.ts')).toBe(true);
215
+ });
216
+
217
+ it('should return false for non-existent files', () => {
218
+ const sandbox = createSandbox({ workspace });
219
+
220
+ expect(sandbox.exists('src/nonexistent.ts')).toBe(false);
221
+ });
222
+
223
+ it('should return false for paths outside sandbox', () => {
224
+ const sandbox = createSandbox({ workspace });
225
+
226
+ // Even if file exists outside, should return false
227
+ expect(sandbox.exists('../outside/secret.txt')).toBe(false);
228
+ });
229
+ });
230
+
231
+ describe('path conversion', () => {
232
+ it('toRelative should convert absolute to relative', () => {
233
+ const sandbox = createSandbox({ workspace });
234
+
235
+ const relative = sandbox.toRelative(join(workspace, 'src', 'app.ts'));
236
+ expect(relative).toBe(join('src', 'app.ts'));
237
+ });
238
+
239
+ it('toAbsolute should convert relative to absolute', () => {
240
+ const sandbox = createSandbox({ workspace });
241
+
242
+ const absolute = sandbox.toAbsolute('src/app.ts');
243
+ expect(absolute).toBe(resolve(workspace, 'src/app.ts'));
244
+ });
245
+
246
+ it('getWorkspace should return workspace root', () => {
247
+ const sandbox = createSandbox({ workspace });
248
+
249
+ expect(sandbox.getWorkspace()).toBe(resolve(workspace));
250
+ });
251
+ });
252
+ });
253
+
254
+ describe('Path Redaction', () => {
255
+ it('should redact paths outside workspace', () => {
256
+ const result = redactPath('/etc/passwd', '/home/user/project');
257
+ expect(result).toBe('[REDACTED]');
258
+ });
259
+
260
+ it('should return relative path for paths within workspace', () => {
261
+ const result = redactPath('/home/user/project/src/app.ts', '/home/user/project');
262
+ expect(result).toBe('src/app.ts');
263
+ });
264
+ });
265
+
266
+ describe('Sensitive Data Redaction', () => {
267
+ it('should redact password fields', () => {
268
+ const obj = { username: 'admin', password: 'secret123' };
269
+ const result = redactSensitive(obj);
270
+ expect(result.username).toBe('admin');
271
+ expect(result.password).toBe('[REDACTED]');
272
+ });
273
+
274
+ it('should redact apiKey fields', () => {
275
+ const obj = { name: 'test', apiKey: 'sk-xxx-yyy' };
276
+ const result = redactSensitive(obj);
277
+ expect(result.name).toBe('test');
278
+ expect(result.apiKey).toBe('[REDACTED]');
279
+ });
280
+
281
+ it('should redact nested sensitive fields', () => {
282
+ const obj = {
283
+ config: {
284
+ database: {
285
+ password: 'dbpass',
286
+ host: 'localhost',
287
+ },
288
+ },
289
+ };
290
+ const result = redactSensitive(obj) as { config: { database: { password: string; host: string } } };
291
+ expect(result.config.database.host).toBe('localhost');
292
+ expect(result.config.database.password).toBe('[REDACTED]');
293
+ });
294
+
295
+ it('should redact arrays with sensitive objects', () => {
296
+ const obj = {
297
+ users: [
298
+ { name: 'alice', secret: 'alice-secret' },
299
+ { name: 'bob', secret: 'bob-secret' },
300
+ ],
301
+ };
302
+ const result = redactSensitive(obj) as { users: Array<{ name: string; secret: string }> };
303
+ expect(result.users[0].name).toBe('alice');
304
+ expect(result.users[0].secret).toBe('[REDACTED]');
305
+ expect(result.users[1].secret).toBe('[REDACTED]');
306
+ });
307
+
308
+ it('should handle various sensitive key patterns', () => {
309
+ const obj = {
310
+ auth_token: 'token',
311
+ credential: 'cred',
312
+ private_key: 'key',
313
+ session_cookie: 'cookie',
314
+ safe_value: 'value',
315
+ };
316
+ const result = redactSensitive(obj);
317
+ expect(result.auth_token).toBe('[REDACTED]');
318
+ expect(result.credential).toBe('[REDACTED]');
319
+ expect(result.private_key).toBe('[REDACTED]');
320
+ expect(result.session_cookie).toBe('[REDACTED]');
321
+ expect(result.safe_value).toBe('value');
322
+ });
323
+ });