@vibecheckai/cli 3.7.0 → 3.9.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 +634 -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/interceptor/base.js +7 -3
- 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/engine/ast-cache.js +210 -210
- package/bin/runners/lib/engine/auth-extractor.js +211 -211
- package/bin/runners/lib/engine/billing-extractor.js +112 -112
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
- package/bin/runners/lib/engine/env-extractor.js +207 -207
- package/bin/runners/lib/engine/express-extractor.js +208 -208
- package/bin/runners/lib/engine/extractors.js +849 -849
- package/bin/runners/lib/engine/index.js +207 -207
- package/bin/runners/lib/engine/repo-index.js +514 -514
- package/bin/runners/lib/engine/types.js +124 -124
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- 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/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/runReality.js +178 -1
- 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/index.ts +2 -2
- package/mcp-server/handlers/tool-handler.ts +50 -11
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/lib/executor.ts +5 -5
- package/mcp-server/lib/index.ts +14 -4
- package/mcp-server/lib/sandbox.test.ts +4 -4
- package/mcp-server/lib/sandbox.ts +2 -2
- 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/registry.test.ts +18 -12
- package/mcp-server/tier-auth.js +68 -11
- package/mcp-server/tools-v3.js +70 -16
- package/mcp-server/tsconfig.json +1 -0
- package/package.json +2 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibecheck safelist - Storage & Persistence
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* Handles loading, saving, and migrating safelist files
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const {
|
|
14
|
+
SCHEMA_VERSION,
|
|
15
|
+
SCOPE_TYPES,
|
|
16
|
+
validateSafelist,
|
|
17
|
+
createEmptySafelist,
|
|
18
|
+
} = require("./schema");
|
|
19
|
+
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
// FILE PATHS
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get safelist file paths for a project
|
|
26
|
+
* @param {string} projectRoot - Project root directory
|
|
27
|
+
* @returns {Object} Paths for repo and local safelists
|
|
28
|
+
*/
|
|
29
|
+
function getSafelistPaths(projectRoot) {
|
|
30
|
+
const vibecheckDir = path.join(projectRoot, ".vibecheck");
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
repo: path.join(vibecheckDir, "safelist.json"),
|
|
34
|
+
local: path.join(vibecheckDir, "safelist.local.json"),
|
|
35
|
+
legacy: path.join(vibecheckDir, "allowlist.json"),
|
|
36
|
+
dir: vibecheckDir,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
// LOADING
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load a safelist file with error handling
|
|
46
|
+
* @param {string} filePath - Path to safelist file
|
|
47
|
+
* @returns {{ success: boolean, data: Object|null, error: string|null }}
|
|
48
|
+
*/
|
|
49
|
+
function loadSafelistFile(filePath) {
|
|
50
|
+
if (!fs.existsSync(filePath)) {
|
|
51
|
+
return { success: true, data: null, error: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
56
|
+
const data = JSON.parse(content);
|
|
57
|
+
return { success: true, data, error: null };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
data: null,
|
|
62
|
+
error: `Failed to parse ${path.basename(filePath)}: ${err.message}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Load all safelists for a project (repo + local, merged)
|
|
69
|
+
* @param {string} projectRoot - Project root directory
|
|
70
|
+
* @returns {{ safelist: Object, warnings: string[], errors: string[] }}
|
|
71
|
+
*/
|
|
72
|
+
function loadSafelist(projectRoot) {
|
|
73
|
+
const paths = getSafelistPaths(projectRoot);
|
|
74
|
+
const warnings = [];
|
|
75
|
+
const errors = [];
|
|
76
|
+
|
|
77
|
+
// Try to load repo safelist
|
|
78
|
+
let repoSafelist = createEmptySafelist();
|
|
79
|
+
const repoResult = loadSafelistFile(paths.repo);
|
|
80
|
+
|
|
81
|
+
if (!repoResult.success) {
|
|
82
|
+
errors.push(repoResult.error);
|
|
83
|
+
} else if (repoResult.data) {
|
|
84
|
+
// Validate
|
|
85
|
+
const validation = validateSafelist(repoResult.data);
|
|
86
|
+
warnings.push(...validation.warnings);
|
|
87
|
+
|
|
88
|
+
if (validation.valid) {
|
|
89
|
+
repoSafelist = repoResult.data;
|
|
90
|
+
} else {
|
|
91
|
+
errors.push(...validation.errors.map(e => `[repo] ${e}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Try to load local safelist
|
|
96
|
+
let localSafelist = createEmptySafelist();
|
|
97
|
+
const localResult = loadSafelistFile(paths.local);
|
|
98
|
+
|
|
99
|
+
if (!localResult.success) {
|
|
100
|
+
errors.push(localResult.error);
|
|
101
|
+
} else if (localResult.data) {
|
|
102
|
+
const validation = validateSafelist(localResult.data);
|
|
103
|
+
warnings.push(...validation.warnings);
|
|
104
|
+
|
|
105
|
+
if (validation.valid) {
|
|
106
|
+
localSafelist = localResult.data;
|
|
107
|
+
} else {
|
|
108
|
+
errors.push(...validation.errors.map(e => `[local] ${e}`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for legacy allowlist.json and migrate
|
|
113
|
+
if (fs.existsSync(paths.legacy) && !fs.existsSync(paths.repo)) {
|
|
114
|
+
const legacyResult = loadSafelistFile(paths.legacy);
|
|
115
|
+
if (legacyResult.success && legacyResult.data) {
|
|
116
|
+
warnings.push("Found legacy allowlist.json - consider migrating with 'vibecheck safelist migrate'");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Merge safelists (local entries override/extend repo entries)
|
|
121
|
+
const mergedSafelist = {
|
|
122
|
+
version: SCHEMA_VERSION,
|
|
123
|
+
metadata: {
|
|
124
|
+
...repoSafelist.metadata,
|
|
125
|
+
loadedAt: new Date().toISOString(),
|
|
126
|
+
},
|
|
127
|
+
entries: [
|
|
128
|
+
...repoSafelist.entries.map(e => ({ ...e, _source: "repo" })),
|
|
129
|
+
...localSafelist.entries.map(e => ({ ...e, _source: "local" })),
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
safelist: mergedSafelist,
|
|
135
|
+
repoSafelist,
|
|
136
|
+
localSafelist,
|
|
137
|
+
warnings,
|
|
138
|
+
errors,
|
|
139
|
+
paths,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
144
|
+
// SAVING
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Save safelist to file
|
|
149
|
+
* @param {string} filePath - Path to save to
|
|
150
|
+
* @param {Object} safelist - Safelist data
|
|
151
|
+
* @returns {{ success: boolean, error: string|null }}
|
|
152
|
+
*/
|
|
153
|
+
function saveSafelistFile(filePath, safelist) {
|
|
154
|
+
try {
|
|
155
|
+
// Ensure directory exists
|
|
156
|
+
const dir = path.dirname(filePath);
|
|
157
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
158
|
+
|
|
159
|
+
// Remove internal properties before saving
|
|
160
|
+
const cleaned = {
|
|
161
|
+
...safelist,
|
|
162
|
+
entries: safelist.entries.map(e => {
|
|
163
|
+
const { _source, ...rest } = e;
|
|
164
|
+
return rest;
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Update metadata
|
|
169
|
+
cleaned.metadata = {
|
|
170
|
+
...cleaned.metadata,
|
|
171
|
+
lastModified: new Date().toISOString(),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
fs.writeFileSync(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
|
|
175
|
+
return { success: true, error: null };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return { success: false, error: err.message };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Save an entry to the appropriate safelist file
|
|
183
|
+
* @param {string} projectRoot - Project root
|
|
184
|
+
* @param {Object} entry - Entry to save
|
|
185
|
+
* @param {string} scope - "repo" or "local"
|
|
186
|
+
* @returns {{ success: boolean, error: string|null }}
|
|
187
|
+
*/
|
|
188
|
+
function saveEntry(projectRoot, entry, scope = "repo") {
|
|
189
|
+
const paths = getSafelistPaths(projectRoot);
|
|
190
|
+
const filePath = scope === "local" ? paths.local : paths.repo;
|
|
191
|
+
|
|
192
|
+
// Load existing
|
|
193
|
+
const result = loadSafelistFile(filePath);
|
|
194
|
+
const safelist = result.data || createEmptySafelist();
|
|
195
|
+
|
|
196
|
+
// Add entry
|
|
197
|
+
safelist.entries.push(entry);
|
|
198
|
+
|
|
199
|
+
// Save
|
|
200
|
+
return saveSafelistFile(filePath, safelist);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Remove an entry from safelist
|
|
205
|
+
* @param {string} projectRoot - Project root
|
|
206
|
+
* @param {string} entryId - Entry ID to remove
|
|
207
|
+
* @returns {{ success: boolean, removed: boolean, source: string|null, error: string|null }}
|
|
208
|
+
*/
|
|
209
|
+
function removeEntry(projectRoot, entryId) {
|
|
210
|
+
const paths = getSafelistPaths(projectRoot);
|
|
211
|
+
let removed = false;
|
|
212
|
+
let source = null;
|
|
213
|
+
|
|
214
|
+
// Try repo safelist first
|
|
215
|
+
const repoResult = loadSafelistFile(paths.repo);
|
|
216
|
+
if (repoResult.data) {
|
|
217
|
+
const before = repoResult.data.entries.length;
|
|
218
|
+
repoResult.data.entries = repoResult.data.entries.filter(e => e.id !== entryId);
|
|
219
|
+
if (repoResult.data.entries.length < before) {
|
|
220
|
+
removed = true;
|
|
221
|
+
source = "repo";
|
|
222
|
+
const saveResult = saveSafelistFile(paths.repo, repoResult.data);
|
|
223
|
+
if (!saveResult.success) {
|
|
224
|
+
return { success: false, removed: false, source: null, error: saveResult.error };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Try local safelist
|
|
230
|
+
if (!removed) {
|
|
231
|
+
const localResult = loadSafelistFile(paths.local);
|
|
232
|
+
if (localResult.data) {
|
|
233
|
+
const before = localResult.data.entries.length;
|
|
234
|
+
localResult.data.entries = localResult.data.entries.filter(e => e.id !== entryId);
|
|
235
|
+
if (localResult.data.entries.length < before) {
|
|
236
|
+
removed = true;
|
|
237
|
+
source = "local";
|
|
238
|
+
const saveResult = saveSafelistFile(paths.local, localResult.data);
|
|
239
|
+
if (!saveResult.success) {
|
|
240
|
+
return { success: false, removed: false, source: null, error: saveResult.error };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { success: true, removed, source, error: null };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Update an existing entry
|
|
251
|
+
* @param {string} projectRoot - Project root
|
|
252
|
+
* @param {string} entryId - Entry ID to update
|
|
253
|
+
* @param {Object} updates - Fields to update
|
|
254
|
+
* @returns {{ success: boolean, updated: boolean, error: string|null }}
|
|
255
|
+
*/
|
|
256
|
+
function updateEntry(projectRoot, entryId, updates) {
|
|
257
|
+
const { safelist, paths } = loadSafelist(projectRoot);
|
|
258
|
+
|
|
259
|
+
const entry = safelist.entries.find(e => e.id === entryId);
|
|
260
|
+
if (!entry) {
|
|
261
|
+
return { success: false, updated: false, error: "Entry not found" };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Apply updates (deep merge)
|
|
265
|
+
const updated = deepMerge(entry, updates);
|
|
266
|
+
|
|
267
|
+
// Add audit trail
|
|
268
|
+
if (!updated.audit) updated.audit = { history: [] };
|
|
269
|
+
updated.audit.history.push({
|
|
270
|
+
action: "update",
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
changes: Object.keys(updates),
|
|
273
|
+
by: process.env.USER || process.env.USERNAME || "cli",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Determine which file to update
|
|
277
|
+
const filePath = entry._source === "local" ? paths.local : paths.repo;
|
|
278
|
+
const result = loadSafelistFile(filePath);
|
|
279
|
+
|
|
280
|
+
if (!result.data) {
|
|
281
|
+
return { success: false, updated: false, error: "Source file not found" };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Update in place
|
|
285
|
+
const idx = result.data.entries.findIndex(e => e.id === entryId);
|
|
286
|
+
if (idx === -1) {
|
|
287
|
+
return { success: false, updated: false, error: "Entry not found in source file" };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
result.data.entries[idx] = updated;
|
|
291
|
+
|
|
292
|
+
const saveResult = saveSafelistFile(filePath, result.data);
|
|
293
|
+
return {
|
|
294
|
+
success: saveResult.success,
|
|
295
|
+
updated: saveResult.success,
|
|
296
|
+
error: saveResult.error
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
301
|
+
// MIGRATION
|
|
302
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Migrate legacy allowlist.json to new safelist format
|
|
306
|
+
* @param {string} projectRoot - Project root
|
|
307
|
+
* @returns {{ success: boolean, migrated: number, error: string|null }}
|
|
308
|
+
*/
|
|
309
|
+
function migrateLegacyAllowlist(projectRoot) {
|
|
310
|
+
const paths = getSafelistPaths(projectRoot);
|
|
311
|
+
|
|
312
|
+
if (!fs.existsSync(paths.legacy)) {
|
|
313
|
+
return { success: true, migrated: 0, error: null };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const legacyResult = loadSafelistFile(paths.legacy);
|
|
317
|
+
if (!legacyResult.success || !legacyResult.data) {
|
|
318
|
+
return { success: false, migrated: 0, error: legacyResult.error || "No data" };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const legacy = legacyResult.data;
|
|
322
|
+
const newSafelist = createEmptySafelist();
|
|
323
|
+
|
|
324
|
+
// Convert each legacy entry
|
|
325
|
+
for (const old of (legacy.entries || [])) {
|
|
326
|
+
const newEntry = {
|
|
327
|
+
id: old.id || `SL_migrated_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
328
|
+
type: old.pattern ? "pattern" : "finding",
|
|
329
|
+
target: {},
|
|
330
|
+
justification: {
|
|
331
|
+
reason: old.reason || "Migrated from legacy allowlist",
|
|
332
|
+
category: "false-positive",
|
|
333
|
+
},
|
|
334
|
+
owner: {
|
|
335
|
+
name: old.addedBy || "migrated",
|
|
336
|
+
},
|
|
337
|
+
scope: {
|
|
338
|
+
type: old.scope === "global" ? "repo" : (old.scope || "repo"),
|
|
339
|
+
},
|
|
340
|
+
lifecycle: {
|
|
341
|
+
createdAt: old.addedAt || new Date().toISOString(),
|
|
342
|
+
createdBy: "migration",
|
|
343
|
+
matchCount: 0,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Map target
|
|
348
|
+
if (old.findingId) newEntry.target.findingId = old.findingId;
|
|
349
|
+
if (old.pattern) newEntry.target.pattern = old.pattern;
|
|
350
|
+
if (old.file) newEntry.target.file = old.file;
|
|
351
|
+
if (old.lines) newEntry.target.lines = old.lines;
|
|
352
|
+
|
|
353
|
+
// Map expiration
|
|
354
|
+
if (old.expiresAt) {
|
|
355
|
+
newEntry.lifecycle.expiresAt = old.expiresAt;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
newSafelist.entries.push(newEntry);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Save new safelist
|
|
362
|
+
const saveResult = saveSafelistFile(paths.repo, newSafelist);
|
|
363
|
+
if (!saveResult.success) {
|
|
364
|
+
return { success: false, migrated: 0, error: saveResult.error };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Rename legacy file
|
|
368
|
+
try {
|
|
369
|
+
fs.renameSync(paths.legacy, paths.legacy + ".migrated");
|
|
370
|
+
} catch {
|
|
371
|
+
// Ignore rename errors
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { success: true, migrated: newSafelist.entries.length, error: null };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
378
|
+
// UTILITIES
|
|
379
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Deep merge objects
|
|
383
|
+
*/
|
|
384
|
+
function deepMerge(target, source) {
|
|
385
|
+
const result = { ...target };
|
|
386
|
+
|
|
387
|
+
for (const key of Object.keys(source)) {
|
|
388
|
+
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
389
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
390
|
+
} else {
|
|
391
|
+
result[key] = source[key];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Ensure .gitignore includes local safelist
|
|
400
|
+
* @param {string} projectRoot - Project root
|
|
401
|
+
*/
|
|
402
|
+
function ensureGitIgnore(projectRoot) {
|
|
403
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
404
|
+
const localSafelistPattern = ".vibecheck/safelist.local.json";
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
let content = "";
|
|
408
|
+
if (fs.existsSync(gitignorePath)) {
|
|
409
|
+
content = fs.readFileSync(gitignorePath, "utf8");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!content.includes(localSafelistPattern)) {
|
|
413
|
+
const addition = `\n# vibecheck local safelist (machine-specific)\n${localSafelistPattern}\n`;
|
|
414
|
+
fs.appendFileSync(gitignorePath, addition);
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
// Ignore errors
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
425
|
+
// EXPORTS
|
|
426
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
427
|
+
|
|
428
|
+
module.exports = {
|
|
429
|
+
getSafelistPaths,
|
|
430
|
+
loadSafelistFile,
|
|
431
|
+
loadSafelist,
|
|
432
|
+
saveSafelistFile,
|
|
433
|
+
saveEntry,
|
|
434
|
+
removeEntry,
|
|
435
|
+
updateEntry,
|
|
436
|
+
migrateLegacyAllowlist,
|
|
437
|
+
ensureGitIgnore,
|
|
438
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "vibecheck/ship-manifest/v1",
|
|
4
|
+
"title": "Ship Manifest",
|
|
5
|
+
"description": "Vibecheck Ship Manifest - The authoritative artifact for ship verdicts",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version", "schema", "verdict", "repo", "evidence", "checks", "meta", "signature"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
12
|
+
"description": "Manifest format version (semver)"
|
|
13
|
+
},
|
|
14
|
+
"schema": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"const": "vibecheck/ship-manifest/v1",
|
|
17
|
+
"description": "Schema identifier"
|
|
18
|
+
},
|
|
19
|
+
"verdict": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["status", "exitCode", "score", "ciStatus", "canShip"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"status": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": ["SHIP", "WARN", "BLOCK"],
|
|
26
|
+
"description": "Final verdict"
|
|
27
|
+
},
|
|
28
|
+
"exitCode": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"minimum": 0,
|
|
31
|
+
"maximum": 10,
|
|
32
|
+
"description": "CLI exit code (0=SHIP, 1=WARN, 2=BLOCK)"
|
|
33
|
+
},
|
|
34
|
+
"score": {
|
|
35
|
+
"type": "integer",
|
|
36
|
+
"minimum": 0,
|
|
37
|
+
"maximum": 100,
|
|
38
|
+
"description": "Quality score out of 100"
|
|
39
|
+
},
|
|
40
|
+
"ciStatus": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"enum": ["success", "warning", "failure"],
|
|
43
|
+
"description": "CI status string"
|
|
44
|
+
},
|
|
45
|
+
"canShip": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"description": "Whether the code is ready to ship"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": false
|
|
51
|
+
},
|
|
52
|
+
"repo": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"required": ["root", "name", "fingerprint", "commit", "branch", "isDirty"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"root": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "Absolute path to repository root"
|
|
59
|
+
},
|
|
60
|
+
"name": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"maxLength": 100,
|
|
63
|
+
"description": "Repository name"
|
|
64
|
+
},
|
|
65
|
+
"fingerprint": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"pattern": "^[a-f0-9]{32}$",
|
|
68
|
+
"description": "Deterministic repo state fingerprint"
|
|
69
|
+
},
|
|
70
|
+
"commit": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Git commit hash (12 chars) or 'unknown'"
|
|
73
|
+
},
|
|
74
|
+
"branch": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "Git branch name or 'unknown'"
|
|
77
|
+
},
|
|
78
|
+
"treeHash": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "Git tree hash (12 chars) or 'unknown'"
|
|
81
|
+
},
|
|
82
|
+
"isDirty": {
|
|
83
|
+
"type": "boolean",
|
|
84
|
+
"description": "Whether working tree has uncommitted changes"
|
|
85
|
+
},
|
|
86
|
+
"gitAvailable": {
|
|
87
|
+
"type": "boolean",
|
|
88
|
+
"description": "Whether git was available for fingerprinting"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"additionalProperties": false
|
|
92
|
+
},
|
|
93
|
+
"evidence": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"required": ["summary", "whyTree", "coverage"],
|
|
96
|
+
"properties": {
|
|
97
|
+
"summary": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"required": ["total", "blockers", "warnings", "info"],
|
|
100
|
+
"properties": {
|
|
101
|
+
"total": { "type": "integer", "minimum": 0 },
|
|
102
|
+
"blockers": { "type": "integer", "minimum": 0 },
|
|
103
|
+
"warnings": { "type": "integer", "minimum": 0 },
|
|
104
|
+
"info": { "type": "integer", "minimum": 0 }
|
|
105
|
+
},
|
|
106
|
+
"additionalProperties": false
|
|
107
|
+
},
|
|
108
|
+
"whyTree": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"required": ["summary", "topIssues", "fixMissions"],
|
|
111
|
+
"properties": {
|
|
112
|
+
"summary": { "type": "string" },
|
|
113
|
+
"topIssues": {
|
|
114
|
+
"type": "array",
|
|
115
|
+
"items": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"required": ["id", "category", "title"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"id": { "type": "string", "maxLength": 64 },
|
|
120
|
+
"category": { "type": "string", "maxLength": 50 },
|
|
121
|
+
"title": { "type": "string", "maxLength": 500 },
|
|
122
|
+
"why": { "type": ["string", "null"], "maxLength": 1000 },
|
|
123
|
+
"evidence": {
|
|
124
|
+
"type": ["object", "null"],
|
|
125
|
+
"properties": {
|
|
126
|
+
"file": { "type": ["string", "null"] },
|
|
127
|
+
"lines": { "type": ["string", "null"] },
|
|
128
|
+
"snippet": { "type": ["string", "null"] },
|
|
129
|
+
"kind": { "type": "string" }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"fixHint": { "type": ["string", "null"] }
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"maxItems": 10
|
|
136
|
+
},
|
|
137
|
+
"fixMissions": {
|
|
138
|
+
"type": "array",
|
|
139
|
+
"items": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"target": { "type": "string" },
|
|
143
|
+
"action": { "type": "string" },
|
|
144
|
+
"priority": { "type": "string", "enum": ["critical", "recommended"] }
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"maxItems": 10
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"coverage": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"routeCoverage": { "type": "number", "minimum": 0, "maximum": 100 },
|
|
155
|
+
"envCoverage": { "type": "number", "minimum": 0, "maximum": 100 },
|
|
156
|
+
"authCoverage": { "type": ["number", "null"] },
|
|
157
|
+
"runtimeCoverage": { "type": ["number", "null"] }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"checks": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"required": ["audit", "reality", "shield"],
|
|
165
|
+
"properties": {
|
|
166
|
+
"audit": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"required": ["ran", "findingsCount"],
|
|
169
|
+
"properties": {
|
|
170
|
+
"ran": { "type": "boolean" },
|
|
171
|
+
"findingsCount": { "type": "integer", "minimum": 0 },
|
|
172
|
+
"truthpackHash": { "type": ["string", "null"] }
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"reality": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"required": ["ran", "actionsCount", "requestsCount"],
|
|
178
|
+
"properties": {
|
|
179
|
+
"ran": { "type": "boolean" },
|
|
180
|
+
"actionsCount": { "type": "integer", "minimum": 0 },
|
|
181
|
+
"requestsCount": { "type": "integer", "minimum": 0 }
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"shield": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"required": ["ran", "mode", "enforcing", "locked"],
|
|
187
|
+
"properties": {
|
|
188
|
+
"ran": { "type": "boolean" },
|
|
189
|
+
"mode": { "type": "string" },
|
|
190
|
+
"enforcing": { "type": "boolean" },
|
|
191
|
+
"locked": { "type": "boolean" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"meta": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"required": ["generatedAt", "durationMs", "tool", "toolVersion"],
|
|
199
|
+
"properties": {
|
|
200
|
+
"generatedAt": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"format": "date-time",
|
|
203
|
+
"description": "ISO 8601 timestamp"
|
|
204
|
+
},
|
|
205
|
+
"durationMs": {
|
|
206
|
+
"type": "integer",
|
|
207
|
+
"minimum": 0,
|
|
208
|
+
"description": "Evaluation duration in milliseconds"
|
|
209
|
+
},
|
|
210
|
+
"tool": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"const": "vibecheck"
|
|
213
|
+
},
|
|
214
|
+
"toolVersion": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"description": "Vibecheck version"
|
|
217
|
+
},
|
|
218
|
+
"platform": {
|
|
219
|
+
"type": "string",
|
|
220
|
+
"description": "Platform identifier (os-arch)"
|
|
221
|
+
},
|
|
222
|
+
"nodeVersion": {
|
|
223
|
+
"type": "string",
|
|
224
|
+
"description": "Node.js version"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
"signature": {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"required": ["algorithm", "digest", "verifiable"],
|
|
231
|
+
"properties": {
|
|
232
|
+
"algorithm": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"enum": ["sha256"],
|
|
235
|
+
"description": "Signature algorithm"
|
|
236
|
+
},
|
|
237
|
+
"digest": {
|
|
238
|
+
"type": "string",
|
|
239
|
+
"pattern": "^[a-f0-9]{32}$",
|
|
240
|
+
"description": "Signature digest"
|
|
241
|
+
},
|
|
242
|
+
"verifiable": {
|
|
243
|
+
"type": "boolean",
|
|
244
|
+
"description": "Whether signature can be verified"
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"additionalProperties": false
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"additionalProperties": false
|
|
251
|
+
}
|