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
package/dist/commands/preview.js
CHANGED
|
@@ -51,6 +51,9 @@ const MIME_TYPES = {
|
|
|
51
51
|
'.txt': 'text/plain',
|
|
52
52
|
'.map': 'application/json',
|
|
53
53
|
};
|
|
54
|
+
export function isPreviewProofRequested(options) {
|
|
55
|
+
return options.proof === true || options.screenshot === true;
|
|
56
|
+
}
|
|
54
57
|
export class PreviewCommand {
|
|
55
58
|
logger;
|
|
56
59
|
api;
|
|
@@ -76,11 +79,17 @@ export class PreviewCommand {
|
|
|
76
79
|
await this.showConsolidatedDiff(projectPath);
|
|
77
80
|
}
|
|
78
81
|
// Run Template Service preview proof gate
|
|
79
|
-
if (options
|
|
80
|
-
|
|
82
|
+
if (isPreviewProofRequested(options)) {
|
|
83
|
+
try {
|
|
84
|
+
await this.runProofGate(projectPath, options.screenshot);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.api.destroy();
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
81
90
|
}
|
|
82
91
|
// If only --diff or --proof was requested (no explicit entry/port), just exit
|
|
83
|
-
if (options.diff || options
|
|
92
|
+
if (options.diff || isPreviewProofRequested(options)) {
|
|
84
93
|
if (!options.entry && !options.port) {
|
|
85
94
|
this.api.destroy();
|
|
86
95
|
return;
|
|
@@ -90,7 +99,7 @@ export class PreviewCommand {
|
|
|
90
99
|
const entryFile = this.detectEntryFile(projectPath, options.entry);
|
|
91
100
|
if (!entryFile) {
|
|
92
101
|
this.logger.warn('No HTML entry file found. Use --entry <file> to specify one.');
|
|
93
|
-
if (!options.diff && !options.proof) {
|
|
102
|
+
if (!options.diff && !options.proof && !options.screenshot) {
|
|
94
103
|
this.api.destroy();
|
|
95
104
|
throw new CliCommandError('No HTML entry file found. Use --entry <file> to specify one.', {
|
|
96
105
|
code: 'PREVIEW_ENTRY_NOT_FOUND', category: 'configuration',
|
|
@@ -162,7 +171,7 @@ export class PreviewCommand {
|
|
|
162
171
|
* Start a local HTTP server for preview
|
|
163
172
|
*/
|
|
164
173
|
async startServer(projectPath, entryFile, port, autoOpen) {
|
|
165
|
-
return new Promise((resolve) => {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
166
175
|
this.server = http.createServer((req, res) => {
|
|
167
176
|
let filePath;
|
|
168
177
|
try {
|
|
@@ -210,6 +219,20 @@ export class PreviewCommand {
|
|
|
210
219
|
res.end('Internal server error');
|
|
211
220
|
}
|
|
212
221
|
});
|
|
222
|
+
const finish = (error) => {
|
|
223
|
+
process.removeListener('SIGINT', shutdown);
|
|
224
|
+
process.removeListener('SIGTERM', shutdown);
|
|
225
|
+
this.api.destroy();
|
|
226
|
+
this.server = null;
|
|
227
|
+
error ? reject(error) : resolve();
|
|
228
|
+
};
|
|
229
|
+
const shutdown = () => {
|
|
230
|
+
console.log(chalk.gray('\n Stopping preview server...'));
|
|
231
|
+
if (!this.server)
|
|
232
|
+
return finish();
|
|
233
|
+
this.server.close((error) => finish(error || undefined));
|
|
234
|
+
};
|
|
235
|
+
this.server.once('error', finish);
|
|
213
236
|
this.server.listen(port, () => {
|
|
214
237
|
const url = `http://localhost:${port}/${entryFile}`;
|
|
215
238
|
console.log(chalk.green(` ${CH.success} Preview server running`));
|
|
@@ -221,15 +244,8 @@ export class PreviewCommand {
|
|
|
221
244
|
this.openBrowser(url);
|
|
222
245
|
}
|
|
223
246
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
console.log(chalk.gray('\n Stopping preview server...'));
|
|
227
|
-
this.server?.close();
|
|
228
|
-
this.api.destroy();
|
|
229
|
-
resolve();
|
|
230
|
-
};
|
|
231
|
-
process.on('SIGINT', shutdown);
|
|
232
|
-
process.on('SIGTERM', shutdown);
|
|
247
|
+
process.once('SIGINT', shutdown);
|
|
248
|
+
process.once('SIGTERM', shutdown);
|
|
233
249
|
});
|
|
234
250
|
}
|
|
235
251
|
/**
|
|
@@ -142,11 +142,13 @@ export function registerProductAndRunCommands(program, config, logger) {
|
|
|
142
142
|
});
|
|
143
143
|
repoCommand
|
|
144
144
|
.command('review [reviewId]')
|
|
145
|
-
.description('Show repository security review
|
|
145
|
+
.description('Show or clean repository security review state')
|
|
146
|
+
.option('--cleanup', 'Remove the quarantined payload and review object while retaining a hash-only audit record', false)
|
|
147
|
+
.option('-y, --yes', 'Confirm cleanup without an interactive prompt', false)
|
|
146
148
|
.option('--json', 'Emit machine-readable JSON output', false)
|
|
147
149
|
.action(async (reviewId, options) => {
|
|
148
150
|
const repo = new RepoCommand(config, logger);
|
|
149
|
-
await repo.review(reviewId, { json: options.json });
|
|
151
|
+
await repo.review(reviewId, { json: options.json, cleanup: options.cleanup, yes: options.yes });
|
|
150
152
|
});
|
|
151
153
|
repoCommand
|
|
152
154
|
.command('pull <name>')
|
package/dist/commands/repo.d.ts
CHANGED
|
@@ -14,6 +14,22 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { Config } from '../utils/config.js';
|
|
16
16
|
import { Logger } from '../utils/logger.js';
|
|
17
|
+
interface Project {
|
|
18
|
+
id: number;
|
|
19
|
+
project_name: string;
|
|
20
|
+
project_path: string;
|
|
21
|
+
description: string;
|
|
22
|
+
tech_stack: string;
|
|
23
|
+
visibility: 'public' | 'private' | 'restricted';
|
|
24
|
+
is_public: boolean;
|
|
25
|
+
demo_url: string | null;
|
|
26
|
+
repo_url: string | null;
|
|
27
|
+
status: string;
|
|
28
|
+
storage_usage_mb: number;
|
|
29
|
+
created_at: string;
|
|
30
|
+
updated_at: string;
|
|
31
|
+
last_synced_at: string | null;
|
|
32
|
+
}
|
|
17
33
|
interface PushOptions {
|
|
18
34
|
path?: string;
|
|
19
35
|
name?: string;
|
|
@@ -34,6 +50,25 @@ interface ListOptions {
|
|
|
34
50
|
interface ShareOptions {
|
|
35
51
|
expires?: string;
|
|
36
52
|
}
|
|
53
|
+
interface RepositoryMaterialization {
|
|
54
|
+
project?: Project;
|
|
55
|
+
projectName?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
downloadUrl?: string;
|
|
58
|
+
inlineComplete?: boolean;
|
|
59
|
+
files?: Array<{
|
|
60
|
+
path: string;
|
|
61
|
+
content?: string;
|
|
62
|
+
encoding?: 'utf8' | 'base64';
|
|
63
|
+
size?: number;
|
|
64
|
+
binary?: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
export declare function isCompleteRepositoryInlinePayload(data: RepositoryMaterialization): boolean;
|
|
68
|
+
export declare function resolveRepositoryDownloadRoute(rawUrl: string, coderApiBase: string, publicDownload?: boolean): {
|
|
69
|
+
url: string;
|
|
70
|
+
audience: 'coder' | 'community';
|
|
71
|
+
};
|
|
37
72
|
export declare class RepositoryMutationAmbiguousError extends Error {
|
|
38
73
|
readonly operationId: string;
|
|
39
74
|
readonly mutationOutcome = "unknown";
|
|
@@ -48,8 +83,6 @@ export declare class RepoCommand {
|
|
|
48
83
|
private communityToken;
|
|
49
84
|
constructor(config: Config, _logger: Logger);
|
|
50
85
|
private getAuthHeaders;
|
|
51
|
-
private getRepositoryAudienceForUrl;
|
|
52
|
-
private getAuthHeadersForUrl;
|
|
53
86
|
private ensureCommunityAuth;
|
|
54
87
|
private repoFetch;
|
|
55
88
|
private collectProjectFiles;
|
|
@@ -72,6 +105,8 @@ export declare class RepoCommand {
|
|
|
72
105
|
*/
|
|
73
106
|
review(reviewId?: string, options?: {
|
|
74
107
|
json?: boolean;
|
|
108
|
+
cleanup?: boolean;
|
|
109
|
+
yes?: boolean;
|
|
75
110
|
}): Promise<void>;
|
|
76
111
|
private materializeProject;
|
|
77
112
|
/**
|
package/dist/commands/repo.js
CHANGED
|
@@ -23,6 +23,35 @@ import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/com
|
|
|
23
23
|
import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from '../utils/runtime-temp.js';
|
|
24
24
|
import { assertSafeDestinationPath, assertSafeRelativePath, inspectZipArchive, mergeValidatedWorkspace, resolveWorkspacePath, validateExtractedWorkspace, } from '../utils/workspace-boundary.js';
|
|
25
25
|
import inquirer from 'inquirer';
|
|
26
|
+
export function isCompleteRepositoryInlinePayload(data) {
|
|
27
|
+
if (!Array.isArray(data.files) || data.files.length === 0 || data.inlineComplete === false)
|
|
28
|
+
return false;
|
|
29
|
+
return data.files.every((file) => typeof file?.path === 'string'
|
|
30
|
+
&& typeof file?.content === 'string'
|
|
31
|
+
&& (file.encoding === undefined || file.encoding === 'utf8' || file.encoding === 'base64')
|
|
32
|
+
&& file.binary !== true);
|
|
33
|
+
}
|
|
34
|
+
export function resolveRepositoryDownloadRoute(rawUrl, coderApiBase, publicDownload = false) {
|
|
35
|
+
const parsed = new URL(rawUrl);
|
|
36
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.port) {
|
|
37
|
+
throw new Error('Repository download URL is not an exact trusted HTTPS origin');
|
|
38
|
+
}
|
|
39
|
+
if (parsed.hostname.toLowerCase() === 'coder.vigthoria.io')
|
|
40
|
+
return { url: parsed.href, audience: 'coder' };
|
|
41
|
+
if (parsed.hostname.toLowerCase() !== 'community.vigthoria.io') {
|
|
42
|
+
throw new Error('Repository download URL is outside the Coder and Community trust boundary');
|
|
43
|
+
}
|
|
44
|
+
if (publicDownload)
|
|
45
|
+
return { url: parsed.href, audience: 'community' };
|
|
46
|
+
const match = parsed.pathname.match(/^\/api\/showcase\/(\d+)\/download$/);
|
|
47
|
+
if (!match || parsed.search || parsed.hash) {
|
|
48
|
+
throw new Error('Authenticated repository archive URL has an unsupported path');
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
url: `${coderApiBase.replace(/\/$/, '')}/api/community-repo/archive/${encodeURIComponent(match[1])}`,
|
|
52
|
+
audience: 'coder',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
26
55
|
const MAX_REPOSITORY_CONTENT_BYTES = 100 * 1024 * 1024;
|
|
27
56
|
const MAX_REPOSITORY_REQUEST_BYTES = 120 * 1024 * 1024;
|
|
28
57
|
class RepositoryApiError extends Error {
|
|
@@ -87,17 +116,6 @@ export class RepoCommand {
|
|
|
87
116
|
'Content-Type': 'application/json',
|
|
88
117
|
};
|
|
89
118
|
}
|
|
90
|
-
getRepositoryAudienceForUrl(rawUrl) {
|
|
91
|
-
const hostname = new URL(rawUrl).hostname.toLowerCase();
|
|
92
|
-
if (hostname === 'coder.vigthoria.io')
|
|
93
|
-
return 'coder';
|
|
94
|
-
if (hostname === 'community.vigthoria.io')
|
|
95
|
-
return 'community';
|
|
96
|
-
throw new Error('Repository download URL is outside the Coder and Community trust boundary');
|
|
97
|
-
}
|
|
98
|
-
getAuthHeadersForUrl(rawUrl) {
|
|
99
|
-
return this.getAuthHeaders(this.getRepositoryAudienceForUrl(rawUrl));
|
|
100
|
-
}
|
|
101
119
|
async ensureCommunityAuth() {
|
|
102
120
|
if (this.communityToken) {
|
|
103
121
|
return;
|
|
@@ -502,6 +520,43 @@ export class RepoCommand {
|
|
|
502
520
|
*/
|
|
503
521
|
async review(reviewId, options = {}) {
|
|
504
522
|
this.requireAuth();
|
|
523
|
+
if (options.cleanup) {
|
|
524
|
+
if (!reviewId) {
|
|
525
|
+
throw new CliCommandError('A review ID is required with --cleanup', {
|
|
526
|
+
code: 'REPOSITORY_REVIEW_ID_REQUIRED', category: 'usage',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (!options.yes) {
|
|
530
|
+
if (options.json || !process.stdin.isTTY) {
|
|
531
|
+
throw new CliCommandError('Repository review cleanup requires --yes in JSON or non-interactive mode', {
|
|
532
|
+
code: 'CONFIRMATION_REQUIRED', category: 'usage',
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const answer = await inquirer.prompt([{
|
|
536
|
+
type: 'confirm',
|
|
537
|
+
name: 'confirmed',
|
|
538
|
+
message: `Permanently remove quarantined payload for review ${reviewId}?`,
|
|
539
|
+
default: false,
|
|
540
|
+
}]);
|
|
541
|
+
if (!answer.confirmed) {
|
|
542
|
+
throw new CliCommandError('Repository security review cleanup cancelled', {
|
|
543
|
+
code: 'REPOSITORY_REVIEW_CLEANUP_CANCELLED', category: 'cancelled',
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const response = await this.repoFetch(`/api/repo/security-reviews/${encodeURIComponent(reviewId)}`, { method: 'DELETE' });
|
|
548
|
+
const payload = await readResponsePayload(response);
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
throw new RepositoryApiError(response.status, payload, 'Failed to clean repository security review');
|
|
551
|
+
}
|
|
552
|
+
if (options.json) {
|
|
553
|
+
console.log(formatSuccessJson('repo review', payload));
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
console.log(chalk.green(`\n✓ Removed quarantined repository review ${reviewId}; a hash-only audit record was retained.\n`));
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
505
560
|
const spinner = createSpinner(reviewId ? 'Loading security review...' : 'Loading security reviews...').start();
|
|
506
561
|
try {
|
|
507
562
|
const apiPath = reviewId
|
|
@@ -559,12 +614,38 @@ export class RepoCommand {
|
|
|
559
614
|
try {
|
|
560
615
|
if (fs.existsSync(outputPath))
|
|
561
616
|
validateExtractedWorkspace(outputPath);
|
|
562
|
-
if (data
|
|
563
|
-
const
|
|
617
|
+
if (isCompleteRepositoryInlinePayload(data)) {
|
|
618
|
+
const stagingRoot = createRuntimeTempDirectory('repo-inline-');
|
|
619
|
+
try {
|
|
620
|
+
let totalBytes = 0;
|
|
621
|
+
for (const file of data.files) {
|
|
622
|
+
const filePath = resolveWorkspacePath(stagingRoot, file.path, { allowMissing: true });
|
|
623
|
+
const content = file.encoding === 'base64'
|
|
624
|
+
? Buffer.from(file.content, 'base64')
|
|
625
|
+
: Buffer.from(file.content, 'utf8');
|
|
626
|
+
if (typeof file.size === 'number' && file.size !== content.byteLength) {
|
|
627
|
+
throw new Error(`Repository file size mismatch: ${file.path}`);
|
|
628
|
+
}
|
|
629
|
+
totalBytes += content.byteLength;
|
|
630
|
+
if (totalBytes > MAX_REPOSITORY_CONTENT_BYTES)
|
|
631
|
+
throw new Error('Repository inline payload exceeds the materialization limit');
|
|
632
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
633
|
+
fs.writeFileSync(filePath, content, { mode: 0o600 });
|
|
634
|
+
}
|
|
635
|
+
validateExtractedWorkspace(stagingRoot);
|
|
636
|
+
mergeValidatedWorkspace(stagingRoot, outputPath);
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
removeRuntimeTempDirectory(stagingRoot);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
else if (data.downloadUrl) {
|
|
643
|
+
const route = resolveRepositoryDownloadRoute(data.downloadUrl, this.apiBase, publicDownload);
|
|
644
|
+
const archiveResponse = await guardedFetch(route.url, {
|
|
564
645
|
headers: publicDownload
|
|
565
646
|
? { 'Content-Type': 'application/json' }
|
|
566
|
-
: this.
|
|
567
|
-
}, { audience:
|
|
647
|
+
: this.getAuthHeaders(route.audience)
|
|
648
|
+
}, { audience: route.audience });
|
|
568
649
|
if (!archiveResponse.ok)
|
|
569
650
|
throw new Error('Failed to download project archive');
|
|
570
651
|
const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
|
|
@@ -601,23 +682,8 @@ export class RepoCommand {
|
|
|
601
682
|
removeRuntimeTempDirectory(stagingRoot);
|
|
602
683
|
}
|
|
603
684
|
}
|
|
604
|
-
else if (data.files) {
|
|
605
|
-
const stagingRoot = createRuntimeTempDirectory('repo-inline-');
|
|
606
|
-
try {
|
|
607
|
-
for (const file of data.files) {
|
|
608
|
-
const filePath = resolveWorkspacePath(stagingRoot, file.path, { allowMissing: true });
|
|
609
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
610
|
-
fs.writeFileSync(filePath, file.content, { mode: 0o600 });
|
|
611
|
-
}
|
|
612
|
-
validateExtractedWorkspace(stagingRoot);
|
|
613
|
-
mergeValidatedWorkspace(stagingRoot, outputPath);
|
|
614
|
-
}
|
|
615
|
-
finally {
|
|
616
|
-
removeRuntimeTempDirectory(stagingRoot);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
685
|
else {
|
|
620
|
-
throw new Error('Repository response contained neither
|
|
686
|
+
throw new Error('Repository response contained neither a complete inline payload nor an archive');
|
|
621
687
|
}
|
|
622
688
|
downloadSpinner.succeed(chalk.green('Project materialized successfully!'));
|
|
623
689
|
console.log(chalk.cyan('\n📁 Project extracted to:'));
|
|
@@ -854,7 +920,8 @@ export class RepoCommand {
|
|
|
854
920
|
const spinner = createSpinner('Deleting project...').start();
|
|
855
921
|
try {
|
|
856
922
|
const operationId = createOperationId();
|
|
857
|
-
const
|
|
923
|
+
const repo = await this.resolveRepoByName(projectName);
|
|
924
|
+
const response = await this.repoFetch(`/api/repo/projects/${encodeURIComponent(String(repo.id))}`, {
|
|
858
925
|
method: 'DELETE',
|
|
859
926
|
headers: { 'X-Vigthoria-Operation-Id': operationId },
|
|
860
927
|
}, { operationId });
|
|
@@ -8,10 +8,13 @@ interface SecurityOptions {
|
|
|
8
8
|
}
|
|
9
9
|
export declare class SecurityCommand {
|
|
10
10
|
private logger;
|
|
11
|
+
private client;
|
|
11
12
|
constructor(_config: Config, logger: Logger);
|
|
12
13
|
private getMcpBaseUrl;
|
|
13
14
|
private resolveDir;
|
|
14
15
|
private execute;
|
|
16
|
+
private useRemoteMcp;
|
|
17
|
+
private runScan;
|
|
15
18
|
scan(options: SecurityOptions): Promise<void>;
|
|
16
19
|
score(options: SecurityOptions): Promise<void>;
|
|
17
20
|
fix(options: SecurityOptions): Promise<void>;
|
|
@@ -2,11 +2,13 @@ import axios from 'axios';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { assertTrustedEndpoint, installAxiosNetworkPolicy } from '../utils/network-policy.js';
|
|
4
4
|
import { formatSuccessJson } from '../utils/command-contract.js';
|
|
5
|
+
import { localSecurityFixPlan, scanLocalWorkspace } from '../utils/local-security-service.js';
|
|
5
6
|
export class SecurityCommand {
|
|
6
7
|
logger;
|
|
8
|
+
client = axios.create({ timeout: 120000 });
|
|
7
9
|
constructor(_config, logger) {
|
|
8
10
|
this.logger = logger;
|
|
9
|
-
installAxiosNetworkPolicy(
|
|
11
|
+
installAxiosNetworkPolicy(this.client, 'mcp');
|
|
10
12
|
}
|
|
11
13
|
getMcpBaseUrl() {
|
|
12
14
|
const fromEnv = process.env.VIGTHORIA_MCP_URL || process.env.MCP_SERVER_URL;
|
|
@@ -25,7 +27,7 @@ export class SecurityCommand {
|
|
|
25
27
|
if (!loopback && typeof parameters.dir === 'string' && path.isAbsolute(parameters.dir)) {
|
|
26
28
|
throw new Error('Remote MCP security execution requires an uploaded workspace binding; refusing to send a local absolute path.');
|
|
27
29
|
}
|
|
28
|
-
const response = await
|
|
30
|
+
const response = await this.client.post(`${baseUrl}/mcp/execute`, {
|
|
29
31
|
tool,
|
|
30
32
|
parameters,
|
|
31
33
|
context: {
|
|
@@ -43,9 +45,17 @@ export class SecurityCommand {
|
|
|
43
45
|
const result = response.data?.result || {};
|
|
44
46
|
return result.result || result;
|
|
45
47
|
}
|
|
48
|
+
useRemoteMcp() {
|
|
49
|
+
return Boolean((process.env.VIGTHORIA_MCP_URL || process.env.MCP_SERVER_URL || '').trim());
|
|
50
|
+
}
|
|
51
|
+
async runScan(dir) {
|
|
52
|
+
if (this.useRemoteMcp())
|
|
53
|
+
return this.execute('security_scan', { dir });
|
|
54
|
+
return scanLocalWorkspace(dir);
|
|
55
|
+
}
|
|
46
56
|
async scan(options) {
|
|
47
57
|
const dir = this.resolveDir(options.dir);
|
|
48
|
-
const result = await this.
|
|
58
|
+
const result = await this.runScan(dir);
|
|
49
59
|
if (options.json) {
|
|
50
60
|
console.log(formatSuccessJson('security scan', result));
|
|
51
61
|
return;
|
|
@@ -64,7 +74,15 @@ export class SecurityCommand {
|
|
|
64
74
|
}
|
|
65
75
|
async score(options) {
|
|
66
76
|
const dir = this.resolveDir(options.dir);
|
|
67
|
-
const
|
|
77
|
+
const scan = await this.runScan(dir);
|
|
78
|
+
const result = {
|
|
79
|
+
score: scan.score,
|
|
80
|
+
grade: scan.grade,
|
|
81
|
+
issueCount: scan.issueCount,
|
|
82
|
+
scannedFiles: scan.scannedFiles,
|
|
83
|
+
summary: `Security score ${scan.score}/100 (${scan.grade}) with ${scan.issueCount} issues`,
|
|
84
|
+
localOnly: scan.localOnly === true,
|
|
85
|
+
};
|
|
68
86
|
if (options.json) {
|
|
69
87
|
console.log(formatSuccessJson('security score', result));
|
|
70
88
|
return;
|
|
@@ -75,11 +93,13 @@ export class SecurityCommand {
|
|
|
75
93
|
}
|
|
76
94
|
async fix(options) {
|
|
77
95
|
const dir = this.resolveDir(options.dir);
|
|
78
|
-
const result =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
const result = this.useRemoteMcp()
|
|
97
|
+
? await this.execute('security_fix', {
|
|
98
|
+
dir,
|
|
99
|
+
issue_id: options.issueId,
|
|
100
|
+
confirm: Boolean(options.apply),
|
|
101
|
+
})
|
|
102
|
+
: localSecurityFixPlan(scanLocalWorkspace(dir), options.issueId, Boolean(options.apply));
|
|
83
103
|
if (options.json) {
|
|
84
104
|
console.log(formatSuccessJson('security fix', result));
|
|
85
105
|
return;
|
|
@@ -252,7 +252,7 @@ export function registerUpdateCommand(program, version) {
|
|
|
252
252
|
if (!signatureResponse.ok)
|
|
253
253
|
throw new Error(`Detached signature returned HTTP ${signatureResponse.status}`);
|
|
254
254
|
verifyReleaseSignature(channel, manifestEntry, await signatureResponse.text());
|
|
255
|
-
|
|
255
|
+
console.log(chalk.gray(`Verified ${channel} manifest: version=${manifestEntry.version} package=${manifestEntry.packageName}@${manifestEntry.packageVersion} size=${manifestEntry.size} sha256=${manifestEntry.sha256}`));
|
|
256
256
|
}
|
|
257
257
|
catch (error) {
|
|
258
258
|
if (error instanceof CliCommandError)
|
|
@@ -303,6 +303,7 @@ export function registerUpdateCommand(program, version) {
|
|
|
303
303
|
&& compareVersions(manifestEntry.version, effectiveLatest) >= 0
|
|
304
304
|
&& compareVersions(manifestEntry.version, currentVersion) > 0);
|
|
305
305
|
if (manifestIsAuthoritative && manifestEntry) {
|
|
306
|
+
assertReleaseTransition(currentVersion, manifestEntry, channel, allowDowngrade);
|
|
306
307
|
const updateTempDirectory = createRuntimeTempDirectory('update-');
|
|
307
308
|
const tmpFile = path.join(updateTempDirectory, 'candidate.tgz');
|
|
308
309
|
try {
|
package/dist/index.js
CHANGED
|
@@ -454,6 +454,7 @@ export async function main(args) {
|
|
|
454
454
|
.option('-g, --get <key>', 'Get a configuration value')
|
|
455
455
|
.option('-l, --list', 'List all settings')
|
|
456
456
|
.option('-r, --reset', 'Reset to defaults')
|
|
457
|
+
.option('-y, --yes', 'Confirm reset without an interactive prompt')
|
|
457
458
|
.action(async (options) => {
|
|
458
459
|
const configCmd = new ConfigCommand(config, logger);
|
|
459
460
|
await configCmd.run(options);
|
|
@@ -495,9 +496,22 @@ export async function main(args) {
|
|
|
495
496
|
program
|
|
496
497
|
.command('init')
|
|
497
498
|
.description('Initialize Vigthoria in current project')
|
|
498
|
-
.
|
|
499
|
+
.option('--model <model>', 'Default model for non-interactive initialization')
|
|
500
|
+
.option('--ignore-patterns <patterns>', 'Additional comma-separated ignore patterns')
|
|
501
|
+
.option('--auto-apply-fixes', 'Enable automatic fix application in the generated project config')
|
|
502
|
+
.option('--profile <profile>', 'Initialization profile: safe, balanced, or fast')
|
|
503
|
+
.option('-y, --yes', 'Confirm overwrite and run non-interactively')
|
|
504
|
+
.option('--non-interactive', 'Disable prompts and use supplied/profile defaults')
|
|
505
|
+
.action(async (options) => {
|
|
499
506
|
const configCmd = new ConfigCommand(config, logger);
|
|
500
|
-
await configCmd.init(
|
|
507
|
+
await configCmd.init({
|
|
508
|
+
model: options.model,
|
|
509
|
+
ignorePatterns: options.ignorePatterns,
|
|
510
|
+
autoApplyFixes: options.autoApplyFixes,
|
|
511
|
+
profile: options.profile,
|
|
512
|
+
yes: options.yes,
|
|
513
|
+
nonInteractive: options.nonInteractive,
|
|
514
|
+
});
|
|
501
515
|
});
|
|
502
516
|
program
|
|
503
517
|
.command('menu')
|
package/dist/utils/api.js
CHANGED
|
@@ -15,14 +15,15 @@ import { buildSemanticContext } from './context-ranker.js';
|
|
|
15
15
|
import { getChangedFiles } from './workspace-cache.js';
|
|
16
16
|
import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
|
|
17
17
|
import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
|
|
18
|
-
import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
|
|
18
|
+
import { assertValidAgentPlanEvent, isV3StreamKeepaliveEvent } from './v3-stream-events.js';
|
|
19
19
|
import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
|
|
20
20
|
import { isLocalTestfarmMode, fetchWithServiceTimeout } from './localTestMode.js';
|
|
21
21
|
import { buildClientManifest } from './clientManifest.js';
|
|
22
|
-
import { assertTrustedEndpoint, installAxiosNetworkPolicy, installGlobalFetchPolicy } from './network-policy.js';
|
|
22
|
+
import { assertTrustedEndpoint, guardedFetch, installAxiosNetworkPolicy, installGlobalFetchPolicy } from './network-policy.js';
|
|
23
23
|
import { assertSafeRelativePath, resolveWorkspacePath } from './workspace-boundary.js';
|
|
24
24
|
import { parseSupportedProcess } from './process-policy.js';
|
|
25
25
|
import { OptionalPuppeteerScreenshotAdapter } from './preview-screenshot-adapter.js';
|
|
26
|
+
import { normalizeSubscriptionResponse } from './subscription-policy.js';
|
|
26
27
|
import { ModelGovernance } from './model-governance.js';
|
|
27
28
|
import { OperatorClient, OperatorClientError } from './operator-client.js';
|
|
28
29
|
import { CodeOperationsService } from './code-operations-service.js';
|
|
@@ -341,7 +342,7 @@ export class APIClient {
|
|
|
341
342
|
this.mcpContextClient = new McpContextClient({
|
|
342
343
|
getBaseUrls: () => this.getMcpBaseUrls(),
|
|
343
344
|
getAccessToken: () => this.getAccessToken(),
|
|
344
|
-
fetch: (input, init) =>
|
|
345
|
+
fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'mcp' }),
|
|
345
346
|
sanitizeError: sanitizeUserFacingErrorText,
|
|
346
347
|
debug: (message, detail) => this.logger.debug(message, detail),
|
|
347
348
|
});
|
|
@@ -357,7 +358,7 @@ export class APIClient {
|
|
|
357
358
|
return response.data?.v3ServiceKey || null;
|
|
358
359
|
},
|
|
359
360
|
refreshToken: () => this.refreshToken(),
|
|
360
|
-
fetch: (input, init) =>
|
|
361
|
+
fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'v3' }),
|
|
361
362
|
sanitizeError: sanitizeUserFacingErrorText,
|
|
362
363
|
debug: (message) => this.logger.debug(message),
|
|
363
364
|
});
|
|
@@ -412,23 +413,26 @@ export class APIClient {
|
|
|
412
413
|
this.operatorClient = new OperatorClient({
|
|
413
414
|
getBaseUrls: () => this.getOperatorBaseUrls(),
|
|
414
415
|
getAuthToken: () => this.config.get('authToken'),
|
|
415
|
-
fetch: (input, init) =>
|
|
416
|
+
fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'operator' }),
|
|
416
417
|
sanitizeError: sanitizeUserFacingErrorText,
|
|
417
418
|
});
|
|
418
419
|
this.frontendPreviewService = new FrontendPreviewService({
|
|
419
420
|
getBaseUrls: () => this.getTemplateServiceBaseUrls(),
|
|
420
421
|
getAccessToken: () => this.getAccessToken(),
|
|
421
|
-
fetch: (input, init) =>
|
|
422
|
+
fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'template' }),
|
|
422
423
|
resolveTargetPath: (context) => this.resolveAgentTargetPath(context),
|
|
423
424
|
extractExpectedFiles: (message, context) => this.extractExpectedWorkspaceFiles(message, context),
|
|
424
425
|
isAnalysisOnlyTask: (message, context) => this.isAnalysisOnlyTask(message, context),
|
|
425
426
|
sanitizeError: sanitizeUserFacingErrorText,
|
|
426
427
|
screenshotAdapter: dependencies.screenshotAdapter || new OptionalPuppeteerScreenshotAdapter(),
|
|
427
428
|
});
|
|
429
|
+
const vigFlowTransport = axios.create({ timeout: 30_000 });
|
|
430
|
+
installAxiosNetworkPolicy(vigFlowTransport, 'vigflow');
|
|
428
431
|
this.vigFlowClient = new VigFlowClient({
|
|
429
432
|
getBaseUrls: () => this.getVigFlowBaseUrls(),
|
|
430
433
|
getAccessToken: () => this.getAccessToken(),
|
|
431
434
|
debug: (message, detail) => this.logger.debug(message, detail),
|
|
435
|
+
transport: vigFlowTransport,
|
|
432
436
|
});
|
|
433
437
|
this.unsubscribeAuthInvalidation = subscribeAuthInvalidation(() => {
|
|
434
438
|
this.vigFlowClient.clearCredentials();
|
|
@@ -598,12 +602,10 @@ export class APIClient {
|
|
|
598
602
|
async getSubscriptionStatus() {
|
|
599
603
|
try {
|
|
600
604
|
const response = await this.client.get('/api/user/subscription');
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
expiresAt: data.expiresAt || data.expires_at
|
|
606
|
-
});
|
|
605
|
+
const subscription = normalizeSubscriptionResponse(response.data);
|
|
606
|
+
if (!subscription)
|
|
607
|
+
throw new Error('Subscription response did not contain a plan');
|
|
608
|
+
this.config.setSubscription(subscription);
|
|
607
609
|
}
|
|
608
610
|
catch (error) {
|
|
609
611
|
this.logger.debug('Failed to get subscription status:', error.message);
|
|
@@ -708,13 +710,10 @@ export class APIClient {
|
|
|
708
710
|
return this.v3AgentClient.approvalUrl(baseUrl);
|
|
709
711
|
}
|
|
710
712
|
getOperatorBaseUrls() {
|
|
711
|
-
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
712
713
|
const urls = [
|
|
713
714
|
process.env.VIGTHORIA_OPERATOR_URL,
|
|
714
715
|
process.env.OPERATOR_URL,
|
|
715
|
-
'
|
|
716
|
-
configuredModelsApiUrl,
|
|
717
|
-
'https://api.vigthoria.io',
|
|
716
|
+
'https://agent.vigthoria.io',
|
|
718
717
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
719
718
|
return [...new Set(urls)];
|
|
720
719
|
}
|
|
@@ -747,6 +746,7 @@ export class APIClient {
|
|
|
747
746
|
process.env.VIGTHORIA_VIGFLOW_URL,
|
|
748
747
|
process.env.VIGFLOW_URL,
|
|
749
748
|
process.env.WORKFLOW_BUILDER_URL,
|
|
749
|
+
'https://workflow.vigthoria.io',
|
|
750
750
|
`${configuredApiUrl}/api/vigflow`,
|
|
751
751
|
'http://127.0.0.1:5060',
|
|
752
752
|
'http://127.0.0.1:5050',
|
|
@@ -3106,6 +3106,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3106
3106
|
if (explicitEventType && (!event.type || event.type === 'message')) {
|
|
3107
3107
|
event.type = explicitEventType;
|
|
3108
3108
|
}
|
|
3109
|
+
assertValidAgentPlanEvent(event);
|
|
3109
3110
|
const userEvent = this.sanitizeV3AgentEventForUser(event);
|
|
3110
3111
|
events.push(userEvent);
|
|
3111
3112
|
if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
|
|
@@ -3357,12 +3358,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3357
3358
|
// A pre-existing local project is not proof that this run produced
|
|
3358
3359
|
// valid output. A terminal planner/executor error fails proof and
|
|
3359
3360
|
// causes the operation journal to roll back streamed mutations.
|
|
3360
|
-
const
|
|
3361
|
-
|
|
3362
|
-
passed: false
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3361
|
+
const analysisOnly = this.isAnalysisOnlyTask(message, executionContext);
|
|
3362
|
+
const failedPreviewGate = analysisOnly
|
|
3363
|
+
? { required: false, passed: true, skipped: false }
|
|
3364
|
+
: {
|
|
3365
|
+
required: true,
|
|
3366
|
+
passed: false,
|
|
3367
|
+
skipped: true,
|
|
3368
|
+
error: 'Agent execution failed before preview validation.',
|
|
3369
|
+
};
|
|
3366
3370
|
return this.finalizeV3AgentWorkflowResponse(data, {
|
|
3367
3371
|
content: '',
|
|
3368
3372
|
taskId: data.task_id || null,
|
|
@@ -22,7 +22,7 @@ export class ChatPromptPolicy {
|
|
|
22
22
|
isRepoGrounded(prompt) {
|
|
23
23
|
const text = prompt.trim();
|
|
24
24
|
return /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b|\.yaml\b|\.yml\b)/i.test(text)
|
|
25
|
-
|| /\b(file|folder|directory|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(text)
|
|
25
|
+
|| /\b(file|folder|directory|project|workspace|repo|repository|codebase|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(text)
|
|
26
26
|
|| /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(text)
|
|
27
27
|
|| /\b(Camera|InputManager|keydown|KeyS|KeyA|KeyW|stopPropagation|addEventListener|handleKeyDown)\b/.test(text)
|
|
28
28
|
|| /\/[a-zA-Z]/.test(text)
|