vigthoria-cli 1.13.25 → 1.13.29

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.
Files changed (54) hide show
  1. package/dist/commands/chat.js +83 -27
  2. package/dist/commands/config.js +4 -4
  3. package/dist/commands/fork.d.ts +3 -2
  4. package/dist/commands/fork.js +124 -123
  5. package/dist/commands/game.d.ts +8 -0
  6. package/dist/commands/game.js +118 -10
  7. package/dist/commands/history.d.ts +0 -1
  8. package/dist/commands/history.js +8 -22
  9. package/dist/commands/hub.d.ts +20 -0
  10. package/dist/commands/hub.js +17 -3
  11. package/dist/commands/preview.d.ts +1 -0
  12. package/dist/commands/preview.js +36 -15
  13. package/dist/commands/product-run-registration.js +1 -1
  14. package/dist/commands/replay.d.ts +0 -1
  15. package/dist/commands/replay.js +10 -19
  16. package/dist/commands/repo.d.ts +35 -2
  17. package/dist/commands/repo.js +76 -34
  18. package/dist/commands/update-registration.js +4 -3
  19. package/dist/commands/workflow.d.ts +4 -0
  20. package/dist/commands/workflow.js +27 -0
  21. package/dist/index.js +6 -4
  22. package/dist/utils/agentRunOutcome.d.ts +7 -0
  23. package/dist/utils/agentRunOutcome.js +13 -0
  24. package/dist/utils/api.d.ts +20 -5
  25. package/dist/utils/api.js +440 -54
  26. package/dist/utils/command-policy.js +1 -1
  27. package/dist/utils/config.d.ts +2 -0
  28. package/dist/utils/config.js +8 -3
  29. package/dist/utils/frontend-preview-service.d.ts +1 -0
  30. package/dist/utils/frontend-preview-service.js +54 -5
  31. package/dist/utils/model-governance.js +23 -14
  32. package/dist/utils/model-transport-service.js +1 -1
  33. package/dist/utils/network-policy.js +16 -4
  34. package/dist/utils/operator-client.js +23 -4
  35. package/dist/utils/post-write-validator.js +7 -3
  36. package/dist/utils/preview-screenshot-adapter.d.ts +19 -3
  37. package/dist/utils/preview-screenshot-adapter.js +289 -28
  38. package/dist/utils/runtime-capability.d.ts +7 -0
  39. package/dist/utils/runtime-capability.js +11 -0
  40. package/dist/utils/runtime-temp.d.ts +6 -2
  41. package/dist/utils/runtime-temp.js +143 -27
  42. package/dist/utils/tools.js +1 -1
  43. package/dist/utils/v3-stream-events.d.ts +8 -0
  44. package/dist/utils/v3-stream-events.js +70 -0
  45. package/dist/utils/v3-workspace-service.d.ts +1 -0
  46. package/dist/utils/v3-workspace-service.js +38 -1
  47. package/dist/utils/vigflow-client.d.ts +9 -0
  48. package/dist/utils/vigflow-client.js +48 -2
  49. package/dist/utils/workspace-reference.d.ts +8 -0
  50. package/dist/utils/workspace-reference.js +21 -0
  51. package/package.json +4 -6
  52. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +2 -2
  53. package/scripts/release/validate-live-service-gates.sh +3 -3
  54. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -1,51 +1,312 @@
1
+ import { spawn } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
1
4
  import { pathToFileURL } from 'node:url';
2
- import { redactSensitiveText } from './secret-policy.js';
5
+ import { redactSensitiveText, safeChildProcessEnv } from './secret-policy.js';
3
6
  import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from './runtime-temp.js';
4
- export class OptionalPuppeteerScreenshotAdapter {
5
- loadPuppeteer;
7
+ const SCREENSHOT_WIDTH = 800;
8
+ const SCREENSHOT_HEIGHT = 600;
9
+ const SCREENSHOT_MAX_BYTES = 10 * 1024 * 1024;
10
+ const BROWSER_TIMEOUT_MS = 30_000;
11
+ // Chromium creates short-lived files below TMPDIR while starting. Keep enough
12
+ // room below the 108-byte POSIX sockaddr_un ceiling without rejecting normal
13
+ // per-user Vigthoria roots (for example ~/.vigthoria/tmp/browser-*/scratch).
14
+ const POSIX_BROWSER_SCRATCH_MAX_BYTES = 76;
15
+ export function resolvePreviewBrowserExecutable(environment = process.env, platform = process.platform, exists = fs.existsSync) {
16
+ const candidates = platform === 'win32'
17
+ ? [
18
+ ...[environment['PROGRAMFILES(X86)'], environment.ProgramFiles, environment.LOCALAPPDATA]
19
+ .filter((value) => typeof value === 'string' && value.length > 0)
20
+ .flatMap((base) => [
21
+ path.win32.join(base, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
22
+ path.win32.join(base, 'Google', 'Chrome', 'Application', 'chrome.exe'),
23
+ ]),
24
+ ]
25
+ : platform === 'darwin'
26
+ ? [
27
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
28
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
29
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
30
+ ]
31
+ : [
32
+ '/usr/bin/google-chrome',
33
+ '/usr/bin/google-chrome-stable',
34
+ '/usr/bin/microsoft-edge',
35
+ '/usr/bin/microsoft-edge-stable',
36
+ '/usr/bin/chromium',
37
+ '/usr/bin/chromium-browser',
38
+ '/snap/bin/chromium',
39
+ ];
40
+ for (const candidate of candidates) {
41
+ if (exists(candidate))
42
+ return { executablePath: candidate, source: 'system' };
43
+ }
44
+ return null;
45
+ }
46
+ async function terminateBrowserProcess(child, platform) {
47
+ const pid = child.pid;
48
+ if (!pid)
49
+ return;
50
+ if (platform === 'win32') {
51
+ const taskkill = path.win32.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'taskkill.exe');
52
+ await new Promise((resolve) => {
53
+ const killer = spawn(taskkill, ['/PID', String(pid), '/T', '/F'], {
54
+ stdio: 'ignore', windowsHide: true, env: safeChildProcessEnv(),
55
+ });
56
+ const timer = setTimeout(() => {
57
+ try {
58
+ killer.kill();
59
+ }
60
+ catch { /* helper already exited */ }
61
+ try {
62
+ child.kill();
63
+ }
64
+ catch { /* exact child already exited */ }
65
+ resolve();
66
+ }, 5_000);
67
+ killer.once('error', () => { clearTimeout(timer); try {
68
+ child.kill();
69
+ }
70
+ catch { /* exact child only */ } resolve(); });
71
+ killer.once('exit', () => { clearTimeout(timer); resolve(); });
72
+ });
73
+ return;
74
+ }
75
+ try {
76
+ process.kill(-pid, 'SIGTERM');
77
+ }
78
+ catch {
79
+ try {
80
+ child.kill('SIGTERM');
81
+ }
82
+ catch { /* already gone */ }
83
+ }
84
+ await new Promise((resolve) => setTimeout(resolve, 200));
85
+ try {
86
+ process.kill(-pid, 0);
87
+ process.kill(-pid, 'SIGKILL');
88
+ }
89
+ catch { /* group exited */ }
90
+ }
91
+ export const runPreviewBrowser = (executablePath, args, platform, environment = process.env) => new Promise((resolve, reject) => {
92
+ const child = spawn(executablePath, [...args], {
93
+ stdio: 'ignore',
94
+ windowsHide: true,
95
+ env: safeChildProcessEnv(environment),
96
+ detached: platform !== 'win32',
97
+ });
98
+ let settled = false;
99
+ const finish = (error) => {
100
+ if (settled)
101
+ return;
102
+ settled = true;
103
+ clearTimeout(timer);
104
+ error ? reject(error) : resolve();
105
+ };
106
+ const timer = setTimeout(() => {
107
+ void terminateBrowserProcess(child, platform).finally(() => finish(new Error(`browser screenshot timed out after ${BROWSER_TIMEOUT_MS}ms`)));
108
+ }, BROWSER_TIMEOUT_MS);
109
+ child.once('error', (error) => finish(error));
110
+ child.once('exit', (code, signal) => {
111
+ if (code === 0)
112
+ finish();
113
+ else
114
+ finish(new Error(`browser screenshot process exited with ${signal || code}`));
115
+ });
116
+ });
117
+ function pngCrc32(data) {
118
+ let crc = 0xffffffff;
119
+ for (const byte of data) {
120
+ crc ^= byte;
121
+ for (let bit = 0; bit < 8; bit += 1) {
122
+ crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
123
+ }
124
+ }
125
+ return (crc ^ 0xffffffff) >>> 0;
126
+ }
127
+ function validatePng(filePath) {
128
+ const stat = fs.statSync(filePath);
129
+ if (!stat.isFile() || stat.size < 57 || stat.size > SCREENSHOT_MAX_BYTES) {
130
+ throw new Error('browser did not produce a bounded PNG screenshot');
131
+ }
132
+ const bytes = fs.readFileSync(filePath);
133
+ if (!bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
134
+ throw new Error('browser screenshot output is not a PNG file');
135
+ }
136
+ let offset = 8;
137
+ let chunks = 0;
138
+ let sawHeader = false;
139
+ let sawImageData = false;
140
+ let sawEnd = false;
141
+ while (offset + 12 <= bytes.length && chunks < 4096) {
142
+ const length = bytes.readUInt32BE(offset);
143
+ const chunkEnd = offset + 12 + length;
144
+ if (chunkEnd > bytes.length)
145
+ throw new Error('browser screenshot PNG contains a truncated chunk');
146
+ const typeBytes = bytes.subarray(offset + 4, offset + 8);
147
+ const type = typeBytes.toString('ascii');
148
+ const data = bytes.subarray(offset + 8, offset + 8 + length);
149
+ const expectedCrc = bytes.readUInt32BE(offset + 8 + length);
150
+ if (pngCrc32(Buffer.concat([typeBytes, data])) !== expectedCrc) {
151
+ throw new Error('browser screenshot PNG contains an invalid chunk checksum');
152
+ }
153
+ chunks += 1;
154
+ if (chunks === 1 && (type !== 'IHDR' || length !== 13)) {
155
+ throw new Error('browser screenshot PNG has no valid leading IHDR chunk');
156
+ }
157
+ if (type === 'IHDR') {
158
+ if (sawHeader || length !== 13)
159
+ throw new Error('browser screenshot PNG has an invalid IHDR chunk');
160
+ sawHeader = true;
161
+ const width = data.readUInt32BE(0);
162
+ const height = data.readUInt32BE(4);
163
+ if (width !== SCREENSHOT_WIDTH || height !== SCREENSHOT_HEIGHT) {
164
+ throw new Error(`browser screenshot dimensions were ${width}x${height}; expected ${SCREENSHOT_WIDTH}x${SCREENSHOT_HEIGHT}`);
165
+ }
166
+ }
167
+ else if (type === 'IDAT') {
168
+ if (!sawHeader || length === 0)
169
+ throw new Error('browser screenshot PNG has invalid image data');
170
+ sawImageData = true;
171
+ }
172
+ else if (type === 'IEND') {
173
+ if (length !== 0 || !sawImageData)
174
+ throw new Error('browser screenshot PNG has an invalid IEND chunk');
175
+ sawEnd = true;
176
+ offset = chunkEnd;
177
+ break;
178
+ }
179
+ offset = chunkEnd;
180
+ }
181
+ if (!sawHeader || !sawImageData || !sawEnd || offset !== bytes.length) {
182
+ throw new Error('browser screenshot PNG is incomplete or contains trailing data');
183
+ }
184
+ }
185
+ function isTransientBrowserFailure(result) {
186
+ if (result.captured || !result.error || /profile cleanup failed/i.test(result.error))
187
+ return false;
188
+ return /(?:process exited with (?:SIG[A-Z]+|[1-9][0-9]*)|timed out|\bE(?:ACCES|PERM|BUSY|AGAIN)\b|resource temporarily unavailable)/i.test(result.error);
189
+ }
190
+ /** Dependency-free system-browser adapter. No browser download or archive
191
+ * extractor is shipped, and only fixed operating-system installation paths
192
+ * are eligible.
193
+ */
194
+ export class SystemBrowserScreenshotAdapter {
6
195
  environment;
7
196
  allocateTemp;
8
197
  releaseTemp;
9
- constructor(loadPuppeteer = () => import('puppeteer'), environment = process.env, allocateTemp = createRuntimeTempDirectory, releaseTemp = removeRuntimeTempDirectory) {
10
- this.loadPuppeteer = loadPuppeteer;
198
+ platform;
199
+ exists;
200
+ runner;
201
+ constructor(environment = process.env, allocateTemp = createRuntimeTempDirectory, releaseTemp = removeRuntimeTempDirectory, platform = process.platform, exists = fs.existsSync, runner = runPreviewBrowser) {
11
202
  this.environment = environment;
12
203
  this.allocateTemp = allocateTemp;
13
204
  this.releaseTemp = releaseTemp;
205
+ this.platform = platform;
206
+ this.exists = exists;
207
+ this.runner = runner;
14
208
  }
15
209
  async capture(entryAbsolutePath, screenshotPath) {
16
210
  if (this.environment.VIGTHORIA_DISABLE_PREVIEW_SCREENSHOT === '1') {
17
211
  return { captured: false, error: 'screenshot capture explicitly unavailable' };
18
212
  }
213
+ const resolution = resolvePreviewBrowserExecutable(this.environment, this.platform, this.exists);
214
+ if (!resolution) {
215
+ return { captured: false, error: 'required system browser is unavailable; install Microsoft Edge, Chrome, or Chromium' };
216
+ }
217
+ // Headless Chromium-family processes can fail transiently while a previous
218
+ // profile/process is releasing OS resources. Screenshot generation is a
219
+ // local idempotent operation, so one fresh-profile retry is safe. Invalid
220
+ // proof bytes and cleanup failures are never retried or credited.
221
+ let lastResult = { captured: false, error: 'browser screenshot was not attempted' };
222
+ for (let attempt = 0; attempt < 2; attempt += 1) {
223
+ lastResult = await this.captureOnce(resolution, entryAbsolutePath, screenshotPath);
224
+ if (lastResult.captured || !isTransientBrowserFailure(lastResult))
225
+ return lastResult;
226
+ await new Promise((resolve) => setTimeout(resolve, 500));
227
+ }
228
+ return lastResult;
229
+ }
230
+ async captureOnce(resolution, entryAbsolutePath, screenshotPath) {
231
+ let browserProfile = null;
232
+ let result;
19
233
  try {
20
- const loaded = await this.loadPuppeteer().catch(() => null);
21
- const puppeteer = (loaded?.default || loaded);
22
- if (!puppeteer || typeof puppeteer.launch !== 'function') {
23
- return { captured: false, error: 'required Puppeteer screenshot runtime is not installed' };
234
+ browserProfile = this.allocateTemp('browser-', 128 * 1024 * 1024);
235
+ const browserUserData = path.join(browserProfile, 'profile');
236
+ const browserScratch = path.join(browserProfile, 'scratch');
237
+ if (this.platform !== 'win32' && Buffer.byteLength(browserScratch) > POSIX_BROWSER_SCRATCH_MAX_BYTES) {
238
+ throw new Error('managed browser temporary path is too long; configure a shorter dedicated VIGTHORIA_TEMP_DIR');
24
239
  }
25
- const browserProfile = this.allocateTemp('browser-');
240
+ fs.mkdirSync(browserUserData, { recursive: false, mode: 0o700 });
241
+ fs.mkdirSync(browserScratch, { recursive: false, mode: 0o700 });
242
+ fs.mkdirSync(path.dirname(screenshotPath), { recursive: true, mode: 0o700 });
26
243
  try {
27
- const browser = await puppeteer.launch({
28
- headless: 'shell',
29
- args: ['--no-sandbox', '--disable-setuid-sandbox'],
30
- userDataDir: browserProfile,
31
- });
32
- try {
33
- const page = await browser.newPage();
34
- await page.setViewport({ width: 1440, height: 960, deviceScaleFactor: 1 });
35
- await page.goto(pathToFileURL(entryAbsolutePath).toString(), { waitUntil: 'networkidle0', timeout: 20_000 });
36
- await page.screenshot({ path: screenshotPath, fullPage: true });
37
- }
38
- finally {
39
- await browser.close();
40
- }
244
+ fs.unlinkSync(screenshotPath);
41
245
  }
42
- finally {
43
- this.releaseTemp(browserProfile);
246
+ catch (error) {
247
+ if (error?.code !== 'ENOENT')
248
+ throw error;
44
249
  }
45
- return { captured: true };
250
+ const args = [
251
+ '--headless=new',
252
+ '--disable-gpu',
253
+ '--disable-dev-shm-usage',
254
+ '--hide-scrollbars',
255
+ '--no-first-run',
256
+ '--no-default-browser-check',
257
+ '--disable-background-networking',
258
+ '--disable-component-update',
259
+ '--disable-sync',
260
+ '--metrics-recording-only',
261
+ '--proxy-server=http://127.0.0.1:9',
262
+ '--proxy-bypass-list=<-loopback>',
263
+ `--user-data-dir=${browserUserData}`,
264
+ `--window-size=${SCREENSHOT_WIDTH},${SCREENSHOT_HEIGHT}`,
265
+ `--screenshot=${screenshotPath}`,
266
+ '--allow-file-access-from-files',
267
+ pathToFileURL(entryAbsolutePath).toString(),
268
+ ];
269
+ const browserEnvironment = safeChildProcessEnv(this.environment, {
270
+ TMPDIR: browserScratch,
271
+ TMP: browserScratch,
272
+ TEMP: browserScratch,
273
+ });
274
+ await this.runner(resolution.executablePath, args, this.platform, browserEnvironment);
275
+ validatePng(screenshotPath);
276
+ result = { captured: true };
46
277
  }
47
278
  catch (error) {
48
- return { captured: false, error: redactSensitiveText(error instanceof Error ? error.message : String(error)) };
279
+ try {
280
+ fs.unlinkSync(screenshotPath);
281
+ }
282
+ catch { /* no failed proof artifact */ }
283
+ result = { captured: false, error: redactSensitiveText(error instanceof Error ? error.message : String(error)) };
284
+ }
285
+ finally {
286
+ if (browserProfile) {
287
+ try {
288
+ this.releaseTemp(browserProfile);
289
+ }
290
+ catch {
291
+ await new Promise((resolve) => setTimeout(resolve, 150));
292
+ try {
293
+ this.releaseTemp(browserProfile);
294
+ }
295
+ catch (cleanupError) {
296
+ try {
297
+ fs.unlinkSync(screenshotPath);
298
+ }
299
+ catch { /* proof is invalid when cleanup is incomplete */ }
300
+ result = {
301
+ captured: false,
302
+ error: redactSensitiveText(`browser profile cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`),
303
+ };
304
+ }
305
+ }
306
+ }
49
307
  }
308
+ return result;
50
309
  }
51
310
  }
311
+ /** @deprecated Internal compatibility alias retained for older deep imports. */
312
+ export { SystemBrowserScreenshotAdapter as OptionalPuppeteerScreenshotAdapter };
@@ -6,3 +6,10 @@ export type RuntimeCapabilities = {
6
6
  export declare function resolveRuntimeCapabilities(environment?: NodeJS.ProcessEnv): RuntimeCapabilities;
7
7
  export declare function hasLoopbackServiceCapability(environment?: NodeJS.ProcessEnv): boolean;
8
8
  export declare function hasLocalV3AgentCapability(environment?: NodeJS.ProcessEnv): boolean;
9
+ /**
10
+ * An internal V3 service key may replace a user session only for an Agent
11
+ * request deliberately pinned to the local V3 service. Requiring both grants
12
+ * prevents a key present on an ecosystem host from bypassing unrelated
13
+ * user-session policies.
14
+ */
15
+ export declare function hasLocalV3ServiceIdentity(environment?: NodeJS.ProcessEnv): boolean;
@@ -14,3 +14,14 @@ export function hasLoopbackServiceCapability(environment = process.env) {
14
14
  export function hasLocalV3AgentCapability(environment = process.env) {
15
15
  return resolveRuntimeCapabilities(environment).localV3Agent;
16
16
  }
17
+ /**
18
+ * An internal V3 service key may replace a user session only for an Agent
19
+ * request deliberately pinned to the local V3 service. Requiring both grants
20
+ * prevents a key present on an ecosystem host from bypassing unrelated
21
+ * user-session policies.
22
+ */
23
+ export function hasLocalV3ServiceIdentity(environment = process.env) {
24
+ return hasLocalV3AgentCapability(environment) && Boolean(environment.VIGTHORIA_V3_SERVICE_KEY
25
+ || environment.V3_SERVICE_KEY
26
+ || environment.HYPERLOOP_SERVICE_KEY);
27
+ }
@@ -44,10 +44,12 @@ 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;
50
51
  private readonly minimumFreeBytes;
52
+ private readonly allocationMaxBytes;
51
53
  private readonly ttlMs;
52
54
  private initializedRoot;
53
55
  private initializedIdentity;
@@ -55,7 +57,7 @@ export declare class RuntimeTempManager {
55
57
  private lastCleanup;
56
58
  constructor(options?: RuntimeTempOptions);
57
59
  initialize(): RuntimeTempStatus;
58
- createDirectory(prefix?: string): string;
60
+ createDirectory(prefix?: string, requestedMaxBytes?: number): string;
59
61
  removeDirectory(directory: string): void;
60
62
  scavenge(options?: {
61
63
  includeLegacy?: boolean;
@@ -67,10 +69,12 @@ export declare class RuntimeTempManager {
67
69
  private freeBytes;
68
70
  private measure;
69
71
  private removeEntry;
72
+ private acquireLock;
73
+ private releaseLock;
70
74
  private cleanupLegacySystemTemp;
71
75
  }
72
76
  export declare function getRuntimeTempManager(): RuntimeTempManager;
73
77
  export declare function initializeRuntimeTempStorage(): RuntimeTempStatus;
74
78
  export declare function runtimeTempStatus(): RuntimeTempStatus;
75
- export declare function createRuntimeTempDirectory(prefix: string): string;
79
+ export declare function createRuntimeTempDirectory(prefix: string, requestedMaxBytes?: number): string;
76
80
  export declare function removeRuntimeTempDirectory(directory: string): void;