vigthoria-cli 1.13.24 → 1.13.26
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/completions/_vigthoria +1 -1
- package/completions/vigthoria.fish +1 -1
- package/dist/commands/auth.js +14 -5
- package/dist/commands/chat.js +51 -2
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +33 -9
- package/dist/commands/game.d.ts +4 -0
- package/dist/commands/game.js +23 -1
- package/dist/commands/hub.d.ts +2 -0
- package/dist/commands/hub.js +61 -78
- package/dist/commands/legion.d.ts +1 -0
- package/dist/commands/legion.js +7 -7
- package/dist/commands/platform-registration.js +1 -1
- package/dist/commands/preview.d.ts +1 -0
- package/dist/commands/preview.js +30 -14
- package/dist/commands/product-run-registration.js +4 -2
- package/dist/commands/repo.d.ts +37 -2
- package/dist/commands/repo.js +99 -32
- package/dist/commands/security.d.ts +3 -0
- package/dist/commands/security.js +29 -9
- package/dist/commands/update-registration.js +2 -1
- package/dist/index.js +16 -2
- package/dist/utils/api.js +26 -22
- package/dist/utils/chat-prompt-policy.js +1 -1
- package/dist/utils/code-operations-service.js +43 -14
- package/dist/utils/config.js +5 -2
- package/dist/utils/local-security-service.d.ts +23 -0
- package/dist/utils/local-security-service.js +210 -0
- package/dist/utils/model-transport-service.js +66 -6
- package/dist/utils/network-policy.d.ts +1 -1
- package/dist/utils/network-policy.js +3 -2
- package/dist/utils/preview-screenshot-adapter.d.ts +42 -1
- package/dist/utils/preview-screenshot-adapter.js +66 -14
- package/dist/utils/runtime-temp.d.ts +1 -0
- package/dist/utils/runtime-temp.js +22 -2
- package/dist/utils/secret-policy.js +27 -18
- package/dist/utils/subscription-policy.d.ts +11 -0
- package/dist/utils/subscription-policy.js +32 -0
- package/dist/utils/v3-stream-events.d.ts +8 -0
- package/dist/utils/v3-stream-events.js +62 -0
- package/dist/utils/v3-workspace-service.js +5 -1
- package/dist/utils/vigflow-client.js +2 -2
- package/package.json +3 -11
- package/release-policy.json +2 -1
- package/scripts/release/generate-release-evidence.mjs +49 -0
- package/scripts/release/publish-cli-release.mjs +19 -8
- package/scripts/release/validate-no-go-gates.sh +2 -0
|
@@ -46,6 +46,8 @@ export class CodeOperationsService {
|
|
|
46
46
|
let code = await this.chatComplete(systemPrompt, buildScopedPrompt(false), model);
|
|
47
47
|
// Strip markdown fences if model included them
|
|
48
48
|
code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
49
|
+
if (!code)
|
|
50
|
+
throw new Error('Code generation backend returned an empty response.');
|
|
49
51
|
// Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
|
|
50
52
|
const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
|
|
51
53
|
this.codeIsOverEngineered(code, prompt));
|
|
@@ -53,6 +55,8 @@ export class CodeOperationsService {
|
|
|
53
55
|
// Retry once with stronger constraint — via Model Router
|
|
54
56
|
code = await this.chatComplete(systemPrompt, buildScopedPrompt(true), model);
|
|
55
57
|
code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
58
|
+
if (!code)
|
|
59
|
+
throw new Error('Code generation retry returned an empty response.');
|
|
56
60
|
// If still polluted, strip DOM code client-side
|
|
57
61
|
if (this.codeContainsDomPollution(code)) {
|
|
58
62
|
code = this.stripDomPollution(code, language);
|
|
@@ -234,6 +238,8 @@ export class CodeOperationsService {
|
|
|
234
238
|
'Return ONLY the JSON object, no markdown fences.',
|
|
235
239
|
].join('\n');
|
|
236
240
|
const raw = await this.chatComplete(sysPrompt, prompt, model, 8192);
|
|
241
|
+
if (!raw.trim())
|
|
242
|
+
throw new Error('Project generation backend returned an empty response.');
|
|
237
243
|
try {
|
|
238
244
|
const cleaned = raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
239
245
|
const parsed = JSON.parse(cleaned);
|
|
@@ -259,7 +265,10 @@ export class CodeOperationsService {
|
|
|
259
265
|
'- Do NOT use raw HTML or excessive blank lines.',
|
|
260
266
|
'- Do NOT nest numbered lists inside sections.',
|
|
261
267
|
].join('\n');
|
|
262
|
-
|
|
268
|
+
const explanation = (await this.chatComplete(sysPrompt, code)).trim();
|
|
269
|
+
if (!explanation)
|
|
270
|
+
throw new Error('Code explanation backend returned an empty response.');
|
|
271
|
+
return explanation;
|
|
263
272
|
}
|
|
264
273
|
async reviewCode(code, language) {
|
|
265
274
|
const sysPrompt = [
|
|
@@ -277,18 +286,29 @@ export class CodeOperationsService {
|
|
|
277
286
|
'- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
|
|
278
287
|
'- Return ONLY the JSON object, no markdown fences or extra text.',
|
|
279
288
|
].join('\n');
|
|
280
|
-
|
|
289
|
+
const result = await this.chatComplete(sysPrompt, code);
|
|
290
|
+
let raw;
|
|
281
291
|
try {
|
|
282
|
-
const result = await this.chatComplete(sysPrompt, code);
|
|
283
292
|
const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
284
293
|
raw = JSON.parse(cleaned);
|
|
285
294
|
}
|
|
286
|
-
catch {
|
|
287
|
-
|
|
295
|
+
catch (error) {
|
|
296
|
+
throw new Error('Code review backend returned malformed JSON.', { cause: error });
|
|
297
|
+
}
|
|
298
|
+
if (!raw || typeof raw !== 'object' || !Number.isFinite(raw.score) || raw.score < 0 || raw.score > 100 || !Array.isArray(raw.issues) || !Array.isArray(raw.suggestions)) {
|
|
299
|
+
throw new Error('Code review backend response does not satisfy the review contract.');
|
|
300
|
+
}
|
|
301
|
+
const lineCount = Math.max(1, code.split('\n').length);
|
|
302
|
+
const validSeverities = new Set(['error', 'warning', 'info']);
|
|
303
|
+
if (raw.issues.some((issue) => !issue || typeof issue.type !== 'string' || !Number.isInteger(issue.line) || issue.line < 1 || issue.line > lineCount || typeof issue.message !== 'string' || !validSeverities.has(issue.severity))) {
|
|
304
|
+
throw new Error('Code review backend returned an invalid or out-of-range finding.');
|
|
305
|
+
}
|
|
306
|
+
if (raw.suggestions.some((suggestion) => typeof suggestion !== 'string')) {
|
|
307
|
+
throw new Error('Code review backend returned an invalid suggestion.');
|
|
288
308
|
}
|
|
289
|
-
const score =
|
|
290
|
-
const issues =
|
|
291
|
-
const suggestions =
|
|
309
|
+
const score = raw.score;
|
|
310
|
+
const issues = raw.issues;
|
|
311
|
+
const suggestions = raw.suggestions;
|
|
292
312
|
// Merge client-side heuristics, but with tight dedup to avoid
|
|
293
313
|
// redundant over-reporting when the model already found the bug.
|
|
294
314
|
const modelFoundError = issues.some(i => i.severity === 'error');
|
|
@@ -480,17 +500,23 @@ export class CodeOperationsService {
|
|
|
480
500
|
'- Do not add comments, do not restructure beyond the minimal fix.',
|
|
481
501
|
'- Return ONLY the JSON object, no markdown fences.',
|
|
482
502
|
].join('\n');
|
|
483
|
-
|
|
503
|
+
const result = await this.chatComplete(sysPrompt, augmentedCode);
|
|
504
|
+
let raw;
|
|
484
505
|
try {
|
|
485
|
-
const result = await this.chatComplete(sysPrompt, augmentedCode);
|
|
486
506
|
const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
487
507
|
raw = JSON.parse(cleaned);
|
|
488
508
|
}
|
|
489
|
-
catch {
|
|
490
|
-
|
|
509
|
+
catch (error) {
|
|
510
|
+
throw new Error('Code fix backend returned malformed JSON.', { cause: error });
|
|
511
|
+
}
|
|
512
|
+
if (!raw || typeof raw !== 'object' || typeof raw.fixed !== 'string' || !raw.fixed.trim() || !Array.isArray(raw.changes)) {
|
|
513
|
+
throw new Error('Code fix backend response does not satisfy the fix contract.');
|
|
491
514
|
}
|
|
492
|
-
|
|
493
|
-
|
|
515
|
+
if (raw.changes.some((change) => !change || !Number.isInteger(change.line) || change.line < 1 || typeof change.before !== 'string' || typeof change.after !== 'string' || typeof change.reason !== 'string')) {
|
|
516
|
+
throw new Error('Code fix backend returned invalid change evidence.');
|
|
517
|
+
}
|
|
518
|
+
let fixed = raw.fixed;
|
|
519
|
+
let changes = raw.changes;
|
|
494
520
|
// If server returned no changes but we found issues, strip
|
|
495
521
|
// our injected comment prefix from the returned code and attempt
|
|
496
522
|
// a basic client-side repair.
|
|
@@ -545,6 +571,9 @@ export class CodeOperationsService {
|
|
|
545
571
|
: 'AI-suggested fix';
|
|
546
572
|
changes = this.computeSemanticDiff(code, fixed, cleanReason);
|
|
547
573
|
}
|
|
574
|
+
if (fixed === code && changes.length === 0) {
|
|
575
|
+
throw new Error('Code fix backend returned no verified mutation evidence.');
|
|
576
|
+
}
|
|
548
577
|
return { fixed, changes };
|
|
549
578
|
}
|
|
550
579
|
/**
|
package/dist/utils/config.js
CHANGED
|
@@ -129,7 +129,10 @@ function isConfigValue(value) {
|
|
|
129
129
|
}
|
|
130
130
|
export class Config {
|
|
131
131
|
store;
|
|
132
|
-
static OPERATOR_PLANS = new Set([
|
|
132
|
+
static OPERATOR_PLANS = new Set([
|
|
133
|
+
'basic', 'pro', 'professional', 'professional_ai', 'enterprise',
|
|
134
|
+
'enterprise_ai', 'whale', 'admin', 'master_admin', 'master_admin_plan',
|
|
135
|
+
]);
|
|
133
136
|
constructor(options = {}) {
|
|
134
137
|
const stateRoot = path.resolve(options.stateRoot || os.homedir());
|
|
135
138
|
const canonicalPath = path.join(stateRoot, '.vigthoria', 'config.json');
|
|
@@ -264,7 +267,7 @@ export class Config {
|
|
|
264
267
|
return true;
|
|
265
268
|
}
|
|
266
269
|
getNormalizedPlan() {
|
|
267
|
-
return (this.get('subscription').plan || '').trim().toLowerCase();
|
|
270
|
+
return (this.get('subscription').plan || '').trim().toLowerCase().replace(/-/g, '_');
|
|
268
271
|
}
|
|
269
272
|
hasOperatorAccess() {
|
|
270
273
|
return Config.OPERATOR_PLANS.has(this.getNormalizedPlan());
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type SecuritySeverity = 'critical' | 'high' | 'medium' | 'low';
|
|
2
|
+
export interface LocalSecurityIssue {
|
|
3
|
+
id: string;
|
|
4
|
+
rule: string;
|
|
5
|
+
severity: SecuritySeverity;
|
|
6
|
+
file: string;
|
|
7
|
+
line: number;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LocalSecurityResult {
|
|
11
|
+
rootDir: string;
|
|
12
|
+
scannedFiles: number;
|
|
13
|
+
scannedBytes: number;
|
|
14
|
+
skippedFiles: number;
|
|
15
|
+
score: number;
|
|
16
|
+
grade: string;
|
|
17
|
+
issueCount: number;
|
|
18
|
+
issues: LocalSecurityIssue[];
|
|
19
|
+
bounded: true;
|
|
20
|
+
localOnly: true;
|
|
21
|
+
}
|
|
22
|
+
export declare function scanLocalWorkspace(inputRoot?: string): LocalSecurityResult;
|
|
23
|
+
export declare function localSecurityFixPlan(result: LocalSecurityResult, issueId?: string, requestedApply?: boolean): Record<string, unknown>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { isSensitivePath } from './secret-policy.js';
|
|
5
|
+
import { WorkspaceBoundaryError } from './workspace-boundary.js';
|
|
6
|
+
const MAX_FILES = 5_000;
|
|
7
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
const SCANNABLE_EXTENSIONS = new Set([
|
|
10
|
+
'.cjs', '.css', '.go', '.html', '.htm', '.java', '.js', '.jsx', '.json',
|
|
11
|
+
'.mjs', '.php', '.ps1', '.py', '.rb', '.sh', '.sql', '.ts', '.tsx',
|
|
12
|
+
'.vue', '.xml', '.yaml', '.yml',
|
|
13
|
+
]);
|
|
14
|
+
const EXCLUDED_DIRECTORIES = new Set([
|
|
15
|
+
'.git', '.hg', '.svn', '.vigthoria', 'build', 'coverage', 'dist',
|
|
16
|
+
'node_modules', 'target', 'vendor', '__pycache__', '.venv', 'venv',
|
|
17
|
+
]);
|
|
18
|
+
// These are intentionally high-confidence source patterns. The scanner does
|
|
19
|
+
// not classify ordinary bind addresses, DOM APIs, or configuration examples as
|
|
20
|
+
// vulnerabilities without the context required to prove exploitability.
|
|
21
|
+
const RULES = [
|
|
22
|
+
{
|
|
23
|
+
id: 'hardcoded-private-key',
|
|
24
|
+
severity: 'critical',
|
|
25
|
+
expression: /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/g,
|
|
26
|
+
message: 'Private key material is embedded in source',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'hardcoded-access-token',
|
|
30
|
+
severity: 'critical',
|
|
31
|
+
expression: /\b(?:ghp|gho|ghu|ghs|github_pat|xox[baprs])_[A-Za-z0-9_-]{20,}\b/g,
|
|
32
|
+
message: 'A high-confidence access token is embedded in source',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'hardcoded-cloud-key',
|
|
36
|
+
severity: 'critical',
|
|
37
|
+
expression: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
38
|
+
message: 'An AWS access key identifier is embedded in source',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'child-process-shell',
|
|
42
|
+
severity: 'high',
|
|
43
|
+
expression: /\b(?:exec|execSync)\s*\(\s*(?:`|[^'"\s][^,\n]*)/g,
|
|
44
|
+
message: 'A child process appears to execute dynamically constructed shell text',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'dynamic-code-evaluation',
|
|
48
|
+
severity: 'high',
|
|
49
|
+
expression: /\b(?:eval|Function)\s*\(\s*(??['"])/g,
|
|
50
|
+
message: 'Dynamic code evaluation requires manual security review',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'unsafe-tls-disable',
|
|
54
|
+
severity: 'high',
|
|
55
|
+
expression: /\b(?:NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0|rejectUnauthorized\s*:\s*false)\b/g,
|
|
56
|
+
message: 'TLS certificate validation is explicitly disabled',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
function assertScanRoot(input) {
|
|
60
|
+
const resolved = path.resolve(input || process.cwd());
|
|
61
|
+
if (process.platform === 'win32' && /^(?:\\\\[?.]\\|\\\\(?!localhost\\))/i.test(resolved)) {
|
|
62
|
+
throw new WorkspaceBoundaryError('UNC and Windows device paths are not valid security scan roots', 'UNSAFE_WINDOWS_PATH');
|
|
63
|
+
}
|
|
64
|
+
const stat = fs.lstatSync(resolved);
|
|
65
|
+
if (stat.isSymbolicLink()) {
|
|
66
|
+
throw new WorkspaceBoundaryError('A symbolic link or junction cannot be used as a security scan root', 'WORKSPACE_LINK_ESCAPE');
|
|
67
|
+
}
|
|
68
|
+
if (!stat.isDirectory())
|
|
69
|
+
throw new WorkspaceBoundaryError('Security scan root must be a directory', 'WORKSPACE_PATH_INVALID');
|
|
70
|
+
return fs.realpathSync(resolved);
|
|
71
|
+
}
|
|
72
|
+
function stableIssueId(rule, file, line) {
|
|
73
|
+
return `SEC-${createHash('sha256').update(`${rule}\0${file}\0${line}`).digest('hex').slice(0, 12).toUpperCase()}`;
|
|
74
|
+
}
|
|
75
|
+
function lineAt(content, offset) {
|
|
76
|
+
let line = 1;
|
|
77
|
+
for (let index = 0; index < offset; index += 1)
|
|
78
|
+
if (content.charCodeAt(index) === 10)
|
|
79
|
+
line += 1;
|
|
80
|
+
return line;
|
|
81
|
+
}
|
|
82
|
+
function scoreFromIssues(issues) {
|
|
83
|
+
const penalty = { critical: 20, high: 10, medium: 4, low: 1 };
|
|
84
|
+
return Math.max(0, 100 - issues.reduce((sum, issue) => sum + penalty[issue.severity], 0));
|
|
85
|
+
}
|
|
86
|
+
function gradeFromScore(score) {
|
|
87
|
+
if (score >= 90)
|
|
88
|
+
return 'A';
|
|
89
|
+
if (score >= 75)
|
|
90
|
+
return 'B';
|
|
91
|
+
if (score >= 50)
|
|
92
|
+
return 'C';
|
|
93
|
+
if (score >= 30)
|
|
94
|
+
return 'D';
|
|
95
|
+
return 'F';
|
|
96
|
+
}
|
|
97
|
+
export function scanLocalWorkspace(inputRoot = process.cwd()) {
|
|
98
|
+
const rootDir = assertScanRoot(inputRoot);
|
|
99
|
+
const stack = [rootDir];
|
|
100
|
+
const issues = [];
|
|
101
|
+
let scannedFiles = 0;
|
|
102
|
+
let scannedBytes = 0;
|
|
103
|
+
let skippedFiles = 0;
|
|
104
|
+
while (stack.length > 0) {
|
|
105
|
+
const directory = stack.pop();
|
|
106
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const absolute = path.join(directory, entry.name);
|
|
109
|
+
const relative = path.relative(rootDir, absolute).split(path.sep).join('/');
|
|
110
|
+
if (entry.isSymbolicLink()) {
|
|
111
|
+
skippedFiles += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
if (!EXCLUDED_DIRECTORIES.has(entry.name) && !isSensitivePath(relative))
|
|
116
|
+
stack.push(absolute);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!entry.isFile()) {
|
|
120
|
+
skippedFiles += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (isSensitivePath(relative)) {
|
|
124
|
+
const basename = path.basename(relative).toLowerCase();
|
|
125
|
+
if (basename === '.env' || basename.startsWith('.env.')) {
|
|
126
|
+
issues.push({
|
|
127
|
+
id: stableIssueId('sensitive-env-file', relative, 1),
|
|
128
|
+
rule: 'sensitive-env-file',
|
|
129
|
+
severity: 'medium',
|
|
130
|
+
file: relative,
|
|
131
|
+
line: 1,
|
|
132
|
+
message: 'Sensitive environment file exists in the scanned tree; verify it is excluded from version control and uploads',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
skippedFiles += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase()))
|
|
139
|
+
continue;
|
|
140
|
+
if (scannedFiles >= MAX_FILES)
|
|
141
|
+
throw new Error(`Security scan file limit exceeded (${MAX_FILES})`);
|
|
142
|
+
const stat = fs.statSync(absolute);
|
|
143
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
144
|
+
skippedFiles += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (scannedBytes + stat.size > MAX_TOTAL_BYTES)
|
|
148
|
+
throw new Error(`Security scan byte limit exceeded (${MAX_TOTAL_BYTES})`);
|
|
149
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
150
|
+
scannedFiles += 1;
|
|
151
|
+
scannedBytes += stat.size;
|
|
152
|
+
for (const rule of RULES) {
|
|
153
|
+
rule.expression.lastIndex = 0;
|
|
154
|
+
let match;
|
|
155
|
+
while ((match = rule.expression.exec(content)) !== null) {
|
|
156
|
+
const line = lineAt(content, match.index);
|
|
157
|
+
issues.push({
|
|
158
|
+
id: stableIssueId(rule.id, relative, line),
|
|
159
|
+
rule: rule.id,
|
|
160
|
+
severity: rule.severity,
|
|
161
|
+
file: relative,
|
|
162
|
+
line,
|
|
163
|
+
message: rule.message,
|
|
164
|
+
});
|
|
165
|
+
if (match[0].length === 0)
|
|
166
|
+
rule.expression.lastIndex += 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
issues.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.rule.localeCompare(right.rule));
|
|
172
|
+
const score = scoreFromIssues(issues);
|
|
173
|
+
return {
|
|
174
|
+
rootDir,
|
|
175
|
+
scannedFiles,
|
|
176
|
+
scannedBytes,
|
|
177
|
+
skippedFiles,
|
|
178
|
+
score,
|
|
179
|
+
grade: gradeFromScore(score),
|
|
180
|
+
issueCount: issues.length,
|
|
181
|
+
issues,
|
|
182
|
+
bounded: true,
|
|
183
|
+
localOnly: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function localSecurityFixPlan(result, issueId, requestedApply = false) {
|
|
187
|
+
const selected = issueId ? result.issues.filter((issue) => issue.id === issueId) : result.issues;
|
|
188
|
+
if (issueId && selected.length === 0)
|
|
189
|
+
throw new Error(`Security issue was not found in the current scan: ${issueId}`);
|
|
190
|
+
const actions = selected.slice(0, 100).map((issue) => ({
|
|
191
|
+
issue_id: issue.id,
|
|
192
|
+
file: issue.file,
|
|
193
|
+
line: issue.line,
|
|
194
|
+
rule: issue.rule,
|
|
195
|
+
action: issue.rule === 'sensitive-env-file'
|
|
196
|
+
? 'Confirm the file is ignored and excluded from every upload/context boundary; rotate any exposed values'
|
|
197
|
+
: 'Review the exact source location and replace the unsafe construct without weakening validation or trust boundaries',
|
|
198
|
+
}));
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
applied: false,
|
|
202
|
+
requestedApply,
|
|
203
|
+
confirmRequired: false,
|
|
204
|
+
plannedActions: actions,
|
|
205
|
+
message: requestedApply
|
|
206
|
+
? 'No mutation was performed: local security fixes require an explicit reviewed source patch.'
|
|
207
|
+
: 'Local security fix plan generated; no files were changed.',
|
|
208
|
+
localOnly: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scanOutboundContext } from './secret-policy.js';
|
|
1
|
+
import { redactSensitiveText, scanOutboundContext } from './secret-policy.js';
|
|
2
2
|
export class ModelTransportService {
|
|
3
3
|
dependencies;
|
|
4
4
|
lastErrors = [];
|
|
@@ -28,15 +28,75 @@ export class ModelTransportService {
|
|
|
28
28
|
}
|
|
29
29
|
async complete(systemPrompt, userPrompt, model, maxTokens) {
|
|
30
30
|
const resolvedModel = model ? this.dependencies.resolvePermittedModelId(model) : 'Vigthoria-v4-Code-27B';
|
|
31
|
-
const
|
|
31
|
+
const messages = scanOutboundContext([{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }]).value;
|
|
32
|
+
const body = {
|
|
32
33
|
model: resolvedModel,
|
|
33
|
-
messages
|
|
34
|
+
messages,
|
|
34
35
|
max_tokens: maxTokens || this.dependencies.getMaxTokens() || 4096,
|
|
35
36
|
temperature: 0.3,
|
|
36
37
|
stream: false,
|
|
37
|
-
}
|
|
38
|
-
const
|
|
39
|
-
|
|
38
|
+
};
|
|
39
|
+
const failures = [];
|
|
40
|
+
const safeFailure = (value) => redactSensitiveText(String(value || 'transport failed')).slice(0, 120);
|
|
41
|
+
const extract = (response) => {
|
|
42
|
+
const content = response?.choices?.[0]?.message?.content
|
|
43
|
+
|| response?.choices?.[0]?.text
|
|
44
|
+
|| response?.response
|
|
45
|
+
|| response?.message
|
|
46
|
+
|| response?.content;
|
|
47
|
+
return response?.success === false || typeof content !== 'string' ? '' : content.trim();
|
|
48
|
+
};
|
|
49
|
+
if (!this.dependencies.shouldSkipCloudRoutes(resolvedModel) && !this.dependencies.isCloudModelId(resolvedModel)) {
|
|
50
|
+
try {
|
|
51
|
+
const content = extract(await this.dependencies.postModels(body));
|
|
52
|
+
if (content)
|
|
53
|
+
return content;
|
|
54
|
+
failures.push('models:empty response');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
failures.push(`models:${safeFailure(error?.response?.data?.error || error?.message)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (this.dependencies.isAuthenticated() && !this.dependencies.shouldSkipCloudRoutes(resolvedModel)) {
|
|
61
|
+
const coderBody = {
|
|
62
|
+
messages,
|
|
63
|
+
model: resolvedModel,
|
|
64
|
+
maxTokens: body.max_tokens,
|
|
65
|
+
temperature: body.temperature,
|
|
66
|
+
cloudConsent: this.dependencies.isCloudModelId(resolvedModel),
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
const content = extract(await this.dependencies.postCoder(coderBody));
|
|
70
|
+
if (content)
|
|
71
|
+
return content;
|
|
72
|
+
failures.push('coder:empty response');
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
failures.push(`coder:${safeFailure(error?.response?.data?.error || error?.message)}`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const content = extract(await this.dependencies.postCanonicalCoder(coderBody));
|
|
79
|
+
if (content)
|
|
80
|
+
return content;
|
|
81
|
+
failures.push('coder-canonical:empty response');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
failures.push(`coder-canonical:${safeFailure(error?.response?.data?.error || error?.message)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (this.dependencies.hasSelfHostedTransport() && this.dependencies.shouldTrySelfHosted(resolvedModel, model || resolvedModel)) {
|
|
88
|
+
try {
|
|
89
|
+
const content = extract(await this.dependencies.postSelfHosted(body));
|
|
90
|
+
if (content)
|
|
91
|
+
return content;
|
|
92
|
+
failures.push('self-hosted:empty response');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
failures.push(`self-hosted:${safeFailure(error?.message)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.lastErrors = failures;
|
|
99
|
+
throw new Error(`No permitted code-completion backend returned a response${failures.length ? ` (${failures.join(' | ')})` : ''}.`);
|
|
40
100
|
}
|
|
41
101
|
response(data, model, requested, prefix) {
|
|
42
102
|
const content = data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || data?.response || data?.message || data?.content;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AxiosInstance } from 'axios';
|
|
2
2
|
export { redactSensitiveText } from './secret-policy.js';
|
|
3
|
-
export type EndpointAudience = 'auto' | 'coder' | 'models' | 'hub' | 'community' | 'music' | 'brain-hub' | 'mcp' | 'v3' | 'template' | 'hyperloop' | 'vigflow' | 'bridge' | 'release' | 'public';
|
|
3
|
+
export type EndpointAudience = 'auto' | 'coder' | 'models' | 'operator' | 'hub' | 'community' | 'music' | 'brain-hub' | 'mcp' | 'v3' | 'template' | 'hyperloop' | 'vigflow' | 'bridge' | 'release' | 'public';
|
|
4
4
|
export declare const DEFAULT_NETWORK_TIMEOUT_MS = 30000;
|
|
5
5
|
export declare class NetworkPolicyError extends Error {
|
|
6
6
|
readonly code: 'offline' | 'untrusted_endpoint' | 'credential_audience' | 'unsafe_redirect';
|
|
@@ -3,7 +3,8 @@ export { redactSensitiveText } from './secret-policy.js';
|
|
|
3
3
|
const AUDIENCE_HOSTS = {
|
|
4
4
|
coder: new Set(['coder.vigthoria.io']),
|
|
5
5
|
models: new Set(['api.vigthoria.io']),
|
|
6
|
-
|
|
6
|
+
operator: new Set(['agent.vigthoria.io', 'operator.vigthoria.io']),
|
|
7
|
+
hub: new Set(['hub.vigthoria.io']),
|
|
7
8
|
community: new Set(['community.vigthoria.io']),
|
|
8
9
|
music: new Set(['music.vigthoria.io']),
|
|
9
10
|
'brain-hub': new Set(['coder.vigthoria.io']),
|
|
@@ -11,7 +12,7 @@ const AUDIENCE_HOSTS = {
|
|
|
11
12
|
v3: new Set(['coder.vigthoria.io']),
|
|
12
13
|
template: new Set(['coder.vigthoria.io', 'template.vigthoria.io']),
|
|
13
14
|
hyperloop: new Set(['coder.vigthoria.io', 'hyperloop.vigthoria.io']),
|
|
14
|
-
vigflow: new Set(['coder.vigthoria.io', '
|
|
15
|
+
vigflow: new Set(['coder.vigthoria.io', 'workflow.vigthoria.io']),
|
|
15
16
|
bridge: new Set(['bridge.vigthoria.io', 'devtools.vigthoria.io']),
|
|
16
17
|
release: new Set([
|
|
17
18
|
'extension.vigthoria.io',
|
|
@@ -5,11 +5,52 @@ export type PreviewScreenshotResult = {
|
|
|
5
5
|
export interface PreviewScreenshotPort {
|
|
6
6
|
capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
|
|
7
7
|
}
|
|
8
|
+
type BrowserPage = {
|
|
9
|
+
setViewport(options: {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
deviceScaleFactor: number;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
goto(url: string, options: {
|
|
15
|
+
waitUntil: string;
|
|
16
|
+
timeout: number;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
screenshot(options: {
|
|
19
|
+
path: string;
|
|
20
|
+
fullPage: boolean;
|
|
21
|
+
}): Promise<unknown>;
|
|
22
|
+
};
|
|
23
|
+
type Browser = {
|
|
24
|
+
newPage(): Promise<BrowserPage>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
process?(): {
|
|
27
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
28
|
+
} | null;
|
|
29
|
+
};
|
|
30
|
+
type PuppeteerLike = {
|
|
31
|
+
executablePath?(): string;
|
|
32
|
+
launch(options: {
|
|
33
|
+
headless: true | 'shell';
|
|
34
|
+
args: string[];
|
|
35
|
+
userDataDir: string;
|
|
36
|
+
executablePath?: string;
|
|
37
|
+
timeout: number;
|
|
38
|
+
protocolTimeout: number;
|
|
39
|
+
}): Promise<Browser>;
|
|
40
|
+
};
|
|
41
|
+
export type PreviewBrowserResolution = {
|
|
42
|
+
executablePath?: string;
|
|
43
|
+
headless: true | 'shell';
|
|
44
|
+
source: 'packaged' | 'system' | 'injected';
|
|
45
|
+
};
|
|
46
|
+
export declare function resolvePreviewBrowserExecutable(puppeteer: PuppeteerLike, environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean): PreviewBrowserResolution | null;
|
|
8
47
|
export declare class OptionalPuppeteerScreenshotAdapter implements PreviewScreenshotPort {
|
|
9
48
|
private readonly loadPuppeteer;
|
|
10
49
|
private readonly environment;
|
|
11
50
|
private readonly allocateTemp;
|
|
12
51
|
private readonly releaseTemp;
|
|
13
|
-
|
|
52
|
+
private readonly platform;
|
|
53
|
+
constructor(loadPuppeteer?: () => Promise<unknown>, environment?: NodeJS.ProcessEnv, allocateTemp?: (prefix: string) => string, releaseTemp?: (directory: string) => void, platform?: NodeJS.Platform);
|
|
14
54
|
capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
|
|
15
55
|
}
|
|
56
|
+
export {};
|
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
2
4
|
import { redactSensitiveText } from './secret-policy.js';
|
|
3
5
|
import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from './runtime-temp.js';
|
|
6
|
+
export function resolvePreviewBrowserExecutable(puppeteer, environment = process.env, platform = process.platform, exists = fs.existsSync) {
|
|
7
|
+
if (typeof puppeteer.executablePath !== 'function')
|
|
8
|
+
return { headless: 'shell', source: 'injected' };
|
|
9
|
+
try {
|
|
10
|
+
const packaged = puppeteer.executablePath();
|
|
11
|
+
if (packaged && exists(packaged))
|
|
12
|
+
return { executablePath: packaged, headless: 'shell', source: 'packaged' };
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Continue to trusted platform browser locations.
|
|
16
|
+
}
|
|
17
|
+
if (platform !== 'win32')
|
|
18
|
+
return null;
|
|
19
|
+
const bases = [environment['PROGRAMFILES(X86)'], environment.ProgramFiles, environment.LOCALAPPDATA]
|
|
20
|
+
.filter((value) => typeof value === 'string' && value.length > 0);
|
|
21
|
+
const relatives = [
|
|
22
|
+
['Microsoft', 'Edge', 'Application', 'msedge.exe'],
|
|
23
|
+
['Google', 'Chrome', 'Application', 'chrome.exe'],
|
|
24
|
+
];
|
|
25
|
+
for (const base of bases) {
|
|
26
|
+
for (const relative of relatives) {
|
|
27
|
+
const candidate = path.win32.join(base, ...relative);
|
|
28
|
+
if (exists(candidate))
|
|
29
|
+
return { executablePath: candidate, headless: true, source: 'system' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
37
|
+
promise.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
4
40
|
export class OptionalPuppeteerScreenshotAdapter {
|
|
5
41
|
loadPuppeteer;
|
|
6
42
|
environment;
|
|
7
43
|
allocateTemp;
|
|
8
44
|
releaseTemp;
|
|
9
|
-
|
|
45
|
+
platform;
|
|
46
|
+
constructor(loadPuppeteer = () => import('puppeteer'), environment = process.env, allocateTemp = createRuntimeTempDirectory, releaseTemp = removeRuntimeTempDirectory, platform = process.platform) {
|
|
10
47
|
this.loadPuppeteer = loadPuppeteer;
|
|
11
48
|
this.environment = environment;
|
|
12
49
|
this.allocateTemp = allocateTemp;
|
|
13
50
|
this.releaseTemp = releaseTemp;
|
|
51
|
+
this.platform = platform;
|
|
14
52
|
}
|
|
15
53
|
async capture(entryAbsolutePath, screenshotPath) {
|
|
16
54
|
if (this.environment.VIGTHORIA_DISABLE_PREVIEW_SCREENSHOT === '1') {
|
|
@@ -20,26 +58,40 @@ export class OptionalPuppeteerScreenshotAdapter {
|
|
|
20
58
|
const loaded = await this.loadPuppeteer().catch(() => null);
|
|
21
59
|
const puppeteer = (loaded?.default || loaded);
|
|
22
60
|
if (!puppeteer || typeof puppeteer.launch !== 'function') {
|
|
23
|
-
return { captured: false, error: '
|
|
61
|
+
return { captured: false, error: 'required Puppeteer screenshot runtime is not installed' };
|
|
62
|
+
}
|
|
63
|
+
const browserResolution = resolvePreviewBrowserExecutable(puppeteer, this.environment, this.platform);
|
|
64
|
+
if (!browserResolution) {
|
|
65
|
+
return { captured: false, error: 'required browser executable is unavailable; install Chrome/Edge or the packaged Puppeteer browser' };
|
|
24
66
|
}
|
|
25
67
|
const browserProfile = this.allocateTemp('browser-');
|
|
68
|
+
let browser = null;
|
|
26
69
|
try {
|
|
27
|
-
|
|
28
|
-
headless:
|
|
70
|
+
browser = await withTimeout(puppeteer.launch({
|
|
71
|
+
headless: browserResolution.headless,
|
|
29
72
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
30
73
|
userDataDir: browserProfile,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
await browser.close();
|
|
40
|
-
}
|
|
74
|
+
executablePath: browserResolution.executablePath,
|
|
75
|
+
timeout: 20_000,
|
|
76
|
+
protocolTimeout: 20_000,
|
|
77
|
+
}), 25_000, 'browser launch');
|
|
78
|
+
const page = await withTimeout(browser.newPage(), 10_000, 'browser page creation');
|
|
79
|
+
await withTimeout(page.setViewport({ width: 800, height: 600, deviceScaleFactor: 1 }), 10_000, 'browser viewport setup');
|
|
80
|
+
await page.goto(pathToFileURL(entryAbsolutePath).toString(), { waitUntil: 'networkidle0', timeout: 20_000 });
|
|
81
|
+
await withTimeout(page.screenshot({ path: screenshotPath, fullPage: false }), 20_000, 'screenshot capture');
|
|
41
82
|
}
|
|
42
83
|
finally {
|
|
84
|
+
if (browser) {
|
|
85
|
+
try {
|
|
86
|
+
await withTimeout(browser.close(), 5_000, 'browser shutdown');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
try {
|
|
90
|
+
browser.process?.()?.kill('SIGKILL');
|
|
91
|
+
}
|
|
92
|
+
catch { /* exact owned browser only */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
43
95
|
this.releaseTemp(browserProfile);
|
|
44
96
|
}
|
|
45
97
|
return { captured: true };
|
|
@@ -44,6 +44,7 @@ export declare class RuntimeTempManager {
|
|
|
44
44
|
private readonly pid;
|
|
45
45
|
private readonly isProcessAlive;
|
|
46
46
|
private readonly systemTempRoot;
|
|
47
|
+
private readonly sharedTempRoots;
|
|
47
48
|
private readonly configuredRoot;
|
|
48
49
|
private readonly source;
|
|
49
50
|
private readonly maxBytes;
|