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.
- package/dist/commands/chat.js +83 -27
- package/dist/commands/config.js +4 -4
- package/dist/commands/fork.d.ts +3 -2
- package/dist/commands/fork.js +124 -123
- package/dist/commands/game.d.ts +8 -0
- package/dist/commands/game.js +118 -10
- package/dist/commands/history.d.ts +0 -1
- package/dist/commands/history.js +8 -22
- package/dist/commands/hub.d.ts +20 -0
- package/dist/commands/hub.js +17 -3
- package/dist/commands/preview.d.ts +1 -0
- package/dist/commands/preview.js +36 -15
- package/dist/commands/product-run-registration.js +1 -1
- package/dist/commands/replay.d.ts +0 -1
- package/dist/commands/replay.js +10 -19
- package/dist/commands/repo.d.ts +35 -2
- package/dist/commands/repo.js +76 -34
- package/dist/commands/update-registration.js +4 -3
- package/dist/commands/workflow.d.ts +4 -0
- package/dist/commands/workflow.js +27 -0
- package/dist/index.js +6 -4
- package/dist/utils/agentRunOutcome.d.ts +7 -0
- package/dist/utils/agentRunOutcome.js +13 -0
- package/dist/utils/api.d.ts +20 -5
- package/dist/utils/api.js +440 -54
- package/dist/utils/command-policy.js +1 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +8 -3
- package/dist/utils/frontend-preview-service.d.ts +1 -0
- package/dist/utils/frontend-preview-service.js +54 -5
- package/dist/utils/model-governance.js +23 -14
- package/dist/utils/model-transport-service.js +1 -1
- package/dist/utils/network-policy.js +16 -4
- package/dist/utils/operator-client.js +23 -4
- package/dist/utils/post-write-validator.js +7 -3
- package/dist/utils/preview-screenshot-adapter.d.ts +19 -3
- package/dist/utils/preview-screenshot-adapter.js +289 -28
- package/dist/utils/runtime-capability.d.ts +7 -0
- package/dist/utils/runtime-capability.js +11 -0
- package/dist/utils/runtime-temp.d.ts +6 -2
- package/dist/utils/runtime-temp.js +143 -27
- package/dist/utils/tools.js +1 -1
- package/dist/utils/v3-stream-events.d.ts +8 -0
- package/dist/utils/v3-stream-events.js +70 -0
- package/dist/utils/v3-workspace-service.d.ts +1 -0
- package/dist/utils/v3-workspace-service.js +38 -1
- package/dist/utils/vigflow-client.d.ts +9 -0
- package/dist/utils/vigflow-client.js +48 -2
- package/dist/utils/workspace-reference.d.ts +8 -0
- package/dist/utils/workspace-reference.js +21 -0
- package/package.json +4 -6
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +2 -2
- package/scripts/release/validate-live-service-gates.sh +3 -3
- package/scripts/release/validate-no-go-gates.sh +2 -0
|
@@ -4,11 +4,14 @@ import * as path from 'node:path';
|
|
|
4
4
|
const MIB = 1024 * 1024;
|
|
5
5
|
const DEFAULT_MAX_BYTES = 1024 * MIB;
|
|
6
6
|
const DEFAULT_MIN_FREE_BYTES = 128 * MIB;
|
|
7
|
+
const DEFAULT_ALLOCATION_MAX_BYTES = 64 * MIB;
|
|
7
8
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
9
|
const CLEANUP_LOCK_MAX_AGE_MS = 10 * 60 * 1000;
|
|
10
|
+
const LOCK_RETRY_WAIT_MS = 20;
|
|
9
11
|
const MAX_SCAN_ENTRIES = 100_000;
|
|
10
12
|
const LEASE_FILE = '.vigthoria-temp-lease.json';
|
|
11
13
|
const CLEANUP_LOCK = '.vigthoria-temp-cleanup.lock';
|
|
14
|
+
const ALLOCATION_LOCK = '.vigthoria-temp-allocation.lock';
|
|
12
15
|
const ROOT_MARKER = '.vigthoria-temp-root.json';
|
|
13
16
|
const LEGACY_CLEANUP_MARKER = '.vigthoria-legacy-temp-cleanup-v1';
|
|
14
17
|
const LEGACY_PREFIXES = [
|
|
@@ -18,6 +21,12 @@ const LEGACY_PREFIXES = [
|
|
|
18
21
|
'vigthoria-cli-install-',
|
|
19
22
|
'puppeteer_dev_chrome_profile-',
|
|
20
23
|
];
|
|
24
|
+
// Capture the operating-system temp root before CLI initialization rewrites
|
|
25
|
+
// TMP/TEMP/TMPDIR to the managed per-user root. `os.tmpdir()` is dynamic on
|
|
26
|
+
// Node: after the rewrite it returns the managed root, which previously meant
|
|
27
|
+
// a second public RuntimeTempManager instance could mistake the original
|
|
28
|
+
// shared %TEMP% for a safe explicit override.
|
|
29
|
+
const PROCESS_START_SYSTEM_TEMP = os.tmpdir();
|
|
21
30
|
export class RuntimeTempError extends Error {
|
|
22
31
|
code;
|
|
23
32
|
details;
|
|
@@ -51,7 +60,15 @@ function rejectUnsafeWindowsPath(value) {
|
|
|
51
60
|
}
|
|
52
61
|
function isKnownSharedPosixTemp(value) {
|
|
53
62
|
const normalized = path.posix.resolve(value);
|
|
54
|
-
return
|
|
63
|
+
return ['/tmp', '/var/tmp', '/usr/tmp'].some((root) => pathIsContained(root, normalized, path.posix));
|
|
64
|
+
}
|
|
65
|
+
function isWithinSharedTemp(candidate, root, platform) {
|
|
66
|
+
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
67
|
+
const normalize = (value) => {
|
|
68
|
+
const resolved = pathApi.resolve(value);
|
|
69
|
+
return platform === 'win32' ? resolved.toLocaleLowerCase('en-US') : resolved;
|
|
70
|
+
};
|
|
71
|
+
return pathIsContained(normalize(root), normalize(candidate), pathApi);
|
|
55
72
|
}
|
|
56
73
|
function pathsEqual(left, right, platform) {
|
|
57
74
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
@@ -91,6 +108,10 @@ function defaultProcessAlive(pid) {
|
|
|
91
108
|
return error?.code === 'EPERM';
|
|
92
109
|
}
|
|
93
110
|
}
|
|
111
|
+
function synchronousWait(milliseconds) {
|
|
112
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
113
|
+
Atomics.wait(signal, 0, 0, milliseconds);
|
|
114
|
+
}
|
|
94
115
|
export class RuntimeTempManager {
|
|
95
116
|
environment;
|
|
96
117
|
platform;
|
|
@@ -99,10 +120,12 @@ export class RuntimeTempManager {
|
|
|
99
120
|
pid;
|
|
100
121
|
isProcessAlive;
|
|
101
122
|
systemTempRoot;
|
|
123
|
+
sharedTempRoots;
|
|
102
124
|
configuredRoot;
|
|
103
125
|
source;
|
|
104
126
|
maxBytes;
|
|
105
127
|
minimumFreeBytes;
|
|
128
|
+
allocationMaxBytes;
|
|
106
129
|
ttlMs;
|
|
107
130
|
initializedRoot = null;
|
|
108
131
|
initializedIdentity = null;
|
|
@@ -121,12 +144,24 @@ export class RuntimeTempManager {
|
|
|
121
144
|
this.now = options.now || Date.now;
|
|
122
145
|
this.pid = options.pid || process.pid;
|
|
123
146
|
this.isProcessAlive = options.isProcessAlive || defaultProcessAlive;
|
|
124
|
-
|
|
147
|
+
const platformTemp = this.platform === 'win32'
|
|
148
|
+
? String(this.environment.TEMP || this.environment.TMP || '').trim()
|
|
149
|
+
: String(this.environment.TMPDIR || '').trim();
|
|
150
|
+
this.systemTempRoot = options.systemTempDirectory || platformTemp || (this.platform === process.platform ? PROCESS_START_SYSTEM_TEMP : os.tmpdir());
|
|
151
|
+
this.sharedTempRoots = [
|
|
152
|
+
options.systemTempDirectory,
|
|
153
|
+
platformTemp,
|
|
154
|
+
this.environment.TEMP,
|
|
155
|
+
this.environment.TMP,
|
|
156
|
+
this.environment.TMPDIR,
|
|
157
|
+
this.platform === process.platform ? PROCESS_START_SYSTEM_TEMP : undefined,
|
|
158
|
+
].filter((value) => typeof value === 'string' && value.trim().length > 0);
|
|
125
159
|
const resolved = resolveRuntimeTempRoot({ ...options, environment: this.environment, platform: this.platform, homeDirectory: this.homeDirectory });
|
|
126
160
|
this.configuredRoot = resolved.root;
|
|
127
161
|
this.source = resolved.source;
|
|
128
162
|
this.maxBytes = numericSetting('VIGTHORIA_TEMP_MAX_BYTES', this.environment.VIGTHORIA_TEMP_MAX_BYTES, DEFAULT_MAX_BYTES, 64 * MIB, 64 * 1024 * MIB);
|
|
129
163
|
this.minimumFreeBytes = numericSetting('VIGTHORIA_TEMP_MIN_FREE_BYTES', this.environment.VIGTHORIA_TEMP_MIN_FREE_BYTES, DEFAULT_MIN_FREE_BYTES, 64 * MIB, 64 * 1024 * MIB);
|
|
164
|
+
this.allocationMaxBytes = numericSetting('VIGTHORIA_TEMP_ALLOCATION_MAX_BYTES', this.environment.VIGTHORIA_TEMP_ALLOCATION_MAX_BYTES, DEFAULT_ALLOCATION_MAX_BYTES, MIB, this.maxBytes);
|
|
130
165
|
const ttlHours = numericSetting('VIGTHORIA_TEMP_TTL_HOURS', this.environment.VIGTHORIA_TEMP_TTL_HOURS, DEFAULT_TTL_MS / 3_600_000, 1, 720);
|
|
131
166
|
this.ttlMs = ttlHours * 3_600_000;
|
|
132
167
|
}
|
|
@@ -134,6 +169,14 @@ export class RuntimeTempManager {
|
|
|
134
169
|
if (this.initializedRoot || this.initializationError)
|
|
135
170
|
return this.status();
|
|
136
171
|
try {
|
|
172
|
+
// Reject the shared OS temp path before touching the filesystem. This is
|
|
173
|
+
// also what makes cross-platform policy probes deterministic: a Windows
|
|
174
|
+
// policy instance running in a non-Windows test host must not attempt to
|
|
175
|
+
// create a drive-letter path before it can reject `%TEMP%`.
|
|
176
|
+
if (this.sharedTempRoots.some((root) => isWithinSharedTemp(this.configuredRoot, root, this.platform))
|
|
177
|
+
|| (this.platform !== 'win32' && isKnownSharedPosixTemp(this.configuredRoot))) {
|
|
178
|
+
throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
|
|
179
|
+
}
|
|
137
180
|
if (this.source === 'per-user-default' && !fs.existsSync(this.homeDirectory)) {
|
|
138
181
|
throw new RuntimeTempError('The user home directory does not exist; managed temporary storage cannot be initialized.', 'TEMP_HOME_UNAVAILABLE');
|
|
139
182
|
}
|
|
@@ -154,7 +197,7 @@ export class RuntimeTempManager {
|
|
|
154
197
|
}
|
|
155
198
|
}
|
|
156
199
|
const pathApi = this.platform === 'win32' ? path.win32 : path.posix;
|
|
157
|
-
if (
|
|
200
|
+
if (this.sharedTempRoots.some((root) => isWithinSharedTemp(realRoot, root, this.platform))
|
|
158
201
|
|| (this.platform !== 'win32' && isKnownSharedPosixTemp(realRoot))) {
|
|
159
202
|
throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
|
|
160
203
|
}
|
|
@@ -198,31 +241,53 @@ export class RuntimeTempManager {
|
|
|
198
241
|
}
|
|
199
242
|
return this.status();
|
|
200
243
|
}
|
|
201
|
-
createDirectory(prefix = 'run-') {
|
|
244
|
+
createDirectory(prefix = 'run-', requestedMaxBytes = this.allocationMaxBytes) {
|
|
202
245
|
this.initialize();
|
|
203
246
|
const root = this.requireRoot();
|
|
204
247
|
if (!/^[a-z0-9][a-z0-9-]{0,47}-$/i.test(prefix)) {
|
|
205
248
|
throw new RuntimeTempError('Temporary directory prefix is invalid.', 'TEMP_PREFIX_INVALID');
|
|
206
249
|
}
|
|
250
|
+
if (!Number.isSafeInteger(requestedMaxBytes) || requestedMaxBytes < MIB || requestedMaxBytes > this.maxBytes) {
|
|
251
|
+
throw new RuntimeTempError('Temporary allocation reservation is outside the managed root contract.', 'TEMP_RESERVATION_INVALID');
|
|
252
|
+
}
|
|
207
253
|
this.lastCleanup = this.scavenge({ includeLegacy: false });
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
throw new RuntimeTempError('
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
254
|
+
const allocationLock = this.acquireLock(path.join(root, ALLOCATION_LOCK));
|
|
255
|
+
if (allocationLock === null) {
|
|
256
|
+
throw new RuntimeTempError('Temporary allocation admission is busy.', 'TEMP_ALLOCATION_BUSY');
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const entries = this.readEntries(root);
|
|
260
|
+
const before = this.measure(root, entries);
|
|
261
|
+
const reservedBytes = entries.reduce((total, entry) => total + Math.max(entry.bytes, entry.active ? entry.reservedBytes : 0), 0);
|
|
262
|
+
if (reservedBytes + requestedMaxBytes > this.maxBytes
|
|
263
|
+
|| (before.freeBytes !== null && before.freeBytes - requestedMaxBytes < this.minimumFreeBytes)) {
|
|
264
|
+
throw new RuntimeTempError('Vigthoria temporary storage has insufficient reserved capacity after cleanup.', 'TEMP_CAPACITY_EXHAUSTED', {
|
|
265
|
+
root,
|
|
266
|
+
usedBytes: before.usedBytes,
|
|
267
|
+
reservedBytes,
|
|
268
|
+
requestedMaxBytes,
|
|
269
|
+
maxBytes: this.maxBytes,
|
|
270
|
+
freeBytes: before.freeBytes,
|
|
271
|
+
minimumFreeBytes: this.minimumFreeBytes,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const directory = fs.mkdtempSync(path.join(root, prefix));
|
|
275
|
+
if (this.platform !== 'win32')
|
|
276
|
+
fs.chmodSync(directory, 0o700);
|
|
277
|
+
fs.writeFileSync(path.join(directory, LEASE_FILE), `${JSON.stringify({
|
|
278
|
+
schemaVersion: 1,
|
|
279
|
+
pid: this.pid,
|
|
280
|
+
createdAt: new Date(this.now()).toISOString(),
|
|
281
|
+
reservedMaxBytes: requestedMaxBytes,
|
|
282
|
+
})}\n`, {
|
|
283
|
+
flag: 'wx',
|
|
284
|
+
mode: 0o600,
|
|
216
285
|
});
|
|
286
|
+
return directory;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
this.releaseLock(path.join(root, ALLOCATION_LOCK), allocationLock);
|
|
217
290
|
}
|
|
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
291
|
}
|
|
227
292
|
removeDirectory(directory) {
|
|
228
293
|
const root = this.requireRoot();
|
|
@@ -230,7 +295,7 @@ export class RuntimeTempManager {
|
|
|
230
295
|
if (path.dirname(absolute) !== root || absolute === root) {
|
|
231
296
|
throw new RuntimeTempError('Refusing to remove a path outside the Vigthoria temporary root.', 'TEMP_CLEANUP_ESCAPE');
|
|
232
297
|
}
|
|
233
|
-
fs.rmSync(absolute, { recursive: true, force: true });
|
|
298
|
+
fs.rmSync(absolute, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
|
|
234
299
|
}
|
|
235
300
|
scavenge(options = {}) {
|
|
236
301
|
const root = this.requireRoot();
|
|
@@ -262,6 +327,13 @@ export class RuntimeTempManager {
|
|
|
262
327
|
}
|
|
263
328
|
if (lockAge <= CLEANUP_LOCK_MAX_AGE_MS)
|
|
264
329
|
return result;
|
|
330
|
+
let ownerPid = 0;
|
|
331
|
+
try {
|
|
332
|
+
ownerPid = Number(fs.readFileSync(lockPath, 'utf8').trim());
|
|
333
|
+
}
|
|
334
|
+
catch { /* malformed stale lock */ }
|
|
335
|
+
if (ownerPid > 0 && this.isProcessAlive(ownerPid))
|
|
336
|
+
return result;
|
|
265
337
|
try {
|
|
266
338
|
fs.unlinkSync(lockPath);
|
|
267
339
|
}
|
|
@@ -367,7 +439,7 @@ export class RuntimeTempManager {
|
|
|
367
439
|
readEntries(root) {
|
|
368
440
|
const entries = [];
|
|
369
441
|
for (const name of fs.readdirSync(root)) {
|
|
370
|
-
if (name === CLEANUP_LOCK || name === ROOT_MARKER || name === LEGACY_CLEANUP_MARKER)
|
|
442
|
+
if (name === CLEANUP_LOCK || name === ALLOCATION_LOCK || name === ROOT_MARKER || name === LEGACY_CLEANUP_MARKER)
|
|
371
443
|
continue;
|
|
372
444
|
const absolutePath = path.join(root, name);
|
|
373
445
|
let stat;
|
|
@@ -380,15 +452,19 @@ export class RuntimeTempManager {
|
|
|
380
452
|
throw error;
|
|
381
453
|
}
|
|
382
454
|
let active = false;
|
|
455
|
+
let reservedBytes = 0;
|
|
383
456
|
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
384
457
|
const leasePath = path.join(absolutePath, LEASE_FILE);
|
|
385
458
|
try {
|
|
386
459
|
const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
387
460
|
active = typeof lease.pid === 'number' && this.isProcessAlive(lease.pid);
|
|
461
|
+
if (Number.isSafeInteger(lease.reservedMaxBytes) && Number(lease.reservedMaxBytes) > 0) {
|
|
462
|
+
reservedBytes = Number(lease.reservedMaxBytes);
|
|
463
|
+
}
|
|
388
464
|
}
|
|
389
465
|
catch { /* third-party and abandoned directories have no live lease */ }
|
|
390
466
|
}
|
|
391
|
-
entries.push({ absolutePath, name, bytes: this.entrySize(absolutePath), modifiedAt: stat.mtimeMs, active });
|
|
467
|
+
entries.push({ absolutePath, name, bytes: this.entrySize(absolutePath), reservedBytes, modifiedAt: stat.mtimeMs, active });
|
|
392
468
|
}
|
|
393
469
|
return entries;
|
|
394
470
|
}
|
|
@@ -436,10 +512,50 @@ export class RuntimeTempManager {
|
|
|
436
512
|
if (path.dirname(entry.absolutePath) !== this.requireRoot()) {
|
|
437
513
|
throw new RuntimeTempError('Temporary cleanup target escaped its root.', 'TEMP_CLEANUP_ESCAPE');
|
|
438
514
|
}
|
|
439
|
-
fs.rmSync(entry.absolutePath, { recursive: true, force: true });
|
|
515
|
+
fs.rmSync(entry.absolutePath, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
|
|
440
516
|
result.removedEntries += 1;
|
|
441
517
|
result.removedBytes += entry.bytes;
|
|
442
518
|
}
|
|
519
|
+
acquireLock(lockPath) {
|
|
520
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
521
|
+
try {
|
|
522
|
+
const descriptor = fs.openSync(lockPath, 'wx', 0o600);
|
|
523
|
+
fs.writeFileSync(descriptor, `${this.pid}\n`);
|
|
524
|
+
return descriptor;
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
if (error?.code !== 'EEXIST')
|
|
528
|
+
throw error;
|
|
529
|
+
try {
|
|
530
|
+
if (this.now() - fs.statSync(lockPath).mtimeMs > CLEANUP_LOCK_MAX_AGE_MS) {
|
|
531
|
+
let ownerPid = 0;
|
|
532
|
+
try {
|
|
533
|
+
ownerPid = Number(fs.readFileSync(lockPath, 'utf8').trim());
|
|
534
|
+
}
|
|
535
|
+
catch { /* malformed stale lock */ }
|
|
536
|
+
if (ownerPid <= 0 || !this.isProcessAlive(ownerPid)) {
|
|
537
|
+
fs.unlinkSync(lockPath);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch (lockError) {
|
|
543
|
+
if (lockError?.code === 'ENOENT')
|
|
544
|
+
continue;
|
|
545
|
+
throw lockError;
|
|
546
|
+
}
|
|
547
|
+
synchronousWait(LOCK_RETRY_WAIT_MS);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
releaseLock(lockPath, descriptor) {
|
|
553
|
+
fs.closeSync(descriptor);
|
|
554
|
+
try {
|
|
555
|
+
fs.unlinkSync(lockPath);
|
|
556
|
+
}
|
|
557
|
+
catch { /* stale-lock recovery may have raced */ }
|
|
558
|
+
}
|
|
443
559
|
cleanupLegacySystemTemp(result) {
|
|
444
560
|
const legacyRoot = this.systemTempRoot;
|
|
445
561
|
if (!fs.existsSync(legacyRoot) || path.resolve(legacyRoot) === this.requireRoot())
|
|
@@ -467,7 +583,7 @@ export class RuntimeTempManager {
|
|
|
467
583
|
if (this.now() - stat.mtimeMs <= this.ttlMs)
|
|
468
584
|
continue;
|
|
469
585
|
const bytes = this.entrySize(candidate);
|
|
470
|
-
fs.rmSync(candidate, { recursive: true, force: true });
|
|
586
|
+
fs.rmSync(candidate, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
|
|
471
587
|
result.legacyRemovedEntries += 1;
|
|
472
588
|
result.legacyRemovedBytes += bytes;
|
|
473
589
|
}
|
|
@@ -484,8 +600,8 @@ export function initializeRuntimeTempStorage() {
|
|
|
484
600
|
export function runtimeTempStatus() {
|
|
485
601
|
return getRuntimeTempManager().initialize();
|
|
486
602
|
}
|
|
487
|
-
export function createRuntimeTempDirectory(prefix) {
|
|
488
|
-
return getRuntimeTempManager().createDirectory(prefix);
|
|
603
|
+
export function createRuntimeTempDirectory(prefix, requestedMaxBytes) {
|
|
604
|
+
return getRuntimeTempManager().createDirectory(prefix, requestedMaxBytes);
|
|
489
605
|
}
|
|
490
606
|
export function removeRuntimeTempDirectory(directory) {
|
|
491
607
|
getRuntimeTempManager().removeDirectory(directory);
|
package/dist/utils/tools.js
CHANGED
|
@@ -1344,7 +1344,7 @@ export class AgenticTools {
|
|
|
1344
1344
|
validateJavaScriptSyntax(source) {
|
|
1345
1345
|
let tempDirectory;
|
|
1346
1346
|
try {
|
|
1347
|
-
tempDirectory = createRuntimeTempDirectory('syntax-');
|
|
1347
|
+
tempDirectory = createRuntimeTempDirectory('syntax-', 16 * 1024 * 1024);
|
|
1348
1348
|
}
|
|
1349
1349
|
catch (error) {
|
|
1350
1350
|
return this.formatExternalToolError('syntax_check', 'allocate bounded temporary storage', error);
|
|
@@ -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,73 @@ 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 === 'error' && record.code === 'AGENT_PLAN_INVALID') {
|
|
65
|
+
throw new AgentPlanContractError(String(record.message || 'Agent plan graph is invalid.'));
|
|
66
|
+
}
|
|
67
|
+
if (record.type !== 'plan')
|
|
68
|
+
return;
|
|
69
|
+
// Accept every server envelope used by V3/Operator integrations, while
|
|
70
|
+
// ignoring planning keepalives that intentionally have no task graph.
|
|
71
|
+
const taskCandidate = record.plan?.tasks ?? record.tasks ?? record.data?.tasks;
|
|
72
|
+
if (!Array.isArray(taskCandidate))
|
|
73
|
+
return;
|
|
74
|
+
const tasks = taskCandidate;
|
|
75
|
+
const ids = tasks.map((task, index) => {
|
|
76
|
+
const id = dependencyId(task.id ?? task.task_id ?? task.taskId);
|
|
77
|
+
if (!id)
|
|
78
|
+
throw new AgentPlanContractError(`Agent plan task at index ${index} has no stable ID.`);
|
|
79
|
+
return id;
|
|
80
|
+
});
|
|
81
|
+
if (new Set(ids).size !== ids.length)
|
|
82
|
+
throw new AgentPlanContractError('Agent plan contains duplicate task IDs.');
|
|
83
|
+
const idSet = new Set(ids);
|
|
84
|
+
const graph = new Map();
|
|
85
|
+
tasks.forEach((task, index) => {
|
|
86
|
+
const raw = task.depends_on ?? task.dependsOn ?? task.dependencies ?? [];
|
|
87
|
+
const dependencies = (Array.isArray(raw) ? raw : [raw]).map(dependencyId).filter(Boolean);
|
|
88
|
+
for (const dependency of dependencies) {
|
|
89
|
+
if (dependency === ids[index])
|
|
90
|
+
throw new AgentPlanContractError(`Agent plan task ${ids[index]} depends on itself.`);
|
|
91
|
+
if (!idSet.has(dependency))
|
|
92
|
+
throw new AgentPlanContractError(`Agent plan task ${ids[index]} depends on missing task ${dependency}.`);
|
|
93
|
+
}
|
|
94
|
+
graph.set(ids[index], dependencies);
|
|
95
|
+
});
|
|
96
|
+
const visiting = new Set();
|
|
97
|
+
const visited = new Set();
|
|
98
|
+
const visit = (id, trail) => {
|
|
99
|
+
if (visiting.has(id))
|
|
100
|
+
throw new AgentPlanContractError(`Agent plan contains a dependency cycle: ${[...trail, id].join(' -> ')}.`);
|
|
101
|
+
if (visited.has(id))
|
|
102
|
+
return;
|
|
103
|
+
visiting.add(id);
|
|
104
|
+
for (const dependency of graph.get(id) || [])
|
|
105
|
+
visit(dependency, [...trail, id]);
|
|
106
|
+
visiting.delete(id);
|
|
107
|
+
visited.add(id);
|
|
108
|
+
};
|
|
109
|
+
for (const id of ids)
|
|
110
|
+
visit(id, []);
|
|
111
|
+
}
|
|
@@ -21,6 +21,7 @@ export declare class V3WorkspaceService {
|
|
|
21
21
|
relativePath: string;
|
|
22
22
|
absolutePath: string;
|
|
23
23
|
} | null;
|
|
24
|
+
private resolveCaseInsensitiveWorkspacePath;
|
|
24
25
|
writeFile(rootPath: string, rawPath: string, content: string, sourceRoot?: string): boolean;
|
|
25
26
|
deleteFile(rootPath: string, rawPath: string): boolean;
|
|
26
27
|
recover(context?: Record<string, any>, streamedFiles?: Record<string, string>, expectedFiles?: string[]): void;
|
|
@@ -133,6 +133,14 @@ export class V3WorkspaceService {
|
|
|
133
133
|
}
|
|
134
134
|
extractExpectedFiles(message = '', context = {}) {
|
|
135
135
|
const candidates = new Set();
|
|
136
|
+
if (Array.isArray(context.expectedFiles)) {
|
|
137
|
+
for (const value of context.expectedFiles) {
|
|
138
|
+
const filePath = String(value || '').trim().replace(/^\.\//, '');
|
|
139
|
+
if (filePath && !/^https?:\/\//i.test(filePath) && !isSensitivePath(filePath)) {
|
|
140
|
+
candidates.add(filePath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
136
144
|
for (const value of [message, context.rawMessage, context.agentPrompt]) {
|
|
137
145
|
const text = String(value || '');
|
|
138
146
|
const extensions = 'c|cc|cpp|cxx|h|hpp|cs|css|go|html|htm|ini|java|js|jsx|json|kt|kts|md|mjs|cjs|php|ps1|py|rb|rs|scss|sh|sql|svelte|toml|ts|tsx|txt|vue|xml|yaml|yml';
|
|
@@ -208,15 +216,44 @@ export class V3WorkspaceService {
|
|
|
208
216
|
if (relativePath && isSensitivePath(relativePath))
|
|
209
217
|
return null;
|
|
210
218
|
try {
|
|
211
|
-
|
|
219
|
+
let absolutePath = relativePath
|
|
212
220
|
? resolveWorkspacePath(rootPath, relativePath, { allowMissing: true })
|
|
213
221
|
: fs.realpathSync(path.resolve(rootPath));
|
|
222
|
+
if (relativePath && !fs.existsSync(absolutePath)) {
|
|
223
|
+
const canonical = this.resolveCaseInsensitiveWorkspacePath(rootPath, relativePath);
|
|
224
|
+
relativePath = canonical.relativePath;
|
|
225
|
+
absolutePath = canonical.absolutePath;
|
|
226
|
+
}
|
|
214
227
|
return { relativePath: relativePath || '.', absolutePath };
|
|
215
228
|
}
|
|
216
229
|
catch {
|
|
217
230
|
return null;
|
|
218
231
|
}
|
|
219
232
|
}
|
|
233
|
+
resolveCaseInsensitiveWorkspacePath(rootPath, relativePath) {
|
|
234
|
+
const suppliedSegments = relativePath.split('/').filter(Boolean);
|
|
235
|
+
const canonicalSegments = [];
|
|
236
|
+
let current = fs.realpathSync(path.resolve(rootPath));
|
|
237
|
+
for (let index = 0; index < suppliedSegments.length; index += 1) {
|
|
238
|
+
const supplied = suppliedSegments[index];
|
|
239
|
+
let selected = supplied;
|
|
240
|
+
if (fs.existsSync(current) && fs.statSync(current).isDirectory()) {
|
|
241
|
+
const exact = path.join(current, supplied);
|
|
242
|
+
if (!fs.existsSync(exact)) {
|
|
243
|
+
const foldedMatches = fs.readdirSync(current).filter((entry) => entry.toLowerCase() === supplied.toLowerCase());
|
|
244
|
+
if (foldedMatches.length === 1)
|
|
245
|
+
selected = foldedMatches[0];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
canonicalSegments.push(selected);
|
|
249
|
+
current = path.join(current, selected);
|
|
250
|
+
}
|
|
251
|
+
const canonicalRelative = canonicalSegments.join('/');
|
|
252
|
+
return {
|
|
253
|
+
relativePath: canonicalRelative,
|
|
254
|
+
absolutePath: resolveWorkspacePath(rootPath, canonicalRelative, { allowMissing: true }),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
220
257
|
writeFile(rootPath, rawPath, content, sourceRoot) {
|
|
221
258
|
const relativePath = this.normalizeRelativePath(rawPath, sourceRoot || rootPath);
|
|
222
259
|
if (!relativePath || isSensitivePath(relativePath)) {
|
|
@@ -61,6 +61,9 @@ export interface VigFlowTransport {
|
|
|
61
61
|
post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<{
|
|
62
62
|
data: T;
|
|
63
63
|
}>;
|
|
64
|
+
delete<T>(url: string, config?: AxiosRequestConfig): Promise<{
|
|
65
|
+
data: T;
|
|
66
|
+
}>;
|
|
64
67
|
}
|
|
65
68
|
export interface VigFlowClientDependencies {
|
|
66
69
|
getBaseUrls(): string[];
|
|
@@ -76,6 +79,7 @@ export declare class VigFlowClient {
|
|
|
76
79
|
clearCredentials(): void;
|
|
77
80
|
private accessToken;
|
|
78
81
|
private withBackend;
|
|
82
|
+
private withMutationBackend;
|
|
79
83
|
private endpoint;
|
|
80
84
|
listTemplates(options?: {
|
|
81
85
|
category?: string;
|
|
@@ -92,4 +96,9 @@ export declare class VigFlowClient {
|
|
|
92
96
|
executionOptions?: Record<string, unknown>;
|
|
93
97
|
}): Promise<VigFlowExecutionResult>;
|
|
94
98
|
executionStatus(executionId: string): Promise<VigFlowExecutionStatus>;
|
|
99
|
+
deleteWorkflow(selector: string): Promise<{
|
|
100
|
+
id: string;
|
|
101
|
+
name?: string;
|
|
102
|
+
alreadyDeleted?: boolean;
|
|
103
|
+
}>;
|
|
95
104
|
}
|
|
@@ -37,6 +37,29 @@ export class VigFlowClient {
|
|
|
37
37
|
}
|
|
38
38
|
throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
|
|
39
39
|
}
|
|
40
|
+
async withMutationBackend(operation, action) {
|
|
41
|
+
const authenticationErrors = [];
|
|
42
|
+
for (const baseUrl of this.dependencies.getBaseUrls()) {
|
|
43
|
+
let token;
|
|
44
|
+
try {
|
|
45
|
+
token = await this.accessToken(baseUrl);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
authenticationErrors.push(`${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
+
this.dependencies.debug(`VigFlow ${operation} authentication via ${baseUrl} failed:`, error instanceof Error ? error.message : String(error));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Once a mutation request is dispatched its outcome can be ambiguous.
|
|
53
|
+
// Never repeat it against a different backend; callers must reconcile
|
|
54
|
+
// the operation by its identifier before choosing to retry.
|
|
55
|
+
return action(baseUrl, {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
Accept: 'application/json',
|
|
58
|
+
Authorization: `Bearer ${token}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`No VigFlow backend accepted authentication for ${operation}. ${authenticationErrors.join(' | ')}`.trim());
|
|
62
|
+
}
|
|
40
63
|
endpoint(baseUrl, subPath) {
|
|
41
64
|
return /\/api\/vigflow\/?$/i.test(baseUrl) ? `${baseUrl.replace(/\/$/, '')}${subPath}` : `${baseUrl}/api${subPath}`;
|
|
42
65
|
}
|
|
@@ -79,7 +102,7 @@ export class VigFlowClient {
|
|
|
79
102
|
throw new Error(`No VigFlow workflow matched "${normalized}".`);
|
|
80
103
|
}
|
|
81
104
|
useTemplate(templateId, options = {}) {
|
|
82
|
-
return this.
|
|
105
|
+
return this.withMutationBackend('use template', async (baseUrl, headers) => {
|
|
83
106
|
const response = await this.transport.post(this.endpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`), scanOutboundContext({ name: options.name, variables: options.variables || {} }).value, { headers, timeout: 30_000 });
|
|
84
107
|
if (!response.data.workflow?.id)
|
|
85
108
|
throw new Error('VigFlow use-template response did not include a workflow id.');
|
|
@@ -87,7 +110,7 @@ export class VigFlowClient {
|
|
|
87
110
|
});
|
|
88
111
|
}
|
|
89
112
|
runWorkflow(workflowId, options = {}) {
|
|
90
|
-
return this.
|
|
113
|
+
return this.withMutationBackend('run workflow', async (baseUrl, headers) => {
|
|
91
114
|
const response = await this.transport.post(this.endpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`), scanOutboundContext({ data: options.data || {}, options: options.executionOptions || {} }).value, { headers, timeout: 60_000 });
|
|
92
115
|
if (!response.data.executionId)
|
|
93
116
|
throw new Error('VigFlow run response did not include an execution id.');
|
|
@@ -102,4 +125,27 @@ export class VigFlowClient {
|
|
|
102
125
|
return response.data.execution;
|
|
103
126
|
});
|
|
104
127
|
}
|
|
128
|
+
async deleteWorkflow(selector) {
|
|
129
|
+
const normalized = String(selector || '').trim();
|
|
130
|
+
if (!normalized)
|
|
131
|
+
throw new Error('Workflow selector is required. Provide a workflow id or name.');
|
|
132
|
+
// Native VigFlow IDs can be retried idempotently without first listing an
|
|
133
|
+
// object that may already be gone. Human-readable names remain resolved
|
|
134
|
+
// through the ownership-scoped list to avoid ambiguous deletion.
|
|
135
|
+
const target = /^(?:wf_[a-z0-9_-]+|[0-9]+|[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i.test(normalized)
|
|
136
|
+
? { id: normalized, name: undefined }
|
|
137
|
+
: await this.resolveWorkflow(normalized);
|
|
138
|
+
return this.withMutationBackend('delete workflow', async (baseUrl, headers) => {
|
|
139
|
+
try {
|
|
140
|
+
const response = await this.transport.delete(this.endpoint(baseUrl, `/workflows/${encodeURIComponent(target.id)}`), { headers, timeout: 30_000 });
|
|
141
|
+
return { id: target.id, name: target.name, alreadyDeleted: response.data.alreadyDeleted === true };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (Number(error?.response?.status) === 404) {
|
|
145
|
+
return { id: target.id, name: target.name, alreadyDeleted: true };
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
105
151
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return a stable, non-filesystem workspace label for hosted history APIs.
|
|
3
|
+
*
|
|
4
|
+
* Client absolute paths are private machine metadata and must never be used as
|
|
5
|
+
* server workspace roots. File access is provided separately by the
|
|
6
|
+
* authenticated client-tool bridge.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildLocalWorkspaceReference(workspacePath: string, accountNamespace?: string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
/**
|
|
4
|
+
* Return a stable, non-filesystem workspace label for hosted history APIs.
|
|
5
|
+
*
|
|
6
|
+
* Client absolute paths are private machine metadata and must never be used as
|
|
7
|
+
* server workspace roots. File access is provided separately by the
|
|
8
|
+
* authenticated client-tool bridge.
|
|
9
|
+
*/
|
|
10
|
+
export function buildLocalWorkspaceReference(workspacePath, accountNamespace = '') {
|
|
11
|
+
const normalized = String(workspacePath || 'workspace').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
12
|
+
const displayName = path.posix.basename(normalized) || 'workspace';
|
|
13
|
+
const canonicalIdentity = process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
14
|
+
const opaqueId = createHash('sha256')
|
|
15
|
+
.update(String(accountNamespace || 'anonymous'))
|
|
16
|
+
.update('\0')
|
|
17
|
+
.update(canonicalIdentity)
|
|
18
|
+
.digest('hex')
|
|
19
|
+
.slice(0, 24);
|
|
20
|
+
return `vigthoria://local-workspace/${encodeURIComponent(displayName)}?id=${opaqueId}`;
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigthoria-cli",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.29",
|
|
4
4
|
"description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -84,13 +84,14 @@
|
|
|
84
84
|
"test:legion:billing:e2e": "npm run build && node scripts/test-legion-godmode-billing-e2e.js",
|
|
85
85
|
"test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js",
|
|
86
86
|
"test:v3-workspace-path": "npm run build && node scripts/test-v3-workspace-path.js",
|
|
87
|
+
"test:workspace:reference": "npm run build && node scripts/test-workspace-reference.mjs",
|
|
87
88
|
"test:v3-server-tool-execution": "npm run build && node scripts/test-v3-server-tool-execution.js",
|
|
88
89
|
"test:session:project-match": "npm run build && node scripts/test-session-project-match.mjs",
|
|
89
90
|
"test:external-client-resume": "npm run build && node scripts/test-external-client-resume.mjs",
|
|
90
91
|
"test:v3-stream-mutation": "npm run build && node scripts/test-v3-stream-mutation.js",
|
|
91
92
|
"test:context:budget": "npm run build && node scripts/test-context-budget.js",
|
|
92
93
|
"test:v3-client-tool-quality": "npm run build && node scripts/test-v3-client-tool-quality.mjs",
|
|
93
|
-
"test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.
|
|
94
|
+
"test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.mjs",
|
|
94
95
|
"test:game:command": "node scripts/test-game-command.mjs",
|
|
95
96
|
"test:security:dependencies": "node scripts/test-dependency-security.mjs",
|
|
96
97
|
"test:security:network": "npm run build && node scripts/test-network-policy.mjs && node scripts/test-installer-trust-policy.mjs && node scripts/test-local-security-service.mjs",
|
|
@@ -131,7 +132,6 @@
|
|
|
131
132
|
"marked": "^11.0.0",
|
|
132
133
|
"marked-terminal": "^6.2.0",
|
|
133
134
|
"ora": "^7.0.1",
|
|
134
|
-
"puppeteer": "^24.40.0",
|
|
135
135
|
"ws": "^8.14.2"
|
|
136
136
|
},
|
|
137
137
|
"devDependencies": {
|
|
@@ -148,8 +148,6 @@
|
|
|
148
148
|
"overrides": {
|
|
149
149
|
"glob": "^13.0.6",
|
|
150
150
|
"brace-expansion": "5.0.9",
|
|
151
|
-
"fast-uri": "3.1.5"
|
|
152
|
-
"ip-address": "10.3.1",
|
|
153
|
-
"js-yaml": "4.3.1"
|
|
151
|
+
"fast-uri": "3.1.5"
|
|
154
152
|
}
|
|
155
153
|
}
|