mustflow 2.18.20 → 2.21.1
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/classify.js +2 -3
- package/dist/cli/commands/doctor.js +46 -6
- package/dist/cli/commands/run/output.js +1 -1
- package/dist/cli/commands/run/receipt.js +1 -0
- package/dist/cli/commands/verify.js +52 -23
- package/dist/cli/i18n/en.js +1 -0
- package/dist/cli/i18n/es.js +1 -0
- package/dist/cli/i18n/fr.js +1 -0
- package/dist/cli/i18n/hi.js +1 -0
- package/dist/cli/i18n/ko.js +1 -0
- package/dist/cli/i18n/zh.js +1 -0
- package/dist/cli/lib/git-changes.js +7 -1
- package/dist/cli/lib/local-index/index.js +9 -30
- package/dist/cli/lib/repo-map.js +3 -2
- package/dist/cli/lib/run-plan.js +8 -4
- package/dist/core/change-classification.js +24 -2
- package/dist/core/check-issues.js +1 -1
- package/dist/core/command-contract-rules.js +6 -0
- package/dist/core/command-contract-validation.js +24 -10
- package/dist/core/command-output-limits.js +2 -1
- package/dist/core/line-endings.js +12 -4
- package/dist/core/repeated-failure.js +3 -3
- package/dist/core/run-performance-history.js +4 -4
- package/dist/core/run-profile.js +2 -3
- package/dist/core/run-receipt.js +11 -3
- package/dist/core/run-write-drift.js +64 -12
- package/dist/core/safe-filesystem.js +155 -0
- package/package.json +1 -1
- package/schemas/commands.schema.json +1 -0
- package/schemas/doctor-report.schema.json +23 -1
- package/schemas/run-receipt.schema.json +6 -2
- package/templates/default/i18n.toml +13 -13
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +13 -13
- package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +72 -4
- package/templates/default/locales/en/.mustflow/skills/command-contract-authoring/SKILL.md +16 -10
- package/templates/default/locales/en/.mustflow/skills/command-pattern/SKILL.md +64 -7
- package/templates/default/locales/en/.mustflow/skills/database-change-safety/SKILL.md +249 -16
- package/templates/default/locales/en/.mustflow/skills/dependency-reality-check/SKILL.md +37 -7
- package/templates/default/locales/en/.mustflow/skills/migration-safety-check/SKILL.md +74 -10
- package/templates/default/locales/en/.mustflow/skills/performance-budget-check/SKILL.md +132 -5
- package/templates/default/locales/en/.mustflow/skills/pure-core-imperative-shell/SKILL.md +12 -5
- package/templates/default/locales/en/.mustflow/skills/result-option/SKILL.md +4 -2
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +112 -29
- package/templates/default/locales/en/.mustflow/skills/state-machine-pattern/SKILL.md +17 -4
- package/templates/default/locales/en/.mustflow/skills/structure-discovery-gate/SKILL.md +193 -2
- package/templates/default/manifest.toml +1 -1
|
@@ -161,6 +161,7 @@ function validateCommandIntent(intentName, intent, issues) {
|
|
|
161
161
|
validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
|
|
162
162
|
validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
|
|
163
163
|
validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
|
|
164
|
+
validatePositiveIntegerField(intent, 'kill_after_seconds', `[commands.intents.${intentName}].kill_after_seconds`, issues);
|
|
164
165
|
validateCommandIntentSelection(intentName, intent, issues);
|
|
165
166
|
if (intent.status !== 'configured') {
|
|
166
167
|
return;
|
|
@@ -223,10 +224,10 @@ function getEffectiveCommandEnvPolicy(defaults, intent) {
|
|
|
223
224
|
}
|
|
224
225
|
return { policy: DEFAULT_COMMAND_ENV_POLICY, source: 'implicit' };
|
|
225
226
|
}
|
|
226
|
-
function
|
|
227
|
-
const
|
|
227
|
+
export function findCommandEnvInheritanceWarnings(commandsToml) {
|
|
228
|
+
const warnings = [];
|
|
228
229
|
if (!commandsToml || !isRecord(commandsToml.intents)) {
|
|
229
|
-
return
|
|
230
|
+
return warnings;
|
|
230
231
|
}
|
|
231
232
|
const defaults = isRecord(commandsToml.defaults) ? commandsToml.defaults : undefined;
|
|
232
233
|
for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
|
|
@@ -237,13 +238,26 @@ function validateCommandEnvInheritanceWarnings(commandsToml) {
|
|
|
237
238
|
if (envPolicy.policy !== 'inherit') {
|
|
238
239
|
continue;
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
warnings.push({
|
|
242
|
+
intentName,
|
|
243
|
+
source: envPolicy.source,
|
|
244
|
+
network: intent.network === true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return warnings;
|
|
248
|
+
}
|
|
249
|
+
function formatCommandEnvInheritanceWarning(warning) {
|
|
250
|
+
const networkScope = warning.network ? ' with network = true' : '';
|
|
251
|
+
const migration = 'set env_policy = "minimal" or "allowlist" unless broad host state is required';
|
|
252
|
+
if (warning.source === 'implicit') {
|
|
253
|
+
return `configured agent-runnable intent ${warning.intentName} implicitly inherits the host environment${networkScope}; ${migration}`;
|
|
254
|
+
}
|
|
255
|
+
return `configured agent-runnable intent ${warning.intentName} uses env_policy = "inherit"${networkScope}; ${migration}`;
|
|
256
|
+
}
|
|
257
|
+
function validateCommandEnvInheritanceWarnings(commandsToml) {
|
|
258
|
+
const issues = [];
|
|
259
|
+
for (const warning of findCommandEnvInheritanceWarnings(commandsToml)) {
|
|
260
|
+
issues.push(commandContractWarning(formatCommandEnvInheritanceWarning(warning)));
|
|
247
261
|
}
|
|
248
262
|
return issues;
|
|
249
263
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_COMMAND_MAX_OUTPUT_BYTES = 1_048_576;
|
|
2
2
|
export const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
3
|
+
export const COMMAND_OUTPUT_LIMIT_SCOPE = 'per_stream';
|
|
3
4
|
export function commandMaxOutputBytesLimitMessage(label) {
|
|
4
|
-
return `${label} must be less than or equal to ${MAX_COMMAND_OUTPUT_BYTES}`;
|
|
5
|
+
return `${label} must be less than or equal to ${MAX_COMMAND_OUTPUT_BYTES} per output stream`;
|
|
5
6
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { readFileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
4
5
|
const GITATTRIBUTES_PATH = '.gitattributes';
|
|
5
6
|
function toPosixPath(value) {
|
|
6
7
|
return value.split(path.sep).join('/');
|
|
@@ -10,7 +11,7 @@ function hasLfPolicy(projectRoot) {
|
|
|
10
11
|
if (!existsSync(attributesPath)) {
|
|
11
12
|
return false;
|
|
12
13
|
}
|
|
13
|
-
const content =
|
|
14
|
+
const content = readFileInsideWithoutSymlinks(projectRoot, attributesPath).toString('utf8');
|
|
14
15
|
return /^\*\s+.*(?:^|\s)eol=lf(?:\s|$)/imu.test(content);
|
|
15
16
|
}
|
|
16
17
|
function gitList(projectRoot, args) {
|
|
@@ -107,14 +108,21 @@ export function inspectLineEndings(projectRoot, mode, options = {}) {
|
|
|
107
108
|
if (!existsSync(absolutePath)) {
|
|
108
109
|
continue;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
let buffer;
|
|
112
|
+
try {
|
|
113
|
+
buffer = readFileInsideWithoutSymlinks(root, absolutePath);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
111
119
|
const lineEnding = detectLineEnding(buffer);
|
|
112
120
|
const wouldChange = policy === 'lf' && (lineEnding === 'crlf' || lineEnding === 'mixed');
|
|
113
121
|
if (!wouldChange) {
|
|
114
122
|
continue;
|
|
115
123
|
}
|
|
116
124
|
if (canApply) {
|
|
117
|
-
|
|
125
|
+
writeFileInsideWithoutSymlinks(root, absolutePath, normalizeLf(buffer));
|
|
118
126
|
changedFiles.push(toPosixPath(relativePath));
|
|
119
127
|
}
|
|
120
128
|
nonCompliantFiles.push({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
4
5
|
export const REPEATED_FAILURE_STATE_PATH = '.mustflow/state/repeated-failures.json';
|
|
5
6
|
export const REPEATED_FAILURE_STATE_LIMIT = 50;
|
|
6
7
|
const UNRESOLVED_VERIFY_STATUSES = new Set(['failed', 'blocked', 'partial']);
|
|
@@ -59,8 +60,7 @@ function readRepeatedFailureState(projectRoot) {
|
|
|
59
60
|
}
|
|
60
61
|
function writeRepeatedFailureState(projectRoot, state) {
|
|
61
62
|
const statePath = repeatedFailureStatePath(projectRoot);
|
|
62
|
-
|
|
63
|
-
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
63
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, statePath, state);
|
|
64
64
|
}
|
|
65
65
|
export function createVerificationFailureFingerprint(input) {
|
|
66
66
|
const failedIntents = normalizeStrings(input.failedIntents);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
3
4
|
const PERFORMANCE_HISTORY_SCHEMA_VERSION = '1';
|
|
4
5
|
const PERFORMANCE_HISTORY_DIR = path.join('.mustflow', 'state', 'perf');
|
|
5
6
|
const PERFORMANCE_SAMPLES_FILE = 'samples.json';
|
|
@@ -297,9 +298,8 @@ export function recordRunPerformanceHistory(projectRoot, receipt) {
|
|
|
297
298
|
const samples = enforceSizeLimit(pruneSamples([...readSamples(samplesPath), sample], sample.observed_day), sample.observed_day);
|
|
298
299
|
const samplesFile = createSamplesFile(samples);
|
|
299
300
|
const summaryFile = createSummary(samples, sample.observed_day);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
writeFileSync(summaryPath, serialize(summaryFile));
|
|
301
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, samplesPath, samplesFile);
|
|
302
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, summaryPath, summaryFile);
|
|
303
303
|
}
|
|
304
304
|
catch {
|
|
305
305
|
// Performance history is a local optimization hint. A write failure must not affect command execution.
|
package/dist/core/run-profile.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
1
|
import { performance } from 'node:perf_hooks';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
4
4
|
const RUN_PROFILE_SCHEMA_VERSION = '1';
|
|
5
5
|
const RUN_PROFILE_ENV = 'MUSTFLOW_RUN_PROFILE';
|
|
6
6
|
const RUN_PROFILE_DIR = path.join('.mustflow', 'state', 'runs');
|
|
@@ -75,8 +75,7 @@ export class RunProfiler {
|
|
|
75
75
|
profile_path: getProfileRelativePath(),
|
|
76
76
|
};
|
|
77
77
|
const profilePath = path.join(input.projectRoot, RUN_PROFILE_DIR, LATEST_RUN_PROFILE);
|
|
78
|
-
|
|
79
|
-
writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
|
|
78
|
+
writeJsonFileInsideWithoutSymlinks(input.projectRoot, profilePath, profile);
|
|
80
79
|
}
|
|
81
80
|
recordPhase(name, startedAtMs) {
|
|
82
81
|
this.phases.push({
|
package/dist/core/run-receipt.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { createStateRunId } from './atomic-state-write.js';
|
|
4
|
+
import { COMMAND_OUTPUT_LIMIT_SCOPE } from './command-output-limits.js';
|
|
4
5
|
import { decodeUtf8Tail } from './bounded-output.js';
|
|
5
6
|
import { DEFAULT_RUN_RECEIPT_TAIL_BYTES } from './retention-policy.js';
|
|
7
|
+
import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
6
8
|
import { redactSecretLikeText } from './secret-redaction.js';
|
|
7
9
|
const RUN_RECEIPT_SCHEMA_VERSION = '1';
|
|
8
10
|
const RUN_RECEIPT_DIR = path.join('.mustflow', 'state', 'runs');
|
|
@@ -111,6 +113,7 @@ function createPerformanceSummary(input) {
|
|
|
111
113
|
cwd: input.cwd,
|
|
112
114
|
env_allowlist: input.envAllowlist,
|
|
113
115
|
env_policy: input.envPolicy,
|
|
116
|
+
kill_after_seconds: input.killAfterSeconds,
|
|
114
117
|
lifecycle: input.lifecycle,
|
|
115
118
|
max_output_bytes: input.maxOutputBytes,
|
|
116
119
|
mode: input.mode,
|
|
@@ -148,8 +151,10 @@ function createPerformanceSummary(input) {
|
|
|
148
151
|
output_summary: {
|
|
149
152
|
stdout_bytes: input.stdout.bytes,
|
|
150
153
|
stderr_bytes: input.stderr.bytes,
|
|
154
|
+
total_bytes: input.stdout.bytes + input.stderr.bytes,
|
|
151
155
|
stdout_truncated: input.stdout.truncated,
|
|
152
156
|
stderr_truncated: input.stderr.truncated,
|
|
157
|
+
max_output_bytes_scope: COMMAND_OUTPUT_LIMIT_SCOPE,
|
|
153
158
|
},
|
|
154
159
|
result_summary: {
|
|
155
160
|
status: input.status,
|
|
@@ -235,7 +240,9 @@ export function createRunReceipt(input) {
|
|
|
235
240
|
env_policy: input.envPolicy,
|
|
236
241
|
env_allowlist: input.envAllowlist,
|
|
237
242
|
timeout_seconds: input.timeoutSeconds,
|
|
243
|
+
kill_after_seconds: input.killAfterSeconds,
|
|
238
244
|
max_output_bytes: input.maxOutputBytes,
|
|
245
|
+
max_output_bytes_scope: COMMAND_OUTPUT_LIMIT_SCOPE,
|
|
239
246
|
success_exit_codes: input.successExitCodes,
|
|
240
247
|
exit_code: input.exitCode,
|
|
241
248
|
signal: input.signal,
|
|
@@ -260,6 +267,7 @@ export function createRunReceipt(input) {
|
|
|
260
267
|
envPolicy: input.envPolicy,
|
|
261
268
|
envAllowlist: input.envAllowlist,
|
|
262
269
|
timeoutSeconds: input.timeoutSeconds,
|
|
270
|
+
killAfterSeconds: input.killAfterSeconds,
|
|
263
271
|
maxOutputBytes: input.maxOutputBytes,
|
|
264
272
|
successExitCodes: input.successExitCodes,
|
|
265
273
|
exitCode: input.exitCode,
|
|
@@ -285,6 +293,6 @@ export function writeRunReceipt(projectRoot, receipt) {
|
|
|
285
293
|
if (relativeToRunDir.startsWith('..') || path.isAbsolute(relativeToRunDir)) {
|
|
286
294
|
throw new Error(`Run receipt path must stay inside ${RUN_RECEIPT_DIR}`);
|
|
287
295
|
}
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, receiptPath, receipt);
|
|
297
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, latestPath, receipt);
|
|
290
298
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync, readdirSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { normalizeCommandEffects } from './command-effects.js';
|
|
5
6
|
const MAX_SNAPSHOT_FILES = 20_000;
|
|
6
7
|
const MAX_REPORTED_PATHS = 200;
|
|
8
|
+
const GIT_STATUS_TIMEOUT_MS = 10_000;
|
|
9
|
+
const GIT_STATUS_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
const GIT_STATUS_UNTRACKED_MODE = 'normal';
|
|
11
|
+
const MAX_HASH_BYTES = 5 * 1024 * 1024;
|
|
7
12
|
const RECURSIVE_SNAPSHOT_ENV = 'MUSTFLOW_WRITE_DRIFT_SNAPSHOT';
|
|
8
13
|
const EXCLUDED_DIRECTORY_NAMES = new Set(['.git', 'node_modules']);
|
|
9
14
|
const EXCLUDED_RELATIVE_DIRECTORY_PATHS = new Set(['.mustflow/state/runs']);
|
|
@@ -33,6 +38,24 @@ function signatureForPath(fullPath) {
|
|
|
33
38
|
const type = stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : 'other';
|
|
34
39
|
return `${type}:${stat.size}:${stat.mtimeMs}`;
|
|
35
40
|
}
|
|
41
|
+
function signatureForGitStatusPath(projectRoot, relativePath, status) {
|
|
42
|
+
const fullPath = path.join(projectRoot, ...relativePath.split('/'));
|
|
43
|
+
if (!existsSync(fullPath)) {
|
|
44
|
+
return `git:${status}:missing`;
|
|
45
|
+
}
|
|
46
|
+
const stat = lstatSync(fullPath);
|
|
47
|
+
if (stat.isSymbolicLink()) {
|
|
48
|
+
return `git:${status}:symlink:${readlinkSync(fullPath)}`;
|
|
49
|
+
}
|
|
50
|
+
if (!stat.isFile()) {
|
|
51
|
+
return `git:${status}:${stat.isDirectory() ? 'directory' : 'other'}:${stat.size}:${stat.mtimeMs}`;
|
|
52
|
+
}
|
|
53
|
+
if (stat.size > MAX_HASH_BYTES) {
|
|
54
|
+
return `git:${status}:file:${stat.size}:${stat.mtimeMs}:unhashed`;
|
|
55
|
+
}
|
|
56
|
+
const digest = createHash('sha256').update(readFileSync(fullPath)).digest('hex');
|
|
57
|
+
return `git:${status}:file:${stat.size}:${digest}`;
|
|
58
|
+
}
|
|
36
59
|
function collectSnapshotEntries(projectRoot, currentPath, entries) {
|
|
37
60
|
const names = readdirSync(currentPath).sort((left, right) => left.localeCompare(right));
|
|
38
61
|
for (const name of names) {
|
|
@@ -59,7 +82,7 @@ function captureSnapshot(projectRoot) {
|
|
|
59
82
|
}
|
|
60
83
|
if (!isRecursiveSnapshotEnabled()) {
|
|
61
84
|
return {
|
|
62
|
-
|
|
85
|
+
status: 'unavailable',
|
|
63
86
|
entries: new Map(),
|
|
64
87
|
reason: 'git_status_unavailable_recursive_snapshot_disabled',
|
|
65
88
|
source: 'unavailable',
|
|
@@ -68,11 +91,11 @@ function captureSnapshot(projectRoot) {
|
|
|
68
91
|
try {
|
|
69
92
|
const entries = new Map();
|
|
70
93
|
collectSnapshotEntries(projectRoot, projectRoot, entries);
|
|
71
|
-
return {
|
|
94
|
+
return { status: 'checked', entries, reason: null, source: 'recursive_snapshot' };
|
|
72
95
|
}
|
|
73
96
|
catch (error) {
|
|
74
97
|
return {
|
|
75
|
-
|
|
98
|
+
status: 'unavailable',
|
|
76
99
|
entries: new Map(),
|
|
77
100
|
reason: error instanceof Error && error.message.length > 0 ? error.message : 'snapshot_unavailable',
|
|
78
101
|
source: 'unavailable',
|
|
@@ -80,12 +103,33 @@ function captureSnapshot(projectRoot) {
|
|
|
80
103
|
}
|
|
81
104
|
}
|
|
82
105
|
function captureGitStatusSnapshot(projectRoot) {
|
|
83
|
-
const result = spawnSync('git', ['-C', projectRoot, 'status', '--porcelain=v1', '-z',
|
|
106
|
+
const result = spawnSync('git', ['-C', projectRoot, 'status', '--porcelain=v1', '-z', `--untracked-files=${GIT_STATUS_UNTRACKED_MODE}`], {
|
|
84
107
|
encoding: 'utf8',
|
|
85
108
|
input: '',
|
|
109
|
+
maxBuffer: GIT_STATUS_MAX_BUFFER_BYTES,
|
|
86
110
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
111
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
87
112
|
windowsHide: true,
|
|
88
113
|
});
|
|
114
|
+
const errorCode = typeof result.error === 'object' && result.error && 'code' in result.error
|
|
115
|
+
? String(result.error.code)
|
|
116
|
+
: null;
|
|
117
|
+
if (errorCode === 'ETIMEDOUT') {
|
|
118
|
+
return {
|
|
119
|
+
status: 'unavailable',
|
|
120
|
+
entries: new Map(),
|
|
121
|
+
reason: 'git_status_timeout',
|
|
122
|
+
source: 'unavailable',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (errorCode === 'ENOBUFS') {
|
|
126
|
+
return {
|
|
127
|
+
status: 'unavailable',
|
|
128
|
+
entries: new Map(),
|
|
129
|
+
reason: 'git_status_output_limit_exceeded',
|
|
130
|
+
source: 'unavailable',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
89
133
|
if (result.error || result.status !== 0) {
|
|
90
134
|
return null;
|
|
91
135
|
}
|
|
@@ -98,15 +142,15 @@ function captureGitStatusSnapshot(projectRoot) {
|
|
|
98
142
|
if (filePath.length === 0) {
|
|
99
143
|
continue;
|
|
100
144
|
}
|
|
101
|
-
entries.set(filePath,
|
|
145
|
+
entries.set(filePath, signatureForGitStatusPath(projectRoot, filePath, status));
|
|
102
146
|
if (status.includes('R') || status.includes('C')) {
|
|
103
147
|
index += 1;
|
|
104
148
|
}
|
|
105
149
|
}
|
|
106
150
|
return {
|
|
107
|
-
|
|
151
|
+
status: 'partial',
|
|
108
152
|
entries,
|
|
109
|
-
reason:
|
|
153
|
+
reason: 'git_status_untracked_files_normal',
|
|
110
154
|
source: 'git_status',
|
|
111
155
|
};
|
|
112
156
|
}
|
|
@@ -130,6 +174,10 @@ function listObservedChangedPaths(before, after) {
|
|
|
130
174
|
function declaredPathCoversObservedPath(declaredPath, observedPath) {
|
|
131
175
|
const declaredKey = pathKey(declaredPath);
|
|
132
176
|
const observedKey = pathKey(observedPath);
|
|
177
|
+
if (declaredKey.endsWith('/**')) {
|
|
178
|
+
const baseKey = declaredKey.slice(0, -3) || '.';
|
|
179
|
+
return baseKey === '.' || observedKey === baseKey || observedKey.startsWith(`${baseKey}/`);
|
|
180
|
+
}
|
|
133
181
|
return declaredKey === '.' || observedKey === declaredKey || observedKey.startsWith(`${declaredKey}/`);
|
|
134
182
|
}
|
|
135
183
|
function truncatePaths(paths) {
|
|
@@ -146,7 +194,7 @@ export function startRunWriteTracking(projectRoot, contract, intentName) {
|
|
|
146
194
|
};
|
|
147
195
|
}
|
|
148
196
|
export function finishRunWriteTracking(tracker) {
|
|
149
|
-
if (
|
|
197
|
+
if (tracker.before.status === 'unavailable') {
|
|
150
198
|
return {
|
|
151
199
|
status: 'unavailable',
|
|
152
200
|
declared_paths: tracker.declaredPaths,
|
|
@@ -161,7 +209,7 @@ export function finishRunWriteTracking(tracker) {
|
|
|
161
209
|
};
|
|
162
210
|
}
|
|
163
211
|
const after = captureSnapshot(tracker.projectRoot);
|
|
164
|
-
if (
|
|
212
|
+
if (after.status === 'unavailable') {
|
|
165
213
|
return {
|
|
166
214
|
status: 'unavailable',
|
|
167
215
|
declared_paths: tracker.declaredPaths,
|
|
@@ -181,8 +229,12 @@ export function finishRunWriteTracking(tracker) {
|
|
|
181
229
|
const observed = truncatePaths(observedPaths);
|
|
182
230
|
const declaredObserved = truncatePaths(declaredObservedPaths);
|
|
183
231
|
const undeclared = truncatePaths(undeclaredPaths);
|
|
232
|
+
const status = tracker.before.status === 'partial' || after.status === 'partial' ? 'partial' : 'checked';
|
|
233
|
+
const reason = status === 'partial'
|
|
234
|
+
? tracker.before.reason ?? after.reason ?? 'partial_snapshot'
|
|
235
|
+
: null;
|
|
184
236
|
return {
|
|
185
|
-
status
|
|
237
|
+
status,
|
|
186
238
|
declared_paths: tracker.declaredPaths,
|
|
187
239
|
observed_paths: observed.paths,
|
|
188
240
|
declared_observed_paths: declaredObserved.paths,
|
|
@@ -191,6 +243,6 @@ export function finishRunWriteTracking(tracker) {
|
|
|
191
243
|
undeclared_count: undeclaredPaths.length,
|
|
192
244
|
has_undeclared_changes: undeclaredPaths.length > 0,
|
|
193
245
|
truncated: observed.truncated || declaredObserved.truncated || undeclared.truncated,
|
|
194
|
-
reason
|
|
246
|
+
reason,
|
|
195
247
|
};
|
|
196
248
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { closeSync, constants, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const NOFOLLOW_FLAG = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
5
|
+
function isMissingPathError(error) {
|
|
6
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
7
|
+
}
|
|
8
|
+
function tempFilePath(targetPath) {
|
|
9
|
+
const suffix = `${process.pid}-${Date.now()}-${randomBytes(6).toString('hex')}`;
|
|
10
|
+
return path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${suffix}.tmp`);
|
|
11
|
+
}
|
|
12
|
+
export function ensureInside(parentPath, childPath) {
|
|
13
|
+
const parent = path.resolve(parentPath);
|
|
14
|
+
const child = path.resolve(childPath);
|
|
15
|
+
const relative = path.relative(parent, child);
|
|
16
|
+
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Path escapes allowed directory: ${childPath}`);
|
|
20
|
+
}
|
|
21
|
+
export function ensureInsideWithoutSymlinks(parentPath, childPath, options = {}) {
|
|
22
|
+
ensureInside(parentPath, childPath);
|
|
23
|
+
const parent = path.resolve(parentPath);
|
|
24
|
+
const child = path.resolve(childPath);
|
|
25
|
+
const relative = path.relative(parent, child);
|
|
26
|
+
const segments = relative === '' ? [] : relative.split(path.sep).filter((segment) => segment.length > 0);
|
|
27
|
+
let currentPath = parent;
|
|
28
|
+
const parentStats = lstatSync(parent);
|
|
29
|
+
if (parentStats.isSymbolicLink()) {
|
|
30
|
+
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
31
|
+
}
|
|
32
|
+
for (const [index, segment] of segments.entries()) {
|
|
33
|
+
currentPath = path.join(currentPath, segment);
|
|
34
|
+
const isLeaf = index === segments.length - 1;
|
|
35
|
+
try {
|
|
36
|
+
const stats = lstatSync(currentPath);
|
|
37
|
+
if (stats.isSymbolicLink()) {
|
|
38
|
+
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
39
|
+
}
|
|
40
|
+
if (!isLeaf && !stats.isDirectory()) {
|
|
41
|
+
throw new Error(`Path component is not a directory: ${currentPath}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (isMissingPathError(error) && options.allowMissingLeaf) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function ensureDirectoryInsideWithoutSymlinks(parentPath, directoryPath) {
|
|
53
|
+
ensureInside(parentPath, directoryPath);
|
|
54
|
+
const parent = path.resolve(parentPath);
|
|
55
|
+
const directory = path.resolve(directoryPath);
|
|
56
|
+
const relative = path.relative(parent, directory);
|
|
57
|
+
const segments = relative === '' ? [] : relative.split(path.sep).filter((segment) => segment.length > 0);
|
|
58
|
+
let currentPath = parent;
|
|
59
|
+
const parentStats = lstatSync(parent);
|
|
60
|
+
if (parentStats.isSymbolicLink()) {
|
|
61
|
+
throw new Error(`Path must not contain symlinks: ${directoryPath}`);
|
|
62
|
+
}
|
|
63
|
+
for (const segment of segments) {
|
|
64
|
+
currentPath = path.join(currentPath, segment);
|
|
65
|
+
try {
|
|
66
|
+
const stats = lstatSync(currentPath);
|
|
67
|
+
if (stats.isSymbolicLink()) {
|
|
68
|
+
throw new Error(`Path must not contain symlinks: ${directoryPath}`);
|
|
69
|
+
}
|
|
70
|
+
if (!stats.isDirectory()) {
|
|
71
|
+
throw new Error(`Path component is not a directory: ${currentPath}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!isMissingPathError(error)) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
mkdirSync(currentPath);
|
|
79
|
+
const stats = lstatSync(currentPath);
|
|
80
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
81
|
+
throw new Error(`Path component is not a directory: ${currentPath}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function ensureFileTargetInsideWithoutSymlinks(parentPath, childPath, options = {}) {
|
|
87
|
+
const absoluteChildPath = path.resolve(childPath);
|
|
88
|
+
ensureInside(parentPath, absoluteChildPath);
|
|
89
|
+
ensureInsideWithoutSymlinks(parentPath, path.dirname(absoluteChildPath), { allowMissingLeaf: true });
|
|
90
|
+
try {
|
|
91
|
+
const stats = lstatSync(absoluteChildPath);
|
|
92
|
+
if (stats.isSymbolicLink()) {
|
|
93
|
+
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
94
|
+
}
|
|
95
|
+
if (!stats.isFile()) {
|
|
96
|
+
throw new Error(`Path must be a regular file: ${childPath}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (isMissingPathError(error) && options.allowMissingLeaf) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function readFileInsideWithoutSymlinks(parentPath, childPath) {
|
|
107
|
+
const absoluteChildPath = path.resolve(childPath);
|
|
108
|
+
ensureInsideWithoutSymlinks(parentPath, absoluteChildPath);
|
|
109
|
+
const fileDescriptor = openSync(absoluteChildPath, constants.O_RDONLY | NOFOLLOW_FLAG);
|
|
110
|
+
try {
|
|
111
|
+
return readFileSync(fileDescriptor);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
closeSync(fileDescriptor);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
118
|
+
const absoluteChildPath = path.resolve(childPath);
|
|
119
|
+
const directoryPath = path.dirname(absoluteChildPath);
|
|
120
|
+
ensureDirectoryInsideWithoutSymlinks(parentPath, directoryPath);
|
|
121
|
+
ensureFileTargetInsideWithoutSymlinks(parentPath, absoluteChildPath, { allowMissingLeaf: true });
|
|
122
|
+
const temporaryPath = tempFilePath(absoluteChildPath);
|
|
123
|
+
let fileDescriptor = null;
|
|
124
|
+
try {
|
|
125
|
+
fileDescriptor = openSync(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW_FLAG);
|
|
126
|
+
writeFileSync(fileDescriptor, content);
|
|
127
|
+
closeSync(fileDescriptor);
|
|
128
|
+
fileDescriptor = null;
|
|
129
|
+
ensureFileTargetInsideWithoutSymlinks(parentPath, absoluteChildPath, { allowMissingLeaf: true });
|
|
130
|
+
renameSync(temporaryPath, absoluteChildPath);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (fileDescriptor !== null) {
|
|
134
|
+
try {
|
|
135
|
+
closeSync(fileDescriptor);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Best-effort cleanup before removing the temporary file.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
unlinkSync(temporaryPath);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Best-effort cleanup for a temporary file that may not have been created.
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
151
|
+
writeFileInsideWithoutSymlinks(parentPath, childPath, content);
|
|
152
|
+
}
|
|
153
|
+
export function writeJsonFileInsideWithoutSymlinks(parentPath, childPath, value) {
|
|
154
|
+
writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"ok",
|
|
14
14
|
"check",
|
|
15
15
|
"context",
|
|
16
|
+
"command_environment",
|
|
16
17
|
"effective_policy",
|
|
17
18
|
"state_policy",
|
|
18
19
|
"blocked_actions",
|
|
@@ -29,13 +30,18 @@
|
|
|
29
30
|
"check": {
|
|
30
31
|
"type": "object",
|
|
31
32
|
"additionalProperties": false,
|
|
32
|
-
"required": ["ok", "issue_count", "issues"],
|
|
33
|
+
"required": ["ok", "issue_count", "issues", "warning_count", "warnings"],
|
|
33
34
|
"properties": {
|
|
34
35
|
"ok": { "type": "boolean" },
|
|
35
36
|
"issue_count": { "type": "integer" },
|
|
36
37
|
"issues": {
|
|
37
38
|
"type": "array",
|
|
38
39
|
"items": { "type": "string" }
|
|
40
|
+
},
|
|
41
|
+
"warning_count": { "type": "integer" },
|
|
42
|
+
"warnings": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": { "type": "string" }
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
},
|
|
@@ -83,6 +89,21 @@
|
|
|
83
89
|
"latest_run_exists": { "type": "boolean" }
|
|
84
90
|
}
|
|
85
91
|
},
|
|
92
|
+
"command_environment": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"additionalProperties": false,
|
|
95
|
+
"required": ["inherited_intents", "inherited_network_intents"],
|
|
96
|
+
"properties": {
|
|
97
|
+
"inherited_intents": {
|
|
98
|
+
"type": "array",
|
|
99
|
+
"items": { "type": "string" }
|
|
100
|
+
},
|
|
101
|
+
"inherited_network_intents": {
|
|
102
|
+
"type": "array",
|
|
103
|
+
"items": { "type": "string" }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
86
107
|
"effective_policy": { "$ref": "#/$defs/effectivePolicy" },
|
|
87
108
|
"state_policy": { "$ref": "#/$defs/statePolicy" },
|
|
88
109
|
"blocked_actions": {
|
|
@@ -102,6 +123,7 @@
|
|
|
102
123
|
"validation",
|
|
103
124
|
"skill_routes",
|
|
104
125
|
"commands",
|
|
126
|
+
"environment",
|
|
105
127
|
"read_order",
|
|
106
128
|
"optional_read_order",
|
|
107
129
|
"repo_map",
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
},
|
|
56
56
|
"cmd": { "type": "string" },
|
|
57
57
|
"timeout_seconds": { "type": "integer" },
|
|
58
|
+
"kill_after_seconds": { "type": "integer" },
|
|
58
59
|
"max_output_bytes": { "type": "integer" },
|
|
60
|
+
"max_output_bytes_scope": { "const": "per_stream" },
|
|
59
61
|
"success_exit_codes": {
|
|
60
62
|
"type": "array",
|
|
61
63
|
"items": { "type": "integer" }
|
|
@@ -174,8 +176,10 @@
|
|
|
174
176
|
"properties": {
|
|
175
177
|
"stdout_bytes": { "type": "integer", "minimum": 0 },
|
|
176
178
|
"stderr_bytes": { "type": "integer", "minimum": 0 },
|
|
179
|
+
"total_bytes": { "type": "integer", "minimum": 0 },
|
|
177
180
|
"stdout_truncated": { "type": "boolean" },
|
|
178
|
-
"stderr_truncated": { "type": "boolean" }
|
|
181
|
+
"stderr_truncated": { "type": "boolean" },
|
|
182
|
+
"max_output_bytes_scope": { "const": "per_stream" }
|
|
179
183
|
}
|
|
180
184
|
},
|
|
181
185
|
"result_summary": {
|
|
@@ -256,7 +260,7 @@
|
|
|
256
260
|
"reason"
|
|
257
261
|
],
|
|
258
262
|
"properties": {
|
|
259
|
-
"status": { "enum": ["checked", "unavailable"] },
|
|
263
|
+
"status": { "enum": ["checked", "partial", "unavailable"] },
|
|
260
264
|
"declared_paths": {
|
|
261
265
|
"type": "array",
|
|
262
266
|
"items": { "type": "string" }
|