vigthoria-cli 1.13.25 → 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/dist/commands/chat.js +10 -1
- package/dist/commands/game.js +5 -1
- package/dist/commands/preview.d.ts +1 -0
- package/dist/commands/preview.js +30 -14
- package/dist/commands/repo.d.ts +35 -2
- package/dist/commands/repo.js +62 -32
- package/dist/commands/update-registration.js +2 -1
- package/dist/utils/api.js +12 -11
- package/dist/utils/network-policy.js +1 -1
- package/dist/utils/preview-screenshot-adapter.d.ts +42 -1
- package/dist/utils/preview-screenshot-adapter.js +65 -13
- package/dist/utils/runtime-temp.d.ts +1 -0
- package/dist/utils/runtime-temp.js +22 -2
- package/dist/utils/v3-stream-events.d.ts +8 -0
- package/dist/utils/v3-stream-events.js +62 -0
- package/package.json +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -2931,15 +2931,24 @@ export class ChatCommand {
|
|
|
2931
2931
|
if (this.streamState.streamingStarted && !this.streamState.streamedAnswerDisplayed) {
|
|
2932
2932
|
this.displayV3StreamedAnswer();
|
|
2933
2933
|
}
|
|
2934
|
-
const
|
|
2934
|
+
const serverPreviewGate = (response.metadata?.previewGate || null);
|
|
2935
2935
|
const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
|
|
2936
2936
|
const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
|
|
2937
2937
|
const requiresWorkspaceChanges = taskRequiresWorkspaceChangesWithContext(contextualPrompt, intentContext);
|
|
2938
|
+
const previewGate = requiresWorkspaceChanges
|
|
2939
|
+
? serverPreviewGate
|
|
2940
|
+
: { required: false, passed: true, skipped: false };
|
|
2938
2941
|
const answerContent = normalizeAgentAnswerContent(response.content, this.streamState.streamedAnswerBuffer);
|
|
2939
2942
|
liveOutcome.changedFileCount = changedFileCount;
|
|
2940
2943
|
liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
|
|
2941
2944
|
liveOutcome.workspaceHasOutput = requiresWorkspaceChanges ? workspaceHasOutput : false;
|
|
2942
2945
|
liveOutcome.answerContent = answerContent;
|
|
2946
|
+
const workflowError = response.metadata?.workflowError;
|
|
2947
|
+
if (workflowError && !liveOutcome.executorError) {
|
|
2948
|
+
liveOutcome.executorError = typeof workflowError === 'string'
|
|
2949
|
+
? workflowError
|
|
2950
|
+
: String(workflowError.message || 'Agent execution failed.');
|
|
2951
|
+
}
|
|
2943
2952
|
const success = previewGate?.required === true
|
|
2944
2953
|
? previewGate?.passed === true && previewGate?.skipped !== true
|
|
2945
2954
|
: true;
|
package/dist/commands/game.js
CHANGED
|
@@ -39,7 +39,11 @@ export function gameProcessInvocation(pm, args, platform = process.platform, com
|
|
|
39
39
|
category: 'usage',
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
// Every token is already restricted to an inert alphabet above. Passing the
|
|
43
|
+
// tokens without embedded quotes is important: on Windows, cmd.exe can hand
|
|
44
|
+
// the quote characters to the .cmd shim as part of argv[0], producing the
|
|
45
|
+
// observed literal `"npm.cmd"` launch failure.
|
|
46
|
+
const command = [executable, ...args].join(' ');
|
|
43
47
|
return { executable: comSpec, args: ['/d', '/s', '/c', command] };
|
|
44
48
|
}
|
|
45
49
|
const run = (pm, args, cwd, timeoutMs = 10 * 60_000) => new Promise((resolve, reject) => { const invocation = gameProcessInvocation(pm, args); const child = spawn(invocation.executable, invocation.args, { cwd, stdio: 'inherit', windowsHide: true, env: safeChildProcessEnv() }); let interrupted = false; const onInterrupt = () => { interrupted = true; child.kill('SIGINT'); }; process.once('SIGINT', onInterrupt); const timer = timeoutMs > 0 ? setTimeout(() => { child.kill('SIGTERM'); reject(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' })); }, timeoutMs) : null; const finish = () => { if (timer)
|
|
@@ -20,6 +20,7 @@ interface PreviewOptions {
|
|
|
20
20
|
proof?: boolean;
|
|
21
21
|
screenshot?: boolean;
|
|
22
22
|
}
|
|
23
|
+
export declare function isPreviewProofRequested(options: Pick<PreviewOptions, 'proof' | 'screenshot'>): boolean;
|
|
23
24
|
export declare class PreviewCommand {
|
|
24
25
|
private logger;
|
|
25
26
|
private api;
|
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
|
/**
|
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;
|
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;
|
|
@@ -596,12 +614,38 @@ export class RepoCommand {
|
|
|
596
614
|
try {
|
|
597
615
|
if (fs.existsSync(outputPath))
|
|
598
616
|
validateExtractedWorkspace(outputPath);
|
|
599
|
-
if (data
|
|
600
|
-
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, {
|
|
601
645
|
headers: publicDownload
|
|
602
646
|
? { 'Content-Type': 'application/json' }
|
|
603
|
-
: this.
|
|
604
|
-
}, { audience:
|
|
647
|
+
: this.getAuthHeaders(route.audience)
|
|
648
|
+
}, { audience: route.audience });
|
|
605
649
|
if (!archiveResponse.ok)
|
|
606
650
|
throw new Error('Failed to download project archive');
|
|
607
651
|
const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
|
|
@@ -638,23 +682,8 @@ export class RepoCommand {
|
|
|
638
682
|
removeRuntimeTempDirectory(stagingRoot);
|
|
639
683
|
}
|
|
640
684
|
}
|
|
641
|
-
else if (data.files) {
|
|
642
|
-
const stagingRoot = createRuntimeTempDirectory('repo-inline-');
|
|
643
|
-
try {
|
|
644
|
-
for (const file of data.files) {
|
|
645
|
-
const filePath = resolveWorkspacePath(stagingRoot, file.path, { allowMissing: true });
|
|
646
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
647
|
-
fs.writeFileSync(filePath, file.content, { mode: 0o600 });
|
|
648
|
-
}
|
|
649
|
-
validateExtractedWorkspace(stagingRoot);
|
|
650
|
-
mergeValidatedWorkspace(stagingRoot, outputPath);
|
|
651
|
-
}
|
|
652
|
-
finally {
|
|
653
|
-
removeRuntimeTempDirectory(stagingRoot);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
685
|
else {
|
|
657
|
-
throw new Error('Repository response contained neither
|
|
686
|
+
throw new Error('Repository response contained neither a complete inline payload nor an archive');
|
|
658
687
|
}
|
|
659
688
|
downloadSpinner.succeed(chalk.green('Project materialized successfully!'));
|
|
660
689
|
console.log(chalk.cyan('\n📁 Project extracted to:'));
|
|
@@ -891,7 +920,8 @@ export class RepoCommand {
|
|
|
891
920
|
const spinner = createSpinner('Deleting project...').start();
|
|
892
921
|
try {
|
|
893
922
|
const operationId = createOperationId();
|
|
894
|
-
const
|
|
923
|
+
const repo = await this.resolveRepoByName(projectName);
|
|
924
|
+
const response = await this.repoFetch(`/api/repo/projects/${encodeURIComponent(String(repo.id))}`, {
|
|
895
925
|
method: 'DELETE',
|
|
896
926
|
headers: { 'X-Vigthoria-Operation-Id': operationId },
|
|
897
927
|
}, { operationId });
|
|
@@ -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/utils/api.js
CHANGED
|
@@ -15,7 +15,7 @@ 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';
|
|
@@ -710,13 +710,10 @@ export class APIClient {
|
|
|
710
710
|
return this.v3AgentClient.approvalUrl(baseUrl);
|
|
711
711
|
}
|
|
712
712
|
getOperatorBaseUrls() {
|
|
713
|
-
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
714
713
|
const urls = [
|
|
715
714
|
process.env.VIGTHORIA_OPERATOR_URL,
|
|
716
715
|
process.env.OPERATOR_URL,
|
|
717
|
-
'
|
|
718
|
-
configuredModelsApiUrl,
|
|
719
|
-
'https://api.vigthoria.io',
|
|
716
|
+
'https://agent.vigthoria.io',
|
|
720
717
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
721
718
|
return [...new Set(urls)];
|
|
722
719
|
}
|
|
@@ -3109,6 +3106,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3109
3106
|
if (explicitEventType && (!event.type || event.type === 'message')) {
|
|
3110
3107
|
event.type = explicitEventType;
|
|
3111
3108
|
}
|
|
3109
|
+
assertValidAgentPlanEvent(event);
|
|
3112
3110
|
const userEvent = this.sanitizeV3AgentEventForUser(event);
|
|
3113
3111
|
events.push(userEvent);
|
|
3114
3112
|
if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
|
|
@@ -3360,12 +3358,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3360
3358
|
// A pre-existing local project is not proof that this run produced
|
|
3361
3359
|
// valid output. A terminal planner/executor error fails proof and
|
|
3362
3360
|
// causes the operation journal to roll back streamed mutations.
|
|
3363
|
-
const
|
|
3364
|
-
|
|
3365
|
-
passed: false
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
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
|
+
};
|
|
3369
3370
|
return this.finalizeV3AgentWorkflowResponse(data, {
|
|
3370
3371
|
content: '',
|
|
3371
3372
|
taskId: data.task_id || null,
|
|
@@ -3,7 +3,7 @@ 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
|
-
operator: new Set(['
|
|
6
|
+
operator: new Set(['agent.vigthoria.io', 'operator.vigthoria.io']),
|
|
7
7
|
hub: new Set(['hub.vigthoria.io']),
|
|
8
8
|
community: new Set(['community.vigthoria.io']),
|
|
9
9
|
music: new Set(['music.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') {
|
|
@@ -22,24 +60,38 @@ export class OptionalPuppeteerScreenshotAdapter {
|
|
|
22
60
|
if (!puppeteer || typeof puppeteer.launch !== 'function') {
|
|
23
61
|
return { captured: false, error: 'required Puppeteer screenshot runtime is not installed' };
|
|
24
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' };
|
|
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;
|
|
@@ -99,6 +99,7 @@ export class RuntimeTempManager {
|
|
|
99
99
|
pid;
|
|
100
100
|
isProcessAlive;
|
|
101
101
|
systemTempRoot;
|
|
102
|
+
sharedTempRoots;
|
|
102
103
|
configuredRoot;
|
|
103
104
|
source;
|
|
104
105
|
maxBytes;
|
|
@@ -121,7 +122,18 @@ export class RuntimeTempManager {
|
|
|
121
122
|
this.now = options.now || Date.now;
|
|
122
123
|
this.pid = options.pid || process.pid;
|
|
123
124
|
this.isProcessAlive = options.isProcessAlive || defaultProcessAlive;
|
|
124
|
-
|
|
125
|
+
const platformTemp = this.platform === 'win32'
|
|
126
|
+
? String(this.environment.TEMP || this.environment.TMP || '').trim()
|
|
127
|
+
: String(this.environment.TMPDIR || '').trim();
|
|
128
|
+
this.systemTempRoot = options.systemTempDirectory || platformTemp || os.tmpdir();
|
|
129
|
+
this.sharedTempRoots = [
|
|
130
|
+
options.systemTempDirectory,
|
|
131
|
+
platformTemp,
|
|
132
|
+
this.environment.TEMP,
|
|
133
|
+
this.environment.TMP,
|
|
134
|
+
this.environment.TMPDIR,
|
|
135
|
+
this.platform === process.platform ? os.tmpdir() : undefined,
|
|
136
|
+
].filter((value) => typeof value === 'string' && value.trim().length > 0);
|
|
125
137
|
const resolved = resolveRuntimeTempRoot({ ...options, environment: this.environment, platform: this.platform, homeDirectory: this.homeDirectory });
|
|
126
138
|
this.configuredRoot = resolved.root;
|
|
127
139
|
this.source = resolved.source;
|
|
@@ -134,6 +146,14 @@ export class RuntimeTempManager {
|
|
|
134
146
|
if (this.initializedRoot || this.initializationError)
|
|
135
147
|
return this.status();
|
|
136
148
|
try {
|
|
149
|
+
// Reject the shared OS temp path before touching the filesystem. This is
|
|
150
|
+
// also what makes cross-platform policy probes deterministic: a Windows
|
|
151
|
+
// policy instance running in a non-Windows test host must not attempt to
|
|
152
|
+
// create a drive-letter path before it can reject `%TEMP%`.
|
|
153
|
+
if (this.sharedTempRoots.some((root) => pathsEqual(this.configuredRoot, root, this.platform))
|
|
154
|
+
|| (this.platform !== 'win32' && isKnownSharedPosixTemp(this.configuredRoot))) {
|
|
155
|
+
throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
|
|
156
|
+
}
|
|
137
157
|
if (this.source === 'per-user-default' && !fs.existsSync(this.homeDirectory)) {
|
|
138
158
|
throw new RuntimeTempError('The user home directory does not exist; managed temporary storage cannot be initialized.', 'TEMP_HOME_UNAVAILABLE');
|
|
139
159
|
}
|
|
@@ -154,7 +174,7 @@ export class RuntimeTempManager {
|
|
|
154
174
|
}
|
|
155
175
|
}
|
|
156
176
|
const pathApi = this.platform === 'win32' ? path.win32 : path.posix;
|
|
157
|
-
if (pathsEqual(realRoot,
|
|
177
|
+
if (this.sharedTempRoots.some((root) => pathsEqual(realRoot, root, this.platform))
|
|
158
178
|
|| (this.platform !== 'win32' && isKnownSharedPosixTemp(realRoot))) {
|
|
159
179
|
throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
|
|
160
180
|
}
|
|
@@ -3,3 +3,11 @@
|
|
|
3
3
|
* connections alive — not substantive work the CLI should surface as progress.
|
|
4
4
|
*/
|
|
5
5
|
export declare function isV3StreamKeepaliveEvent(event: unknown): boolean;
|
|
6
|
+
export declare class AgentPlanContractError extends Error {
|
|
7
|
+
readonly code = "AGENT_PLAN_INVALID";
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
/** Validate a server-authored execution graph before any task can be credited.
|
|
11
|
+
* A cyclic, duplicate, self-referencing, or dangling graph is never runnable
|
|
12
|
+
* and must not reach the UI callback where it could later be counted as 2/2. */
|
|
13
|
+
export declare function assertValidAgentPlanEvent(event: unknown): void;
|
|
@@ -39,3 +39,65 @@ export function isV3StreamKeepaliveEvent(event) {
|
|
|
39
39
|
}
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
42
|
+
export class AgentPlanContractError extends Error {
|
|
43
|
+
code = 'AGENT_PLAN_INVALID';
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = 'AgentPlanContractError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function dependencyId(value) {
|
|
50
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
51
|
+
return String(value).trim();
|
|
52
|
+
if (!value || typeof value !== 'object')
|
|
53
|
+
return '';
|
|
54
|
+
const record = value;
|
|
55
|
+
return String(record.id || record.task_id || record.taskId || '').trim();
|
|
56
|
+
}
|
|
57
|
+
/** Validate a server-authored execution graph before any task can be credited.
|
|
58
|
+
* A cyclic, duplicate, self-referencing, or dangling graph is never runnable
|
|
59
|
+
* and must not reach the UI callback where it could later be counted as 2/2. */
|
|
60
|
+
export function assertValidAgentPlanEvent(event) {
|
|
61
|
+
if (!event || typeof event !== 'object')
|
|
62
|
+
return;
|
|
63
|
+
const record = event;
|
|
64
|
+
if (record.type !== 'plan' || !Array.isArray(record.plan?.tasks))
|
|
65
|
+
return;
|
|
66
|
+
const tasks = record.plan.tasks;
|
|
67
|
+
const ids = tasks.map((task, index) => {
|
|
68
|
+
const id = dependencyId(task.id ?? task.task_id ?? task.taskId);
|
|
69
|
+
if (!id)
|
|
70
|
+
throw new AgentPlanContractError(`Agent plan task at index ${index} has no stable ID.`);
|
|
71
|
+
return id;
|
|
72
|
+
});
|
|
73
|
+
if (new Set(ids).size !== ids.length)
|
|
74
|
+
throw new AgentPlanContractError('Agent plan contains duplicate task IDs.');
|
|
75
|
+
const idSet = new Set(ids);
|
|
76
|
+
const graph = new Map();
|
|
77
|
+
tasks.forEach((task, index) => {
|
|
78
|
+
const raw = task.depends_on ?? task.dependsOn ?? task.dependencies ?? [];
|
|
79
|
+
const dependencies = (Array.isArray(raw) ? raw : [raw]).map(dependencyId).filter(Boolean);
|
|
80
|
+
for (const dependency of dependencies) {
|
|
81
|
+
if (dependency === ids[index])
|
|
82
|
+
throw new AgentPlanContractError(`Agent plan task ${ids[index]} depends on itself.`);
|
|
83
|
+
if (!idSet.has(dependency))
|
|
84
|
+
throw new AgentPlanContractError(`Agent plan task ${ids[index]} depends on missing task ${dependency}.`);
|
|
85
|
+
}
|
|
86
|
+
graph.set(ids[index], dependencies);
|
|
87
|
+
});
|
|
88
|
+
const visiting = new Set();
|
|
89
|
+
const visited = new Set();
|
|
90
|
+
const visit = (id, trail) => {
|
|
91
|
+
if (visiting.has(id))
|
|
92
|
+
throw new AgentPlanContractError(`Agent plan contains a dependency cycle: ${[...trail, id].join(' -> ')}.`);
|
|
93
|
+
if (visited.has(id))
|
|
94
|
+
return;
|
|
95
|
+
visiting.add(id);
|
|
96
|
+
for (const dependency of graph.get(id) || [])
|
|
97
|
+
visit(dependency, [...trail, id]);
|
|
98
|
+
visiting.delete(id);
|
|
99
|
+
visited.add(id);
|
|
100
|
+
};
|
|
101
|
+
for (const id of ids)
|
|
102
|
+
visit(id, []);
|
|
103
|
+
}
|