mustflow 2.17.0 → 2.18.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/README.md +3 -3
- package/dist/cli/commands/classify.js +13 -3
- package/dist/cli/commands/dashboard.js +2 -1
- package/dist/cli/commands/explain-verify.js +2 -2
- package/dist/cli/commands/impact.js +13 -3
- package/dist/cli/commands/run.js +156 -104
- package/dist/cli/commands/verify.js +157 -45
- package/dist/cli/i18n/en.js +10 -1
- package/dist/cli/i18n/es.js +10 -1
- package/dist/cli/i18n/fr.js +10 -1
- package/dist/cli/i18n/hi.js +10 -1
- package/dist/cli/i18n/ko.js +10 -1
- package/dist/cli/i18n/zh.js +10 -1
- package/dist/cli/lib/git-changes.js +25 -2
- package/dist/cli/lib/local-index/constants.js +4 -1
- package/dist/cli/lib/local-index/index.js +22 -5
- package/dist/cli/lib/repo-map.js +90 -30
- package/dist/cli/lib/run-plan.js +25 -2
- package/dist/cli/lib/validation/index.js +2 -1
- package/dist/core/atomic-state-write.js +31 -0
- package/dist/core/bounded-output.js +23 -1
- package/dist/core/check-issues.js +3 -0
- package/dist/core/command-contract-rules.js +104 -2
- package/dist/core/command-contract-validation.js +71 -9
- package/dist/core/command-intent-eligibility.js +9 -1
- package/dist/core/command-output-limits.js +5 -0
- package/dist/core/completion-verdict.js +2 -1
- package/dist/core/contract-lint.js +10 -1
- package/dist/core/public-json-contracts.js +1 -1
- package/dist/core/run-receipt.js +20 -13
- package/dist/core/source-anchors.js +96 -24
- package/dist/core/verification-evidence.js +4 -1
- package/package.json +1 -1
- package/schemas/README.md +4 -4
- package/schemas/change-verification-report.schema.json +2 -1
- package/schemas/contract-lint-report.schema.json +2 -1
- package/schemas/explain-report.schema.json +1 -0
- package/schemas/latest-run-pointer.schema.json +1 -0
- package/schemas/run-receipt.schema.json +26 -3
- package/schemas/verify-report.schema.json +2 -1
- package/schemas/verify-run-manifest.schema.json +2 -1
- package/templates/default/manifest.toml +1 -1
|
@@ -4,14 +4,17 @@ const CHECK_ISSUE_ID_RULES = [
|
|
|
4
4
|
['mustflow.command_contract.configured_missing_lifecycle', /^Configured intent [^\s]+ must define lifecycle$/u],
|
|
5
5
|
['mustflow.command_contract.configured_missing_run_policy', /^Configured intent [^\s]+ must define run_policy$/u],
|
|
6
6
|
['mustflow.command_contract.oneshot_missing_timeout', /^Oneshot intent [^\s]+ must define timeout_seconds$/u],
|
|
7
|
+
['mustflow.command_contract.max_output_bytes_exceeds_limit', /^\[commands\.(?:defaults|intents\.[^\]]+)\]\.max_output_bytes must be less than or equal to \d+$/u],
|
|
7
8
|
['mustflow.command_contract.oneshot_stdin_not_closed', /^Oneshot intent [^\s]+ must set stdin = "closed"$/u],
|
|
8
9
|
['mustflow.command_contract.long_running_agent_allowed', /^Long-running intent [^\s]+ must not use run_policy = "agent_allowed"$/u],
|
|
9
10
|
['mustflow.command_contract.executable_source_missing', /^Configured intent [^\s]+ must define argv or mode = "shell" with cmd$/u],
|
|
10
11
|
['mustflow.command_contract.shell_background_pattern', /^Shell intent [^\s]+ contains a blocked long-running or background pattern$/u],
|
|
12
|
+
['mustflow.command_contract.long_running_command_pattern', /^Intent [^\s]+ contains a blocked long-running or background command pattern$/u],
|
|
11
13
|
['mustflow.command_contract.success_exit_codes_invalid', /^\[commands\.intents\.[^\]]+\]\.success_exit_codes must be an integer array$/u],
|
|
12
14
|
['mustflow.command_contract.effects_invalid', /^(?:Strict: )?(?:\[commands\.(?:resources|intents\.[^\]]+\.effects)[^\]]*\]|Command effect for intent [^\s]+ must define path, paths, or lock)/u],
|
|
13
15
|
['mustflow.command_contract.effect_path_escape', /^Strict: Command effect path must stay inside the current root:/u],
|
|
14
16
|
['mustflow.command_contract.shared_writes_without_effects', /^Strict warning: configured agent-runnable intents .+ share path:.+ through writes without explicit effects or resource locks$/u],
|
|
17
|
+
['mustflow.command_contract.broad_env_inheritance', /^Strict warning: configured agent-runnable intent [^\s]+ (?:implicitly inherits the host environment|uses env_policy = "inherit")/u],
|
|
15
18
|
['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
|
|
16
19
|
['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
|
|
17
20
|
['mustflow.refresh.hash_method_required', /^Strict: \[refresh\]\.default_method should be "hash_check" for cache-friendly refresh$/u],
|
|
@@ -1,16 +1,39 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { readString, readStringArray } from './config-loading.js';
|
|
2
3
|
const SAFE_COMMAND_INTENT_NAME_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
4
|
+
const SHELL_WRAPPER_COMMANDS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'cmd', 'powershell', 'pwsh']);
|
|
5
|
+
const SHELL_EVALUATION_FLAGS = new Set(['-c', '/c', '-command', '-commandwithargs']);
|
|
6
|
+
const INTERPRETER_EVALUATION_FLAGS = new Map([
|
|
7
|
+
['node', new Set(['-e', '--eval'])],
|
|
8
|
+
['python', new Set(['-c'])],
|
|
9
|
+
['python3', new Set(['-c'])],
|
|
10
|
+
['py', new Set(['-c'])],
|
|
11
|
+
['ruby', new Set(['-e'])],
|
|
12
|
+
['perl', new Set(['-e'])],
|
|
13
|
+
]);
|
|
14
|
+
const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
|
|
15
|
+
const LONG_RUNNING_PACKAGE_SCRIPTS = new Set(['dev', 'start', 'serve', 'watch', 'preview']);
|
|
16
|
+
const LONG_RUNNING_EXECUTABLES = new Set(['nodemon', 'pm2', 'serve', 'http-server', 'live-server', 'webpack-dev-server']);
|
|
3
17
|
export const BACKGROUND_SHELL_PATTERNS = [
|
|
4
|
-
|
|
18
|
+
/(?:^|[^&])&(?!&)\s*$/u,
|
|
5
19
|
/\bnohup\b/iu,
|
|
6
20
|
/\bdisown\b/iu,
|
|
7
21
|
/\bStart-Process\b/iu,
|
|
8
|
-
|
|
22
|
+
/(?:^|[;&|]\s*)start\s+/iu,
|
|
9
23
|
/\bxdg-open\b/iu,
|
|
10
24
|
/\bopen\s+/iu,
|
|
11
25
|
/\bchrome(?:\.exe)?\b/iu,
|
|
12
26
|
/\bchromium(?:\.exe)?\b/iu,
|
|
13
27
|
];
|
|
28
|
+
export const LONG_RUNNING_COMMAND_TEXT_PATTERNS = [
|
|
29
|
+
/\b(?:npm|pnpm|bun|yarn)\s+(?:run\s+)?(?:dev|start|serve|watch|preview)\b/iu,
|
|
30
|
+
/\b(?:nohup|disown)\b/iu,
|
|
31
|
+
/(?:^|[^&])&(?!&)\s*$/u,
|
|
32
|
+
/\bsetInterval\s*\(/u,
|
|
33
|
+
/\bwhile\s*(?:\(\s*true\s*\)|true)\b/iu,
|
|
34
|
+
/\bserve_forever\s*\(/iu,
|
|
35
|
+
/\bcreateServer\s*\([^)]*\)\s*\.listen\s*\(/u,
|
|
36
|
+
];
|
|
14
37
|
export function commandIntentNameIsSafe(intentName) {
|
|
15
38
|
return SAFE_COMMAND_INTENT_NAME_PATTERN.test(intentName);
|
|
16
39
|
}
|
|
@@ -25,3 +48,82 @@ export function shellCommandHasBlockedBackgroundPattern(command) {
|
|
|
25
48
|
export function commandIntentHasBlockedShellBackgroundPattern(intent) {
|
|
26
49
|
return intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd);
|
|
27
50
|
}
|
|
51
|
+
function normalizeExecutableName(value) {
|
|
52
|
+
return path.basename(value).replace(/\.(?:cmd|exe|ps1)$/iu, '').toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
function findFlagPayload(argv, flags) {
|
|
55
|
+
for (let index = 1; index < argv.length - 1; index += 1) {
|
|
56
|
+
if (flags.has(argv[index].toLowerCase())) {
|
|
57
|
+
return argv[index + 1];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function commandTextHasLongRunningPattern(command) {
|
|
63
|
+
return LONG_RUNNING_COMMAND_TEXT_PATTERNS.some((pattern) => pattern.test(command));
|
|
64
|
+
}
|
|
65
|
+
function readPackageScriptName(command, args) {
|
|
66
|
+
if (!PACKAGE_SCRIPT_RUNNERS.has(command)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (args[0] === 'run' && args[1] && !args[1].startsWith('-')) {
|
|
70
|
+
return args[1];
|
|
71
|
+
}
|
|
72
|
+
if (args[0] && LONG_RUNNING_PACKAGE_SCRIPTS.has(args[0])) {
|
|
73
|
+
return args[0];
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function argvHasBlockedLongRunningPattern(argv) {
|
|
78
|
+
const [rawCommand = '', ...args] = argv;
|
|
79
|
+
const command = normalizeExecutableName(rawCommand);
|
|
80
|
+
const shellPayload = SHELL_WRAPPER_COMMANDS.has(command) ? findFlagPayload(argv, SHELL_EVALUATION_FLAGS) : null;
|
|
81
|
+
if (shellPayload && (shellCommandHasBlockedBackgroundPattern(shellPayload) || commandTextHasLongRunningPattern(shellPayload))) {
|
|
82
|
+
return `shell wrapper payload contains a blocked long-running or background pattern: ${shellPayload}`;
|
|
83
|
+
}
|
|
84
|
+
const interpreterFlags = INTERPRETER_EVALUATION_FLAGS.get(command);
|
|
85
|
+
const interpreterPayload = interpreterFlags ? findFlagPayload(argv, interpreterFlags) : null;
|
|
86
|
+
if (interpreterPayload && commandTextHasLongRunningPattern(interpreterPayload)) {
|
|
87
|
+
return `interpreter evaluation payload contains a blocked long-running pattern: ${interpreterPayload}`;
|
|
88
|
+
}
|
|
89
|
+
const packageScriptName = readPackageScriptName(command, args);
|
|
90
|
+
if (packageScriptName && LONG_RUNNING_PACKAGE_SCRIPTS.has(packageScriptName)) {
|
|
91
|
+
return `package-manager script "${packageScriptName}" is commonly long-running`;
|
|
92
|
+
}
|
|
93
|
+
if (LONG_RUNNING_EXECUTABLES.has(command)) {
|
|
94
|
+
return `executable "${command}" is commonly long-running`;
|
|
95
|
+
}
|
|
96
|
+
if (command === 'vite' && !args.includes('build')) {
|
|
97
|
+
return 'vite without build is commonly a development server';
|
|
98
|
+
}
|
|
99
|
+
if (command === 'next' && ['dev', 'start'].includes(args[0] ?? '')) {
|
|
100
|
+
return `next ${args[0]} is commonly long-running`;
|
|
101
|
+
}
|
|
102
|
+
if (command === 'webpack' && (args.includes('--watch') || args.includes('-w') || args.includes('serve'))) {
|
|
103
|
+
return 'webpack watch or serve mode is commonly long-running';
|
|
104
|
+
}
|
|
105
|
+
if (command === 'tsc' && (args.includes('--watch') || args.includes('-w'))) {
|
|
106
|
+
return 'tsc watch mode is long-running';
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
export function commandIntentBlockedCommandPattern(intent) {
|
|
111
|
+
if (intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd)) {
|
|
112
|
+
return {
|
|
113
|
+
code: 'shell_background_pattern',
|
|
114
|
+
detail: 'Shell command contains a blocked long-running or background pattern.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const argv = readStringArray(intent, 'argv');
|
|
118
|
+
if (!argv) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const detail = argvHasBlockedLongRunningPattern(argv);
|
|
122
|
+
if (!detail) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
code: 'long_running_command_pattern',
|
|
127
|
+
detail: `Argv command contains a blocked long-running or background pattern: ${detail}.`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, } from './config-loading.js';
|
|
2
|
-
import { COMMAND_ENV_POLICIES } from './command-env.js';
|
|
2
|
+
import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY } from './command-env.js';
|
|
3
3
|
import { COMMAND_EFFECT_CONCURRENCY, COMMAND_EFFECT_MODES, COMMAND_EFFECT_TYPES, validateCommandEffectLockWarnings, validateCommandEffects, } from './command-effects.js';
|
|
4
|
-
import { commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
4
|
+
import { commandIntentBlockedCommandPattern, commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
5
|
+
import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage } from './command-output-limits.js';
|
|
5
6
|
function commandContractIssue(message) {
|
|
6
7
|
return { message };
|
|
7
8
|
}
|
|
9
|
+
function commandContractWarning(message) {
|
|
10
|
+
return { message, severity: 'warning' };
|
|
11
|
+
}
|
|
8
12
|
function hasOwn(table, key) {
|
|
9
13
|
return Object.prototype.hasOwnProperty.call(table, key);
|
|
10
14
|
}
|
|
@@ -46,6 +50,12 @@ function validatePositiveIntegerField(table, key, label, issues) {
|
|
|
46
50
|
issues.push(commandContractIssue(`${label} must be a positive integer`));
|
|
47
51
|
}
|
|
48
52
|
}
|
|
53
|
+
function validateMaxOutputBytesField(table, key, label, issues) {
|
|
54
|
+
validatePositiveIntegerField(table, key, label, issues);
|
|
55
|
+
if (isPositiveInteger(table[key]) && Number(table[key]) > MAX_COMMAND_OUTPUT_BYTES) {
|
|
56
|
+
issues.push(commandContractIssue(commandMaxOutputBytesLimitMessage(label)));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
49
59
|
function validateAllowedStringField(table, key, label, allowedValues, issues) {
|
|
50
60
|
if (!hasOwn(table, key)) {
|
|
51
61
|
return;
|
|
@@ -70,7 +80,7 @@ function validateCommandDefaults(commandsToml, issues) {
|
|
|
70
80
|
validateAllowedStringField(defaults, 'env_policy', '[commands.defaults].env_policy', COMMAND_ENV_POLICIES, issues);
|
|
71
81
|
validateStringArrayField(defaults, 'env_allowlist', '[commands.defaults].env_allowlist', issues);
|
|
72
82
|
validatePositiveIntegerField(defaults, 'default_timeout_seconds', '[commands.defaults].default_timeout_seconds', issues);
|
|
73
|
-
|
|
83
|
+
validateMaxOutputBytesField(defaults, 'max_output_bytes', '[commands.defaults].max_output_bytes', issues);
|
|
74
84
|
validatePositiveIntegerField(defaults, 'kill_after_seconds', '[commands.defaults].kill_after_seconds', issues);
|
|
75
85
|
}
|
|
76
86
|
function validateCommandResources(commandsToml, issues) {
|
|
@@ -148,6 +158,7 @@ function validateCommandIntent(intentName, intent, issues) {
|
|
|
148
158
|
validateAllowedStringField(intent, 'run_policy', `[commands.intents.${intentName}].run_policy`, COMMAND_RUN_POLICIES, issues);
|
|
149
159
|
validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
|
|
150
160
|
validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
|
|
161
|
+
validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
|
|
151
162
|
validateCommandIntentSelection(intentName, intent, issues);
|
|
152
163
|
if (intent.status !== 'configured') {
|
|
153
164
|
return;
|
|
@@ -178,6 +189,10 @@ function validateCommandIntent(intentName, intent, issues) {
|
|
|
178
189
|
if (commandIntentHasBlockedShellBackgroundPattern(intent)) {
|
|
179
190
|
issues.push(commandContractIssue(`Shell intent ${intentName} contains a blocked long-running or background pattern`));
|
|
180
191
|
}
|
|
192
|
+
const blockedCommandPattern = commandIntentBlockedCommandPattern(intent);
|
|
193
|
+
if (blockedCommandPattern?.code === 'long_running_command_pattern') {
|
|
194
|
+
issues.push(commandContractIssue(`Intent ${intentName} contains a blocked long-running or background command pattern`));
|
|
195
|
+
}
|
|
181
196
|
if (hasOwn(intent, 'success_exit_codes')) {
|
|
182
197
|
const value = intent.success_exit_codes;
|
|
183
198
|
if (!Array.isArray(value) || value.length === 0 || value.some((entry) => !Number.isInteger(entry))) {
|
|
@@ -186,6 +201,50 @@ function validateCommandIntent(intentName, intent, issues) {
|
|
|
186
201
|
}
|
|
187
202
|
validateCommandIntentEffects(intentName, intent, issues);
|
|
188
203
|
}
|
|
204
|
+
function readValidCommandEnvPolicy(table) {
|
|
205
|
+
if (!table || !hasOwn(table, 'env_policy')) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const value = table.env_policy;
|
|
209
|
+
return typeof value === 'string' && COMMAND_ENV_POLICIES.has(value)
|
|
210
|
+
? value
|
|
211
|
+
: undefined;
|
|
212
|
+
}
|
|
213
|
+
function getEffectiveCommandEnvPolicy(defaults, intent) {
|
|
214
|
+
const intentPolicy = readValidCommandEnvPolicy(intent);
|
|
215
|
+
if (intentPolicy) {
|
|
216
|
+
return { policy: intentPolicy, source: 'intent' };
|
|
217
|
+
}
|
|
218
|
+
const defaultPolicy = readValidCommandEnvPolicy(defaults);
|
|
219
|
+
if (defaultPolicy) {
|
|
220
|
+
return { policy: defaultPolicy, source: 'defaults' };
|
|
221
|
+
}
|
|
222
|
+
return { policy: DEFAULT_COMMAND_ENV_POLICY, source: 'implicit' };
|
|
223
|
+
}
|
|
224
|
+
function validateCommandEnvInheritanceWarnings(commandsToml) {
|
|
225
|
+
const issues = [];
|
|
226
|
+
if (!commandsToml || !isRecord(commandsToml.intents)) {
|
|
227
|
+
return issues;
|
|
228
|
+
}
|
|
229
|
+
const defaults = isRecord(commandsToml.defaults) ? commandsToml.defaults : undefined;
|
|
230
|
+
for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
|
|
231
|
+
if (!isRecord(intent) || intent.status !== 'configured' || intent.run_policy !== 'agent_allowed') {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const envPolicy = getEffectiveCommandEnvPolicy(defaults, intent);
|
|
235
|
+
if (envPolicy.policy !== 'inherit') {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const networkScope = intent.network === true ? ' with network = true' : '';
|
|
239
|
+
const migration = 'set env_policy = "minimal" or "allowlist" unless broad host state is required';
|
|
240
|
+
if (envPolicy.source === 'implicit') {
|
|
241
|
+
issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} implicitly inherits the host environment${networkScope}; ${migration}`));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} uses env_policy = "inherit"${networkScope}; ${migration}`));
|
|
245
|
+
}
|
|
246
|
+
return issues;
|
|
247
|
+
}
|
|
189
248
|
/**
|
|
190
249
|
* mf:anchor core.command-contract-validation
|
|
191
250
|
* purpose: Validate command intent declarations that gate agent-executable repository commands.
|
|
@@ -216,15 +275,18 @@ export function validateCommandContractConfig(commandsToml) {
|
|
|
216
275
|
}
|
|
217
276
|
export function validateCommandContractStrictDefaults(projectRoot, commandsToml) {
|
|
218
277
|
const issues = [];
|
|
219
|
-
if (!commandsToml
|
|
278
|
+
if (!commandsToml) {
|
|
220
279
|
return issues;
|
|
221
280
|
}
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
281
|
+
if (isRecord(commandsToml.defaults)) {
|
|
282
|
+
if (!hasOwn(commandsToml.defaults, 'max_output_bytes')) {
|
|
283
|
+
issues.push(commandContractIssue('[commands.defaults].max_output_bytes is required'));
|
|
284
|
+
}
|
|
285
|
+
if (!hasOwn(commandsToml.defaults, 'on_timeout')) {
|
|
286
|
+
issues.push(commandContractIssue('[commands.defaults].on_timeout is required'));
|
|
287
|
+
}
|
|
227
288
|
}
|
|
289
|
+
issues.push(...validateCommandEnvInheritanceWarnings(commandsToml));
|
|
228
290
|
issues.push(...validateCommandEffects(projectRoot, commandsToml));
|
|
229
291
|
issues.push(...validateCommandEffectLockWarnings(commandsToml));
|
|
230
292
|
return issues;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRecord, readString } from './config-loading.js';
|
|
2
|
-
import { commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
2
|
+
import { commandIntentBlockedCommandPattern, commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
3
3
|
export function evaluateCommandIntentEligibility(intentName, rawIntent) {
|
|
4
4
|
if (!commandIntentNameIsSafe(intentName)) {
|
|
5
5
|
return {
|
|
@@ -68,6 +68,14 @@ export function evaluateCommandIntentEligibility(intentName, rawIntent) {
|
|
|
68
68
|
detail: 'Shell command contains a blocked long-running or background pattern.',
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
+
const blockedPattern = commandIntentBlockedCommandPattern(rawIntent);
|
|
72
|
+
if (blockedPattern?.code === 'long_running_command_pattern') {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
code: 'blocked_long_running_command_pattern',
|
|
76
|
+
detail: blockedPattern.detail,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
71
79
|
return {
|
|
72
80
|
ok: true,
|
|
73
81
|
code: 'ok',
|
|
@@ -244,7 +244,8 @@ export function createDashboardCompletionVerdict(input) {
|
|
|
244
244
|
const receiptBinding = input.receiptBinding ?? emptyReceiptBindingEvidence();
|
|
245
245
|
const latestRunFailed = input.latestRunStatus === 'failed' ||
|
|
246
246
|
input.latestRunStatus === 'timed_out' ||
|
|
247
|
-
input.latestRunStatus === 'start_failed'
|
|
247
|
+
input.latestRunStatus === 'start_failed' ||
|
|
248
|
+
input.latestRunStatus === 'output_limit_exceeded';
|
|
248
249
|
let status = 'unverified';
|
|
249
250
|
let primaryReason = 'dashboard_does_not_execute_verification';
|
|
250
251
|
const blockers = [];
|
|
@@ -2,7 +2,8 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readPositiveInteger, readString, readStringArray, } from './config-loading.js';
|
|
4
4
|
import { evaluateCommandIntentEligibility, } from './command-intent-eligibility.js';
|
|
5
|
-
import { commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
5
|
+
import { commandIntentBlockedCommandPattern, commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
6
|
+
import { MAX_COMMAND_OUTPUT_BYTES } from './command-output-limits.js';
|
|
6
7
|
import { commandEffectsConflict, normalizeCommandEffects } from './command-effects.js';
|
|
7
8
|
import { listChangeClassificationValidationReasons } from './change-classification.js';
|
|
8
9
|
import { parseSkillIndexRoutes } from './skill-route-alignment.js';
|
|
@@ -309,6 +310,10 @@ function lintIntent(name, value, issues) {
|
|
|
309
310
|
if (lifecycle === 'oneshot' && readPositiveInteger(value, 'timeout_seconds') === undefined) {
|
|
310
311
|
pushIssue(issues, 'error', 'oneshot_missing_timeout', name, `Oneshot intent ${name} must define timeout_seconds.`);
|
|
311
312
|
}
|
|
313
|
+
const maxOutputBytes = readPositiveInteger(value, 'max_output_bytes');
|
|
314
|
+
if (maxOutputBytes !== undefined && maxOutputBytes > MAX_COMMAND_OUTPUT_BYTES) {
|
|
315
|
+
pushIssue(issues, 'error', 'max_output_bytes_exceeds_limit', name, `Intent ${name} max_output_bytes must be less than or equal to ${MAX_COMMAND_OUTPUT_BYTES}.`);
|
|
316
|
+
}
|
|
312
317
|
if (lifecycle === 'oneshot' && readString(value, 'stdin') !== 'closed') {
|
|
313
318
|
pushIssue(issues, 'error', 'oneshot_stdin_not_closed', name, `Oneshot intent ${name} must set stdin to closed.`);
|
|
314
319
|
}
|
|
@@ -321,6 +326,10 @@ function lintIntent(name, value, issues) {
|
|
|
321
326
|
if (commandIntentHasBlockedShellBackgroundPattern(value)) {
|
|
322
327
|
pushIssue(issues, 'error', 'shell_background_pattern', name, `Shell intent ${name} contains a blocked long-running or background pattern.`);
|
|
323
328
|
}
|
|
329
|
+
const blockedCommandPattern = commandIntentBlockedCommandPattern(value);
|
|
330
|
+
if (blockedCommandPattern?.code === 'long_running_command_pattern') {
|
|
331
|
+
pushIssue(issues, 'error', 'long_running_command_pattern', name, `Intent ${name} contains a blocked long-running or background command pattern.`);
|
|
332
|
+
}
|
|
324
333
|
if (!successExitCodesAreValid(value)) {
|
|
325
334
|
pushIssue(issues, 'error', 'invalid_success_exit_codes', name, `Intent ${name} success_exit_codes must be an integer array.`);
|
|
326
335
|
}
|
|
@@ -135,7 +135,7 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
135
135
|
{
|
|
136
136
|
id: 'verify-run-manifest',
|
|
137
137
|
schemaFile: 'verify-run-manifest.schema.json',
|
|
138
|
-
producer: '.mustflow/state/runs/verify
|
|
138
|
+
producer: '.mustflow/state/runs/verify-*/manifest.json',
|
|
139
139
|
packaged: true,
|
|
140
140
|
documented: true,
|
|
141
141
|
},
|
package/dist/core/run-receipt.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
1
|
import { createHash } from 'node:crypto';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { atomicWriteJsonFile, createStateRunId } from './atomic-state-write.js';
|
|
4
|
+
import { decodeUtf8Tail } from './bounded-output.js';
|
|
4
5
|
import { DEFAULT_RUN_RECEIPT_TAIL_BYTES } from './retention-policy.js';
|
|
5
6
|
import { redactSecretLikeText } from './secret-redaction.js';
|
|
6
7
|
const RUN_RECEIPT_SCHEMA_VERSION = '1';
|
|
@@ -11,13 +12,7 @@ function toPosixPath(value) {
|
|
|
11
12
|
}
|
|
12
13
|
function truncateTextByBytes(text, maxBytes) {
|
|
13
14
|
const buffer = Buffer.from(text, 'utf8');
|
|
14
|
-
|
|
15
|
-
return { text, truncated: false };
|
|
16
|
-
}
|
|
17
|
-
return {
|
|
18
|
-
text: buffer.subarray(buffer.byteLength - maxBytes).toString('utf8'),
|
|
19
|
-
truncated: true,
|
|
20
|
-
};
|
|
15
|
+
return decodeUtf8Tail(buffer, maxBytes);
|
|
21
16
|
}
|
|
22
17
|
function recordRedaction(state, field, result) {
|
|
23
18
|
if (!result.redacted) {
|
|
@@ -62,8 +57,11 @@ function summarizeOutput(output, maxOutputBytes, tailBytes, field, state) {
|
|
|
62
57
|
redaction_kinds: redaction.redactionKinds,
|
|
63
58
|
};
|
|
64
59
|
}
|
|
65
|
-
function
|
|
66
|
-
return toPosixPath(path.join(RUN_RECEIPT_DIR,
|
|
60
|
+
export function createRunReceiptRelativePath() {
|
|
61
|
+
return toPosixPath(path.join(RUN_RECEIPT_DIR, createStateRunId('run'), 'receipt.json'));
|
|
62
|
+
}
|
|
63
|
+
function getReceiptRelativePath(receiptPath) {
|
|
64
|
+
return receiptPath ?? toPosixPath(path.join(RUN_RECEIPT_DIR, LATEST_RUN_RECEIPT));
|
|
67
65
|
}
|
|
68
66
|
function stableJson(value) {
|
|
69
67
|
if (Array.isArray(value)) {
|
|
@@ -100,6 +98,9 @@ function getErrorKind(status, exitCode) {
|
|
|
100
98
|
if (status === 'start_failed') {
|
|
101
99
|
return 'start_failed';
|
|
102
100
|
}
|
|
101
|
+
if (status === 'output_limit_exceeded') {
|
|
102
|
+
return 'output_limit_exceeded';
|
|
103
|
+
}
|
|
103
104
|
if (status === 'failed' && exitCode !== null) {
|
|
104
105
|
return 'exit_code';
|
|
105
106
|
}
|
|
@@ -240,6 +241,7 @@ export function createRunReceipt(input) {
|
|
|
240
241
|
signal: input.signal,
|
|
241
242
|
error,
|
|
242
243
|
kill_method: input.killMethod,
|
|
244
|
+
...(input.termination ? { termination: input.termination } : {}),
|
|
243
245
|
stdout,
|
|
244
246
|
stderr,
|
|
245
247
|
write_drift: input.writeDrift,
|
|
@@ -272,12 +274,17 @@ export function createRunReceipt(input) {
|
|
|
272
274
|
redaction_kinds: [...redactionState.kinds].sort(),
|
|
273
275
|
fields: [...redactionState.fields].sort(),
|
|
274
276
|
},
|
|
275
|
-
receipt_path: getReceiptRelativePath(),
|
|
277
|
+
receipt_path: getReceiptRelativePath(input.receiptPath),
|
|
276
278
|
};
|
|
277
279
|
}
|
|
278
280
|
export function writeRunReceipt(projectRoot, receipt) {
|
|
279
281
|
const receiptDir = path.join(projectRoot, RUN_RECEIPT_DIR);
|
|
280
282
|
const latestPath = path.join(receiptDir, LATEST_RUN_RECEIPT);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
const receiptPath = path.resolve(projectRoot, receipt.receipt_path);
|
|
284
|
+
const relativeToRunDir = path.relative(receiptDir, receiptPath);
|
|
285
|
+
if (relativeToRunDir.startsWith('..') || path.isAbsolute(relativeToRunDir)) {
|
|
286
|
+
throw new Error(`Run receipt path must stay inside ${RUN_RECEIPT_DIR}`);
|
|
287
|
+
}
|
|
288
|
+
atomicWriteJsonFile(receiptPath, receipt);
|
|
289
|
+
atomicWriteJsonFile(latestPath, receipt);
|
|
283
290
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { SECRET_LIKE_PATTERNS, textContainsSecretLike } from './secret-redaction.js';
|
|
4
4
|
export const SOURCE_ANCHOR_EXTENSIONS = new Set(['.cjs', '.go', '.js', '.jsx', '.mjs', '.py', '.rs', '.ts', '.tsx']);
|
|
@@ -49,23 +49,94 @@ export const SOURCE_ANCHOR_SECRET_LIKE_PATTERNS = SECRET_LIKE_PATTERNS;
|
|
|
49
49
|
function toPosixPath(value) {
|
|
50
50
|
return value.split(path.sep).join('/');
|
|
51
51
|
}
|
|
52
|
-
function
|
|
52
|
+
function pathIsInsideRoot(rootRealPath, candidateRealPath) {
|
|
53
|
+
const relative = path.relative(rootRealPath, candidateRealPath);
|
|
54
|
+
return relative.length === 0 || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
55
|
+
}
|
|
56
|
+
function shouldIncludeSourceAnchorFile(relativePath, options) {
|
|
57
|
+
if (!options.allowedExtensions.has(path.posix.extname(relativePath))) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (options.excludeGeneratedOrVendor && sourceAnchorPathIsGeneratedOrVendor(relativePath)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (options.include.length > 0 && !matchesAnyGlob(relativePath, options.include)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (options.exclude.length > 0 && matchesAnyGlob(relativePath, options.exclude)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
function fileIsWithinSizeLimit(filePath, maxFileBytes) {
|
|
72
|
+
if (typeof maxFileBytes !== 'number' || !Number.isFinite(maxFileBytes) || maxFileBytes <= 0) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return statSync(filePath).size <= maxFileBytes;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function listFilesRecursive(root, options, current = root) {
|
|
53
83
|
if (!existsSync(current)) {
|
|
54
84
|
return [];
|
|
55
85
|
}
|
|
86
|
+
const currentRealPath = realpathSync(current);
|
|
87
|
+
if (!pathIsInsideRoot(options.rootRealPath, currentRealPath) || options.visitedRealDirectories.has(currentRealPath)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
options.visitedRealDirectories.add(currentRealPath);
|
|
56
91
|
const files = [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
92
|
+
const entries = readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const entryPath = path.join(current, entry.name);
|
|
95
|
+
if (options.ignoredDirectoryNames.has(entry.name)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (entry.isDirectory()) {
|
|
99
|
+
files.push(...listFilesRecursive(root, options, entryPath));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (entry.isSymbolicLink()) {
|
|
103
|
+
if (!options.followSymlinks) {
|
|
62
104
|
continue;
|
|
63
105
|
}
|
|
64
|
-
|
|
106
|
+
let realPath;
|
|
107
|
+
try {
|
|
108
|
+
realPath = realpathSync(entryPath);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!pathIsInsideRoot(options.rootRealPath, realPath)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let stat;
|
|
117
|
+
try {
|
|
118
|
+
stat = statSync(entryPath);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (stat.isDirectory()) {
|
|
124
|
+
files.push(...listFilesRecursive(root, options, entryPath));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (stat.isFile()) {
|
|
128
|
+
const relativePath = toPosixPath(path.relative(root, entryPath));
|
|
129
|
+
if (shouldIncludeSourceAnchorFile(relativePath, options) && fileIsWithinSizeLimit(entryPath, options.maxFileBytes)) {
|
|
130
|
+
files.push(relativePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
65
133
|
continue;
|
|
66
134
|
}
|
|
67
|
-
if (
|
|
68
|
-
|
|
135
|
+
if (entry.isFile()) {
|
|
136
|
+
const relativePath = toPosixPath(path.relative(root, entryPath));
|
|
137
|
+
if (shouldIncludeSourceAnchorFile(relativePath, options) && fileIsWithinSizeLimit(entryPath, options.maxFileBytes)) {
|
|
138
|
+
files.push(relativePath);
|
|
139
|
+
}
|
|
69
140
|
}
|
|
70
141
|
}
|
|
71
142
|
return files.sort((left, right) => left.localeCompare(right));
|
|
@@ -127,25 +198,26 @@ function normalizeAllowedExtensions(allowedExtensions) {
|
|
|
127
198
|
function mergeIgnoredDirectoryNames(ignoredDirectoryNames) {
|
|
128
199
|
return new Set([...(ignoredDirectoryNames ?? []), ...SOURCE_ANCHOR_DEFAULT_EXCLUDED_PATH_PARTS]);
|
|
129
200
|
}
|
|
130
|
-
function fileIsWithinSizeLimit(root, relativePath, maxFileBytes) {
|
|
131
|
-
if (typeof maxFileBytes !== 'number' || !Number.isFinite(maxFileBytes) || maxFileBytes <= 0) {
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
const filePath = path.join(root, ...relativePath.split('/'));
|
|
135
|
-
return statSync(filePath).size <= maxFileBytes;
|
|
136
|
-
}
|
|
137
201
|
export function listSourceAnchorFiles(root, options = {}) {
|
|
202
|
+
if (!existsSync(root)) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
138
205
|
const ignoredDirectoryNames = mergeIgnoredDirectoryNames(options.ignoredDirectoryNames);
|
|
139
206
|
const allowedExtensions = normalizeAllowedExtensions(options.allowedExtensions);
|
|
140
207
|
const include = (options.include ?? []).map((pattern) => globToRegExp(pattern));
|
|
141
208
|
const exclude = (options.exclude ?? []).map((pattern) => globToRegExp(pattern));
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.
|
|
209
|
+
const rootRealPath = realpathSync(root);
|
|
210
|
+
return listFilesRecursive(root, {
|
|
211
|
+
ignoredDirectoryNames,
|
|
212
|
+
allowedExtensions,
|
|
213
|
+
include,
|
|
214
|
+
exclude,
|
|
215
|
+
excludeGeneratedOrVendor: options.excludeGeneratedOrVendor === true,
|
|
216
|
+
maxFileBytes: options.maxFileBytes,
|
|
217
|
+
followSymlinks: options.followSymlinks === true,
|
|
218
|
+
rootRealPath,
|
|
219
|
+
visitedRealDirectories: new Set(),
|
|
220
|
+
});
|
|
149
221
|
}
|
|
150
222
|
export function stripSourceAnchorCommentPrefix(line) {
|
|
151
223
|
return line
|
|
@@ -28,7 +28,10 @@ function resultForIntent(results, intent) {
|
|
|
28
28
|
function requirementOutcome(input) {
|
|
29
29
|
if (input.selectedIntents.some((intent) => {
|
|
30
30
|
const result = resultForIntent(input.results, intent);
|
|
31
|
-
return result?.status === 'failed' ||
|
|
31
|
+
return (result?.status === 'failed' ||
|
|
32
|
+
result?.status === 'timed_out' ||
|
|
33
|
+
result?.status === 'start_failed' ||
|
|
34
|
+
result?.status === 'output_limit_exceeded');
|
|
32
35
|
})) {
|
|
33
36
|
return 'contradicted';
|
|
34
37
|
}
|
package/package.json
CHANGED
package/schemas/README.md
CHANGED
|
@@ -35,10 +35,10 @@ Current schemas:
|
|
|
35
35
|
`mf explain verify --reason <event> --json`, `mf explain retention --json`, `mf explain skills --json`,
|
|
36
36
|
and `mf explain surface --json`. Verify explanations include the shared `decisionGraph` evidence model.
|
|
37
37
|
- `verify-report.schema.json`: output of `mf verify --reason <event> --json`, including an
|
|
38
|
-
evidence-based completion verdict and evidence model with a
|
|
39
|
-
selected receipts and skipped checks
|
|
40
|
-
- `verify-run-manifest.schema.json`: `.mustflow/state/runs/verify
|
|
41
|
-
the same completion verdict, evidence model, and coverage matrix as the verify report
|
|
38
|
+
explicit execution aggregate, evidence-based completion verdict, and evidence model with a
|
|
39
|
+
conservative coverage matrix for the selected receipts and skipped checks
|
|
40
|
+
- `verify-run-manifest.schema.json`: `.mustflow/state/runs/verify-*/manifest.json`, including
|
|
41
|
+
the same execution aggregate, completion verdict, evidence model, and coverage matrix as the verify report
|
|
42
42
|
- `change-verification-report.schema.json`: output of `mf verify --reason <event> --plan-only --json` and
|
|
43
43
|
`mf verify --from-classification <classify-report.json> --plan-only --json`, including the `decision_graph` that links
|
|
44
44
|
changed surfaces, classification reasons, command candidates, eligibility, selected or not-selected state,
|
|
@@ -139,7 +139,8 @@
|
|
|
139
139
|
"missing_timeout",
|
|
140
140
|
"missing_command_source",
|
|
141
141
|
"unsafe_intent_name",
|
|
142
|
-
"blocked_shell_background_pattern"
|
|
142
|
+
"blocked_shell_background_pattern",
|
|
143
|
+
"blocked_long_running_command_pattern"
|
|
143
144
|
]
|
|
144
145
|
},
|
|
145
146
|
"runnable": { "type": "boolean" },
|