@vibecheckai/cli 3.6.1 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +31 -38
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +68 -3
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +4 -2
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +5 -4
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/error-messages.js +1 -1
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/report-output.js +6 -6
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/tool-handler.ts +3 -3
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/tier-auth.js +164 -16
- package/mcp-server/tools-v3.js +70 -16
- package/package.json +1 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -1,1202 +1,320 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* vibecheck approve -
|
|
2
|
+
* vibecheck approve - Session Approval CLI
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* SEAMLESS INTENT - SESSION APPROVAL COMMAND
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* vibecheck approve safe-consolidation
|
|
11
|
-
* vibecheck approve --list
|
|
8
|
+
* Review and approve changes made during a vibe session.
|
|
9
|
+
* Creates intent retroactively from approved changes.
|
|
12
10
|
*
|
|
13
|
-
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* vibecheck approve # Interactive review
|
|
13
|
+
* vibecheck approve -y # Auto-approve with generated intent
|
|
14
|
+
* vibecheck approve -m "..." # Approve with custom message
|
|
15
|
+
* vibecheck approve --reject # Reject pending changes
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - Input validation for all parameters
|
|
18
|
-
* - Timeout enforcement for long-running analyses
|
|
19
|
-
* - Audit trail for all executions
|
|
20
|
-
* - Signed verdicts (PRO tier)
|
|
17
|
+
* @module runApprove
|
|
18
|
+
* @version 1.0.0
|
|
21
19
|
*/
|
|
22
20
|
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
23
|
const path = require("path");
|
|
24
|
-
const fs = require("fs");
|
|
25
|
-
const { withErrorHandling, createUserError } = require("./lib/error-handler");
|
|
26
|
-
const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
|
|
27
|
-
const { EXIT } = require("./lib/exit-codes");
|
|
28
24
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
// Lazy imports
|
|
26
|
+
let _globalFlags = null;
|
|
27
|
+
let _exitCodes = null;
|
|
28
|
+
let _cliOutput = null;
|
|
29
|
+
let _sessionModule = null;
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
function getGlobalFlags() {
|
|
32
|
+
if (!_globalFlags) {
|
|
33
|
+
_globalFlags = require("./lib/global-flags");
|
|
34
|
+
}
|
|
35
|
+
return _globalFlags;
|
|
36
|
+
}
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
${ansi.rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${ansi.reset}
|
|
38
|
+
function getExitCodes() {
|
|
39
|
+
if (!_exitCodes) {
|
|
40
|
+
_exitCodes = require("./lib/exit-codes");
|
|
41
|
+
}
|
|
42
|
+
return _exitCodes;
|
|
43
|
+
}
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
function getCliOutput() {
|
|
46
|
+
if (!_cliOutput) {
|
|
47
|
+
_cliOutput = require("./lib/unified-cli-output");
|
|
48
|
+
}
|
|
49
|
+
return _cliOutput;
|
|
50
|
+
}
|
|
51
51
|
|
|
52
|
-
function
|
|
53
|
-
|
|
52
|
+
function getSessionModule() {
|
|
53
|
+
if (!_sessionModule) {
|
|
54
|
+
_sessionModule = require("./lib/agent-firewall/session");
|
|
55
|
+
}
|
|
56
|
+
return _sessionModule;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
57
|
-
//
|
|
60
|
+
// HELP
|
|
58
61
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
59
62
|
|
|
60
|
-
function
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
const opts = {
|
|
64
|
-
path: globalFlags.path || process.cwd(),
|
|
65
|
-
json: globalFlags.json || false,
|
|
66
|
-
verbose: globalFlags.verbose || false,
|
|
67
|
-
help: globalFlags.help || false,
|
|
68
|
-
noBanner: globalFlags.noBanner || false,
|
|
69
|
-
ci: globalFlags.ci || false,
|
|
70
|
-
quiet: globalFlags.quiet || false,
|
|
71
|
-
// Authority options
|
|
72
|
-
authority: null, // Authority ID to execute
|
|
73
|
-
list: false, // List available authorities
|
|
74
|
-
// Output options
|
|
75
|
-
output: null, // Output file path
|
|
76
|
-
badge: false, // Generate badge for PROCEED
|
|
77
|
-
// Dry run
|
|
78
|
-
dryRun: false, // Don't save results, just analyze
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < cleanArgs.length; i++) {
|
|
82
|
-
const arg = cleanArgs[i];
|
|
83
|
-
|
|
84
|
-
if (arg === '--list' || arg === '-l') opts.list = true;
|
|
85
|
-
else if (arg === '--output' || arg === '-o') opts.output = cleanArgs[++i];
|
|
86
|
-
else if (arg === '--badge' || arg === '-b') opts.badge = true;
|
|
87
|
-
else if (arg === '--dry-run') opts.dryRun = true;
|
|
88
|
-
else if (arg === '--path' || arg === '-p') opts.path = cleanArgs[++i] || process.cwd();
|
|
89
|
-
else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
|
|
90
|
-
else if (!arg.startsWith('-') && !opts.authority) opts.authority = arg;
|
|
91
|
-
}
|
|
63
|
+
function printHelp() {
|
|
64
|
+
const { ansi } = getCliOutput();
|
|
92
65
|
|
|
93
|
-
return opts;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function printHelp(showBanner = true) {
|
|
97
|
-
if (showBanner && shouldShowBanner({})) {
|
|
98
|
-
printBanner();
|
|
99
|
-
}
|
|
100
66
|
console.log(`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
67
|
+
${ansi.bold}${ansi.cyan}╔═══════════════════════════════════════════════════════════════════════════════╗
|
|
68
|
+
║ ║
|
|
69
|
+
║ ${ansi.reset}${ansi.bold}SESSION APPROVAL${ansi.cyan} ║
|
|
70
|
+
║ ${ansi.reset}${ansi.dim}Review and approve vibe session changes${ansi.cyan} ║
|
|
71
|
+
║ ║
|
|
72
|
+
╚═══════════════════════════════════════════════════════════════════════════════╝${ansi.reset}
|
|
104
73
|
|
|
105
|
-
${ansi.
|
|
106
|
-
|
|
107
|
-
Execute an authority to get a structured verdict with proofs.
|
|
108
|
-
Authorities are policy engines that analyze your code and produce
|
|
109
|
-
PROCEED/STOP/DEFER verdicts with evidence.
|
|
74
|
+
${ansi.bold}USAGE${ansi.reset}
|
|
75
|
+
${ansi.cyan}vibecheck approve${ansi.reset} [options]
|
|
110
76
|
|
|
111
|
-
${ansi.
|
|
112
|
-
|
|
113
|
-
${colors.accent}inventory${ansi.reset} Read-only duplication/legacy maps ${ansi.dim}(use 'classify')${ansi.reset}
|
|
77
|
+
${ansi.dim}Review AI changes made during a vibe session.
|
|
78
|
+
Approved changes create intent retroactively.${ansi.reset}
|
|
114
79
|
|
|
115
80
|
${ansi.bold}OPTIONS${ansi.reset}
|
|
116
|
-
${
|
|
117
|
-
${
|
|
118
|
-
${
|
|
119
|
-
${
|
|
81
|
+
${ansi.cyan}-y, --yes${ansi.reset} Auto-approve with generated intent
|
|
82
|
+
${ansi.cyan}-m, --message <text>${ansi.reset} Approve with custom intent message
|
|
83
|
+
${ansi.cyan}--reject${ansi.reset} Reject all pending changes
|
|
84
|
+
${ansi.cyan}--clear${ansi.reset} Clear session without creating intent
|
|
120
85
|
|
|
121
|
-
${ansi.bold}
|
|
122
|
-
${
|
|
123
|
-
|
|
124
|
-
${colors.accent}--verbose, -v${ansi.reset} Show detailed analysis
|
|
125
|
-
${colors.accent}--quiet, -q${ansi.reset} Suppress non-essential output
|
|
126
|
-
${colors.accent}--ci${ansi.reset} CI mode (quiet + exit codes)
|
|
127
|
-
${colors.accent}--help, -h${ansi.reset} Show this help
|
|
86
|
+
${ansi.bold}EXAMPLES${ansi.reset}
|
|
87
|
+
${ansi.dim}# Interactive review${ansi.reset}
|
|
88
|
+
vibecheck approve
|
|
128
89
|
|
|
129
|
-
|
|
90
|
+
${ansi.dim}# Auto-approve${ansi.reset}
|
|
91
|
+
vibecheck approve -y
|
|
130
92
|
|
|
131
|
-
${ansi.dim}#
|
|
132
|
-
vibecheck approve
|
|
93
|
+
${ansi.dim}# Custom intent${ansi.reset}
|
|
94
|
+
vibecheck approve -m "Added user profile feature"
|
|
133
95
|
|
|
134
|
-
${ansi.dim}#
|
|
135
|
-
vibecheck approve
|
|
96
|
+
${ansi.dim}# Reject changes${ansi.reset}
|
|
97
|
+
vibecheck approve --reject
|
|
136
98
|
|
|
137
|
-
|
|
138
|
-
vibecheck approve safe-consolidation --badge
|
|
139
|
-
|
|
140
|
-
${ansi.dim}# List available authorities${ansi.reset}
|
|
141
|
-
vibecheck approve --list
|
|
142
|
-
|
|
143
|
-
${ansi.bold}📊 VERDICT ACTIONS${ansi.reset}
|
|
144
|
-
${colors.success}PROCEED${ansi.reset} Safe to continue, all proofs satisfied (exit 0)
|
|
145
|
-
${colors.warning}DEFER${ansi.reset} Manual review required (exit 1)
|
|
146
|
-
${colors.error}STOP${ansi.reset} Unsafe, hard stops triggered (exit 2)
|
|
147
|
-
|
|
148
|
-
${ansi.bold}🔗 RELATED COMMANDS${ansi.reset}
|
|
149
|
-
${colors.accent}vibecheck classify${ansi.reset} Read-only inventory ${ansi.dim}(FREE)${ansi.reset}
|
|
150
|
-
${colors.accent}vibecheck scan${ansi.reset} Full code analysis
|
|
151
|
-
${colors.accent}vibecheck ship${ansi.reset} SHIP/WARN/BLOCK verdict
|
|
152
|
-
|
|
153
|
-
${ansi.dim}─────────────────────────────────────────────────────────────${ansi.reset}
|
|
99
|
+
${ansi.dim}────────────────────────────────────────────────────────────────────${ansi.reset}
|
|
154
100
|
${ansi.dim}Documentation: https://docs.vibecheckai.dev/cli/approve${ansi.reset}
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
159
|
-
// AUTHORITY DEFINITIONS (Inline for now - will use packages/core in production)
|
|
160
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
161
|
-
|
|
162
|
-
const AUTHORITIES = {
|
|
163
|
-
'safe-consolidation': {
|
|
164
|
-
id: 'safe-consolidation',
|
|
165
|
-
version: '1.0.0',
|
|
166
|
-
description: 'Zero-behavior-change cleanup of duplicated and legacy code',
|
|
167
|
-
tier: 'starter',
|
|
168
|
-
scope: {
|
|
169
|
-
allowedActions: ['analyze', 'propose_diff'],
|
|
170
|
-
disallowedActions: ['delete', 'rename_public_exports', 'force_publish', 'modify_env', 'modify_migrations'],
|
|
171
|
-
allowedRisk: ['LOW'],
|
|
172
|
-
excludedPaths: [
|
|
173
|
-
'packages/cli/**',
|
|
174
|
-
'**/migrations/**',
|
|
175
|
-
'**/prisma/migrations/**',
|
|
176
|
-
'**/*.env*',
|
|
177
|
-
'**/node_modules/**',
|
|
178
|
-
'**/dist/**',
|
|
179
|
-
'**/build/**',
|
|
180
|
-
'**/.git/**',
|
|
181
|
-
'**/auth/**',
|
|
182
|
-
'**/secrets/**',
|
|
183
|
-
'**/billing/**',
|
|
184
|
-
],
|
|
185
|
-
},
|
|
186
|
-
permissions: {
|
|
187
|
-
readRepo: true,
|
|
188
|
-
writeProposals: true,
|
|
189
|
-
applyChanges: false,
|
|
190
|
-
enforceCI: true,
|
|
191
|
-
},
|
|
192
|
-
requiredProofs: {
|
|
193
|
-
reachability: 'Static + dynamic analysis proving no runtime imports',
|
|
194
|
-
compatibility: 'Import path preservation via aliases/re-exports',
|
|
195
|
-
behaviorLock: 'Observable behavior unchanged (errors, timing, side effects)',
|
|
196
|
-
rollback: 'Single git revert restores previous state',
|
|
197
|
-
},
|
|
198
|
-
hardStops: {
|
|
199
|
-
dynamicImportDetected: true,
|
|
200
|
-
migrationFlagDetected: true,
|
|
201
|
-
behaviorChangeUncertain: true,
|
|
202
|
-
securitySensitive: true,
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
'security-remediation': {
|
|
206
|
-
id: 'security-remediation',
|
|
207
|
-
version: '1.0.0',
|
|
208
|
-
description: 'Verified security fixes with proof of safety',
|
|
209
|
-
tier: 'pro',
|
|
210
|
-
scope: {
|
|
211
|
-
allowedActions: ['analyze', 'propose_diff'],
|
|
212
|
-
disallowedActions: [
|
|
213
|
-
'delete_auth_code',
|
|
214
|
-
'modify_crypto_primitives',
|
|
215
|
-
'alter_permission_checks',
|
|
216
|
-
'remove_rate_limits',
|
|
217
|
-
'expose_internal_apis',
|
|
218
|
-
],
|
|
219
|
-
allowedRisk: ['LOW', 'MEDIUM'],
|
|
220
|
-
excludedPaths: [
|
|
221
|
-
'**/node_modules/**',
|
|
222
|
-
'**/dist/**',
|
|
223
|
-
'**/build/**',
|
|
224
|
-
'**/.git/**',
|
|
225
|
-
'**/migrations/**',
|
|
226
|
-
'**/*.env*',
|
|
227
|
-
],
|
|
228
|
-
},
|
|
229
|
-
permissions: {
|
|
230
|
-
readRepo: true,
|
|
231
|
-
writeProposals: true,
|
|
232
|
-
applyChanges: false,
|
|
233
|
-
enforceCI: true,
|
|
234
|
-
},
|
|
235
|
-
requiredProofs: {
|
|
236
|
-
reachability: 'Proof that vulnerable code path is reachable and fix addresses it',
|
|
237
|
-
compatibility: 'Proof that fix does not break existing functionality',
|
|
238
|
-
behaviorLock: 'Proof that security behavior is strengthened, not weakened',
|
|
239
|
-
rollback: 'Proof that fix can be reverted without leaving system vulnerable',
|
|
240
|
-
},
|
|
241
|
-
hardStops: {
|
|
242
|
-
dynamicImportDetected: true,
|
|
243
|
-
migrationFlagDetected: false,
|
|
244
|
-
behaviorChangeUncertain: true,
|
|
245
|
-
securitySensitive: false,
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
'inventory': {
|
|
249
|
-
id: 'inventory',
|
|
250
|
-
version: '1.0.0',
|
|
251
|
-
description: 'Read-only inventory of duplication and legacy code',
|
|
252
|
-
tier: 'free',
|
|
253
|
-
note: 'Use "vibecheck classify" for this authority',
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
258
|
-
// TIER CHECKING
|
|
259
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
260
|
-
|
|
261
|
-
async function checkTierAccess(authorityTier) {
|
|
262
|
-
try {
|
|
263
|
-
const entitlementsV2 = require("./lib/entitlements-v2");
|
|
264
|
-
const access = await entitlementsV2.enforce(`authority.${authorityTier}`, {
|
|
265
|
-
silent: true,
|
|
266
|
-
});
|
|
267
|
-
return access;
|
|
268
|
-
} catch (err) {
|
|
269
|
-
// Fallback to free tier if entitlements fail
|
|
270
|
-
return {
|
|
271
|
-
allowed: authorityTier === 'free',
|
|
272
|
-
tier: 'free',
|
|
273
|
-
reason: authorityTier === 'free' ? 'Free tier access' : 'Upgrade required',
|
|
274
|
-
};
|
|
275
|
-
}
|
|
101
|
+
`);
|
|
276
102
|
}
|
|
277
103
|
|
|
278
104
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
279
|
-
//
|
|
105
|
+
// MAIN ENTRY
|
|
280
106
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
281
107
|
|
|
282
108
|
/**
|
|
283
|
-
*
|
|
109
|
+
* Main approve command handler
|
|
110
|
+
* @param {string[]} args - Command arguments
|
|
111
|
+
* @param {Object} context - Execution context
|
|
112
|
+
* @returns {Promise<number>} Exit code
|
|
284
113
|
*/
|
|
285
|
-
function
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
{ pattern: /process\.env\./, reason: 'Environment-dependent behavior' },
|
|
290
|
-
{ pattern: /FEATURE_FLAG/i, reason: 'Feature flag detected' },
|
|
291
|
-
{ pattern: /feature\s*toggle/i, reason: 'Feature toggle detected' },
|
|
292
|
-
{ pattern: /eval\s*\(/, reason: 'Eval detected' },
|
|
293
|
-
{ pattern: /Function\s*\(/, reason: 'Dynamic function construction' },
|
|
294
|
-
];
|
|
295
|
-
|
|
296
|
-
const triggered = [];
|
|
297
|
-
for (const { pattern, reason } of patterns) {
|
|
298
|
-
if (pattern.test(content)) {
|
|
299
|
-
triggered.push(reason);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
114
|
+
async function runApprove(args = [], context = {}) {
|
|
115
|
+
const { parseGlobalFlags, shouldSuppressOutput, isJsonMode } = getGlobalFlags();
|
|
116
|
+
const { EXIT } = getExitCodes();
|
|
117
|
+
const { ansi, renderSuccess, renderError, renderWarning } = getCliOutput();
|
|
302
118
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
* Check if file is in excluded paths
|
|
308
|
-
*/
|
|
309
|
-
function isExcludedPath(filePath, excludedPaths) {
|
|
310
|
-
const minimatch = require('minimatch');
|
|
119
|
+
const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
|
|
120
|
+
const quiet = shouldSuppressOutput(globalFlags);
|
|
121
|
+
const json = isJsonMode(globalFlags) || args.includes("--json");
|
|
122
|
+
const projectRoot = context.repoRoot || globalFlags.path || process.cwd();
|
|
311
123
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
124
|
+
// Handle help
|
|
125
|
+
if (globalFlags.help || args.includes("--help") || args.includes("-h")) {
|
|
126
|
+
printHelp();
|
|
127
|
+
return EXIT.SUCCESS;
|
|
316
128
|
}
|
|
317
129
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
*/
|
|
324
|
-
async function analyzeSafeConsolidation(projectPath, authority, opts, spinner) {
|
|
325
|
-
const startTime = Date.now();
|
|
326
|
-
const findings = {
|
|
327
|
-
safeToConsolidate: [],
|
|
328
|
-
needsReview: [],
|
|
329
|
-
blocked: [],
|
|
330
|
-
};
|
|
331
|
-
const hardStopsTriggered = [];
|
|
332
|
-
const filesTouched = [];
|
|
333
|
-
|
|
334
|
-
// Get inventory first
|
|
335
|
-
const { runClassify } = require("./runClassify");
|
|
336
|
-
|
|
337
|
-
spinner.update('Running inventory analysis...');
|
|
338
|
-
|
|
339
|
-
// Run classify in quiet mode to get data
|
|
340
|
-
const inventoryResult = await runInventoryForApprove(projectPath, opts);
|
|
341
|
-
|
|
342
|
-
spinner.update('Analyzing consolidation safety...');
|
|
343
|
-
|
|
344
|
-
// Analyze each duplicate group
|
|
345
|
-
for (const dupGroup of inventoryResult.duplicationMap) {
|
|
346
|
-
const primary = dupGroup.primary;
|
|
347
|
-
filesTouched.push(primary);
|
|
348
|
-
|
|
349
|
-
// Skip if in excluded paths
|
|
350
|
-
if (isExcludedPath(primary, authority.scope.excludedPaths)) {
|
|
351
|
-
findings.blocked.push({
|
|
352
|
-
file: primary,
|
|
353
|
-
reason: 'In excluded path',
|
|
354
|
-
recommendation: 'No action needed - file is intentionally excluded',
|
|
355
|
-
});
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Read primary file content
|
|
360
|
-
const primaryPath = path.join(projectPath, primary);
|
|
361
|
-
let content = '';
|
|
362
|
-
try {
|
|
363
|
-
content = await fs.promises.readFile(primaryPath, 'utf-8');
|
|
364
|
-
} catch (err) {
|
|
365
|
-
findings.blocked.push({
|
|
366
|
-
file: primary,
|
|
367
|
-
reason: 'Unable to read file',
|
|
368
|
-
recommendation: 'Check file permissions',
|
|
369
|
-
});
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Check for unsafe patterns
|
|
374
|
-
const unsafeReasons = checkUnsafePatterns(content);
|
|
375
|
-
if (unsafeReasons.length > 0) {
|
|
376
|
-
findings.blocked.push({
|
|
377
|
-
file: primary,
|
|
378
|
-
reason: unsafeReasons.join(', '),
|
|
379
|
-
recommendation: 'Manual review required - contains patterns that prevent safe automated consolidation',
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
if (authority.hardStops.dynamicImportDetected && unsafeReasons.some(r => r.includes('import'))) {
|
|
383
|
-
hardStopsTriggered.push(`Dynamic import in ${primary}`);
|
|
384
|
-
}
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Check similarity level
|
|
389
|
-
if (dupGroup.similarity >= 0.95) {
|
|
390
|
-
findings.safeToConsolidate.push({
|
|
391
|
-
primary: primary,
|
|
392
|
-
duplicates: dupGroup.duplicates,
|
|
393
|
-
similarity: dupGroup.similarity,
|
|
394
|
-
type: dupGroup.type,
|
|
395
|
-
action: 'Create re-export from duplicates to primary',
|
|
396
|
-
linesSaved: dupGroup.lineCount * dupGroup.duplicates.length,
|
|
397
|
-
});
|
|
398
|
-
} else {
|
|
399
|
-
findings.needsReview.push({
|
|
400
|
-
file: primary,
|
|
401
|
-
duplicates: dupGroup.duplicates,
|
|
402
|
-
similarity: dupGroup.similarity,
|
|
403
|
-
reason: `Similarity ${Math.round(dupGroup.similarity * 100)}% below threshold for auto-consolidation`,
|
|
404
|
-
recommendation: 'Review differences manually before consolidating',
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
}
|
|
130
|
+
// Parse options
|
|
131
|
+
const autoApprove = args.includes("-y") || args.includes("--yes");
|
|
132
|
+
const reject = args.includes("--reject");
|
|
133
|
+
const clear = args.includes("--clear");
|
|
134
|
+
const customMessage = getArgValue(args, "-m") || getArgValue(args, "--message");
|
|
408
135
|
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (legacyEntry.type === 'backup' && legacyEntry.confidence >= 0.9) {
|
|
418
|
-
findings.safeToConsolidate.push({
|
|
419
|
-
primary: legacyEntry.file,
|
|
420
|
-
duplicates: [],
|
|
421
|
-
type: 'legacy-backup',
|
|
422
|
-
action: 'Archive or remove backup file',
|
|
423
|
-
evidence: legacyEntry.evidence,
|
|
424
|
-
});
|
|
136
|
+
// Load session module
|
|
137
|
+
let SessionCollector;
|
|
138
|
+
try {
|
|
139
|
+
SessionCollector = getSessionModule().SessionCollector;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (json) {
|
|
142
|
+
console.log(JSON.stringify({ success: false, error: "Session module not available" }));
|
|
425
143
|
} else {
|
|
426
|
-
|
|
427
|
-
file: legacyEntry.file,
|
|
428
|
-
type: legacyEntry.type,
|
|
429
|
-
confidence: legacyEntry.confidence,
|
|
430
|
-
reason: 'Legacy code - requires manual review',
|
|
431
|
-
recommendation: 'Verify no active references before removal',
|
|
432
|
-
});
|
|
144
|
+
renderError("Session module not available");
|
|
433
145
|
}
|
|
146
|
+
return EXIT.INTERNAL_ERROR;
|
|
434
147
|
}
|
|
435
148
|
|
|
436
|
-
|
|
437
|
-
const proofs = {
|
|
438
|
-
reachability: hardStopsTriggered.length > 0
|
|
439
|
-
? `FAILED: ${hardStopsTriggered.join('; ')}`
|
|
440
|
-
: 'PASSED: No dynamic imports detected in safe files',
|
|
441
|
-
compatibility: findings.safeToConsolidate.length > 0
|
|
442
|
-
? 'PASSED: Re-exports preserve import paths'
|
|
443
|
-
: 'N/A: No consolidations proposed',
|
|
444
|
-
rollback: 'PASSED: Single git revert restores state',
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
// Determine verdict
|
|
448
|
-
let action = 'PROCEED';
|
|
449
|
-
let confidence = 0.95;
|
|
450
|
-
|
|
451
|
-
if (hardStopsTriggered.length > 0) {
|
|
452
|
-
action = 'STOP';
|
|
453
|
-
confidence = 1.0;
|
|
454
|
-
} else if (findings.needsReview.length > findings.safeToConsolidate.length) {
|
|
455
|
-
action = 'DEFER';
|
|
456
|
-
confidence = 0.6;
|
|
457
|
-
} else if (findings.blocked.length > 0) {
|
|
458
|
-
action = 'DEFER';
|
|
459
|
-
confidence = 0.7;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return {
|
|
463
|
-
authority: authority.id,
|
|
464
|
-
version: authority.version,
|
|
465
|
-
timestamp: new Date().toISOString(),
|
|
466
|
-
action,
|
|
467
|
-
riskLevel: action === 'STOP' ? 'HIGH' : action === 'DEFER' ? 'MEDIUM' : 'LOW',
|
|
468
|
-
exitCode: action === 'PROCEED' ? 0 : action === 'DEFER' ? 1 : 2,
|
|
469
|
-
filesTouched,
|
|
470
|
-
proofs,
|
|
471
|
-
behaviorChange: action === 'STOP',
|
|
472
|
-
confidence,
|
|
473
|
-
hardStopsTriggered,
|
|
474
|
-
notes: generateNotes(action, findings, hardStopsTriggered),
|
|
475
|
-
analysis: {
|
|
476
|
-
safeToConsolidate: findings.safeToConsolidate,
|
|
477
|
-
needsReview: findings.needsReview,
|
|
478
|
-
blocked: findings.blocked,
|
|
479
|
-
summary: {
|
|
480
|
-
safeCount: findings.safeToConsolidate.length,
|
|
481
|
-
reviewCount: findings.needsReview.length,
|
|
482
|
-
blockedCount: findings.blocked.length,
|
|
483
|
-
totalLinesSavings: findings.safeToConsolidate.reduce((sum, f) => sum + (f.linesSaved || 0), 0),
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
|
-
analysisTimeMs: Date.now() - startTime,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Run inventory analysis for approve command
|
|
492
|
-
*/
|
|
493
|
-
async function runInventoryForApprove(projectPath, opts) {
|
|
494
|
-
const { runClassify } = require("./runClassify");
|
|
495
|
-
|
|
496
|
-
// Create a minimal spinner that captures output
|
|
497
|
-
const inventoryOpts = {
|
|
498
|
-
path: projectPath,
|
|
499
|
-
json: true,
|
|
500
|
-
quiet: true,
|
|
501
|
-
includeNear: true,
|
|
502
|
-
includeSemantic: false,
|
|
503
|
-
threshold: 0.8,
|
|
504
|
-
maxFiles: 5000,
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
// Directly run the inventory analysis logic
|
|
508
|
-
const crypto = require("crypto");
|
|
509
|
-
|
|
510
|
-
const EXCLUDED_DIRS = new Set([
|
|
511
|
-
'node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.vibecheck',
|
|
512
|
-
]);
|
|
149
|
+
const collector = new SessionCollector(projectRoot);
|
|
513
150
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
522
|
-
for (const entry of entries) {
|
|
523
|
-
if (files.length >= maxFiles) break;
|
|
524
|
-
const fullPath = path.join(dir, entry.name);
|
|
525
|
-
const relativePath = path.relative(rootPath, fullPath);
|
|
526
|
-
if (entry.isDirectory()) {
|
|
527
|
-
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
528
|
-
await walk(fullPath, depth + 1);
|
|
529
|
-
}
|
|
530
|
-
} else if (entry.isFile()) {
|
|
531
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
532
|
-
if (extensions.has(ext)) {
|
|
533
|
-
files.push({ path: fullPath, relativePath, name: entry.name, ext });
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
} catch (err) { /* skip */ }
|
|
151
|
+
// Check for pending changes
|
|
152
|
+
if (!collector.hasPending()) {
|
|
153
|
+
if (json) {
|
|
154
|
+
console.log(JSON.stringify({ success: true, message: "No pending changes", changes: 0 }));
|
|
155
|
+
} else if (!quiet) {
|
|
156
|
+
renderWarning("No pending changes to review");
|
|
157
|
+
console.log(`\n ${ansi.dim}Changes are logged when AI modifies files in OBSERVE mode.${ansi.reset}\n`);
|
|
538
158
|
}
|
|
539
|
-
|
|
540
|
-
return files;
|
|
159
|
+
return EXIT.SUCCESS;
|
|
541
160
|
}
|
|
542
161
|
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
const fileHashes = new Map();
|
|
546
|
-
|
|
547
|
-
for (const file of files) {
|
|
548
|
-
try {
|
|
549
|
-
const content = await fs.promises.readFile(file.path, 'utf-8');
|
|
550
|
-
const lines = content.split('\n').length;
|
|
551
|
-
fileContents.set(file.relativePath, { content, lines });
|
|
552
|
-
fileHashes.set(file.relativePath, crypto.createHash('sha256').update(content).digest('hex').slice(0, 16));
|
|
553
|
-
} catch (err) { /* skip */ }
|
|
554
|
-
}
|
|
162
|
+
const session = collector.getPending();
|
|
163
|
+
const summary = collector.getSummary();
|
|
555
164
|
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
|
|
165
|
+
// Handle reject
|
|
166
|
+
if (reject) {
|
|
167
|
+
collector.reject();
|
|
168
|
+
if (json) {
|
|
169
|
+
console.log(JSON.stringify({ success: true, action: "rejected", changes: summary.total_changes }));
|
|
170
|
+
} else if (!quiet) {
|
|
171
|
+
renderSuccess("Session changes rejected");
|
|
172
|
+
console.log(`\n ${ansi.dim}${summary.total_changes} change(s) discarded.${ansi.reset}\n`);
|
|
173
|
+
}
|
|
174
|
+
return EXIT.SUCCESS;
|
|
561
175
|
}
|
|
562
176
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
type: 'exact',
|
|
572
|
-
lineCount: lines,
|
|
573
|
-
});
|
|
177
|
+
// Handle clear (no intent created)
|
|
178
|
+
if (clear) {
|
|
179
|
+
collector.clear();
|
|
180
|
+
if (json) {
|
|
181
|
+
console.log(JSON.stringify({ success: true, action: "cleared", changes: summary.total_changes }));
|
|
182
|
+
} else if (!quiet) {
|
|
183
|
+
renderSuccess("Session cleared");
|
|
184
|
+
console.log(`\n ${ansi.dim}Session reset. No intent created.${ansi.reset}\n`);
|
|
574
185
|
}
|
|
186
|
+
return EXIT.SUCCESS;
|
|
575
187
|
}
|
|
576
188
|
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
189
|
+
// Show changes summary
|
|
190
|
+
if (!json && !quiet) {
|
|
191
|
+
console.log(`
|
|
192
|
+
${ansi.cyan}${ansi.bold}╔═══════════════════════════════════════════════════════════════════╗
|
|
193
|
+
║ ║
|
|
194
|
+
║ SESSION REVIEW ║
|
|
195
|
+
║ ║
|
|
196
|
+
╚═══════════════════════════════════════════════════════════════════╝${ansi.reset}
|
|
197
|
+
|
|
198
|
+
${ansi.bold}Session Info${ansi.reset}
|
|
199
|
+
ID: ${session.id}
|
|
200
|
+
Started: ${new Date(session.started_at).toLocaleString()}
|
|
201
|
+
Duration: ${summary.duration_minutes} minutes
|
|
202
|
+
|
|
203
|
+
${ansi.bold}Changes (${summary.total_changes})${ansi.reset}`);
|
|
204
|
+
|
|
205
|
+
// Group by domain
|
|
206
|
+
const byDomain = {};
|
|
207
|
+
for (const change of session.changes) {
|
|
208
|
+
const domain = change.domain || "general";
|
|
209
|
+
if (!byDomain[domain]) byDomain[domain] = [];
|
|
210
|
+
byDomain[domain].push(change);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const [domain, changes] of Object.entries(byDomain)) {
|
|
214
|
+
console.log(`\n ${ansi.bold}${domain}${ansi.reset} (${changes.length})`);
|
|
215
|
+
for (const change of changes.slice(0, 5)) {
|
|
216
|
+
const icon = change.type === "file_create" ? "+" : change.type === "file_delete" ? "-" : "~";
|
|
217
|
+
const color = change.type === "file_create" ? ansi.green : change.type === "file_delete" ? ansi.red : ansi.yellow;
|
|
218
|
+
console.log(` ${color}${icon}${ansi.reset} ${change.path}`);
|
|
219
|
+
}
|
|
220
|
+
if (changes.length > 5) {
|
|
221
|
+
console.log(` ${ansi.dim}... and ${changes.length - 5} more${ansi.reset}`);
|
|
596
222
|
}
|
|
597
223
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
224
|
+
|
|
225
|
+
// Show auto-generated intent
|
|
226
|
+
const generatedIntent = collector.generateIntentSummary();
|
|
227
|
+
console.log(`
|
|
228
|
+
${ansi.bold}Auto-Generated Intent${ansi.reset}
|
|
229
|
+
"${generatedIntent}"
|
|
230
|
+
`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Handle auto-approve
|
|
234
|
+
if (autoApprove || customMessage) {
|
|
235
|
+
const intentMessage = customMessage || collector.generateIntentSummary();
|
|
236
|
+
const result = collector.approve(intentMessage);
|
|
237
|
+
|
|
238
|
+
if (json) {
|
|
239
|
+
console.log(JSON.stringify({
|
|
240
|
+
success: result.success,
|
|
241
|
+
action: "approved",
|
|
242
|
+
intent: result.intent ? {
|
|
243
|
+
hash: result.intent.hash,
|
|
244
|
+
summary: result.intent.summary,
|
|
245
|
+
} : null,
|
|
246
|
+
changes: summary.total_changes,
|
|
247
|
+
error: result.error,
|
|
248
|
+
}, null, 2));
|
|
249
|
+
} else if (!quiet) {
|
|
250
|
+
if (result.success) {
|
|
251
|
+
console.log(`
|
|
252
|
+
${ansi.green}${ansi.bold}╔═══════════════════════════════════════════════════════════════════╗
|
|
253
|
+
║ ║
|
|
254
|
+
║ ✓ SESSION APPROVED ║
|
|
255
|
+
║ ║
|
|
256
|
+
╚═══════════════════════════════════════════════════════════════════╝${ansi.reset}
|
|
257
|
+
|
|
258
|
+
${ansi.bold}Intent Created${ansi.reset}
|
|
259
|
+
"${result.intent.summary}"
|
|
260
|
+
|
|
261
|
+
${ansi.bold}Changes Approved${ansi.reset}
|
|
262
|
+
${result.changes_approved} change(s)
|
|
263
|
+
|
|
264
|
+
${ansi.dim}Hash: ${result.intent.hash.slice(0, 16)}...${ansi.reset}
|
|
265
|
+
|
|
266
|
+
${ansi.bold}The Agent Firewall is now in ENFORCE mode.${ansi.reset}
|
|
267
|
+
${ansi.dim}Future changes must align with this intent.${ansi.reset}
|
|
268
|
+
`);
|
|
269
|
+
} else {
|
|
270
|
+
renderError(result.error || "Failed to approve session");
|
|
608
271
|
}
|
|
609
272
|
}
|
|
273
|
+
|
|
274
|
+
return result.success ? EXIT.SUCCESS : EXIT.INTERNAL_ERROR;
|
|
610
275
|
}
|
|
611
276
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
},
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Generate human-readable notes for verdict
|
|
625
|
-
*/
|
|
626
|
-
function generateNotes(action, findings, hardStopsTriggered) {
|
|
627
|
-
if (action === 'STOP') {
|
|
628
|
-
return `BLOCKED: Hard stops triggered. ${hardStopsTriggered.join('. ')}. Manual intervention required.`;
|
|
277
|
+
// Interactive mode - show options
|
|
278
|
+
if (!json && !quiet) {
|
|
279
|
+
console.log(` ${ansi.bold}Options${ansi.reset}
|
|
280
|
+
${ansi.cyan}vibecheck approve -y${ansi.reset} Approve with auto-generated intent
|
|
281
|
+
${ansi.cyan}vibecheck approve -m "..."${ansi.reset} Approve with custom message
|
|
282
|
+
${ansi.cyan}vibecheck approve --reject${ansi.reset} Reject all changes
|
|
283
|
+
`);
|
|
629
284
|
}
|
|
630
285
|
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
286
|
+
if (json) {
|
|
287
|
+
console.log(JSON.stringify({
|
|
288
|
+
pending: true,
|
|
289
|
+
session_id: session.id,
|
|
290
|
+
changes: summary.total_changes,
|
|
291
|
+
files_touched: summary.files_touched,
|
|
292
|
+
domains: summary.domains,
|
|
293
|
+
generated_intent: collector.generateIntentSummary(),
|
|
294
|
+
}, null, 2));
|
|
635
295
|
}
|
|
636
296
|
|
|
637
|
-
|
|
638
|
-
return `SAFE TO PROCEED: ${findings.safeToConsolidate.length} consolidations identified. Estimated ${linesSaved} lines can be reduced.`;
|
|
297
|
+
return EXIT.SUCCESS;
|
|
639
298
|
}
|
|
640
299
|
|
|
641
300
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
642
|
-
//
|
|
301
|
+
// UTILITIES
|
|
643
302
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
644
303
|
|
|
645
|
-
function
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
lines.push('');
|
|
652
|
-
lines.push('┌────────────────────────────────────────────────────────────────────┐');
|
|
653
|
-
lines.push(`│ ${actionColor}${actionSymbol}${ansi.reset} Authority Verdict: ${ansi.bold}${verdict.action}${ansi.reset} │`);
|
|
654
|
-
lines.push('├────────────────────────────────────────────────────────────────────┤');
|
|
655
|
-
lines.push(`│ Authority: ${verdict.authority} v${verdict.version}`.padEnd(68) + '│');
|
|
656
|
-
lines.push(`│ Risk Level: ${verdict.riskLevel}`.padEnd(68) + '│');
|
|
657
|
-
lines.push(`│ Confidence: ${Math.round(verdict.confidence * 100)}%`.padEnd(68) + '│');
|
|
658
|
-
lines.push(`│ Exit Code: ${verdict.exitCode}`.padEnd(68) + '│');
|
|
659
|
-
lines.push('└────────────────────────────────────────────────────────────────────┘');
|
|
660
|
-
lines.push('');
|
|
661
|
-
|
|
662
|
-
// Hard stops
|
|
663
|
-
if (verdict.hardStopsTriggered.length > 0) {
|
|
664
|
-
lines.push(`${ansi.bold}${colors.error}Hard Stops Triggered:${ansi.reset}`);
|
|
665
|
-
for (const stop of verdict.hardStopsTriggered) {
|
|
666
|
-
lines.push(` ${colors.error}✗${ansi.reset} ${stop}`);
|
|
304
|
+
function getArgValue(args, ...flags) {
|
|
305
|
+
for (const flag of flags) {
|
|
306
|
+
const index = args.indexOf(flag);
|
|
307
|
+
if (index !== -1 && args[index + 1] && !args[index + 1].startsWith("-")) {
|
|
308
|
+
return args[index + 1];
|
|
667
309
|
}
|
|
668
|
-
lines.push('');
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Proofs
|
|
672
|
-
lines.push(`${ansi.bold}Proofs:${ansi.reset}`);
|
|
673
|
-
for (const [key, value] of Object.entries(verdict.proofs)) {
|
|
674
|
-
const icon = value.startsWith('PASSED') ? colors.success + '✓' : value.startsWith('FAILED') ? colors.error + '✗' : colors.warning + '○';
|
|
675
|
-
lines.push(` ${icon}${ansi.reset} ${key}: ${ansi.dim}${value}${ansi.reset}`);
|
|
676
|
-
}
|
|
677
|
-
lines.push('');
|
|
678
|
-
|
|
679
|
-
// Analysis summary
|
|
680
|
-
if (verdict.analysis) {
|
|
681
|
-
const { summary } = verdict.analysis;
|
|
682
|
-
lines.push(`${ansi.bold}Analysis Summary:${ansi.reset}`);
|
|
683
|
-
lines.push(` ${colors.success}✓${ansi.reset} Safe to consolidate: ${summary.safeCount}`);
|
|
684
|
-
lines.push(` ${colors.warning}⚠${ansi.reset} Needs review: ${summary.reviewCount}`);
|
|
685
|
-
lines.push(` ${colors.error}✗${ansi.reset} Blocked: ${summary.blockedCount}`);
|
|
686
|
-
lines.push(` ${ansi.dim}Estimated lines saved: ${summary.totalLinesSavings}${ansi.reset}`);
|
|
687
|
-
lines.push('');
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Notes
|
|
691
|
-
lines.push(`${ansi.bold}Notes:${ansi.reset}`);
|
|
692
|
-
lines.push(` ${verdict.notes}`);
|
|
693
|
-
lines.push('');
|
|
694
|
-
|
|
695
|
-
// Timestamp
|
|
696
|
-
lines.push(`${ansi.dim}${verdict.timestamp}${ansi.reset}`);
|
|
697
|
-
lines.push('');
|
|
698
|
-
|
|
699
|
-
return lines.join('\n');
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Generate authority approved badge SVG
|
|
704
|
-
*/
|
|
705
|
-
function generateBadge(verdict) {
|
|
706
|
-
const color = verdict.action === 'PROCEED' ? '#22C55E' : verdict.action === 'DEFER' ? '#F59E0B' : '#EF4444';
|
|
707
|
-
const text = verdict.action;
|
|
708
|
-
const conf = Math.round(verdict.confidence * 100);
|
|
709
|
-
|
|
710
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="240" height="20">
|
|
711
|
-
<linearGradient id="b" x2="0" y2="100%">
|
|
712
|
-
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
713
|
-
<stop offset="1" stop-opacity=".1"/>
|
|
714
|
-
</linearGradient>
|
|
715
|
-
<clipPath id="a">
|
|
716
|
-
<rect width="240" height="20" rx="3" fill="#fff"/>
|
|
717
|
-
</clipPath>
|
|
718
|
-
<g clip-path="url(#a)">
|
|
719
|
-
<path fill="#555" d="M0 0h150v20H0z"/>
|
|
720
|
-
<path fill="${color}" d="M150 0h90v20H150z"/>
|
|
721
|
-
<path fill="url(#b)" d="M0 0h240v20H0z"/>
|
|
722
|
-
</g>
|
|
723
|
-
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
724
|
-
<text x="75" y="15" fill="#010101" fill-opacity=".3">Authority Approved</text>
|
|
725
|
-
<text x="75" y="14">Authority Approved</text>
|
|
726
|
-
<text x="195" y="15" fill="#010101" fill-opacity=".3">${text} ${conf}%</text>
|
|
727
|
-
<text x="195" y="14">${text} ${conf}%</text>
|
|
728
|
-
</g>
|
|
729
|
-
</svg>`;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
733
|
-
// HARDENING CONSTANTS
|
|
734
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
735
|
-
|
|
736
|
-
/** Maximum execution time for an authority (ms) */
|
|
737
|
-
const MAX_EXECUTION_TIME_MS = 5 * 60 * 1000; // 5 minutes
|
|
738
|
-
|
|
739
|
-
/** Rate limit tracking */
|
|
740
|
-
const rateLimitStore = new Map();
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Check rate limit for user
|
|
744
|
-
*/
|
|
745
|
-
function checkRateLimit(userId, windowMs, maxRequests) {
|
|
746
|
-
const key = `${userId}:${windowMs}`;
|
|
747
|
-
const now = Date.now();
|
|
748
|
-
|
|
749
|
-
let entry = rateLimitStore.get(key);
|
|
750
|
-
|
|
751
|
-
if (!entry || entry.resetAt < now) {
|
|
752
|
-
entry = { count: 0, resetAt: now + windowMs };
|
|
753
|
-
rateLimitStore.set(key, entry);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const allowed = entry.count < maxRequests;
|
|
757
|
-
if (allowed) {
|
|
758
|
-
entry.count++;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
return {
|
|
762
|
-
allowed,
|
|
763
|
-
remaining: Math.max(0, maxRequests - entry.count),
|
|
764
|
-
resetAt: new Date(entry.resetAt),
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Validate authority ID format
|
|
770
|
-
*/
|
|
771
|
-
function validateAuthorityId(id) {
|
|
772
|
-
const pattern = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/;
|
|
773
|
-
|
|
774
|
-
if (!id || typeof id !== 'string') {
|
|
775
|
-
return { valid: false, error: 'Authority ID must be a non-empty string' };
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
if (id.length < 2 || id.length > 64) {
|
|
779
|
-
return { valid: false, error: 'Authority ID must be 2-64 characters' };
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
if (!pattern.test(id)) {
|
|
783
|
-
return { valid: false, error: 'Authority ID must be lowercase alphanumeric with hyphens' };
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
return { valid: true };
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Validate project path for security
|
|
791
|
-
*/
|
|
792
|
-
function validateProjectPath(projectPath) {
|
|
793
|
-
if (!projectPath || typeof projectPath !== 'string') {
|
|
794
|
-
return { valid: false, error: 'Project path must be a non-empty string' };
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Check for path traversal attempts
|
|
798
|
-
if (projectPath.includes('..') || projectPath.includes('\0')) {
|
|
799
|
-
return { valid: false, error: 'Invalid characters in project path' };
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// Must be absolute path
|
|
803
|
-
const isAbsolute = projectPath.startsWith('/') || /^[A-Za-z]:[\\/]/.test(projectPath);
|
|
804
|
-
if (!isAbsolute) {
|
|
805
|
-
return { valid: false, error: 'Project path must be absolute' };
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
return { valid: true };
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* Execute with timeout
|
|
813
|
-
*/
|
|
814
|
-
async function withTimeout(fn, timeoutMs, timeoutError = 'Operation timed out') {
|
|
815
|
-
let timeoutId;
|
|
816
|
-
|
|
817
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
818
|
-
timeoutId = setTimeout(() => {
|
|
819
|
-
reject(new Error(timeoutError));
|
|
820
|
-
}, timeoutMs);
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
try {
|
|
824
|
-
const result = await Promise.race([fn(), timeoutPromise]);
|
|
825
|
-
clearTimeout(timeoutId);
|
|
826
|
-
return result;
|
|
827
|
-
} catch (error) {
|
|
828
|
-
clearTimeout(timeoutId);
|
|
829
|
-
throw error;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Sanitize error for safe logging
|
|
835
|
-
*/
|
|
836
|
-
function sanitizeError(error) {
|
|
837
|
-
if (error instanceof Error) {
|
|
838
|
-
let message = error.message;
|
|
839
|
-
// Remove file paths
|
|
840
|
-
message = message.replace(/[A-Za-z]:[\\\/][^\s]+/g, '[PATH]');
|
|
841
|
-
message = message.replace(/\/[^\s]+/g, '[PATH]');
|
|
842
|
-
// Remove potential secrets
|
|
843
|
-
message = message.replace(/['"][^'"]{20,}['"]/g, '[REDACTED]');
|
|
844
|
-
|
|
845
|
-
return {
|
|
846
|
-
message: message.slice(0, 200),
|
|
847
|
-
code: error.name || 'Error',
|
|
848
|
-
};
|
|
849
310
|
}
|
|
850
|
-
|
|
851
|
-
return {
|
|
852
|
-
message: 'An unexpected error occurred',
|
|
853
|
-
code: 'UNKNOWN_ERROR',
|
|
854
|
-
};
|
|
311
|
+
return null;
|
|
855
312
|
}
|
|
856
313
|
|
|
857
314
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
858
|
-
//
|
|
315
|
+
// EXPORTS
|
|
859
316
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
860
317
|
|
|
861
|
-
async function runApprove(args) {
|
|
862
|
-
const opts = parseArgs(args);
|
|
863
|
-
const executionId = `exec_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
864
|
-
const startTime = Date.now();
|
|
865
|
-
|
|
866
|
-
// Show help
|
|
867
|
-
if (opts.help) {
|
|
868
|
-
printHelp(shouldShowBanner(opts));
|
|
869
|
-
return 0;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// Print banner
|
|
873
|
-
if (shouldShowBanner(opts)) {
|
|
874
|
-
printBanner();
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// List authorities
|
|
878
|
-
if (opts.list) {
|
|
879
|
-
console.log(`\n ${ansi.bold}Available Authorities:${ansi.reset}\n`);
|
|
880
|
-
|
|
881
|
-
for (const [id, auth] of Object.entries(AUTHORITIES)) {
|
|
882
|
-
const tierLabel = auth.tier === 'free' ? colors.success + '[FREE]' :
|
|
883
|
-
auth.tier === 'starter' ? colors.accent + '[STARTER]' :
|
|
884
|
-
auth.tier === 'pro' ? colors.warning + '[PRO]' :
|
|
885
|
-
colors.error + '[ENTERPRISE]';
|
|
886
|
-
console.log(` ${colors.accent}${id}${ansi.reset} ${tierLabel}${ansi.reset}`);
|
|
887
|
-
console.log(` ${ansi.dim}${auth.description}${ansi.reset}`);
|
|
888
|
-
if (auth.note) {
|
|
889
|
-
console.log(` ${colors.warning}Note: ${auth.note}${ansi.reset}`);
|
|
890
|
-
}
|
|
891
|
-
console.log();
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
console.log(` ${ansi.dim}Usage: vibecheck approve <authority-id>${ansi.reset}\n`);
|
|
895
|
-
return 0;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// Require authority argument
|
|
899
|
-
if (!opts.authority) {
|
|
900
|
-
console.error(`\n ${colors.error}✗${ansi.reset} Authority ID required\n`);
|
|
901
|
-
console.log(` ${ansi.dim}Usage: vibecheck approve <authority-id>${ansi.reset}`);
|
|
902
|
-
console.log(` ${ansi.dim}List: vibecheck approve --list${ansi.reset}\n`);
|
|
903
|
-
return EXIT.USER_ERROR;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// HARDENING: Validate authority ID format
|
|
907
|
-
const idValidation = validateAuthorityId(opts.authority);
|
|
908
|
-
if (!idValidation.valid) {
|
|
909
|
-
console.error(`\n ${colors.error}✗${ansi.reset} Invalid authority ID: ${idValidation.error}\n`);
|
|
910
|
-
return EXIT.USER_ERROR;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Check authority exists
|
|
914
|
-
const authority = AUTHORITIES[opts.authority];
|
|
915
|
-
if (!authority) {
|
|
916
|
-
console.error(`\n ${colors.error}✗${ansi.reset} Unknown authority: ${opts.authority}\n`);
|
|
917
|
-
console.log(` ${ansi.dim}Available: ${Object.keys(AUTHORITIES).join(', ')}${ansi.reset}\n`);
|
|
918
|
-
return EXIT.USER_ERROR;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Special case for inventory - redirect to classify
|
|
922
|
-
if (opts.authority === 'inventory') {
|
|
923
|
-
console.log(`\n ${colors.warning}⚠${ansi.reset} Use 'vibecheck classify' for the inventory authority\n`);
|
|
924
|
-
return EXIT.USER_ERROR;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// HARDENING: Rate limiting (10 requests per minute)
|
|
928
|
-
const userId = process.env.VIBECHECK_USER_ID || 'anonymous';
|
|
929
|
-
const rateCheck = checkRateLimit(userId, 60 * 1000, 10);
|
|
930
|
-
if (!rateCheck.allowed) {
|
|
931
|
-
console.error(`\n ${colors.error}✗${ansi.reset} Rate limit exceeded`);
|
|
932
|
-
console.log(` ${ansi.dim}Try again at: ${rateCheck.resetAt.toISOString()}${ansi.reset}\n`);
|
|
933
|
-
return EXIT.RATE_LIMITED || 429;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// Check tier access
|
|
937
|
-
const access = await checkTierAccess(authority.tier);
|
|
938
|
-
if (!access.allowed) {
|
|
939
|
-
console.log(`\n ${colors.error}✗${ansi.reset} ${ansi.bold}${authority.tier.toUpperCase()} tier required${ansi.reset}`);
|
|
940
|
-
console.log(` ${ansi.dim}Current tier: ${access.tier || 'FREE'}${ansi.reset}`);
|
|
941
|
-
console.log(` ${ansi.dim}Upgrade at: https://vibecheckai.dev/pricing${ansi.reset}\n`);
|
|
942
|
-
return EXIT.TIER_REQUIRED;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const projectPath = path.resolve(opts.path);
|
|
946
|
-
|
|
947
|
-
// HARDENING: Validate project path
|
|
948
|
-
const pathValidation = validateProjectPath(projectPath);
|
|
949
|
-
if (!pathValidation.valid) {
|
|
950
|
-
throw createUserError(pathValidation.error, "SecurityError");
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// Validate project path exists
|
|
954
|
-
if (!fs.existsSync(projectPath)) {
|
|
955
|
-
throw createUserError(`Project path does not exist: ${projectPath}`, "ValidationError");
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (!opts.quiet) {
|
|
959
|
-
console.log(` ${ansi.dim}Execution:${ansi.reset} ${ansi.dim}${executionId}${ansi.reset}`);
|
|
960
|
-
console.log(` ${ansi.dim}Project:${ansi.reset} ${ansi.bold}${path.basename(projectPath)}${ansi.reset}`);
|
|
961
|
-
console.log(` ${ansi.dim}Authority:${ansi.reset} ${colors.accent}${authority.id}${ansi.reset} v${authority.version}`);
|
|
962
|
-
console.log(` ${ansi.dim}Tier:${ansi.reset} ${authority.tier.toUpperCase()}`);
|
|
963
|
-
console.log();
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Run analysis with timeout
|
|
967
|
-
const spinner = new Spinner({ color: colors.primary });
|
|
968
|
-
spinner.start('Executing authority...');
|
|
969
|
-
|
|
970
|
-
try {
|
|
971
|
-
let verdict;
|
|
972
|
-
|
|
973
|
-
// HARDENING: Execute with timeout
|
|
974
|
-
verdict = await withTimeout(
|
|
975
|
-
async () => {
|
|
976
|
-
if (opts.authority === 'safe-consolidation') {
|
|
977
|
-
return await analyzeSafeConsolidation(projectPath, authority, opts, spinner);
|
|
978
|
-
} else if (opts.authority === 'security-remediation') {
|
|
979
|
-
// Forward to security analysis
|
|
980
|
-
return await analyzeSecurityRemediation(projectPath, authority, opts, spinner);
|
|
981
|
-
} else {
|
|
982
|
-
throw createUserError(`Authority executor not implemented: ${opts.authority}`, "NotImplemented");
|
|
983
|
-
}
|
|
984
|
-
},
|
|
985
|
-
MAX_EXECUTION_TIME_MS,
|
|
986
|
-
`Authority execution timed out after ${MAX_EXECUTION_TIME_MS / 1000}s`
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
// Add execution metadata
|
|
990
|
-
verdict.executionId = executionId;
|
|
991
|
-
verdict.analysisTimeMs = Date.now() - startTime;
|
|
992
|
-
|
|
993
|
-
spinner.succeed(`Analysis complete (${verdict.analysisTimeMs}ms)`);
|
|
994
|
-
|
|
995
|
-
// Output
|
|
996
|
-
if (opts.json) {
|
|
997
|
-
console.log(JSON.stringify(verdict, null, 2));
|
|
998
|
-
} else {
|
|
999
|
-
// Use Mission Control format
|
|
1000
|
-
const { formatApproveOutput } = require('./lib/approve-output');
|
|
1001
|
-
console.log(formatApproveOutput(verdict, { projectPath, version: 'v3.5.5' }));
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Save to file if requested
|
|
1005
|
-
if (opts.output && !opts.dryRun) {
|
|
1006
|
-
const outputPath = path.resolve(opts.output);
|
|
1007
|
-
await fs.promises.writeFile(outputPath, JSON.stringify(verdict, null, 2));
|
|
1008
|
-
|
|
1009
|
-
if (!opts.quiet && !opts.json) {
|
|
1010
|
-
console.log(` ${colors.success}✓${ansi.reset} Verdict saved to: ${outputPath}`);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Generate badge if requested
|
|
1015
|
-
if (opts.badge && verdict.action === 'PROCEED' && !opts.dryRun) {
|
|
1016
|
-
const badgePath = path.join(projectPath, '.vibecheck', 'badges', `${authority.id}-badge.svg`);
|
|
1017
|
-
const badgeDir = path.dirname(badgePath);
|
|
1018
|
-
|
|
1019
|
-
if (!fs.existsSync(badgeDir)) {
|
|
1020
|
-
fs.mkdirSync(badgeDir, { recursive: true });
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
await fs.promises.writeFile(badgePath, generateBadge(verdict));
|
|
1024
|
-
|
|
1025
|
-
if (!opts.quiet && !opts.json) {
|
|
1026
|
-
console.log(` ${colors.success}✓${ansi.reset} Badge saved to: ${badgePath}`);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Log execution summary (for audit)
|
|
1031
|
-
if (opts.verbose) {
|
|
1032
|
-
console.log(`\n ${ansi.dim}Execution Summary:${ansi.reset}`);
|
|
1033
|
-
console.log(` ${ansi.dim}├─ ID: ${executionId}${ansi.reset}`);
|
|
1034
|
-
console.log(` ${ansi.dim}├─ Duration: ${verdict.analysisTimeMs}ms${ansi.reset}`);
|
|
1035
|
-
console.log(` ${ansi.dim}├─ Files Analyzed: ${verdict.filesTouched.length}${ansi.reset}`);
|
|
1036
|
-
console.log(` ${ansi.dim}└─ Verdict: ${verdict.action}${ansi.reset}\n`);
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
return verdict.exitCode;
|
|
1040
|
-
|
|
1041
|
-
} catch (error) {
|
|
1042
|
-
const sanitized = sanitizeError(error);
|
|
1043
|
-
spinner.fail(`Authority execution failed: ${sanitized.message}`);
|
|
1044
|
-
|
|
1045
|
-
// Log sanitized error details
|
|
1046
|
-
if (opts.verbose) {
|
|
1047
|
-
console.error(` ${ansi.dim}Error Code: ${sanitized.code}${ansi.reset}`);
|
|
1048
|
-
console.error(` ${ansi.dim}Execution ID: ${executionId}${ansi.reset}`);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
throw error;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
/**
|
|
1056
|
-
* Security remediation authority executor
|
|
1057
|
-
*/
|
|
1058
|
-
async function analyzeSecurityRemediation(projectPath, authority, opts, spinner) {
|
|
1059
|
-
const startTime = Date.now();
|
|
1060
|
-
spinner.update('Scanning for security vulnerabilities...');
|
|
1061
|
-
|
|
1062
|
-
// Security patterns to detect
|
|
1063
|
-
const SECURITY_PATTERNS = {
|
|
1064
|
-
COMMAND_INJECTION: {
|
|
1065
|
-
pattern: /execSync\s*\(\s*[`$]/,
|
|
1066
|
-
severity: 'CRITICAL',
|
|
1067
|
-
description: 'Command injection via shell interpolation',
|
|
1068
|
-
},
|
|
1069
|
-
SQL_INJECTION: {
|
|
1070
|
-
pattern: /query\s*\(\s*[`$]/,
|
|
1071
|
-
severity: 'CRITICAL',
|
|
1072
|
-
description: 'SQL injection via string interpolation',
|
|
1073
|
-
},
|
|
1074
|
-
EVAL_USAGE: {
|
|
1075
|
-
pattern: /eval\s*\(/,
|
|
1076
|
-
severity: 'HIGH',
|
|
1077
|
-
description: 'Unsafe eval usage',
|
|
1078
|
-
},
|
|
1079
|
-
HARDCODED_SECRET: {
|
|
1080
|
-
pattern: /process\.env\.\w+\s*\|\|\s*['"][^'"]{8,}['"]/,
|
|
1081
|
-
severity: 'HIGH',
|
|
1082
|
-
description: 'Hardcoded fallback secret',
|
|
1083
|
-
},
|
|
1084
|
-
PATH_TRAVERSAL: {
|
|
1085
|
-
pattern: /path\.join\s*\([^)]*req\./,
|
|
1086
|
-
severity: 'HIGH',
|
|
1087
|
-
description: 'Path traversal via user input',
|
|
1088
|
-
},
|
|
1089
|
-
};
|
|
1090
|
-
|
|
1091
|
-
const findings = [];
|
|
1092
|
-
const filesTouched = [];
|
|
1093
|
-
|
|
1094
|
-
// Scan files
|
|
1095
|
-
const extensions = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
1096
|
-
const excludedDirs = new Set(['node_modules', '.git', 'dist', 'build']);
|
|
1097
|
-
|
|
1098
|
-
async function walkDir(dir, depth = 0) {
|
|
1099
|
-
if (depth > 15) return;
|
|
1100
|
-
|
|
1101
|
-
try {
|
|
1102
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1103
|
-
|
|
1104
|
-
for (const entry of entries) {
|
|
1105
|
-
const fullPath = path.join(dir, entry.name);
|
|
1106
|
-
const relativePath = path.relative(projectPath, fullPath);
|
|
1107
|
-
|
|
1108
|
-
if (entry.isDirectory()) {
|
|
1109
|
-
if (!excludedDirs.has(entry.name) && !entry.name.startsWith('.')) {
|
|
1110
|
-
await walkDir(fullPath, depth + 1);
|
|
1111
|
-
}
|
|
1112
|
-
} else if (entry.isFile()) {
|
|
1113
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
1114
|
-
if (extensions.has(ext)) {
|
|
1115
|
-
try {
|
|
1116
|
-
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
|
1117
|
-
filesTouched.push(relativePath);
|
|
1118
|
-
|
|
1119
|
-
for (const [name, { pattern, severity, description }] of Object.entries(SECURITY_PATTERNS)) {
|
|
1120
|
-
if (pattern.test(content)) {
|
|
1121
|
-
const match = content.match(pattern);
|
|
1122
|
-
const beforeMatch = content.slice(0, match.index);
|
|
1123
|
-
const line = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
1124
|
-
|
|
1125
|
-
findings.push({
|
|
1126
|
-
file: relativePath,
|
|
1127
|
-
line,
|
|
1128
|
-
type: name,
|
|
1129
|
-
severity,
|
|
1130
|
-
description,
|
|
1131
|
-
evidence: match[0].slice(0, 50),
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
} catch (err) {
|
|
1136
|
-
// Skip unreadable files
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
} catch (err) {
|
|
1142
|
-
// Skip inaccessible directories
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
await walkDir(projectPath);
|
|
1147
|
-
|
|
1148
|
-
// Determine verdict
|
|
1149
|
-
const criticalCount = findings.filter(f => f.severity === 'CRITICAL').length;
|
|
1150
|
-
const highCount = findings.filter(f => f.severity === 'HIGH').length;
|
|
1151
|
-
|
|
1152
|
-
let action = 'PROCEED';
|
|
1153
|
-
let riskLevel = 'LOW';
|
|
1154
|
-
let confidence = 0.9;
|
|
1155
|
-
|
|
1156
|
-
if (criticalCount > 0) {
|
|
1157
|
-
action = 'STOP';
|
|
1158
|
-
riskLevel = 'CRITICAL';
|
|
1159
|
-
confidence = 1.0;
|
|
1160
|
-
} else if (highCount > 0) {
|
|
1161
|
-
action = 'DEFER';
|
|
1162
|
-
riskLevel = 'HIGH';
|
|
1163
|
-
confidence = 0.8;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
return {
|
|
1167
|
-
authority: authority.id,
|
|
1168
|
-
version: authority.version,
|
|
1169
|
-
timestamp: new Date().toISOString(),
|
|
1170
|
-
action,
|
|
1171
|
-
riskLevel,
|
|
1172
|
-
exitCode: action === 'PROCEED' ? 0 : action === 'DEFER' ? 1 : 2,
|
|
1173
|
-
filesTouched,
|
|
1174
|
-
proofs: {
|
|
1175
|
-
reachability: 'Static analysis - pattern matching',
|
|
1176
|
-
compatibility: 'N/A - read-only scan',
|
|
1177
|
-
rollback: 'N/A - no changes made',
|
|
1178
|
-
},
|
|
1179
|
-
behaviorChange: false,
|
|
1180
|
-
confidence,
|
|
1181
|
-
hardStopsTriggered: criticalCount > 0 ? [`${criticalCount} CRITICAL vulnerabilities`] : [],
|
|
1182
|
-
notes: criticalCount > 0
|
|
1183
|
-
? `BLOCKED: ${criticalCount} critical vulnerabilities found. Fix before deployment.`
|
|
1184
|
-
: highCount > 0
|
|
1185
|
-
? `REVIEW: ${highCount} high severity issues found. Manual review recommended.`
|
|
1186
|
-
: `PASSED: No critical security issues detected.`,
|
|
1187
|
-
analysis: {
|
|
1188
|
-
findings,
|
|
1189
|
-
summary: {
|
|
1190
|
-
critical: criticalCount,
|
|
1191
|
-
high: highCount,
|
|
1192
|
-
total: findings.length,
|
|
1193
|
-
filesScanned: filesTouched.length,
|
|
1194
|
-
},
|
|
1195
|
-
},
|
|
1196
|
-
analysisTimeMs: Date.now() - startTime,
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
318
|
module.exports = {
|
|
1201
|
-
runApprove
|
|
319
|
+
runApprove,
|
|
1202
320
|
};
|