vigthoria-cli 1.13.26 → 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 +73 -26
- 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 +113 -9
- 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.js +7 -2
- 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.js +16 -4
- package/dist/commands/update-registration.js +2 -2
- 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 +428 -43
- 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 +15 -3
- 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 +16 -41
- package/dist/utils/preview-screenshot-adapter.js +273 -64
- package/dist/utils/runtime-capability.d.ts +7 -0
- package/dist/utils/runtime-capability.js +11 -0
- package/dist/utils/runtime-temp.d.ts +5 -2
- package/dist/utils/runtime-temp.js +125 -29
- package/dist/utils/tools.js +1 -1
- package/dist/utils/v3-stream-events.js +10 -2
- 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;
|
|
@@ -104,6 +125,7 @@ export class RuntimeTempManager {
|
|
|
104
125
|
source;
|
|
105
126
|
maxBytes;
|
|
106
127
|
minimumFreeBytes;
|
|
128
|
+
allocationMaxBytes;
|
|
107
129
|
ttlMs;
|
|
108
130
|
initializedRoot = null;
|
|
109
131
|
initializedIdentity = null;
|
|
@@ -125,20 +147,21 @@ export class RuntimeTempManager {
|
|
|
125
147
|
const platformTemp = this.platform === 'win32'
|
|
126
148
|
? String(this.environment.TEMP || this.environment.TMP || '').trim()
|
|
127
149
|
: String(this.environment.TMPDIR || '').trim();
|
|
128
|
-
this.systemTempRoot = options.systemTempDirectory || platformTemp || os.tmpdir();
|
|
150
|
+
this.systemTempRoot = options.systemTempDirectory || platformTemp || (this.platform === process.platform ? PROCESS_START_SYSTEM_TEMP : os.tmpdir());
|
|
129
151
|
this.sharedTempRoots = [
|
|
130
152
|
options.systemTempDirectory,
|
|
131
153
|
platformTemp,
|
|
132
154
|
this.environment.TEMP,
|
|
133
155
|
this.environment.TMP,
|
|
134
156
|
this.environment.TMPDIR,
|
|
135
|
-
this.platform === process.platform ?
|
|
157
|
+
this.platform === process.platform ? PROCESS_START_SYSTEM_TEMP : undefined,
|
|
136
158
|
].filter((value) => typeof value === 'string' && value.trim().length > 0);
|
|
137
159
|
const resolved = resolveRuntimeTempRoot({ ...options, environment: this.environment, platform: this.platform, homeDirectory: this.homeDirectory });
|
|
138
160
|
this.configuredRoot = resolved.root;
|
|
139
161
|
this.source = resolved.source;
|
|
140
162
|
this.maxBytes = numericSetting('VIGTHORIA_TEMP_MAX_BYTES', this.environment.VIGTHORIA_TEMP_MAX_BYTES, DEFAULT_MAX_BYTES, 64 * MIB, 64 * 1024 * MIB);
|
|
141
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);
|
|
142
165
|
const ttlHours = numericSetting('VIGTHORIA_TEMP_TTL_HOURS', this.environment.VIGTHORIA_TEMP_TTL_HOURS, DEFAULT_TTL_MS / 3_600_000, 1, 720);
|
|
143
166
|
this.ttlMs = ttlHours * 3_600_000;
|
|
144
167
|
}
|
|
@@ -150,7 +173,7 @@ export class RuntimeTempManager {
|
|
|
150
173
|
// also what makes cross-platform policy probes deterministic: a Windows
|
|
151
174
|
// policy instance running in a non-Windows test host must not attempt to
|
|
152
175
|
// create a drive-letter path before it can reject `%TEMP%`.
|
|
153
|
-
if (this.sharedTempRoots.some((root) =>
|
|
176
|
+
if (this.sharedTempRoots.some((root) => isWithinSharedTemp(this.configuredRoot, root, this.platform))
|
|
154
177
|
|| (this.platform !== 'win32' && isKnownSharedPosixTemp(this.configuredRoot))) {
|
|
155
178
|
throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
|
|
156
179
|
}
|
|
@@ -174,7 +197,7 @@ export class RuntimeTempManager {
|
|
|
174
197
|
}
|
|
175
198
|
}
|
|
176
199
|
const pathApi = this.platform === 'win32' ? path.win32 : path.posix;
|
|
177
|
-
if (this.sharedTempRoots.some((root) =>
|
|
200
|
+
if (this.sharedTempRoots.some((root) => isWithinSharedTemp(realRoot, root, this.platform))
|
|
178
201
|
|| (this.platform !== 'win32' && isKnownSharedPosixTemp(realRoot))) {
|
|
179
202
|
throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
|
|
180
203
|
}
|
|
@@ -218,31 +241,53 @@ export class RuntimeTempManager {
|
|
|
218
241
|
}
|
|
219
242
|
return this.status();
|
|
220
243
|
}
|
|
221
|
-
createDirectory(prefix = 'run-') {
|
|
244
|
+
createDirectory(prefix = 'run-', requestedMaxBytes = this.allocationMaxBytes) {
|
|
222
245
|
this.initialize();
|
|
223
246
|
const root = this.requireRoot();
|
|
224
247
|
if (!/^[a-z0-9][a-z0-9-]{0,47}-$/i.test(prefix)) {
|
|
225
248
|
throw new RuntimeTempError('Temporary directory prefix is invalid.', 'TEMP_PREFIX_INVALID');
|
|
226
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
|
+
}
|
|
227
253
|
this.lastCleanup = this.scavenge({ includeLegacy: false });
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
230
|
-
throw new RuntimeTempError('
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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,
|
|
236
285
|
});
|
|
286
|
+
return directory;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
this.releaseLock(path.join(root, ALLOCATION_LOCK), allocationLock);
|
|
237
290
|
}
|
|
238
|
-
const directory = fs.mkdtempSync(path.join(root, prefix));
|
|
239
|
-
if (this.platform !== 'win32')
|
|
240
|
-
fs.chmodSync(directory, 0o700);
|
|
241
|
-
fs.writeFileSync(path.join(directory, LEASE_FILE), `${JSON.stringify({ schemaVersion: 1, pid: this.pid, createdAt: new Date(this.now()).toISOString() })}\n`, {
|
|
242
|
-
flag: 'wx',
|
|
243
|
-
mode: 0o600,
|
|
244
|
-
});
|
|
245
|
-
return directory;
|
|
246
291
|
}
|
|
247
292
|
removeDirectory(directory) {
|
|
248
293
|
const root = this.requireRoot();
|
|
@@ -250,7 +295,7 @@ export class RuntimeTempManager {
|
|
|
250
295
|
if (path.dirname(absolute) !== root || absolute === root) {
|
|
251
296
|
throw new RuntimeTempError('Refusing to remove a path outside the Vigthoria temporary root.', 'TEMP_CLEANUP_ESCAPE');
|
|
252
297
|
}
|
|
253
|
-
fs.rmSync(absolute, { recursive: true, force: true });
|
|
298
|
+
fs.rmSync(absolute, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
|
|
254
299
|
}
|
|
255
300
|
scavenge(options = {}) {
|
|
256
301
|
const root = this.requireRoot();
|
|
@@ -282,6 +327,13 @@ export class RuntimeTempManager {
|
|
|
282
327
|
}
|
|
283
328
|
if (lockAge <= CLEANUP_LOCK_MAX_AGE_MS)
|
|
284
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;
|
|
285
337
|
try {
|
|
286
338
|
fs.unlinkSync(lockPath);
|
|
287
339
|
}
|
|
@@ -387,7 +439,7 @@ export class RuntimeTempManager {
|
|
|
387
439
|
readEntries(root) {
|
|
388
440
|
const entries = [];
|
|
389
441
|
for (const name of fs.readdirSync(root)) {
|
|
390
|
-
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)
|
|
391
443
|
continue;
|
|
392
444
|
const absolutePath = path.join(root, name);
|
|
393
445
|
let stat;
|
|
@@ -400,15 +452,19 @@ export class RuntimeTempManager {
|
|
|
400
452
|
throw error;
|
|
401
453
|
}
|
|
402
454
|
let active = false;
|
|
455
|
+
let reservedBytes = 0;
|
|
403
456
|
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
404
457
|
const leasePath = path.join(absolutePath, LEASE_FILE);
|
|
405
458
|
try {
|
|
406
459
|
const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
407
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
|
+
}
|
|
408
464
|
}
|
|
409
465
|
catch { /* third-party and abandoned directories have no live lease */ }
|
|
410
466
|
}
|
|
411
|
-
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 });
|
|
412
468
|
}
|
|
413
469
|
return entries;
|
|
414
470
|
}
|
|
@@ -456,10 +512,50 @@ export class RuntimeTempManager {
|
|
|
456
512
|
if (path.dirname(entry.absolutePath) !== this.requireRoot()) {
|
|
457
513
|
throw new RuntimeTempError('Temporary cleanup target escaped its root.', 'TEMP_CLEANUP_ESCAPE');
|
|
458
514
|
}
|
|
459
|
-
fs.rmSync(entry.absolutePath, { recursive: true, force: true });
|
|
515
|
+
fs.rmSync(entry.absolutePath, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
|
|
460
516
|
result.removedEntries += 1;
|
|
461
517
|
result.removedBytes += entry.bytes;
|
|
462
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
|
+
}
|
|
463
559
|
cleanupLegacySystemTemp(result) {
|
|
464
560
|
const legacyRoot = this.systemTempRoot;
|
|
465
561
|
if (!fs.existsSync(legacyRoot) || path.resolve(legacyRoot) === this.requireRoot())
|
|
@@ -487,7 +583,7 @@ export class RuntimeTempManager {
|
|
|
487
583
|
if (this.now() - stat.mtimeMs <= this.ttlMs)
|
|
488
584
|
continue;
|
|
489
585
|
const bytes = this.entrySize(candidate);
|
|
490
|
-
fs.rmSync(candidate, { recursive: true, force: true });
|
|
586
|
+
fs.rmSync(candidate, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
|
|
491
587
|
result.legacyRemovedEntries += 1;
|
|
492
588
|
result.legacyRemovedBytes += bytes;
|
|
493
589
|
}
|
|
@@ -504,8 +600,8 @@ export function initializeRuntimeTempStorage() {
|
|
|
504
600
|
export function runtimeTempStatus() {
|
|
505
601
|
return getRuntimeTempManager().initialize();
|
|
506
602
|
}
|
|
507
|
-
export function createRuntimeTempDirectory(prefix) {
|
|
508
|
-
return getRuntimeTempManager().createDirectory(prefix);
|
|
603
|
+
export function createRuntimeTempDirectory(prefix, requestedMaxBytes) {
|
|
604
|
+
return getRuntimeTempManager().createDirectory(prefix, requestedMaxBytes);
|
|
509
605
|
}
|
|
510
606
|
export function removeRuntimeTempDirectory(directory) {
|
|
511
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);
|
|
@@ -61,9 +61,17 @@ export function assertValidAgentPlanEvent(event) {
|
|
|
61
61
|
if (!event || typeof event !== 'object')
|
|
62
62
|
return;
|
|
63
63
|
const record = event;
|
|
64
|
-
if (record.type
|
|
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))
|
|
65
73
|
return;
|
|
66
|
-
const tasks =
|
|
74
|
+
const tasks = taskCandidate;
|
|
67
75
|
const ids = tasks.map((task, index) => {
|
|
68
76
|
const id = dependencyId(task.id ?? task.task_id ?? task.taskId);
|
|
69
77
|
if (!id)
|
|
@@ -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
|
}
|
|
@@ -10,7 +10,7 @@ Use this checklist on real user machines before and after rollout. The goal is t
|
|
|
10
10
|
- [ ] Network access to:
|
|
11
11
|
- `https://coder.vigthoria.io`
|
|
12
12
|
- `https://api.vigthoria.io`
|
|
13
|
-
- [ ] If running browser runtime proofs: Chrome
|
|
13
|
+
- [ ] If running browser runtime proofs: Microsoft Edge, Chrome, or Chromium is installed in a standard OS location
|
|
14
14
|
|
|
15
15
|
Commands:
|
|
16
16
|
|
|
@@ -131,7 +131,7 @@ These are external dependencies, not CLI code regressions:
|
|
|
131
131
|
- Live repo/game/platform E2E requires:
|
|
132
132
|
- `VIGTHORIA_COMMUNITY_EMAIL`
|
|
133
133
|
- `VIGTHORIA_COMMUNITY_PASSWORD`
|
|
134
|
-
- Browser proof requires Chrome
|
|
134
|
+
- Browser proof requires Microsoft Edge, Chrome, or Chromium in a standard OS installation location; the CLI never downloads a browser
|
|
135
135
|
|
|
136
136
|
## 9. Rollback Readiness
|
|
137
137
|
|
|
@@ -58,8 +58,8 @@ MODELS_RESULT="$LIVE_TMP/models.json" node --input-type=module - <<'EOF'
|
|
|
58
58
|
import fs from 'node:fs';
|
|
59
59
|
const ids = new Set((JSON.parse(fs.readFileSync(process.env.MODELS_RESULT, 'utf8')).data || []).map((model) => model.id || ''));
|
|
60
60
|
const hasRouter = ['Vigthoria-v4-Assistant-9B', 'vigthoria-balanced-4b', 'vigthoria-balanced-4b:latest', 'vigthoria-v3-balanced-4b', 'vigthoria-v3-balanced-4b:latest', 'vigthoria-fast-9b', 'vigthoria-fast-9b:latest'].some((model) => ids.has(model));
|
|
61
|
-
const hasCreative = ['Vigthoria-v4-Creative-27B'
|
|
62
|
-
const hasCode = ids.has('Vigthoria-v4-Code-27B') || [...ids].some((model) => /^vigthoria-v3(?:\.\d+)?-code-35b(?:-|:|$)/.test(model));
|
|
61
|
+
const hasCreative = ['Vigthoria-v4.2-Creative-27B'].some((model) => ids.has(model));
|
|
62
|
+
const hasCode = ids.has('Vigthoria-v4.2-Code-27B') || [...ids].some((model) => /^vigthoria-v3(?:\.\d+)?-code-35b(?:-|:|$)/.test(model));
|
|
63
63
|
if (!hasRouter || !hasCreative || !hasCode) throw new Error(`required model families missing: ${JSON.stringify([...ids].sort())}`);
|
|
64
64
|
console.log('[pass] live model inventory gates');
|
|
65
65
|
EOF
|
|
@@ -85,7 +85,7 @@ done
|
|
|
85
85
|
CREATIVE_RESULT="$LIVE_TMP/creative.json" node --input-type=module - <<'EOF'
|
|
86
86
|
import fs from 'node:fs';
|
|
87
87
|
const result = JSON.parse(fs.readFileSync(process.env.CREATIVE_RESULT, 'utf8'));
|
|
88
|
-
if (result.success !== true || result.model !== 'Vigthoria-v4-Code-27B') throw new Error(`unexpected governance result: ${JSON.stringify(result)}`);
|
|
88
|
+
if (result.success !== true || result.model !== 'Vigthoria-v4.2-Code-27B') throw new Error(`unexpected governance result: ${JSON.stringify(result)}`);
|
|
89
89
|
if (result.metadata?.modelFallback?.reason !== 'governance-blocked-model') throw new Error(`missing governance fallback metadata: ${JSON.stringify(result)}`);
|
|
90
90
|
console.log('[pass] live governance fallback metadata');
|
|
91
91
|
EOF
|
|
@@ -12,6 +12,7 @@ VALIDATION_TMP="$(mktemp -d "$VALIDATION_TEMP_ROOT/run.XXXXXX")"
|
|
|
12
12
|
trap 'rm -rf "$VALIDATION_TMP"' EXIT
|
|
13
13
|
mkdir -p "$VALIDATION_TMP/home"
|
|
14
14
|
mkdir -p "$VALIDATION_TMP/runtime-tmp"
|
|
15
|
+
mkdir -p "$VALIDATION_TMP/test-homes"
|
|
15
16
|
chmod 700 "$VALIDATION_TMP/runtime-tmp"
|
|
16
17
|
|
|
17
18
|
export HOME="$VALIDATION_TMP/home"
|
|
@@ -19,6 +20,7 @@ export USERPROFILE="$HOME"
|
|
|
19
20
|
export TMPDIR="$VALIDATION_TMP/runtime-tmp"
|
|
20
21
|
export TMP="$TMPDIR"
|
|
21
22
|
export TEMP="$TMPDIR"
|
|
23
|
+
export VIGTHORIA_TEST_HOME_ROOT="$VALIDATION_TMP/test-homes"
|
|
22
24
|
export npm_config_cache="$VALIDATION_TMP/npm-cache"
|
|
23
25
|
export npm_config_update_notifier=false
|
|
24
26
|
export VIGTHORIA_NO_BANNER=1
|