@saiteja1123/mcp-server 1.1.4 → 1.1.5
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/package.json +59 -55
- package/src/api-scan.mjs +362 -93
- package/src/cli.js +713 -322
- package/src/deep-scan/contracts.js +201 -0
- package/src/deep-scan/deterministic-scan.js +337 -0
- package/src/deep-scan/index.js +109 -0
- package/src/deep-scan/project-map.js +507 -0
- package/src/deep-scan/ralph-accept.js +510 -0
- package/src/deep-scan/ralph-compare.js +498 -0
- package/src/deep-scan/ralph-tasks.js +598 -0
- package/src/deep-scan/ralph-track.js +548 -0
- package/src/deep-scan/registry.js +159 -0
- package/src/deep-scan/runtime.js +275 -0
- package/src/deep-scan/sample-steppers.js +128 -0
- package/src/deep-scan/sourceSafe.js +73 -0
- package/src/deep-scan/status.js +70 -0
- package/src/deep-scan/store.js +57 -0
- package/src/deep-scan/test-plan.js +760 -0
- package/src/index.js +6 -5
- package/src/lock.mjs +55 -14
- package/src/middleware/governance.js +135 -0
- package/src/orchestrator/runScan.js +211 -0
- package/src/project-bindings.mjs +215 -0
- package/src/rule-engine/index.js +2 -1
- package/src/rule-engine/localScan.js +39 -12
- package/src/rule-engine/metadata.js +20 -0
- package/src/rule-engine/prompt.js +6 -5
- package/src/rule-engine/rules.js +71 -43
- package/src/rule-engine/score.js +5 -4
- package/src/security/pathGuard.js +170 -0
- package/src/selftest.js +2473 -0
- package/src/server.js +109 -150
- package/src/tools/deepScan.js +286 -0
- package/src/tools/localScan.js +85 -0
- package/src/tools/projects.js +124 -0
- package/src/tools/scanFile.js +131 -0
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Re-export bundled local rule engine for MCP tooling.
|
|
3
|
-
* This keeps the MCP package self-contained at runtime.
|
|
4
|
-
*/
|
|
5
|
-
export * from './rule-engine/index.js';
|
|
1
|
+
/**
|
|
2
|
+
* Re-export bundled local rule engine for MCP tooling.
|
|
3
|
+
* This keeps the MCP package self-contained at runtime.
|
|
4
|
+
*/
|
|
5
|
+
export * from './rule-engine/index.js';
|
|
6
|
+
export * from './deep-scan/index.js';
|
package/src/lock.mjs
CHANGED
|
@@ -11,6 +11,23 @@ export function randomToken() {
|
|
|
11
11
|
return crypto.randomBytes(32).toString('hex');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function normalizeLockedRoot(rawPath = '') {
|
|
15
|
+
if (typeof rawPath !== 'string') return '';
|
|
16
|
+
const trimmed = rawPath.trim();
|
|
17
|
+
if (!trimmed) return '';
|
|
18
|
+
const slashed = trimmed.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
|
19
|
+
const withoutTrailing = slashed.replace(/\/+$/, '');
|
|
20
|
+
return withoutTrailing.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildLockedRootHash(lockedRootPath) {
|
|
24
|
+
return sha256Hex(`vibesecur:mcp:root:${normalizeLockedRoot(lockedRootPath)}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isRuntimeCompatibleLock(lock) {
|
|
28
|
+
return lock?.source === 'server';
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
export function deriveLockDir(rootPath) {
|
|
15
32
|
return path.join(path.resolve(rootPath), '.vibesecur');
|
|
16
33
|
}
|
|
@@ -34,17 +51,19 @@ export async function writeLock(rootPath, lockData) {
|
|
|
34
51
|
await fs.writeFile(deriveLockFile(rootPath), JSON.stringify(lockData, null, 2), 'utf8');
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
export async function createLock({ rootPath, account }) {
|
|
54
|
+
export async function createLock({ rootPath, account, installToken, lockedRootHash, source = 'local' }) {
|
|
38
55
|
const resolved = path.resolve(rootPath);
|
|
39
|
-
const
|
|
40
|
-
const
|
|
56
|
+
const finalInstallToken = installToken || randomToken();
|
|
57
|
+
const finalLockedRootHash = lockedRootHash || buildLockedRootHash(resolved);
|
|
41
58
|
const lock = {
|
|
42
|
-
installToken,
|
|
43
|
-
rootHash,
|
|
59
|
+
installToken: finalInstallToken,
|
|
60
|
+
rootHash: finalLockedRootHash,
|
|
61
|
+
lockedRootHash: finalLockedRootHash,
|
|
44
62
|
boundRoot: resolved,
|
|
45
63
|
createdAt: new Date().toISOString(),
|
|
46
64
|
account: account || 'anonymous',
|
|
47
|
-
|
|
65
|
+
source,
|
|
66
|
+
version: 2,
|
|
48
67
|
};
|
|
49
68
|
await writeLock(resolved, lock);
|
|
50
69
|
return lock;
|
|
@@ -66,10 +85,23 @@ export async function validateScanPath(requestedPath, installToken) {
|
|
|
66
85
|
};
|
|
67
86
|
}
|
|
68
87
|
|
|
69
|
-
if (
|
|
88
|
+
if (!/^[a-f0-9]{64}$/i.test(String(installToken || ''))) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
code: 'TOKEN_MISSING',
|
|
92
|
+
httpStatus: 403,
|
|
93
|
+
message:
|
|
94
|
+
'Missing or invalid install token. ' +
|
|
95
|
+
'Use a server-issued binding via "vibesecur-mcp bind <folder>" and update MCP env.',
|
|
96
|
+
rebindHint: `vibesecur-mcp rebind ${lock.boundRoot}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (lock.installToken !== installToken) {
|
|
70
101
|
return {
|
|
71
102
|
ok: false,
|
|
72
103
|
code: 'TOKEN_MISMATCH',
|
|
104
|
+
httpStatus: 403,
|
|
73
105
|
message:
|
|
74
106
|
'Install token does not match the lock for this folder. ' +
|
|
75
107
|
'Run "vibesecur-mcp rebind" to get a new token.',
|
|
@@ -91,17 +123,19 @@ export async function validateScanPath(requestedPath, installToken) {
|
|
|
91
123
|
};
|
|
92
124
|
}
|
|
93
125
|
|
|
94
|
-
const expectedHash =
|
|
95
|
-
|
|
126
|
+
const expectedHash = buildLockedRootHash(boundRoot);
|
|
127
|
+
const storedHash = lock.lockedRootHash || lock.rootHash || '';
|
|
128
|
+
if (storedHash !== expectedHash) {
|
|
96
129
|
return {
|
|
97
130
|
ok: false,
|
|
98
131
|
code: 'HASH_MISMATCH',
|
|
132
|
+
httpStatus: 403,
|
|
99
133
|
message: 'Lock rootHash does not match bound folder. The lock file may be corrupted. Rebind.',
|
|
100
134
|
rebindHint: `vibesecur-mcp rebind ${boundRoot}`,
|
|
101
135
|
};
|
|
102
136
|
}
|
|
103
137
|
|
|
104
|
-
return { ok: true, lock, boundRoot };
|
|
138
|
+
return { ok: true, lock, boundRoot, lockedRootHash: expectedHash };
|
|
105
139
|
}
|
|
106
140
|
|
|
107
141
|
export async function findLockForPath(startPath) {
|
|
@@ -127,16 +161,17 @@ export async function findLockForPath(startPath) {
|
|
|
127
161
|
return null;
|
|
128
162
|
}
|
|
129
163
|
|
|
130
|
-
export async function rebindLock({ rootPath, account }) {
|
|
164
|
+
export async function rebindLock({ rootPath, account, source = 'local' }) {
|
|
131
165
|
const resolved = path.resolve(rootPath);
|
|
132
166
|
const oldLock = await readLock(resolved);
|
|
133
|
-
const newLock = await createLock({ rootPath: resolved, account });
|
|
167
|
+
const newLock = await createLock({ rootPath: resolved, account, source });
|
|
134
168
|
|
|
135
169
|
return {
|
|
136
170
|
previousToken: oldLock?.installToken || null,
|
|
137
171
|
newToken: newLock.installToken,
|
|
138
172
|
boundRoot: newLock.boundRoot,
|
|
139
173
|
rootHash: newLock.rootHash,
|
|
174
|
+
source: newLock.source,
|
|
140
175
|
message: 'Lock rebound. Previous token is now invalid. Copy your new install token.',
|
|
141
176
|
};
|
|
142
177
|
}
|
|
@@ -154,22 +189,28 @@ export async function diagnosticLock(rootPath) {
|
|
|
154
189
|
};
|
|
155
190
|
}
|
|
156
191
|
|
|
157
|
-
const expectedHash =
|
|
158
|
-
const
|
|
192
|
+
const expectedHash = buildLockedRootHash(path.resolve(lock.boundRoot));
|
|
193
|
+
const storedHash = lock.lockedRootHash || lock.rootHash || '';
|
|
194
|
+
const hashOk = storedHash === expectedHash;
|
|
159
195
|
const boundRoot = path.resolve(lock.boundRoot);
|
|
160
196
|
const inFolder = resolved.startsWith(boundRoot + path.sep) || resolved === boundRoot;
|
|
161
197
|
|
|
162
198
|
return {
|
|
163
199
|
healthy: hashOk && inFolder,
|
|
200
|
+
runtimeCompatible: hashOk && inFolder && isRuntimeCompatibleLock(lock),
|
|
164
201
|
lockFile: deriveLockFile(lock.boundRoot),
|
|
165
202
|
boundRoot: lock.boundRoot,
|
|
166
203
|
account: lock.account,
|
|
167
204
|
createdAt: lock.createdAt,
|
|
205
|
+
source: lock.source || 'unknown',
|
|
168
206
|
rootHash: { expected: expectedHash, stored: lock.rootHash, ok: hashOk },
|
|
169
207
|
pathInBoundFolder: inFolder,
|
|
170
208
|
issues: [
|
|
171
209
|
!hashOk ? 'rootHash mismatch - lock may be corrupted' : null,
|
|
172
210
|
!inFolder ? `requested path ${resolved} is outside bound folder ${boundRoot}` : null,
|
|
211
|
+
!isRuntimeCompatibleLock(lock)
|
|
212
|
+
? 'lock is local diagnostic-only; run vibesecur-mcp bind <folder> with a backend auth token'
|
|
213
|
+
: null,
|
|
173
214
|
].filter(Boolean),
|
|
174
215
|
};
|
|
175
216
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/middleware/governance.js
|
|
3
|
+
*
|
|
4
|
+
* Dynamic Context Governance middleware interface.
|
|
5
|
+
*
|
|
6
|
+
* ── Phase 1 behaviour ────────────────────────────────────────────────────────
|
|
7
|
+
* This module is a transparent pass-through. Every request is allowed.
|
|
8
|
+
* No governance policies exist yet. No blocking occurs.
|
|
9
|
+
* No hardcoded permissions. No simulated enforcement.
|
|
10
|
+
*
|
|
11
|
+
* ── Why this exists now ──────────────────────────────────────────────────────
|
|
12
|
+
* Inserting this seam now means future governance features (rate limiting,
|
|
13
|
+
* quota enforcement, context-aware scan policies, tier restrictions) can be
|
|
14
|
+
* added here without touching any tool handler, scan orchestrator, or MCP
|
|
15
|
+
* server bootstrap code. The interface is locked; the implementation evolves.
|
|
16
|
+
*
|
|
17
|
+
* ── Future phases will add inside evaluateGovernance() ──────────────────────
|
|
18
|
+
* Phase 2: fetch live governance context from VIBESECUR_API_BASE/mcp/governance
|
|
19
|
+
* Phase 3: evaluate local policy rules against GovernanceContext
|
|
20
|
+
* Phase 4: propagate GovernanceDecision.annotations into scan results
|
|
21
|
+
*
|
|
22
|
+
* ── What this module does NOT own ────────────────────────────────────────────
|
|
23
|
+
* - Path guard / binding validation (orchestrator caller's responsibility)
|
|
24
|
+
* - Scan execution (runScan.js)
|
|
25
|
+
* - Response formatting (tool handler)
|
|
26
|
+
* - Authentication (lock.mjs + api-scan.mjs)
|
|
27
|
+
*
|
|
28
|
+
* IMPORTS: only from api-scan.mjs — never from server.js or runScan.js.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { getMcpSessionId } from '../api-scan.mjs';
|
|
32
|
+
|
|
33
|
+
// ── Type definitions ──────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Context object passed to the governance middleware.
|
|
37
|
+
* Built by the scan orchestrator from validated guard + caller inputs.
|
|
38
|
+
*
|
|
39
|
+
* @typedef {Object} GovernanceContext
|
|
40
|
+
* @property {string} toolName MCP tool being invoked ('localScan' | 'scanFile' | …)
|
|
41
|
+
* @property {string} requestedPath Resolved, normalised project root (from guard.resolvedRoot)
|
|
42
|
+
* @property {string} installToken Opaque install token — do not log raw value
|
|
43
|
+
* @property {string} lockedRootHash SHA-256 of the bound root — safe to log
|
|
44
|
+
* @property {string} sessionId Process-stable session identifier
|
|
45
|
+
* @property {GovernanceScanMetadata} scanMetadata Scan-specific metadata
|
|
46
|
+
* @property {string} executionMode Caller hint: 'unknown' before scan executes
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} GovernanceScanMetadata
|
|
51
|
+
* @property {string} lang Resolved language ('js'|'ts'|'py'|'json'|'auto')
|
|
52
|
+
* @property {number} codeSize Length of the code string in characters
|
|
53
|
+
* @property {number|undefined} maxFiles File cap for repo tools; undefined for code-string tools
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decision returned by the governance middleware to the scan orchestrator.
|
|
58
|
+
*
|
|
59
|
+
* @typedef {Object} GovernanceDecision
|
|
60
|
+
* @property {boolean} allowed Whether the request is permitted to proceed
|
|
61
|
+
* @property {string} reason Human-readable explanation (logged, not returned to IDE)
|
|
62
|
+
* @property {Record<string,*>} annotations Metadata to attach to the scan result (currently empty)
|
|
63
|
+
* @property {Record<string,*>} metadata Governance system metadata (tier, policy id, etc.)
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a GovernanceContext from the parameters available at scan-time.
|
|
70
|
+
* The caller (runScan) passes these directly from its own validated inputs.
|
|
71
|
+
*
|
|
72
|
+
* @param {Object} params
|
|
73
|
+
* @param {string} params.toolName
|
|
74
|
+
* @param {string} params.requestedPath
|
|
75
|
+
* @param {string} params.installToken
|
|
76
|
+
* @param {string} params.lockedRootHash
|
|
77
|
+
* @param {string} params.lang
|
|
78
|
+
* @param {number} params.codeSize
|
|
79
|
+
* @param {number|undefined} params.maxFiles
|
|
80
|
+
* @returns {GovernanceContext}
|
|
81
|
+
*/
|
|
82
|
+
export function buildGovernanceContext({
|
|
83
|
+
toolName,
|
|
84
|
+
requestedPath,
|
|
85
|
+
installToken,
|
|
86
|
+
lockedRootHash,
|
|
87
|
+
lang,
|
|
88
|
+
codeSize,
|
|
89
|
+
maxFiles,
|
|
90
|
+
}) {
|
|
91
|
+
return {
|
|
92
|
+
toolName,
|
|
93
|
+
requestedPath,
|
|
94
|
+
installToken, // caller must not log this field
|
|
95
|
+
lockedRootHash, // SHA-256 — safe to log
|
|
96
|
+
sessionId: getMcpSessionId(),
|
|
97
|
+
scanMetadata: {
|
|
98
|
+
lang,
|
|
99
|
+
codeSize,
|
|
100
|
+
maxFiles,
|
|
101
|
+
},
|
|
102
|
+
executionMode: 'unknown', // resolved after scan returns
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Core middleware ───────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Evaluate governance policy for the given execution context.
|
|
110
|
+
*
|
|
111
|
+
* Phase 1: transparent pass-through. Always returns allowed=true.
|
|
112
|
+
* No network calls. No blocking. No policy evaluation. Zero latency overhead.
|
|
113
|
+
*
|
|
114
|
+
* Contract for future phases:
|
|
115
|
+
* - Must never throw — errors must be caught and result in fail-open (allowed=true)
|
|
116
|
+
* during Phase 1. Future phases may choose fail-closed after explicit opt-in.
|
|
117
|
+
* - Must return a GovernanceDecision synchronously or asynchronously.
|
|
118
|
+
* - Must not mutate the GovernanceContext object.
|
|
119
|
+
* - Must not perform authentication — that is the path guard's responsibility.
|
|
120
|
+
* - Must not read or write lock files.
|
|
121
|
+
*
|
|
122
|
+
* @param {GovernanceContext} ctx
|
|
123
|
+
* @returns {Promise<GovernanceDecision>}
|
|
124
|
+
*/
|
|
125
|
+
export async function evaluateGovernance(ctx) { // eslint-disable-line no-unused-vars
|
|
126
|
+
// ── Phase 1: pass-through ─────────────────────────────────────────────────
|
|
127
|
+
// ctx is accepted but intentionally not evaluated.
|
|
128
|
+
// The parameter is kept to lock the function signature for future phases.
|
|
129
|
+
return {
|
|
130
|
+
allowed: true,
|
|
131
|
+
reason: 'pass-through',
|
|
132
|
+
annotations: {},
|
|
133
|
+
metadata: {},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/orchestrator/runScan.js
|
|
3
|
+
*
|
|
4
|
+
* Central scan execution pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Owns ONLY the remote→local decision tree:
|
|
7
|
+
* 1. Attempt remote scan via /api/v1/scan/local
|
|
8
|
+
* 2. On 402 → surface upgrade requirement (checked first; never fall back)
|
|
9
|
+
* 3. On remote unavailable/skipped/error → surface hard error (no silent fallback)
|
|
10
|
+
* 4. On remote success → return remote result with quota metadata
|
|
11
|
+
* 5. On remote ok-but-no-data → fall back to local rule engine
|
|
12
|
+
*
|
|
13
|
+
* DOES NOT OWN:
|
|
14
|
+
* - Path guard / binding validation (caller's responsibility)
|
|
15
|
+
* - File I/O / realpath (caller's responsibility)
|
|
16
|
+
* - Language inference from filename (caller resolves before passing)
|
|
17
|
+
* - MCP response formatting (caller's responsibility)
|
|
18
|
+
*
|
|
19
|
+
* This function is the future insertion point for Dynamic Context Governance.
|
|
20
|
+
* A governance middleware call will be added between the guard result and the
|
|
21
|
+
* postRemoteLocalScan() call — no other files need to change at that time.
|
|
22
|
+
*
|
|
23
|
+
* IMPORTS: only from api-scan.mjs and rule-engine — never from server.js.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { postRemoteLocalScan, postScanLog } from '../api-scan.mjs';
|
|
27
|
+
import { localScan } from '../rule-engine/index.js';
|
|
28
|
+
import { buildGovernanceContext, evaluateGovernance } from '../middleware/governance.js';
|
|
29
|
+
|
|
30
|
+
const UPGRADE_URL = 'https://vibesecur.com/#pricing';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} ScanOutcomeError
|
|
34
|
+
* @property {false} ok
|
|
35
|
+
* @property {'REMOTE_VERIFICATION_REQUIRED'|'UPGRADE_REQUIRED'|'GOVERNANCE_BLOCKED'} errorCode
|
|
36
|
+
* @property {Object} errorPayload - Shape ready for JSON serialisation in error response
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} ScanOutcomeSuccess
|
|
41
|
+
* @property {true} ok
|
|
42
|
+
* @property {'remote'|'offline'} mode
|
|
43
|
+
* @property {Object} result - Raw scan result (findings, score, grade, verdict, checklist…)
|
|
44
|
+
* @property {Object|undefined} quota - Remote quota metadata; undefined when mode='offline'
|
|
45
|
+
* @property {string} engineVersion - mcpPkg.version passed in by caller
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** @typedef {ScanOutcomeError|ScanOutcomeSuccess} ScanOutcome */
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Execute a single-code-string security scan.
|
|
52
|
+
*
|
|
53
|
+
* Callers must resolve 'auto' language before passing when the exact language
|
|
54
|
+
* is known (e.g. via inferLang). Passing 'auto' is safe — both
|
|
55
|
+
* postRemoteLocalScan and localScan handle it.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} params
|
|
58
|
+
* @param {string} params.code Source code to scan
|
|
59
|
+
* @param {string} params.lang Language: 'js'|'ts'|'py'|'json'|'auto'
|
|
60
|
+
* @param {string} params.projectRoot Resolved root path (from guard.resolvedRoot)
|
|
61
|
+
* @param {string} params.installToken VIBESECUR_INSTALL_TOKEN value
|
|
62
|
+
* @param {string} [params.lockedRootHash] From guard.lock?.lockedRootHash || guard.lock?.rootHash
|
|
63
|
+
* @param {string} params.engineVersion Package version string for result annotation
|
|
64
|
+
* @returns {Promise<ScanOutcome>}
|
|
65
|
+
*/
|
|
66
|
+
export async function runScan({
|
|
67
|
+
code,
|
|
68
|
+
lang,
|
|
69
|
+
projectRoot,
|
|
70
|
+
installToken,
|
|
71
|
+
lockedRootHash,
|
|
72
|
+
engineVersion,
|
|
73
|
+
}) {
|
|
74
|
+
// ── Governance middleware ─────────────────────────────────────────────────
|
|
75
|
+
// Phase 1: always passes through (evaluateGovernance returns allowed:true).
|
|
76
|
+
// Future phases: evaluateGovernance may return allowed:false to block the
|
|
77
|
+
// scan before any compute or network cost is incurred.
|
|
78
|
+
//
|
|
79
|
+
// Fail-open policy: if governance itself throws, we log and allow.
|
|
80
|
+
// This keeps Phase 1 safe — governance bugs never silently break scans.
|
|
81
|
+
// Future phases that want fail-closed must handle errors inside
|
|
82
|
+
// evaluateGovernance() before returning a GovernanceDecision.
|
|
83
|
+
let govDecision;
|
|
84
|
+
try {
|
|
85
|
+
const govCtx = buildGovernanceContext({
|
|
86
|
+
toolName: 'scan', // refined callers may pass this explicitly in Phase 2
|
|
87
|
+
requestedPath: projectRoot,
|
|
88
|
+
installToken,
|
|
89
|
+
lockedRootHash: lockedRootHash || '',
|
|
90
|
+
lang,
|
|
91
|
+
codeSize: code.length,
|
|
92
|
+
maxFiles: undefined, // code-string scan; repo tools will pass maxFiles in Phase 2
|
|
93
|
+
});
|
|
94
|
+
govDecision = await evaluateGovernance(govCtx);
|
|
95
|
+
} catch (govErr) {
|
|
96
|
+
// Governance error — fail-open in Phase 1, surface to stderr for observability
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
`[vibesecur] governance middleware error (fail-open): ${govErr.message}\n`,
|
|
99
|
+
);
|
|
100
|
+
govDecision = { allowed: true, reason: 'error-fail-open', annotations: {}, metadata: {} };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!govDecision.allowed) {
|
|
104
|
+
// Phase 1: this branch is unreachable (allowed is always true).
|
|
105
|
+
// Present now so Phase 2 only needs to set allowed:false inside
|
|
106
|
+
// evaluateGovernance() — no changes to this file or any tool handler.
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
errorCode: 'GOVERNANCE_BLOCKED',
|
|
110
|
+
errorPayload: {
|
|
111
|
+
error: 'GOVERNANCE_BLOCKED',
|
|
112
|
+
message: govDecision.reason || 'Request blocked by governance policy.',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Remote scan attempt ───────────────────────────────────────────────────
|
|
118
|
+
const remote = await postRemoteLocalScan({
|
|
119
|
+
code,
|
|
120
|
+
lang,
|
|
121
|
+
projectRoot,
|
|
122
|
+
platform: 'mcp',
|
|
123
|
+
installToken,
|
|
124
|
+
lockedRootHash,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── Case 1: Payment required (402) ───────────────────────────────────────
|
|
128
|
+
// MUST be checked before the generic !remote.ok guard. Fetch API sets
|
|
129
|
+
// Response.ok = false for all non-2xx statuses including 402, so without
|
|
130
|
+
// this early check the generic guard would intercept quota-exceeded
|
|
131
|
+
// responses and surface REMOTE_VERIFICATION_REQUIRED instead.
|
|
132
|
+
// upgradeUrl is always included so both callers (localScan tool, scanFile
|
|
133
|
+
// tool) get a consistent, actionable error shape.
|
|
134
|
+
if (remote.status === 402) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
errorCode: 'UPGRADE_REQUIRED',
|
|
138
|
+
errorPayload: {
|
|
139
|
+
...remote.json,
|
|
140
|
+
upgradeUrl: UPGRADE_URL,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Case 2: Remote unavailable or network/auth failure ───────────────────
|
|
146
|
+
// This covers: VIBESECUR_API_BASE not set (skipped:true), fetch error,
|
|
147
|
+
// non-2xx response (ok:false). We surface a hard error — there is no silent
|
|
148
|
+
// local fallback for this case. The caller must have a verified server
|
|
149
|
+
// binding before a scan is meaningful.
|
|
150
|
+
if (remote.skipped || !remote.ok) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
errorCode: 'REMOTE_VERIFICATION_REQUIRED',
|
|
154
|
+
errorPayload: {
|
|
155
|
+
error: 'REMOTE_VERIFICATION_REQUIRED',
|
|
156
|
+
message:
|
|
157
|
+
remote.error
|
|
158
|
+
|| remote.reason
|
|
159
|
+
|| `Remote scan failed with status ${remote.status || 'unknown'}`,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Case 3: Remote success ────────────────────────────────────────────────
|
|
165
|
+
if (remote.ok && remote.json?.success && remote.json?.data) {
|
|
166
|
+
const result = remote.json.data;
|
|
167
|
+
|
|
168
|
+
// Persist scan metadata so the Projects dashboard reflects this scan.
|
|
169
|
+
// Best-effort and metadata-only: a logging failure must never fail the
|
|
170
|
+
// scan the user already paid quota for. Reuses codeHash + scanReceipt so
|
|
171
|
+
// the backend does not double-count quota.
|
|
172
|
+
let logged = { ok: false };
|
|
173
|
+
try {
|
|
174
|
+
const logRes = await postScanLog({
|
|
175
|
+
result,
|
|
176
|
+
lang,
|
|
177
|
+
projectRoot,
|
|
178
|
+
platform: 'mcp',
|
|
179
|
+
installToken,
|
|
180
|
+
lockedRootHash,
|
|
181
|
+
});
|
|
182
|
+
logged = logRes?.ok
|
|
183
|
+
? { ok: true, scanId: logRes.json?.data?.scanId || null }
|
|
184
|
+
: { ok: false, reason: logRes?.reason || logRes?.error || `status ${logRes?.status || 'unknown'}` };
|
|
185
|
+
} catch (logErr) {
|
|
186
|
+
process.stderr.write(`[vibesecur] scan log failed (non-fatal): ${logErr.message}\n`);
|
|
187
|
+
logged = { ok: false, reason: logErr.message };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
mode: 'remote',
|
|
193
|
+
result,
|
|
194
|
+
quota: remote.json.quota,
|
|
195
|
+
engineVersion,
|
|
196
|
+
logged,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Case 4: Remote returned 2xx but no usable data ────────────────────────
|
|
201
|
+
// Edge case: API responded OK but body lacks success/data (e.g. partial
|
|
202
|
+
// degradation). Fall back to the local rule engine so the scan still runs.
|
|
203
|
+
const result = localScan(code, lang);
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
mode: 'offline',
|
|
207
|
+
result,
|
|
208
|
+
quota: undefined,
|
|
209
|
+
engineVersion,
|
|
210
|
+
};
|
|
211
|
+
}
|