mustflow 2.21.1 → 2.21.2
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/cli/commands/run.js +3 -1
- package/dist/cli/commands/verify.js +8 -2
- package/dist/cli/lib/filesystem.js +3 -96
- package/dist/cli/lib/local-index/index.js +1 -1
- package/dist/core/command-effects.js +3 -4
- package/dist/core/run-write-drift.js +7 -3
- package/dist/core/safe-filesystem.js +3 -0
- package/package.json +1 -1
- package/templates/default/manifest.toml +1 -1
package/dist/cli/commands/run.js
CHANGED
|
@@ -171,7 +171,9 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
171
171
|
}
|
|
172
172
|
const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
|
|
173
173
|
const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
|
|
174
|
-
const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName
|
|
174
|
+
const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName, {
|
|
175
|
+
additionalDeclaredPaths: options.additionalDeclaredWritePaths,
|
|
176
|
+
}));
|
|
175
177
|
const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
|
|
176
178
|
const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
|
|
177
179
|
let streamedOutput = false;
|
|
@@ -628,11 +628,12 @@ function testTargetsByScheduledIntent(report) {
|
|
|
628
628
|
candidate.appliedTestTargets.length > 0)
|
|
629
629
|
.map((candidate) => [candidate.intent, candidate.appliedTestTargets]));
|
|
630
630
|
}
|
|
631
|
-
async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = []) {
|
|
631
|
+
async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = [], additionalDeclaredWritePaths = []) {
|
|
632
632
|
const output = createBufferedOutput();
|
|
633
633
|
const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
|
|
634
634
|
writeLatestReceipt: false,
|
|
635
635
|
testTargets,
|
|
636
|
+
additionalDeclaredWritePaths,
|
|
636
637
|
});
|
|
637
638
|
const rawStdout = output.stdout().trim();
|
|
638
639
|
let receipt = null;
|
|
@@ -680,7 +681,12 @@ async function runVerificationEntriesInParallelChunks(entries, parallelism, lang
|
|
|
680
681
|
const results = [];
|
|
681
682
|
for (let index = 0; index < entries.length; index += parallelism) {
|
|
682
683
|
const chunk = entries.slice(index, index + parallelism);
|
|
683
|
-
|
|
684
|
+
const batchDeclaredWritePaths = [
|
|
685
|
+
...new Set(chunk.flatMap((entry) => entry.effects
|
|
686
|
+
.filter((effect) => effect.access === 'write' && typeof effect.path === 'string')
|
|
687
|
+
.map((effect) => effect.path))),
|
|
688
|
+
].sort((left, right) => left.localeCompare(right));
|
|
689
|
+
results.push(...(await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? [], batchDeclaredWritePaths)))));
|
|
684
690
|
}
|
|
685
691
|
return results;
|
|
686
692
|
}
|
|
@@ -1,103 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
3
|
+
export { ensureFileTargetInsideWithoutSymlinks, ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
|
|
4
|
+
import { readFileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
|
|
4
5
|
export function toPosixPath(value) {
|
|
5
6
|
return value.split(path.sep).join('/');
|
|
6
7
|
}
|
|
7
|
-
export function ensureInside(parentPath, childPath) {
|
|
8
|
-
const parent = path.resolve(parentPath);
|
|
9
|
-
const child = path.resolve(childPath);
|
|
10
|
-
const relative = path.relative(parent, child);
|
|
11
|
-
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
throw new Error(`Path escapes allowed directory: ${childPath}`);
|
|
15
|
-
}
|
|
16
|
-
function isMissingPathError(error) {
|
|
17
|
-
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
18
|
-
}
|
|
19
|
-
export function ensureInsideWithoutSymlinks(parentPath, childPath, options = {}) {
|
|
20
|
-
ensureInside(parentPath, childPath);
|
|
21
|
-
const parent = path.resolve(parentPath);
|
|
22
|
-
const child = path.resolve(childPath);
|
|
23
|
-
const relative = path.relative(parent, child);
|
|
24
|
-
const segments = relative === '' ? [] : relative.split(path.sep).filter((segment) => segment.length > 0);
|
|
25
|
-
let currentPath = parent;
|
|
26
|
-
const parentStats = lstatSync(parent);
|
|
27
|
-
if (parentStats.isSymbolicLink()) {
|
|
28
|
-
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
29
|
-
}
|
|
30
|
-
for (const [index, segment] of segments.entries()) {
|
|
31
|
-
currentPath = path.join(currentPath, segment);
|
|
32
|
-
const isLeaf = index === segments.length - 1;
|
|
33
|
-
try {
|
|
34
|
-
const stats = lstatSync(currentPath);
|
|
35
|
-
if (stats.isSymbolicLink()) {
|
|
36
|
-
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
37
|
-
}
|
|
38
|
-
if (!isLeaf && !stats.isDirectory()) {
|
|
39
|
-
throw new Error(`Path component is not a directory: ${currentPath}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (isMissingPathError(error) && options.allowMissingLeaf) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath) {
|
|
51
|
-
return readFileInsideWithoutSymlinks(parentPath, childPath).toString('utf8');
|
|
52
|
-
}
|
|
53
|
-
export function readFileInsideWithoutSymlinks(parentPath, childPath) {
|
|
54
|
-
const absoluteChildPath = path.resolve(childPath);
|
|
55
|
-
ensureInsideWithoutSymlinks(parentPath, absoluteChildPath);
|
|
56
|
-
const fileDescriptor = openSync(absoluteChildPath, constants.O_RDONLY | NOFOLLOW_FLAG);
|
|
57
|
-
try {
|
|
58
|
-
return readFileSync(fileDescriptor);
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
closeSync(fileDescriptor);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export function ensureFileTargetInsideWithoutSymlinks(parentPath, childPath, options = {}) {
|
|
65
|
-
const absoluteChildPath = path.resolve(childPath);
|
|
66
|
-
ensureInside(parentPath, absoluteChildPath);
|
|
67
|
-
ensureInsideWithoutSymlinks(parentPath, path.dirname(absoluteChildPath), { allowMissingLeaf: true });
|
|
68
|
-
try {
|
|
69
|
-
const stats = lstatSync(absoluteChildPath);
|
|
70
|
-
if (stats.isSymbolicLink()) {
|
|
71
|
-
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
72
|
-
}
|
|
73
|
-
if (!stats.isFile()) {
|
|
74
|
-
throw new Error(`Path must be a regular file: ${childPath}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
if (isMissingPathError(error) && options.allowMissingLeaf) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
export function writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
85
|
-
writeFileInsideWithoutSymlinks(parentPath, childPath, content);
|
|
86
|
-
}
|
|
87
|
-
export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
88
|
-
const absoluteChildPath = path.resolve(childPath);
|
|
89
|
-
const directoryPath = path.dirname(absoluteChildPath);
|
|
90
|
-
ensureInsideWithoutSymlinks(parentPath, directoryPath, { allowMissingLeaf: true });
|
|
91
|
-
mkdirSync(directoryPath, { recursive: true });
|
|
92
|
-
ensureFileTargetInsideWithoutSymlinks(parentPath, absoluteChildPath, { allowMissingLeaf: true });
|
|
93
|
-
const fileDescriptor = openSync(absoluteChildPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | NOFOLLOW_FLAG);
|
|
94
|
-
try {
|
|
95
|
-
writeFileSync(fileDescriptor, content);
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
closeSync(fileDescriptor);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
8
|
export function copyFileInsideWithoutSymlinks(sourceParentPath, sourcePath, targetParentPath, targetPath) {
|
|
102
9
|
const content = readFileInsideWithoutSymlinks(sourceParentPath, sourcePath);
|
|
103
10
|
writeFileInsideWithoutSymlinks(targetParentPath, targetPath, content);
|
|
@@ -2245,7 +2245,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2245
2245
|
max_snippet_bytes_per_document: MAX_SNIPPET_BYTES_PER_DOCUMENT,
|
|
2246
2246
|
excluded_raw_data_kinds: [...LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS],
|
|
2247
2247
|
indexed_file_count: indexedFiles.length,
|
|
2248
|
-
indexed_paths:
|
|
2248
|
+
indexed_paths: indexedFiles.map((file) => file.path),
|
|
2249
2249
|
};
|
|
2250
2250
|
}
|
|
2251
2251
|
function readStoredSchemaVersion(database) {
|
|
@@ -17,12 +17,11 @@ function validateEffectPath(projectRoot, intent, rawPath) {
|
|
|
17
17
|
const cwd = resolveSafeProjectCwd(projectRoot, readString(intent, 'cwd'));
|
|
18
18
|
const resolved = path.resolve(cwd, rawPath);
|
|
19
19
|
const root = path.resolve(projectRoot);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
if (resolvedLower !== rootLower && !resolvedLower.startsWith(`${rootLower}${path.sep}`)) {
|
|
20
|
+
const relative = path.relative(root, resolved);
|
|
21
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
23
22
|
throw new Error(`Command effect path must stay inside the current root: ${rawPath}`);
|
|
24
23
|
}
|
|
25
|
-
return normalizeRelativePath(
|
|
24
|
+
return normalizeRelativePath(relative);
|
|
26
25
|
}
|
|
27
26
|
function readResourcePaths(commandContract, lock) {
|
|
28
27
|
const resource = commandContract.resources[lock];
|
|
@@ -11,7 +11,7 @@ const GIT_STATUS_UNTRACKED_MODE = 'normal';
|
|
|
11
11
|
const MAX_HASH_BYTES = 5 * 1024 * 1024;
|
|
12
12
|
const RECURSIVE_SNAPSHOT_ENV = 'MUSTFLOW_WRITE_DRIFT_SNAPSHOT';
|
|
13
13
|
const EXCLUDED_DIRECTORY_NAMES = new Set(['.git', 'node_modules']);
|
|
14
|
-
const EXCLUDED_RELATIVE_DIRECTORY_PATHS = new Set(['.mustflow/state/runs']);
|
|
14
|
+
const EXCLUDED_RELATIVE_DIRECTORY_PATHS = new Set(['.mustflow/state/perf', '.mustflow/state/runs']);
|
|
15
15
|
function isRecursiveSnapshotEnabled() {
|
|
16
16
|
const value = process.env[RECURSIVE_SNAPSHOT_ENV];
|
|
17
17
|
return value === '1' || value?.toLowerCase() === 'true';
|
|
@@ -186,10 +186,14 @@ function truncatePaths(paths) {
|
|
|
186
186
|
}
|
|
187
187
|
return { paths: paths.slice(0, MAX_REPORTED_PATHS), truncated: true };
|
|
188
188
|
}
|
|
189
|
-
export function startRunWriteTracking(projectRoot, contract, intentName) {
|
|
189
|
+
export function startRunWriteTracking(projectRoot, contract, intentName, options = {}) {
|
|
190
|
+
const declaredPaths = [
|
|
191
|
+
...listDeclaredWritePaths(projectRoot, contract, intentName),
|
|
192
|
+
...(options.additionalDeclaredPaths ?? []).map(normalizeRelativePath),
|
|
193
|
+
];
|
|
190
194
|
return {
|
|
191
195
|
projectRoot,
|
|
192
|
-
declaredPaths:
|
|
196
|
+
declaredPaths: [...new Set(declaredPaths)].sort((left, right) => left.localeCompare(right)),
|
|
193
197
|
before: captureSnapshot(projectRoot),
|
|
194
198
|
};
|
|
195
199
|
}
|
|
@@ -114,6 +114,9 @@ export function readFileInsideWithoutSymlinks(parentPath, childPath) {
|
|
|
114
114
|
closeSync(fileDescriptor);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath) {
|
|
118
|
+
return readFileInsideWithoutSymlinks(parentPath, childPath).toString('utf8');
|
|
119
|
+
}
|
|
117
120
|
export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
118
121
|
const absoluteChildPath = path.resolve(childPath);
|
|
119
122
|
const directoryPath = path.dirname(absoluteChildPath);
|
package/package.json
CHANGED