vigthoria-cli 1.13.22 → 1.13.23
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/doctor-registration.js +8 -1
- package/dist/commands/repo.js +6 -9
- package/dist/commands/update-registration.js +36 -6
- package/dist/index.js +2 -0
- package/dist/utils/preview-screenshot-adapter.d.ts +3 -1
- package/dist/utils/preview-screenshot-adapter.js +22 -7
- package/dist/utils/runtime-temp.d.ts +76 -0
- package/dist/utils/runtime-temp.js +492 -0
- package/dist/utils/tools.js +10 -4
- package/install.ps1 +32 -2
- package/install.sh +37 -1
- package/package.json +1 -1
- package/scripts/release/test-balanced-model-live.sh +4 -1
- package/scripts/release/validate-live-service-gates.sh +4 -1
- package/scripts/release/validate-no-go-gates.sh +11 -1
|
@@ -9,6 +9,7 @@ import { createAPIClient } from '../utils/api-client-factory.js';
|
|
|
9
9
|
import { sanitizeUserFacingErrorText } from '../utils/api.js';
|
|
10
10
|
import { getCliStateFile, isOfflineMode, isUpdateCheckSuppressed } from '../utils/cli-state.js';
|
|
11
11
|
import { CliCommandError, formatSuccessJson } from '../utils/command-contract.js';
|
|
12
|
+
import { runtimeTempStatus } from '../utils/runtime-temp.js';
|
|
12
13
|
export function registerDoctorCommand(program, config, logger, version) {
|
|
13
14
|
program
|
|
14
15
|
.command('doctor')
|
|
@@ -76,6 +77,7 @@ export function registerDoctorCommand(program, config, logger, version) {
|
|
|
76
77
|
const envToken = String(process.env.VIGTHORIA_AUTH_TOKEN || process.env.VIGTHORIA_TOKEN || '').trim();
|
|
77
78
|
const configuredToken = envToken || String(config.get('authToken') || '').trim();
|
|
78
79
|
const tokenSegments = configuredToken ? configuredToken.split('.').length : 0;
|
|
80
|
+
const tempStorage = runtimeTempStatus();
|
|
79
81
|
const report = {
|
|
80
82
|
cliVersion: version,
|
|
81
83
|
nodeVersion: process.version,
|
|
@@ -102,6 +104,7 @@ export function registerDoctorCommand(program, config, logger, version) {
|
|
|
102
104
|
writable: sessionStorageWritable,
|
|
103
105
|
latestCheckpoint,
|
|
104
106
|
},
|
|
107
|
+
tempStorage,
|
|
105
108
|
subscriptionPlan: subscription.plan || null,
|
|
106
109
|
subscriptionStatus: subscription.status || null,
|
|
107
110
|
offlineMode: offline,
|
|
@@ -180,7 +183,11 @@ export function registerDoctorCommand(program, config, logger, version) {
|
|
|
180
183
|
console.log(chalk.gray('\nTip: pass --check-api to verify API reachability.'));
|
|
181
184
|
}
|
|
182
185
|
};
|
|
183
|
-
const requiredLocalChecksPassed = nodeCompatible
|
|
186
|
+
const requiredLocalChecksPassed = nodeCompatible
|
|
187
|
+
&& missingRuntimePackages.length === 0
|
|
188
|
+
&& sessionStorageWritable
|
|
189
|
+
&& tempStorage.writable
|
|
190
|
+
&& !tempStorage.sharedSystemTemp;
|
|
184
191
|
const coderReachable = !options.checkApi || offline || report.apiHealth === 'online';
|
|
185
192
|
if (!requiredLocalChecksPassed || !coderReachable) {
|
|
186
193
|
if (!options.json)
|
package/dist/commands/repo.js
CHANGED
|
@@ -15,14 +15,13 @@
|
|
|
15
15
|
import chalk from 'chalk';
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
|
-
import { createRequire } from 'node:module';
|
|
19
18
|
import { guardedFetch } from '../utils/network-policy.js';
|
|
20
19
|
import { createSpinner, CH } from '../utils/logger.js';
|
|
21
20
|
import { createOperationId } from '../utils/mutation-journal.js';
|
|
22
21
|
import { containsHighConfidenceSecret, isSensitivePath, safeChildProcessEnv } from '../utils/secret-policy.js';
|
|
23
22
|
import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
|
|
23
|
+
import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from '../utils/runtime-temp.js';
|
|
24
24
|
import { assertSafeDestinationPath, assertSafeRelativePath, inspectZipArchive, mergeValidatedWorkspace, resolveWorkspacePath, validateExtractedWorkspace, } from '../utils/workspace-boundary.js';
|
|
25
|
-
const require = createRequire(import.meta.url);
|
|
26
25
|
import inquirer from 'inquirer';
|
|
27
26
|
const MAX_REPOSITORY_CONTENT_BYTES = 100 * 1024 * 1024;
|
|
28
27
|
const MAX_REPOSITORY_REQUEST_BYTES = 120 * 1024 * 1024;
|
|
@@ -570,14 +569,13 @@ export class RepoCommand {
|
|
|
570
569
|
throw new Error('Failed to download project archive');
|
|
571
570
|
const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
|
|
572
571
|
inspectZipArchive(archiveBuffer);
|
|
573
|
-
const
|
|
574
|
-
const stagingRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vigthoria-pull-'));
|
|
572
|
+
const stagingRoot = createRuntimeTempDirectory('repo-pull-');
|
|
575
573
|
const tempArchive = path.join(stagingRoot, 'archive.zip');
|
|
576
574
|
const extractedPath = path.join(stagingRoot, 'extracted');
|
|
577
575
|
try {
|
|
578
576
|
fs.mkdirSync(extractedPath, { mode: 0o700 });
|
|
579
577
|
fs.writeFileSync(tempArchive, archiveBuffer, { mode: 0o600 });
|
|
580
|
-
if (
|
|
578
|
+
if (process.platform === 'win32') {
|
|
581
579
|
const { execFileSync } = await import('child_process');
|
|
582
580
|
execFileSync('powershell', [
|
|
583
581
|
'-NoProfile', '-NonInteractive', '-Command',
|
|
@@ -600,12 +598,11 @@ export class RepoCommand {
|
|
|
600
598
|
mergeValidatedWorkspace(extractedPath, outputPath);
|
|
601
599
|
}
|
|
602
600
|
finally {
|
|
603
|
-
|
|
601
|
+
removeRuntimeTempDirectory(stagingRoot);
|
|
604
602
|
}
|
|
605
603
|
}
|
|
606
604
|
else if (data.files) {
|
|
607
|
-
const
|
|
608
|
-
const stagingRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vigthoria-pull-inline-'));
|
|
605
|
+
const stagingRoot = createRuntimeTempDirectory('repo-inline-');
|
|
609
606
|
try {
|
|
610
607
|
for (const file of data.files) {
|
|
611
608
|
const filePath = resolveWorkspacePath(stagingRoot, file.path, { allowMissing: true });
|
|
@@ -616,7 +613,7 @@ export class RepoCommand {
|
|
|
616
613
|
mergeValidatedWorkspace(stagingRoot, outputPath);
|
|
617
614
|
}
|
|
618
615
|
finally {
|
|
619
|
-
|
|
616
|
+
removeRuntimeTempDirectory(stagingRoot);
|
|
620
617
|
}
|
|
621
618
|
}
|
|
622
619
|
else {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { execFileSync as execFileSyncProcess } from 'node:child_process';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
-
import * as os from 'node:os';
|
|
5
4
|
import * as path from 'node:path';
|
|
6
5
|
import axios from 'axios';
|
|
7
6
|
import chalk from 'chalk';
|
|
@@ -13,6 +12,7 @@ import { CliCommandError, commandFailure } from '../utils/command-contract.js';
|
|
|
13
12
|
import { compareSemanticVersions as compareVersions, resolveUpdateSource } from '../utils/update-policy.js';
|
|
14
13
|
import { assertReleaseTransition, assertReleaseUrl, getReleasePolicy, parseReleaseManifestText, verifyReleaseSignature } from '../utils/release-policy.js';
|
|
15
14
|
import { installReleaseTransaction } from '../utils/release-install.js';
|
|
15
|
+
import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from '../utils/runtime-temp.js';
|
|
16
16
|
function maxVersion(...versions) {
|
|
17
17
|
let best = null;
|
|
18
18
|
for (const version of versions) {
|
|
@@ -42,6 +42,34 @@ async function fetchNpmLatestVersion() {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
const VIGTHORIA_DEFAULT_MANIFEST_URL = getReleasePolicy().origins.manifest;
|
|
45
|
+
const MAX_RELEASE_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
46
|
+
async function readBoundedResponse(response, maximumBytes) {
|
|
47
|
+
const declaredLength = Number(response.headers.get('content-length') || 0);
|
|
48
|
+
if (declaredLength > maximumBytes)
|
|
49
|
+
throw new Error(`Release archive exceeds the ${maximumBytes}-byte download limit.`);
|
|
50
|
+
if (!response.body)
|
|
51
|
+
return Buffer.alloc(0);
|
|
52
|
+
const reader = response.body.getReader();
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let total = 0;
|
|
55
|
+
try {
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done)
|
|
59
|
+
break;
|
|
60
|
+
total += value.byteLength;
|
|
61
|
+
if (total > maximumBytes) {
|
|
62
|
+
await reader.cancel('release archive size limit exceeded');
|
|
63
|
+
throw new Error(`Release archive exceeds the ${maximumBytes}-byte download limit.`);
|
|
64
|
+
}
|
|
65
|
+
chunks.push(Buffer.from(value));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
}
|
|
71
|
+
return Buffer.concat(chunks, total);
|
|
72
|
+
}
|
|
45
73
|
async function downloadFile(url, targetPath, explicitLocal = false) {
|
|
46
74
|
let bytes;
|
|
47
75
|
if (explicitLocal) {
|
|
@@ -49,6 +77,8 @@ async function downloadFile(url, targetPath, explicitLocal = false) {
|
|
|
49
77
|
responseType: 'arraybuffer',
|
|
50
78
|
timeout: 20000,
|
|
51
79
|
maxRedirects: 0,
|
|
80
|
+
maxContentLength: MAX_RELEASE_ARCHIVE_BYTES,
|
|
81
|
+
maxBodyLength: MAX_RELEASE_ARCHIVE_BYTES,
|
|
52
82
|
validateStatus: (status) => status >= 200 && status < 300,
|
|
53
83
|
});
|
|
54
84
|
bytes = Buffer.from(response.data);
|
|
@@ -57,7 +87,7 @@ async function downloadFile(url, targetPath, explicitLocal = false) {
|
|
|
57
87
|
const response = await guardedFetch(url, {}, { audience: 'release' });
|
|
58
88
|
if (!response.ok)
|
|
59
89
|
throw new Error(`Release download returned HTTP ${response.status}`);
|
|
60
|
-
bytes =
|
|
90
|
+
bytes = await readBoundedResponse(response, MAX_RELEASE_ARCHIVE_BYTES);
|
|
61
91
|
}
|
|
62
92
|
fs.writeFileSync(targetPath, bytes, { mode: 0o600, flag: 'wx' });
|
|
63
93
|
}
|
|
@@ -156,7 +186,7 @@ export function registerUpdateCommand(program, version) {
|
|
|
156
186
|
let updateTempDirectory = null;
|
|
157
187
|
try {
|
|
158
188
|
if (source.kind === 'remote') {
|
|
159
|
-
updateTempDirectory =
|
|
189
|
+
updateTempDirectory = createRuntimeTempDirectory('update-');
|
|
160
190
|
installTarget = path.join(updateTempDirectory, 'candidate.tgz');
|
|
161
191
|
await downloadFile(source.downloadUrl, installTarget);
|
|
162
192
|
}
|
|
@@ -186,7 +216,7 @@ export function registerUpdateCommand(program, version) {
|
|
|
186
216
|
}
|
|
187
217
|
finally {
|
|
188
218
|
if (updateTempDirectory)
|
|
189
|
-
|
|
219
|
+
removeRuntimeTempDirectory(updateTempDirectory);
|
|
190
220
|
}
|
|
191
221
|
console.log(chalk.green('Update installed successfully'));
|
|
192
222
|
console.log(chalk.gray('Please restart the CLI to use the new version'));
|
|
@@ -273,7 +303,7 @@ export function registerUpdateCommand(program, version) {
|
|
|
273
303
|
&& compareVersions(manifestEntry.version, effectiveLatest) >= 0
|
|
274
304
|
&& compareVersions(manifestEntry.version, currentVersion) > 0);
|
|
275
305
|
if (manifestIsAuthoritative && manifestEntry) {
|
|
276
|
-
const updateTempDirectory =
|
|
306
|
+
const updateTempDirectory = createRuntimeTempDirectory('update-');
|
|
277
307
|
const tmpFile = path.join(updateTempDirectory, 'candidate.tgz');
|
|
278
308
|
try {
|
|
279
309
|
console.log(chalk.cyan(`Downloading release package (${manifestEntry.version})...`));
|
|
@@ -306,7 +336,7 @@ export function registerUpdateCommand(program, version) {
|
|
|
306
336
|
return;
|
|
307
337
|
}
|
|
308
338
|
finally {
|
|
309
|
-
|
|
339
|
+
removeRuntimeTempDirectory(updateTempDirectory);
|
|
310
340
|
}
|
|
311
341
|
}
|
|
312
342
|
const npmSpec = npmVersion && compareVersions(npmVersion, currentVersion) > 0
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,8 @@ import { installAxiosNetworkPolicy, installGlobalFetchPolicy } from './utils/net
|
|
|
50
50
|
import { installConsoleRedaction } from './utils/secret-policy.js';
|
|
51
51
|
import { commandNameFromArgv, failureEnvelope, normalizeCommandError, CliCommandError, } from './utils/command-contract.js';
|
|
52
52
|
import { commandAuthRequirement, commanderCommandPath } from './utils/command-policy.js';
|
|
53
|
+
import { initializeRuntimeTempStorage } from './utils/runtime-temp.js';
|
|
54
|
+
initializeRuntimeTempStorage();
|
|
53
55
|
applyLocalTestfarmDefaults();
|
|
54
56
|
if (process.env.VIGTHORIA_CAPTURE_RUNTIME_MODEL !== '1')
|
|
55
57
|
installConsoleRedaction();
|
|
@@ -8,6 +8,8 @@ export interface PreviewScreenshotPort {
|
|
|
8
8
|
export declare class OptionalPuppeteerScreenshotAdapter implements PreviewScreenshotPort {
|
|
9
9
|
private readonly loadPuppeteer;
|
|
10
10
|
private readonly environment;
|
|
11
|
-
|
|
11
|
+
private readonly allocateTemp;
|
|
12
|
+
private readonly releaseTemp;
|
|
13
|
+
constructor(loadPuppeteer?: () => Promise<unknown>, environment?: NodeJS.ProcessEnv, allocateTemp?: (prefix: string) => string, releaseTemp?: (directory: string) => void);
|
|
12
14
|
capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
|
|
13
15
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
2
|
import { redactSensitiveText } from './secret-policy.js';
|
|
3
|
+
import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from './runtime-temp.js';
|
|
3
4
|
export class OptionalPuppeteerScreenshotAdapter {
|
|
4
5
|
loadPuppeteer;
|
|
5
6
|
environment;
|
|
6
|
-
|
|
7
|
+
allocateTemp;
|
|
8
|
+
releaseTemp;
|
|
9
|
+
constructor(loadPuppeteer = () => import('puppeteer'), environment = process.env, allocateTemp = createRuntimeTempDirectory, releaseTemp = removeRuntimeTempDirectory) {
|
|
7
10
|
this.loadPuppeteer = loadPuppeteer;
|
|
8
11
|
this.environment = environment;
|
|
12
|
+
this.allocateTemp = allocateTemp;
|
|
13
|
+
this.releaseTemp = releaseTemp;
|
|
9
14
|
}
|
|
10
15
|
async capture(entryAbsolutePath, screenshotPath) {
|
|
11
16
|
if (this.environment.VIGTHORIA_DISABLE_PREVIEW_SCREENSHOT === '1') {
|
|
@@ -17,15 +22,25 @@ export class OptionalPuppeteerScreenshotAdapter {
|
|
|
17
22
|
if (!puppeteer || typeof puppeteer.launch !== 'function') {
|
|
18
23
|
return { captured: false, error: 'puppeteer optional adapter is not installed' };
|
|
19
24
|
}
|
|
20
|
-
const
|
|
25
|
+
const browserProfile = this.allocateTemp('browser-');
|
|
21
26
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const browser = await puppeteer.launch({
|
|
28
|
+
headless: true,
|
|
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
|
+
}
|
|
26
41
|
}
|
|
27
42
|
finally {
|
|
28
|
-
|
|
43
|
+
this.releaseTemp(browserProfile);
|
|
29
44
|
}
|
|
30
45
|
return { captured: true };
|
|
31
46
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface RuntimeTempOptions {
|
|
2
|
+
homeDirectory?: string;
|
|
3
|
+
environment?: NodeJS.ProcessEnv;
|
|
4
|
+
platform?: NodeJS.Platform;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
pid?: number;
|
|
7
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
8
|
+
systemTempDirectory?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface RuntimeTempCleanupResult {
|
|
11
|
+
removedEntries: number;
|
|
12
|
+
removedBytes: number;
|
|
13
|
+
skippedActiveEntries: number;
|
|
14
|
+
legacyRemovedEntries: number;
|
|
15
|
+
legacyRemovedBytes: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RuntimeTempStatus extends RuntimeTempCleanupResult {
|
|
18
|
+
root: string;
|
|
19
|
+
source: 'per-user-default' | 'explicit-override';
|
|
20
|
+
writable: boolean;
|
|
21
|
+
sharedSystemTemp: boolean;
|
|
22
|
+
maxBytes: number;
|
|
23
|
+
minimumFreeBytes: number;
|
|
24
|
+
ttlHours: number;
|
|
25
|
+
usedBytes: number;
|
|
26
|
+
entryCount: number;
|
|
27
|
+
freeBytes: number | null;
|
|
28
|
+
error: string | null;
|
|
29
|
+
}
|
|
30
|
+
export declare class RuntimeTempError extends Error {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
readonly details: Record<string, unknown>;
|
|
33
|
+
constructor(message: string, code: string, details?: Record<string, unknown>);
|
|
34
|
+
}
|
|
35
|
+
export declare function resolveRuntimeTempRoot(options?: RuntimeTempOptions): {
|
|
36
|
+
root: string;
|
|
37
|
+
source: RuntimeTempStatus['source'];
|
|
38
|
+
};
|
|
39
|
+
export declare class RuntimeTempManager {
|
|
40
|
+
private readonly environment;
|
|
41
|
+
private readonly platform;
|
|
42
|
+
private readonly homeDirectory;
|
|
43
|
+
private readonly now;
|
|
44
|
+
private readonly pid;
|
|
45
|
+
private readonly isProcessAlive;
|
|
46
|
+
private readonly systemTempRoot;
|
|
47
|
+
private readonly configuredRoot;
|
|
48
|
+
private readonly source;
|
|
49
|
+
private readonly maxBytes;
|
|
50
|
+
private readonly minimumFreeBytes;
|
|
51
|
+
private readonly ttlMs;
|
|
52
|
+
private initializedRoot;
|
|
53
|
+
private initializedIdentity;
|
|
54
|
+
private initializationError;
|
|
55
|
+
private lastCleanup;
|
|
56
|
+
constructor(options?: RuntimeTempOptions);
|
|
57
|
+
initialize(): RuntimeTempStatus;
|
|
58
|
+
createDirectory(prefix?: string): string;
|
|
59
|
+
removeDirectory(directory: string): void;
|
|
60
|
+
scavenge(options?: {
|
|
61
|
+
includeLegacy?: boolean;
|
|
62
|
+
}): RuntimeTempCleanupResult;
|
|
63
|
+
status(): RuntimeTempStatus;
|
|
64
|
+
private requireRoot;
|
|
65
|
+
private readEntries;
|
|
66
|
+
private entrySize;
|
|
67
|
+
private freeBytes;
|
|
68
|
+
private measure;
|
|
69
|
+
private removeEntry;
|
|
70
|
+
private cleanupLegacySystemTemp;
|
|
71
|
+
}
|
|
72
|
+
export declare function getRuntimeTempManager(): RuntimeTempManager;
|
|
73
|
+
export declare function initializeRuntimeTempStorage(): RuntimeTempStatus;
|
|
74
|
+
export declare function runtimeTempStatus(): RuntimeTempStatus;
|
|
75
|
+
export declare function createRuntimeTempDirectory(prefix: string): string;
|
|
76
|
+
export declare function removeRuntimeTempDirectory(directory: string): void;
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
const MIB = 1024 * 1024;
|
|
5
|
+
const DEFAULT_MAX_BYTES = 1024 * MIB;
|
|
6
|
+
const DEFAULT_MIN_FREE_BYTES = 128 * MIB;
|
|
7
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const CLEANUP_LOCK_MAX_AGE_MS = 10 * 60 * 1000;
|
|
9
|
+
const MAX_SCAN_ENTRIES = 100_000;
|
|
10
|
+
const LEASE_FILE = '.vigthoria-temp-lease.json';
|
|
11
|
+
const CLEANUP_LOCK = '.vigthoria-temp-cleanup.lock';
|
|
12
|
+
const ROOT_MARKER = '.vigthoria-temp-root.json';
|
|
13
|
+
const LEGACY_CLEANUP_MARKER = '.vigthoria-legacy-temp-cleanup-v1';
|
|
14
|
+
const LEGACY_PREFIXES = [
|
|
15
|
+
'vigthoria-pull-',
|
|
16
|
+
'vigthoria-pull-inline-',
|
|
17
|
+
'vigthoria-update-',
|
|
18
|
+
'vigthoria-cli-install-',
|
|
19
|
+
'puppeteer_dev_chrome_profile-',
|
|
20
|
+
];
|
|
21
|
+
export class RuntimeTempError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
details;
|
|
24
|
+
constructor(message, code, details = {}) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'RuntimeTempError';
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.details = details;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function numericSetting(name, value, fallback, minimum, maximum) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return fallback;
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
if (!Number.isFinite(parsed) || parsed < minimum || parsed > maximum) {
|
|
36
|
+
throw new RuntimeTempError(`${name} must be a number between ${minimum} and ${maximum}.`, 'TEMP_SETTING_INVALID', {
|
|
37
|
+
setting: name,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return Math.floor(parsed);
|
|
41
|
+
}
|
|
42
|
+
function pathIsContained(root, candidate, pathApi) {
|
|
43
|
+
const relative = pathApi.relative(root, candidate);
|
|
44
|
+
return relative === '' || (!relative.startsWith(`..${pathApi.sep}`) && relative !== '..' && !pathApi.isAbsolute(relative));
|
|
45
|
+
}
|
|
46
|
+
function rejectUnsafeWindowsPath(value) {
|
|
47
|
+
const normalized = value.replace(/\//g, '\\');
|
|
48
|
+
if (/^\\\\[?.]\\/i.test(normalized) || /^\\\\/.test(normalized)) {
|
|
49
|
+
throw new RuntimeTempError('VIGTHORIA_TEMP_DIR cannot use a UNC or Windows device path.', 'TEMP_ROOT_UNSAFE');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function isKnownSharedPosixTemp(value) {
|
|
53
|
+
const normalized = path.posix.resolve(value);
|
|
54
|
+
return normalized === '/tmp' || normalized === '/var/tmp' || normalized === '/usr/tmp';
|
|
55
|
+
}
|
|
56
|
+
function pathsEqual(left, right, platform) {
|
|
57
|
+
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
58
|
+
const normalize = (value) => {
|
|
59
|
+
const resolved = pathApi.resolve(value);
|
|
60
|
+
return platform === 'win32' ? resolved.toLocaleLowerCase('en-US') : resolved;
|
|
61
|
+
};
|
|
62
|
+
return normalize(left) === normalize(right);
|
|
63
|
+
}
|
|
64
|
+
export function resolveRuntimeTempRoot(options = {}) {
|
|
65
|
+
const environment = options.environment || process.env;
|
|
66
|
+
const platform = options.platform || process.platform;
|
|
67
|
+
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
68
|
+
const homeDirectory = options.homeDirectory || os.homedir();
|
|
69
|
+
const override = String(environment.VIGTHORIA_TEMP_DIR || '').trim();
|
|
70
|
+
if (override.includes('\0'))
|
|
71
|
+
throw new RuntimeTempError('VIGTHORIA_TEMP_DIR contains a null byte.', 'TEMP_ROOT_UNSAFE');
|
|
72
|
+
if (platform === 'win32' && override)
|
|
73
|
+
rejectUnsafeWindowsPath(override);
|
|
74
|
+
if (override && !pathApi.isAbsolute(override)) {
|
|
75
|
+
throw new RuntimeTempError('VIGTHORIA_TEMP_DIR must be an absolute local path.', 'TEMP_ROOT_UNSAFE');
|
|
76
|
+
}
|
|
77
|
+
const root = pathApi.resolve(override || pathApi.join(homeDirectory, '.vigthoria', 'tmp'));
|
|
78
|
+
if (root === pathApi.parse(root).root) {
|
|
79
|
+
throw new RuntimeTempError('The filesystem root cannot be used for Vigthoria temporary storage.', 'TEMP_ROOT_UNSAFE');
|
|
80
|
+
}
|
|
81
|
+
return { root, source: override ? 'explicit-override' : 'per-user-default' };
|
|
82
|
+
}
|
|
83
|
+
function defaultProcessAlive(pid) {
|
|
84
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
85
|
+
return false;
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return error?.code === 'EPERM';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export class RuntimeTempManager {
|
|
95
|
+
environment;
|
|
96
|
+
platform;
|
|
97
|
+
homeDirectory;
|
|
98
|
+
now;
|
|
99
|
+
pid;
|
|
100
|
+
isProcessAlive;
|
|
101
|
+
systemTempRoot;
|
|
102
|
+
configuredRoot;
|
|
103
|
+
source;
|
|
104
|
+
maxBytes;
|
|
105
|
+
minimumFreeBytes;
|
|
106
|
+
ttlMs;
|
|
107
|
+
initializedRoot = null;
|
|
108
|
+
initializedIdentity = null;
|
|
109
|
+
initializationError = null;
|
|
110
|
+
lastCleanup = {
|
|
111
|
+
removedEntries: 0,
|
|
112
|
+
removedBytes: 0,
|
|
113
|
+
skippedActiveEntries: 0,
|
|
114
|
+
legacyRemovedEntries: 0,
|
|
115
|
+
legacyRemovedBytes: 0,
|
|
116
|
+
};
|
|
117
|
+
constructor(options = {}) {
|
|
118
|
+
this.environment = options.environment || process.env;
|
|
119
|
+
this.platform = options.platform || process.platform;
|
|
120
|
+
this.homeDirectory = options.homeDirectory || os.homedir();
|
|
121
|
+
this.now = options.now || Date.now;
|
|
122
|
+
this.pid = options.pid || process.pid;
|
|
123
|
+
this.isProcessAlive = options.isProcessAlive || defaultProcessAlive;
|
|
124
|
+
this.systemTempRoot = options.systemTempDirectory || os.tmpdir();
|
|
125
|
+
const resolved = resolveRuntimeTempRoot({ ...options, environment: this.environment, platform: this.platform, homeDirectory: this.homeDirectory });
|
|
126
|
+
this.configuredRoot = resolved.root;
|
|
127
|
+
this.source = resolved.source;
|
|
128
|
+
this.maxBytes = numericSetting('VIGTHORIA_TEMP_MAX_BYTES', this.environment.VIGTHORIA_TEMP_MAX_BYTES, DEFAULT_MAX_BYTES, 64 * MIB, 64 * 1024 * MIB);
|
|
129
|
+
this.minimumFreeBytes = numericSetting('VIGTHORIA_TEMP_MIN_FREE_BYTES', this.environment.VIGTHORIA_TEMP_MIN_FREE_BYTES, DEFAULT_MIN_FREE_BYTES, 64 * MIB, 64 * 1024 * MIB);
|
|
130
|
+
const ttlHours = numericSetting('VIGTHORIA_TEMP_TTL_HOURS', this.environment.VIGTHORIA_TEMP_TTL_HOURS, DEFAULT_TTL_MS / 3_600_000, 1, 720);
|
|
131
|
+
this.ttlMs = ttlHours * 3_600_000;
|
|
132
|
+
}
|
|
133
|
+
initialize() {
|
|
134
|
+
if (this.initializedRoot || this.initializationError)
|
|
135
|
+
return this.status();
|
|
136
|
+
try {
|
|
137
|
+
if (this.source === 'per-user-default' && !fs.existsSync(this.homeDirectory)) {
|
|
138
|
+
throw new RuntimeTempError('The user home directory does not exist; managed temporary storage cannot be initialized.', 'TEMP_HOME_UNAVAILABLE');
|
|
139
|
+
}
|
|
140
|
+
const existed = fs.existsSync(this.configuredRoot);
|
|
141
|
+
if (existed && fs.lstatSync(this.configuredRoot).isSymbolicLink()) {
|
|
142
|
+
throw new RuntimeTempError('Vigthoria temporary storage root cannot be a symbolic link or junction.', 'TEMP_ROOT_SYMLINK');
|
|
143
|
+
}
|
|
144
|
+
fs.mkdirSync(this.configuredRoot, { recursive: true, mode: 0o700 });
|
|
145
|
+
if (fs.lstatSync(this.configuredRoot).isSymbolicLink()) {
|
|
146
|
+
throw new RuntimeTempError('Vigthoria temporary storage root cannot be a symbolic link or junction.', 'TEMP_ROOT_SYMLINK');
|
|
147
|
+
}
|
|
148
|
+
const realRoot = fs.realpathSync(this.configuredRoot);
|
|
149
|
+
const markerPath = path.join(realRoot, ROOT_MARKER);
|
|
150
|
+
if (this.source === 'explicit-override' && existed && !fs.existsSync(markerPath)) {
|
|
151
|
+
const existingEntries = fs.readdirSync(realRoot);
|
|
152
|
+
if (existingEntries.length > 0) {
|
|
153
|
+
throw new RuntimeTempError('Explicit temporary storage must be empty or already marked as Vigthoria-managed.', 'TEMP_ROOT_NOT_DEDICATED');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const pathApi = this.platform === 'win32' ? path.win32 : path.posix;
|
|
157
|
+
if (pathsEqual(realRoot, this.systemTempRoot, this.platform)
|
|
158
|
+
|| (this.platform !== 'win32' && isKnownSharedPosixTemp(realRoot))) {
|
|
159
|
+
throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
|
|
160
|
+
}
|
|
161
|
+
if (this.source === 'per-user-default') {
|
|
162
|
+
const realHome = fs.realpathSync(this.homeDirectory);
|
|
163
|
+
if (!pathIsContained(realHome, realRoot, pathApi)) {
|
|
164
|
+
throw new RuntimeTempError('Default temporary storage escaped the user home through a link.', 'TEMP_ROOT_ESCAPE');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (this.platform !== 'win32')
|
|
168
|
+
fs.chmodSync(realRoot, 0o700);
|
|
169
|
+
if (!fs.existsSync(markerPath)) {
|
|
170
|
+
try {
|
|
171
|
+
fs.writeFileSync(markerPath, `${JSON.stringify({ schemaVersion: 1, owner: 'vigthoria-cli', createdAt: new Date(this.now()).toISOString() })}\n`, {
|
|
172
|
+
flag: 'wx',
|
|
173
|
+
mode: 0o600,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error?.code !== 'EEXIST')
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
182
|
+
if (marker.schemaVersion !== 1 || marker.owner !== 'vigthoria-cli') {
|
|
183
|
+
throw new RuntimeTempError('Temporary storage ownership marker is invalid.', 'TEMP_ROOT_MARKER_INVALID');
|
|
184
|
+
}
|
|
185
|
+
const rootStat = fs.statSync(realRoot, { bigint: true });
|
|
186
|
+
const probe = path.join(realRoot, `.write-probe-${this.pid}-${this.now()}`);
|
|
187
|
+
fs.writeFileSync(probe, '', { flag: 'wx', mode: 0o600 });
|
|
188
|
+
fs.unlinkSync(probe);
|
|
189
|
+
this.initializedRoot = realRoot;
|
|
190
|
+
this.initializedIdentity = { dev: rootStat.dev, ino: rootStat.ino };
|
|
191
|
+
this.environment.TMPDIR = realRoot;
|
|
192
|
+
this.environment.TMP = realRoot;
|
|
193
|
+
this.environment.TEMP = realRoot;
|
|
194
|
+
this.lastCleanup = this.scavenge({ includeLegacy: true });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
this.initializationError = error instanceof Error ? error.message : String(error);
|
|
198
|
+
}
|
|
199
|
+
return this.status();
|
|
200
|
+
}
|
|
201
|
+
createDirectory(prefix = 'run-') {
|
|
202
|
+
this.initialize();
|
|
203
|
+
const root = this.requireRoot();
|
|
204
|
+
if (!/^[a-z0-9][a-z0-9-]{0,47}-$/i.test(prefix)) {
|
|
205
|
+
throw new RuntimeTempError('Temporary directory prefix is invalid.', 'TEMP_PREFIX_INVALID');
|
|
206
|
+
}
|
|
207
|
+
this.lastCleanup = this.scavenge({ includeLegacy: false });
|
|
208
|
+
const before = this.measure(root);
|
|
209
|
+
if (before.usedBytes >= this.maxBytes || (before.freeBytes !== null && before.freeBytes < this.minimumFreeBytes)) {
|
|
210
|
+
throw new RuntimeTempError('Vigthoria temporary storage has insufficient capacity after cleanup.', 'TEMP_CAPACITY_EXHAUSTED', {
|
|
211
|
+
root,
|
|
212
|
+
usedBytes: before.usedBytes,
|
|
213
|
+
maxBytes: this.maxBytes,
|
|
214
|
+
freeBytes: before.freeBytes,
|
|
215
|
+
minimumFreeBytes: this.minimumFreeBytes,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const directory = fs.mkdtempSync(path.join(root, prefix));
|
|
219
|
+
if (this.platform !== 'win32')
|
|
220
|
+
fs.chmodSync(directory, 0o700);
|
|
221
|
+
fs.writeFileSync(path.join(directory, LEASE_FILE), `${JSON.stringify({ schemaVersion: 1, pid: this.pid, createdAt: new Date(this.now()).toISOString() })}\n`, {
|
|
222
|
+
flag: 'wx',
|
|
223
|
+
mode: 0o600,
|
|
224
|
+
});
|
|
225
|
+
return directory;
|
|
226
|
+
}
|
|
227
|
+
removeDirectory(directory) {
|
|
228
|
+
const root = this.requireRoot();
|
|
229
|
+
const absolute = path.resolve(directory);
|
|
230
|
+
if (path.dirname(absolute) !== root || absolute === root) {
|
|
231
|
+
throw new RuntimeTempError('Refusing to remove a path outside the Vigthoria temporary root.', 'TEMP_CLEANUP_ESCAPE');
|
|
232
|
+
}
|
|
233
|
+
fs.rmSync(absolute, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
scavenge(options = {}) {
|
|
236
|
+
const root = this.requireRoot();
|
|
237
|
+
const result = {
|
|
238
|
+
removedEntries: 0,
|
|
239
|
+
removedBytes: 0,
|
|
240
|
+
skippedActiveEntries: 0,
|
|
241
|
+
legacyRemovedEntries: 0,
|
|
242
|
+
legacyRemovedBytes: 0,
|
|
243
|
+
};
|
|
244
|
+
const lockPath = path.join(root, CLEANUP_LOCK);
|
|
245
|
+
let lock = null;
|
|
246
|
+
try {
|
|
247
|
+
for (let attempt = 0; attempt < 3 && lock === null; attempt += 1) {
|
|
248
|
+
try {
|
|
249
|
+
lock = fs.openSync(lockPath, 'wx', 0o600);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error?.code !== 'EEXIST')
|
|
253
|
+
throw error;
|
|
254
|
+
let lockAge = 0;
|
|
255
|
+
try {
|
|
256
|
+
lockAge = this.now() - fs.statSync(lockPath).mtimeMs;
|
|
257
|
+
}
|
|
258
|
+
catch (statError) {
|
|
259
|
+
if (statError?.code === 'ENOENT')
|
|
260
|
+
continue;
|
|
261
|
+
throw statError;
|
|
262
|
+
}
|
|
263
|
+
if (lockAge <= CLEANUP_LOCK_MAX_AGE_MS)
|
|
264
|
+
return result;
|
|
265
|
+
try {
|
|
266
|
+
fs.unlinkSync(lockPath);
|
|
267
|
+
}
|
|
268
|
+
catch (unlinkError) {
|
|
269
|
+
if (unlinkError?.code !== 'ENOENT')
|
|
270
|
+
throw unlinkError;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (lock === null)
|
|
275
|
+
return result;
|
|
276
|
+
fs.writeFileSync(lock, `${this.pid}\n`);
|
|
277
|
+
let entries = this.readEntries(root);
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
if (entry.active) {
|
|
280
|
+
result.skippedActiveEntries += 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (this.now() - entry.modifiedAt > this.ttlMs)
|
|
284
|
+
this.removeEntry(entry, result);
|
|
285
|
+
}
|
|
286
|
+
entries = this.readEntries(root);
|
|
287
|
+
let measurement = this.measure(root, entries);
|
|
288
|
+
for (const entry of entries.filter((candidate) => !candidate.active).sort((left, right) => left.modifiedAt - right.modifiedAt)) {
|
|
289
|
+
if (measurement.usedBytes <= this.maxBytes && (measurement.freeBytes === null || measurement.freeBytes >= this.minimumFreeBytes))
|
|
290
|
+
break;
|
|
291
|
+
this.removeEntry(entry, result);
|
|
292
|
+
measurement = this.measure(root);
|
|
293
|
+
}
|
|
294
|
+
if (options.includeLegacy && !fs.existsSync(path.join(root, LEGACY_CLEANUP_MARKER))) {
|
|
295
|
+
this.cleanupLegacySystemTemp(result);
|
|
296
|
+
try {
|
|
297
|
+
fs.writeFileSync(path.join(root, LEGACY_CLEANUP_MARKER), `${new Date(this.now()).toISOString()}\n`, { flag: 'wx', mode: 0o600 });
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
if (error?.code !== 'EEXIST')
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
if (lock !== null) {
|
|
308
|
+
fs.closeSync(lock);
|
|
309
|
+
try {
|
|
310
|
+
fs.unlinkSync(lockPath);
|
|
311
|
+
}
|
|
312
|
+
catch { /* another process may have recovered a stale lock */ }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
status() {
|
|
317
|
+
const root = this.initializedRoot || this.configuredRoot;
|
|
318
|
+
let measurement = { usedBytes: 0, entryCount: 0, freeBytes: null };
|
|
319
|
+
let runtimeError = this.initializationError;
|
|
320
|
+
if (this.initializedRoot) {
|
|
321
|
+
try {
|
|
322
|
+
measurement = this.measure(this.requireRoot());
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
runtimeError = error instanceof Error ? error.message : String(error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
root,
|
|
330
|
+
source: this.source,
|
|
331
|
+
writable: Boolean(this.initializedRoot) && !runtimeError,
|
|
332
|
+
sharedSystemTemp: this.initializedRoot ? pathsEqual(this.initializedRoot, this.systemTempRoot, this.platform) : false,
|
|
333
|
+
maxBytes: this.maxBytes,
|
|
334
|
+
minimumFreeBytes: this.minimumFreeBytes,
|
|
335
|
+
ttlHours: this.ttlMs / 3_600_000,
|
|
336
|
+
usedBytes: measurement.usedBytes,
|
|
337
|
+
entryCount: measurement.entryCount,
|
|
338
|
+
freeBytes: measurement.freeBytes,
|
|
339
|
+
error: runtimeError,
|
|
340
|
+
...this.lastCleanup,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
requireRoot() {
|
|
344
|
+
if (!this.initializedRoot) {
|
|
345
|
+
throw new RuntimeTempError(this.initializationError || 'Vigthoria temporary storage is unavailable.', 'TEMP_STORAGE_UNAVAILABLE', {
|
|
346
|
+
root: this.configuredRoot,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const stat = fs.lstatSync(this.initializedRoot);
|
|
351
|
+
const identity = fs.statSync(this.initializedRoot, { bigint: true });
|
|
352
|
+
if (stat.isSymbolicLink()
|
|
353
|
+
|| fs.realpathSync(this.initializedRoot) !== this.initializedRoot
|
|
354
|
+
|| !this.initializedIdentity
|
|
355
|
+
|| identity.dev !== this.initializedIdentity.dev
|
|
356
|
+
|| identity.ino !== this.initializedIdentity.ino) {
|
|
357
|
+
throw new Error('root identity changed');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
throw new RuntimeTempError('Vigthoria temporary storage root changed after initialization.', 'TEMP_ROOT_CHANGED', {
|
|
362
|
+
root: this.initializedRoot,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return this.initializedRoot;
|
|
366
|
+
}
|
|
367
|
+
readEntries(root) {
|
|
368
|
+
const entries = [];
|
|
369
|
+
for (const name of fs.readdirSync(root)) {
|
|
370
|
+
if (name === CLEANUP_LOCK || name === ROOT_MARKER || name === LEGACY_CLEANUP_MARKER)
|
|
371
|
+
continue;
|
|
372
|
+
const absolutePath = path.join(root, name);
|
|
373
|
+
let stat;
|
|
374
|
+
try {
|
|
375
|
+
stat = fs.lstatSync(absolutePath);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (error?.code === 'ENOENT')
|
|
379
|
+
continue;
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
let active = false;
|
|
383
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
384
|
+
const leasePath = path.join(absolutePath, LEASE_FILE);
|
|
385
|
+
try {
|
|
386
|
+
const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
387
|
+
active = typeof lease.pid === 'number' && this.isProcessAlive(lease.pid);
|
|
388
|
+
}
|
|
389
|
+
catch { /* third-party and abandoned directories have no live lease */ }
|
|
390
|
+
}
|
|
391
|
+
entries.push({ absolutePath, name, bytes: this.entrySize(absolutePath), modifiedAt: stat.mtimeMs, active });
|
|
392
|
+
}
|
|
393
|
+
return entries;
|
|
394
|
+
}
|
|
395
|
+
entrySize(entryPath) {
|
|
396
|
+
let bytes = 0;
|
|
397
|
+
let visited = 0;
|
|
398
|
+
const walk = (candidate) => {
|
|
399
|
+
visited += 1;
|
|
400
|
+
if (visited > MAX_SCAN_ENTRIES) {
|
|
401
|
+
bytes = Math.max(bytes, this.maxBytes + 1);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const stat = fs.lstatSync(candidate);
|
|
405
|
+
bytes += stat.size;
|
|
406
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
407
|
+
return;
|
|
408
|
+
for (const name of fs.readdirSync(candidate))
|
|
409
|
+
walk(path.join(candidate, name));
|
|
410
|
+
};
|
|
411
|
+
try {
|
|
412
|
+
walk(entryPath);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return this.maxBytes + 1;
|
|
416
|
+
}
|
|
417
|
+
return bytes;
|
|
418
|
+
}
|
|
419
|
+
freeBytes(root) {
|
|
420
|
+
try {
|
|
421
|
+
const stat = fs.statfsSync(root);
|
|
422
|
+
return Number(stat.bavail) * Number(stat.bsize);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
measure(root, entries = this.readEntries(root)) {
|
|
429
|
+
return {
|
|
430
|
+
usedBytes: entries.reduce((total, entry) => total + entry.bytes, 0),
|
|
431
|
+
entryCount: entries.length,
|
|
432
|
+
freeBytes: this.freeBytes(root),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
removeEntry(entry, result) {
|
|
436
|
+
if (path.dirname(entry.absolutePath) !== this.requireRoot()) {
|
|
437
|
+
throw new RuntimeTempError('Temporary cleanup target escaped its root.', 'TEMP_CLEANUP_ESCAPE');
|
|
438
|
+
}
|
|
439
|
+
fs.rmSync(entry.absolutePath, { recursive: true, force: true });
|
|
440
|
+
result.removedEntries += 1;
|
|
441
|
+
result.removedBytes += entry.bytes;
|
|
442
|
+
}
|
|
443
|
+
cleanupLegacySystemTemp(result) {
|
|
444
|
+
const legacyRoot = this.systemTempRoot;
|
|
445
|
+
if (!fs.existsSync(legacyRoot) || path.resolve(legacyRoot) === this.requireRoot())
|
|
446
|
+
return;
|
|
447
|
+
const currentUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
448
|
+
if (currentUid === null)
|
|
449
|
+
return;
|
|
450
|
+
let scanned = 0;
|
|
451
|
+
for (const name of fs.readdirSync(legacyRoot)) {
|
|
452
|
+
scanned += 1;
|
|
453
|
+
if (scanned > MAX_SCAN_ENTRIES)
|
|
454
|
+
break;
|
|
455
|
+
if (!LEGACY_PREFIXES.some((prefix) => name.startsWith(prefix)))
|
|
456
|
+
continue;
|
|
457
|
+
const candidate = path.join(legacyRoot, name);
|
|
458
|
+
let stat;
|
|
459
|
+
try {
|
|
460
|
+
stat = fs.lstatSync(candidate);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (currentUid !== null && stat.uid !== currentUid)
|
|
466
|
+
continue;
|
|
467
|
+
if (this.now() - stat.mtimeMs <= this.ttlMs)
|
|
468
|
+
continue;
|
|
469
|
+
const bytes = this.entrySize(candidate);
|
|
470
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
471
|
+
result.legacyRemovedEntries += 1;
|
|
472
|
+
result.legacyRemovedBytes += bytes;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
let defaultManager = null;
|
|
477
|
+
export function getRuntimeTempManager() {
|
|
478
|
+
defaultManager ||= new RuntimeTempManager();
|
|
479
|
+
return defaultManager;
|
|
480
|
+
}
|
|
481
|
+
export function initializeRuntimeTempStorage() {
|
|
482
|
+
return getRuntimeTempManager().initialize();
|
|
483
|
+
}
|
|
484
|
+
export function runtimeTempStatus() {
|
|
485
|
+
return getRuntimeTempManager().initialize();
|
|
486
|
+
}
|
|
487
|
+
export function createRuntimeTempDirectory(prefix) {
|
|
488
|
+
return getRuntimeTempManager().createDirectory(prefix);
|
|
489
|
+
}
|
|
490
|
+
export function removeRuntimeTempDirectory(directory) {
|
|
491
|
+
getRuntimeTempManager().removeDirectory(directory);
|
|
492
|
+
}
|
package/dist/utils/tools.js
CHANGED
|
@@ -28,6 +28,7 @@ import { assertContainedAbsolutePath } from './workspace-boundary.js';
|
|
|
28
28
|
import { assertProcessNetworkAllowed, buildApprovalScope, hardenGitArguments, parseGitArguments, parseSupportedProcess, safeGitEnvironment, } from './process-policy.js';
|
|
29
29
|
import { containsHighConfidenceSecret, isSensitivePath, safeChildProcessEnv } from './secret-policy.js';
|
|
30
30
|
import { isOfflineNetworkMode } from './network-policy.js';
|
|
31
|
+
import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from './runtime-temp.js';
|
|
31
32
|
import { ToolApprovalService } from './tool-approval-service.js';
|
|
32
33
|
import { DelegationToolProvider, FilesystemToolProvider, NetworkBrowserToolProvider, ProcessToolProvider, RepositoryToolProvider, SearchToolProvider, SshToolProvider, ToolCapabilityRegistry, } from './tool-capability-providers.js';
|
|
33
34
|
const STREAM_RESPONSE_MAX_YIELD_CHARS = 32 * 1024;
|
|
@@ -1341,7 +1342,14 @@ export class AgenticTools {
|
|
|
1341
1342
|
return errors;
|
|
1342
1343
|
}
|
|
1343
1344
|
validateJavaScriptSyntax(source) {
|
|
1344
|
-
|
|
1345
|
+
let tempDirectory;
|
|
1346
|
+
try {
|
|
1347
|
+
tempDirectory = createRuntimeTempDirectory('syntax-');
|
|
1348
|
+
}
|
|
1349
|
+
catch (error) {
|
|
1350
|
+
return this.formatExternalToolError('syntax_check', 'allocate bounded temporary storage', error);
|
|
1351
|
+
}
|
|
1352
|
+
const tempFile = path.join(tempDirectory, 'candidate.js');
|
|
1345
1353
|
try {
|
|
1346
1354
|
try {
|
|
1347
1355
|
fs.writeFileSync(tempFile, source, 'utf-8');
|
|
@@ -1360,9 +1368,7 @@ export class AgenticTools {
|
|
|
1360
1368
|
}
|
|
1361
1369
|
finally {
|
|
1362
1370
|
this.cleanupAfterToolError('syntax_check', `remove temporary file ${tempFile}`, () => {
|
|
1363
|
-
|
|
1364
|
-
fs.unlinkSync(tempFile);
|
|
1365
|
-
}
|
|
1371
|
+
removeRuntimeTempDirectory(tempDirectory);
|
|
1366
1372
|
});
|
|
1367
1373
|
}
|
|
1368
1374
|
}
|
package/install.ps1
CHANGED
|
@@ -129,12 +129,42 @@ function Test-Prerequisites {
|
|
|
129
129
|
return $true
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function Get-VigthoriaTempRoot {
|
|
133
|
+
$explicitRoot = $env:VIGTHORIA_TEMP_DIR
|
|
134
|
+
$candidate = if ($explicitRoot) { $explicitRoot } else { Join-Path $env:USERPROFILE ".vigthoria\tmp" }
|
|
135
|
+
if (-not [IO.Path]::IsPathRooted($candidate)) { throw "VIGTHORIA_TEMP_DIR must be an absolute local path" }
|
|
136
|
+
if ($candidate.StartsWith("\\") -or $candidate.StartsWith("\\?\") -or $candidate.StartsWith("\\.\")) {
|
|
137
|
+
throw "VIGTHORIA_TEMP_DIR cannot use a UNC or Windows device path"
|
|
138
|
+
}
|
|
139
|
+
$fullRoot = [IO.Path]::GetFullPath($candidate).TrimEnd([IO.Path]::DirectorySeparatorChar)
|
|
140
|
+
$volumeRoot = [IO.Path]::GetPathRoot($fullRoot).TrimEnd([IO.Path]::DirectorySeparatorChar)
|
|
141
|
+
if ($fullRoot -eq $volumeRoot -or $fullRoot -eq [IO.Path]::GetTempPath().TrimEnd([IO.Path]::DirectorySeparatorChar)) {
|
|
142
|
+
throw "Refusing shared or filesystem-root temporary storage"
|
|
143
|
+
}
|
|
144
|
+
New-Item -ItemType Directory -Path $fullRoot -Force -ErrorAction Stop | Out-Null
|
|
145
|
+
$cursor = Get-Item -LiteralPath $fullRoot -Force -ErrorAction Stop
|
|
146
|
+
while ($cursor) {
|
|
147
|
+
if (($cursor.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
148
|
+
throw "Refusing a reparse-point temporary storage path"
|
|
149
|
+
}
|
|
150
|
+
if ($cursor.FullName -eq [IO.Path]::GetPathRoot($cursor.FullName)) { break }
|
|
151
|
+
$cursor = $cursor.Parent
|
|
152
|
+
}
|
|
153
|
+
if (-not $explicitRoot) {
|
|
154
|
+
$homeRoot = [IO.Path]::GetFullPath($env:USERPROFILE).TrimEnd([IO.Path]::DirectorySeparatorChar)
|
|
155
|
+
if (-not ($fullRoot.Equals($homeRoot, [StringComparison]::OrdinalIgnoreCase) -or $fullRoot.StartsWith($homeRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase))) {
|
|
156
|
+
throw "Default temporary storage escaped the user profile"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return $fullRoot
|
|
160
|
+
}
|
|
161
|
+
|
|
132
162
|
function Install-VigthoriaCLI-NPM {
|
|
133
163
|
Write-Host ""
|
|
134
164
|
Write-Host "[INSTALL] Installing Vigthoria CLI..." -ForegroundColor Cyan
|
|
135
165
|
|
|
136
166
|
if ($HOSTED_TARBALL_SHA256) {
|
|
137
|
-
$releaseTemp = Join-Path (
|
|
167
|
+
$releaseTemp = Join-Path (Get-VigthoriaTempRoot) ("install-" + [Guid]::NewGuid().ToString("N"))
|
|
138
168
|
$releaseArchive = Join-Path $releaseTemp "candidate.tgz"
|
|
139
169
|
try {
|
|
140
170
|
New-Item -ItemType Directory -Path $releaseTemp -Force | Out-Null
|
|
@@ -178,7 +208,7 @@ function Install-VigthoriaCLI-NPM {
|
|
|
178
208
|
function Install-VigthoriaCLI-Direct {
|
|
179
209
|
Write-Host ""
|
|
180
210
|
Write-Host "[INSTALL] Installing Vigthoria CLI (Direct Download)..." -ForegroundColor Cyan
|
|
181
|
-
$releaseTemp = Join-Path (
|
|
211
|
+
$releaseTemp = Join-Path (Get-VigthoriaTempRoot) ("direct-" + [Guid]::NewGuid().ToString("N"))
|
|
182
212
|
$tarballPath = Join-Path $releaseTemp "candidate.tgz"
|
|
183
213
|
try {
|
|
184
214
|
if (-not $HOSTED_TARBALL_SHA256) { throw "Direct installation requires a signed manifest SHA-256" }
|
package/install.sh
CHANGED
|
@@ -211,6 +211,41 @@ check_requirements() {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
# Install CLI
|
|
214
|
+
prepare_temp_root() {
|
|
215
|
+
local requested_root="${VIGTHORIA_TEMP_DIR:-$HOME/.vigthoria/tmp}"
|
|
216
|
+
case "$requested_root" in
|
|
217
|
+
/*) ;;
|
|
218
|
+
*) echo -e "${RED}VIGTHORIA_TEMP_DIR must be an absolute local path${NC}" >&2; return 1 ;;
|
|
219
|
+
esac
|
|
220
|
+
if [[ "$requested_root" == "/" || "$requested_root" == "${TMPDIR:-/tmp}" ]]; then
|
|
221
|
+
echo -e "${RED}Refusing shared or filesystem-root temporary storage${NC}" >&2
|
|
222
|
+
return 1
|
|
223
|
+
fi
|
|
224
|
+
mkdir -p "$requested_root"
|
|
225
|
+
local path_cursor="$requested_root"
|
|
226
|
+
while [[ "$path_cursor" != "/" ]]; do
|
|
227
|
+
if [[ -L "$path_cursor" ]]; then
|
|
228
|
+
echo -e "${RED}Refusing a symbolic-link temporary storage path${NC}" >&2
|
|
229
|
+
return 1
|
|
230
|
+
fi
|
|
231
|
+
path_cursor="$(dirname -- "$path_cursor")"
|
|
232
|
+
done
|
|
233
|
+
local resolved_root resolved_home
|
|
234
|
+
resolved_root="$(cd "$requested_root" && pwd -P)"
|
|
235
|
+
resolved_home="$(cd "$HOME" && pwd -P)"
|
|
236
|
+
case "$resolved_root" in
|
|
237
|
+
/tmp|/var/tmp|/usr/tmp) echo -e "${RED}Refusing shared temporary storage${NC}" >&2; return 1 ;;
|
|
238
|
+
esac
|
|
239
|
+
if [[ -z "${VIGTHORIA_TEMP_DIR:-}" ]]; then
|
|
240
|
+
case "$resolved_root/" in
|
|
241
|
+
"$resolved_home/"*) ;;
|
|
242
|
+
*) echo -e "${RED}Default temporary storage escaped the user home${NC}" >&2; return 1 ;;
|
|
243
|
+
esac
|
|
244
|
+
fi
|
|
245
|
+
chmod 700 "$resolved_root"
|
|
246
|
+
printf '%s\n' "$resolved_root"
|
|
247
|
+
}
|
|
248
|
+
|
|
214
249
|
install_cli() {
|
|
215
250
|
echo -e "${CYAN}Installing Vigthoria CLI...${NC}"
|
|
216
251
|
|
|
@@ -220,7 +255,8 @@ install_cli() {
|
|
|
220
255
|
# Option 1: Download and verify the exact hosted release package.
|
|
221
256
|
if [[ -n "$HOSTED_TARBALL_SHA256" ]]; then
|
|
222
257
|
echo "Downloading checksum-verified hosted release package..."
|
|
223
|
-
|
|
258
|
+
VIGTHORIA_INSTALL_TEMP_ROOT="$(prepare_temp_root)" || return 1
|
|
259
|
+
RELEASE_TMP_DIR="$(mktemp -d "$VIGTHORIA_INSTALL_TEMP_ROOT/install.XXXXXX")"
|
|
224
260
|
RELEASE_ARCHIVE="$RELEASE_TMP_DIR/vigthoria-cli-${CLI_VERSION}.tgz"
|
|
225
261
|
if ! validate_release_url "$HOSTED_TARBALL_URL"; then
|
|
226
262
|
echo -e "${RED}Refusing untrusted release artifact URL${NC}"
|
package/package.json
CHANGED
|
@@ -18,7 +18,10 @@ if ! curl --silent --show-error --fail --connect-timeout 5 --max-time 15 "$MODEL
|
|
|
18
18
|
block "balanced-model infrastructure is unavailable at the configured endpoint"
|
|
19
19
|
fi
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
PROBE_TEMP_ROOT="${VIGTHORIA_TEMP_DIR:-$HOME/.vigthoria/tmp}/live-tests"
|
|
22
|
+
mkdir -p "$PROBE_TEMP_ROOT"
|
|
23
|
+
chmod 700 "$PROBE_TEMP_ROOT"
|
|
24
|
+
PROBE_TMP="$(mktemp -d "$PROBE_TEMP_ROOT/balanced.XXXXXX")"
|
|
22
25
|
trap 'rm -rf "$PROBE_TMP"' EXIT
|
|
23
26
|
|
|
24
27
|
for attempt in 1 2 3; do
|
|
@@ -15,7 +15,10 @@ block() {
|
|
|
15
15
|
[[ -n "${VIGTHORIA_TEMPLATE_SERVICE_URL:-}" ]] || block "the live Template Service endpoint is not configured"
|
|
16
16
|
[[ -x dist/index.js ]] || block "dist/index.js is unavailable; build the candidate first"
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
LIVE_TEMP_ROOT="${VIGTHORIA_TEMP_DIR:-$HOME/.vigthoria/tmp}/live-tests"
|
|
19
|
+
mkdir -p "$LIVE_TEMP_ROOT"
|
|
20
|
+
chmod 700 "$LIVE_TEMP_ROOT"
|
|
21
|
+
LIVE_TMP="$(mktemp -d "$LIVE_TEMP_ROOT/services.XXXXXX")"
|
|
19
22
|
trap 'rm -rf "$LIVE_TMP"' EXIT
|
|
20
23
|
export VIGTHORIA_NO_BANNER=1
|
|
21
24
|
export VIGTHORIA_NO_UPDATE_CHECK=1
|
|
@@ -4,12 +4,21 @@ set -euo pipefail
|
|
|
4
4
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
5
5
|
cd "$ROOT"
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
VALIDATION_TEMP_ROOT="${VIGTHORIA_VALIDATION_TEMP_DIR:-$(dirname "$ROOT")/.vigthoria-cli-validation}"
|
|
8
|
+
mkdir -p "$VALIDATION_TEMP_ROOT"
|
|
9
|
+
chmod 700 "$VALIDATION_TEMP_ROOT"
|
|
10
|
+
find "$VALIDATION_TEMP_ROOT" -mindepth 1 -maxdepth 1 -type d -name 'run.*' -mmin +1440 -user "$(id -un)" -exec rm -rf -- {} +
|
|
11
|
+
VALIDATION_TMP="$(mktemp -d "$VALIDATION_TEMP_ROOT/run.XXXXXX")"
|
|
8
12
|
trap 'rm -rf "$VALIDATION_TMP"' EXIT
|
|
9
13
|
mkdir -p "$VALIDATION_TMP/home"
|
|
14
|
+
mkdir -p "$VALIDATION_TMP/runtime-tmp"
|
|
15
|
+
chmod 700 "$VALIDATION_TMP/runtime-tmp"
|
|
10
16
|
|
|
11
17
|
export HOME="$VALIDATION_TMP/home"
|
|
12
18
|
export USERPROFILE="$HOME"
|
|
19
|
+
export TMPDIR="$VALIDATION_TMP/runtime-tmp"
|
|
20
|
+
export TMP="$TMPDIR"
|
|
21
|
+
export TEMP="$TMPDIR"
|
|
13
22
|
export npm_config_cache="$VALIDATION_TMP/npm-cache"
|
|
14
23
|
export npm_config_update_notifier=false
|
|
15
24
|
export VIGTHORIA_NO_BANNER=1
|
|
@@ -77,6 +86,7 @@ node scripts/test-chat-prompt-policy.mjs
|
|
|
77
86
|
node scripts/test-tool-capability-providers.mjs
|
|
78
87
|
node scripts/test-agent-stream-state.mjs
|
|
79
88
|
node scripts/test-direct-output-policy.mjs
|
|
89
|
+
node scripts/test-runtime-temp.mjs
|
|
80
90
|
|
|
81
91
|
echo "[0.8] authoritative Phase 2 state/auth/session contracts"
|
|
82
92
|
node scripts/test-durable-json-state.mjs
|