mustflow 2.115.15 → 2.115.16
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/args.js +8 -4
- package/dist/cli/commands/run/execution.js +1 -0
- package/dist/cli/commands/run/preview.js +4 -1
- package/dist/cli/commands/run.js +1 -1
- package/dist/cli/lib/run-plan.js +11 -4
- package/dist/cli/lib/templates.js +20 -1
- package/dist/cli/lib/validation/constants.js +1 -12
- package/dist/cli/lib/validation/index.js +16 -2
- package/dist/core/approval-actions.js +13 -0
- package/dist/core/command-contract-validation.js +54 -0
- package/package.json +1 -1
- package/schemas/commands.schema.json +18 -0
- package/templates/default/common/.mustflow/config/commands.toml +11 -3
- package/templates/default/manifest.toml +1 -1
|
@@ -2,8 +2,8 @@ import { renderHelp } from '../../lib/cli-output.js';
|
|
|
2
2
|
import { t } from '../../lib/i18n.js';
|
|
3
3
|
import { getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../../lib/option-parser.js';
|
|
4
4
|
import { ALLOW_UNTRUSTED_ROOT_OPTION } from '../../lib/run-root-trust.js';
|
|
5
|
+
import { APPROVAL_ACTION_TYPE_SET, APPROVAL_ACTION_TYPES } from '../../../core/approval-actions.js';
|
|
5
6
|
const DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS = 300;
|
|
6
|
-
const SUPPORTED_RUN_APPROVAL_ACTIONS = new Set(['network_access', 'destructive_command']);
|
|
7
7
|
const RUN_OPTIONS = [
|
|
8
8
|
{ name: '--json', kind: 'boolean' },
|
|
9
9
|
{ name: '--dry-run', kind: 'boolean' },
|
|
@@ -17,7 +17,7 @@ export function hasRunHelpOption(args) {
|
|
|
17
17
|
return hasCliOptionToken(args, '--help', ['-h']);
|
|
18
18
|
}
|
|
19
19
|
export function getSupportedRunApprovalActions() {
|
|
20
|
-
return [...
|
|
20
|
+
return [...APPROVAL_ACTION_TYPES].sort((left, right) => left.localeCompare(right));
|
|
21
21
|
}
|
|
22
22
|
function getAllowApprovalValues(parsed) {
|
|
23
23
|
const values = parsed.occurrences
|
|
@@ -27,7 +27,7 @@ function getAllowApprovalValues(parsed) {
|
|
|
27
27
|
return [...new Set(values)];
|
|
28
28
|
}
|
|
29
29
|
function findInvalidApprovalAction(values) {
|
|
30
|
-
return values.find((value) => !
|
|
30
|
+
return values.find((value) => !APPROVAL_ACTION_TYPE_SET.has(value)) ?? null;
|
|
31
31
|
}
|
|
32
32
|
export function parseRunArguments(args) {
|
|
33
33
|
const parsed = parseCliOptions(args, RUN_OPTIONS, { allowPositionals: true });
|
|
@@ -93,7 +93,11 @@ export function getRunHelp(lang = 'en') {
|
|
|
93
93
|
{ label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
|
|
94
94
|
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
95
95
|
],
|
|
96
|
-
examples: [
|
|
96
|
+
examples: [
|
|
97
|
+
'mf run test',
|
|
98
|
+
'mf run lint --json',
|
|
99
|
+
'mf run release_npm_publish --allow-approval release --allow-approval network_access --json',
|
|
100
|
+
],
|
|
97
101
|
exitCodes: [
|
|
98
102
|
{
|
|
99
103
|
label: '0',
|
|
@@ -72,6 +72,7 @@ function reportRunPlanFailure(plan, reporter, lang) {
|
|
|
72
72
|
break;
|
|
73
73
|
case 'network_requires_approval':
|
|
74
74
|
case 'destructive_requires_approval':
|
|
75
|
+
case 'explicit_approval_required':
|
|
75
76
|
case 'approval_policy_unreadable':
|
|
76
77
|
message = t(lang, 'run.error.approvalRequired', {
|
|
77
78
|
intent: plan.intentName,
|
|
@@ -10,7 +10,10 @@ export function executeRunPreviewCommand(input, reporter, lang, options) {
|
|
|
10
10
|
const profiler = new RunProfiler();
|
|
11
11
|
const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
|
|
12
12
|
const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
|
|
13
|
-
const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, input.intentName, {
|
|
13
|
+
const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, input.intentName, {
|
|
14
|
+
testTargets: options.testTargets,
|
|
15
|
+
approvedActions: input.allowApprovals,
|
|
16
|
+
}));
|
|
14
17
|
profiler.measure('preview_render', () => {
|
|
15
18
|
if (input.json) {
|
|
16
19
|
reporter.stdout(JSON.stringify(createRunPreview(plan, input.previewMode), null, 2));
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -72,5 +72,5 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
72
72
|
}, reporter, lang, options);
|
|
73
73
|
return result.exitCode;
|
|
74
74
|
}
|
|
75
|
-
return executeRunPreviewCommand({ intentName, json, previewMode }, reporter, lang, options);
|
|
75
|
+
return executeRunPreviewCommand({ intentName, json, previewMode, allowApprovals: parsedArgs.allowApprovals }, reporter, lang, options);
|
|
76
76
|
}
|
package/dist/cli/lib/run-plan.js
CHANGED
|
@@ -123,6 +123,7 @@ function readRunIntentMetadata(contract, intent) {
|
|
|
123
123
|
effects: readArray(intent, 'effects'),
|
|
124
124
|
network: readBoolean(intent, 'network'),
|
|
125
125
|
destructive: readBoolean(intent, 'destructive'),
|
|
126
|
+
approvalActions: readStringArray(intent, 'approval_actions') ?? [],
|
|
126
127
|
envPolicy: env.policy,
|
|
127
128
|
envAllowlist: env.allowlist,
|
|
128
129
|
testTargets: [],
|
|
@@ -133,17 +134,18 @@ function readRunIntentMetadata(contract, intent) {
|
|
|
133
134
|
};
|
|
134
135
|
}
|
|
135
136
|
function createApprovalBlock(projectRoot, metadata, approvedActions) {
|
|
136
|
-
const actionTypes = [];
|
|
137
|
+
const actionTypes = [...metadata.approvalActions];
|
|
137
138
|
if (metadata.network === true) {
|
|
138
139
|
actionTypes.push('network_access');
|
|
139
140
|
}
|
|
140
141
|
if (metadata.destructive === true) {
|
|
141
142
|
actionTypes.push('destructive_command');
|
|
142
143
|
}
|
|
143
|
-
|
|
144
|
+
const uniqueActionTypes = [...new Set(actionTypes)];
|
|
145
|
+
if (uniqueActionTypes.length === 0) {
|
|
144
146
|
return null;
|
|
145
147
|
}
|
|
146
|
-
const approvalReport = checkRepoApprovalGate(projectRoot,
|
|
148
|
+
const approvalReport = checkRepoApprovalGate(projectRoot, uniqueActionTypes);
|
|
147
149
|
if (approvalReport.issues.length > 0) {
|
|
148
150
|
return {
|
|
149
151
|
reasonCode: 'approval_policy_unreadable',
|
|
@@ -159,7 +161,9 @@ function createApprovalBlock(projectRoot, metadata, approvedActions) {
|
|
|
159
161
|
}
|
|
160
162
|
const reasonCode = missingActions.includes('destructive_command')
|
|
161
163
|
? 'destructive_requires_approval'
|
|
162
|
-
: '
|
|
164
|
+
: missingActions.includes('network_access')
|
|
165
|
+
? 'network_requires_approval'
|
|
166
|
+
: 'explicit_approval_required';
|
|
163
167
|
return {
|
|
164
168
|
reasonCode,
|
|
165
169
|
detail: `Action ${missingActions.map((action) => JSON.stringify(action)).join(', ')} requires explicit approval before mf run can execute this intent.`,
|
|
@@ -193,6 +197,7 @@ function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonC
|
|
|
193
197
|
effects: metadata?.effects,
|
|
194
198
|
network: metadata?.network,
|
|
195
199
|
destructive: metadata?.destructive,
|
|
200
|
+
approvalActions: metadata?.approvalActions ?? [],
|
|
196
201
|
envPolicy: metadata?.envPolicy ?? null,
|
|
197
202
|
envAllowlist: metadata?.envAllowlist ?? [],
|
|
198
203
|
testTargets: [],
|
|
@@ -277,6 +282,7 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
|
|
|
277
282
|
effects: metadata.effects,
|
|
278
283
|
network: metadata.network,
|
|
279
284
|
destructive: metadata.destructive,
|
|
285
|
+
approvalActions: metadata.approvalActions,
|
|
280
286
|
envPolicy: metadata.envPolicy,
|
|
281
287
|
envAllowlist: metadata.envAllowlist,
|
|
282
288
|
testTargets,
|
|
@@ -360,6 +366,7 @@ export function createRunPreview(plan, previewMode) {
|
|
|
360
366
|
effects: plan.effects,
|
|
361
367
|
network: plan.network,
|
|
362
368
|
destructive: plan.destructive,
|
|
369
|
+
approval_actions: plan.approvalActions,
|
|
363
370
|
env_policy: plan.envPolicy,
|
|
364
371
|
env_allowlist: plan.envAllowlist,
|
|
365
372
|
test_targets: plan.testTargets,
|
|
@@ -72,6 +72,9 @@ function shouldIncludeTemplatePath(relativePath, selectedSkills) {
|
|
|
72
72
|
return selectedSkills.includes(skillName);
|
|
73
73
|
}
|
|
74
74
|
const SKILL_INDEX_SKILL_PATH_PATTERN = /`\.mustflow\/skills\/([^/]+)\/SKILL\.md`/u;
|
|
75
|
+
const SKILL_NAME_REFERENCE_PATTERN = /`([a-z][a-z0-9-]+)`/gu;
|
|
76
|
+
const SKILL_PATH_REFERENCE_PATTERN = /`\.mustflow\/skills\/([^/]+)\/SKILL\.md`/gu;
|
|
77
|
+
const UNAVAILABLE_SKILL_REFERENCE_FALLBACK = 'the closest installed route for this scope';
|
|
75
78
|
const SKILL_INDEX_HEADING_PATTERN = /^(#{2,3})\s+(.+?)\s*$/u;
|
|
76
79
|
const SKILL_INDEX_ROUTE_CATEGORY_NAMES = [
|
|
77
80
|
'Bug and Failure',
|
|
@@ -190,6 +193,16 @@ function filterSkillIndexContent(content, selectedSkills) {
|
|
|
190
193
|
}
|
|
191
194
|
return filteredLines.join('\n').replace(/\n{3,}/gu, '\n\n');
|
|
192
195
|
}
|
|
196
|
+
function filterUnavailableSkillReferences(content, selectedSkills, knownSkills) {
|
|
197
|
+
const selectedSkillSet = new Set(selectedSkills);
|
|
198
|
+
const knownSkillSet = new Set(knownSkills);
|
|
199
|
+
const replaceUnavailableReference = (reference, skillName) => knownSkillSet.has(skillName) && !selectedSkillSet.has(skillName)
|
|
200
|
+
? UNAVAILABLE_SKILL_REFERENCE_FALLBACK
|
|
201
|
+
: reference;
|
|
202
|
+
return content
|
|
203
|
+
.replace(SKILL_PATH_REFERENCE_PATTERN, replaceUnavailableReference)
|
|
204
|
+
.replace(SKILL_NAME_REFERENCE_PATTERN, replaceUnavailableReference);
|
|
205
|
+
}
|
|
193
206
|
function filterSkillRouteMetadataContent(content, selectedSkills) {
|
|
194
207
|
const selectedSkillSet = new Set(selectedSkills);
|
|
195
208
|
let keepCurrentRoute = true;
|
|
@@ -298,6 +311,7 @@ export function getTemplateFiles(template, locale = template.manifest.defaultLoc
|
|
|
298
311
|
? path.join(template.templateRoot, template.manifest.localesRoot, template.manifest.defaultLocale)
|
|
299
312
|
: undefined;
|
|
300
313
|
const selectedSkills = selectedSkillNames(template.manifest, profile, options);
|
|
314
|
+
const knownSkills = templateSkillNames(template.manifest.creates);
|
|
301
315
|
return template.manifest.creates.filter((relativePath) => shouldIncludeTemplatePath(relativePath, selectedSkills)).map((relativePath) => {
|
|
302
316
|
const localePath = localeRoot ? path.join(localeRoot, ...relativePath.split('/')) : undefined;
|
|
303
317
|
const sourceLocalePath = sourceLocaleRoot ? path.join(sourceLocaleRoot, ...relativePath.split('/')) : undefined;
|
|
@@ -306,11 +320,16 @@ export function getTemplateFiles(template, locale = template.manifest.defaultLoc
|
|
|
306
320
|
const fallbackLocalePath = sourceLocalePath && existsSync(sourceLocalePath) ? sourceLocalePath : undefined;
|
|
307
321
|
const commonSourcePath = existsSync(commonPath) ? commonPath : undefined;
|
|
308
322
|
const selectedSourcePath = localizedPath ?? fallbackLocalePath ?? commonSourcePath;
|
|
309
|
-
const
|
|
323
|
+
const selectedContent = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
|
|
310
324
|
? filterSkillIndexContent(readFileSync(selectedSourcePath, 'utf8'), selectedSkills)
|
|
311
325
|
: selectedSourcePath && relativePath === '.mustflow/skills/routes.toml'
|
|
312
326
|
? filterSkillRouteMetadataContent(readFileSync(selectedSourcePath, 'utf8'), selectedSkills)
|
|
313
327
|
: undefined;
|
|
328
|
+
const content = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
|
|
329
|
+
? filterUnavailableSkillReferences(selectedContent ?? '', selectedSkills, knownSkills)
|
|
330
|
+
: selectedSourcePath && /^\.mustflow\/skills\/[^/]+\/SKILL\.md$/u.test(relativePath)
|
|
331
|
+
? filterUnavailableSkillReferences(readFileSync(selectedSourcePath, 'utf8'), selectedSkills, knownSkills)
|
|
332
|
+
: selectedContent;
|
|
314
333
|
if (localizedPath) {
|
|
315
334
|
return {
|
|
316
335
|
relativePath,
|
|
@@ -130,18 +130,7 @@ export const ALLOWED_PROMPT_CACHE_STRATEGIES = new Set(['stable_prefix']);
|
|
|
130
130
|
export const ALLOWED_PROMPT_CACHE_STABLE_PREFIX_POLICIES = new Set(['hash_verified']);
|
|
131
131
|
export const ALLOWED_PROMPT_CACHE_TASK_READ_POLICIES = new Set(['task_relevant_only']);
|
|
132
132
|
export const ALLOWED_BUDGET_LIMIT_ACTIONS = new Set(['stop_and_handoff', 'stop_and_report']);
|
|
133
|
-
export
|
|
134
|
-
'git_commit',
|
|
135
|
-
'git_push',
|
|
136
|
-
'dependency_install',
|
|
137
|
-
'dependency_upgrade',
|
|
138
|
-
'network_access',
|
|
139
|
-
'database_migration',
|
|
140
|
-
'destructive_command',
|
|
141
|
-
'secret_access',
|
|
142
|
-
'release',
|
|
143
|
-
'cross_repository_change',
|
|
144
|
-
]);
|
|
133
|
+
export { APPROVAL_ACTION_TYPE_SET as ALLOWED_APPROVAL_GATES } from '../../../core/approval-actions.js';
|
|
145
134
|
export const ALLOWED_APPROVAL_ACTIONS = new Set(['stop_and_request_approval']);
|
|
146
135
|
export const ALLOWED_ISOLATION_PREFERENCES = new Set(['none', 'git_worktree', 'sandbox']);
|
|
147
136
|
export const ALLOWED_REFRESH_MODES = new Set(['checkpoint']);
|
|
@@ -1066,7 +1066,7 @@ function readSkillRouteMetadataFromTomlContent(content, sourceLabel, issues) {
|
|
|
1066
1066
|
}
|
|
1067
1067
|
return metadata;
|
|
1068
1068
|
}
|
|
1069
|
-
function validateTemplateProfileSkillRoutes(profile, files, issues) {
|
|
1069
|
+
function validateTemplateProfileSkillRoutes(profile, files, knownSkillNames, issues) {
|
|
1070
1070
|
const contentByPath = new Map(files.map((file) => [file.relativePath, templateFileContent(file)]));
|
|
1071
1071
|
const indexContent = contentByPath.get(SKILL_INDEX_PATH);
|
|
1072
1072
|
const routesContent = contentByPath.get(SKILL_ROUTES_METADATA_PATH);
|
|
@@ -1104,6 +1104,17 @@ function validateTemplateProfileSkillRoutes(profile, files, issues) {
|
|
|
1104
1104
|
selectedSkillContents.set(skillName, content);
|
|
1105
1105
|
}
|
|
1106
1106
|
}
|
|
1107
|
+
for (const [relativePath, content] of contentByPath.entries()) {
|
|
1108
|
+
if (relativePath !== SKILL_INDEX_PATH && !skillRouteName(relativePath)) {
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
for (const match of content.matchAll(/`([a-z][a-z0-9-]+)`/gu)) {
|
|
1112
|
+
const referencedSkill = match[1];
|
|
1113
|
+
if (referencedSkill && knownSkillNames.has(referencedSkill) && !selectedSkillContents.has(referencedSkill)) {
|
|
1114
|
+
pushStrictIssue(issues, `template profile "${profile}" ${relativePath} references skill "${referencedSkill}" not installed by that profile`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1107
1118
|
for (const route of skillRoutes) {
|
|
1108
1119
|
const routeSkillName = skillRouteName(route.skillPath);
|
|
1109
1120
|
if (!routeSkillName) {
|
|
@@ -1178,8 +1189,11 @@ function validateStrictTemplateSkillProfiles(issues) {
|
|
|
1178
1189
|
pushStrictIssue(issues, `default template skill profiles could not be loaded: ${message}`);
|
|
1179
1190
|
return;
|
|
1180
1191
|
}
|
|
1192
|
+
const knownSkillNames = new Set(template.manifest.creates
|
|
1193
|
+
.map((relativePath) => skillRouteName(relativePath))
|
|
1194
|
+
.filter((skillName) => Boolean(skillName)));
|
|
1181
1195
|
for (const profile of template.manifest.profiles) {
|
|
1182
|
-
validateTemplateProfileSkillRoutes(profile, getTemplateFiles(template, template.manifest.defaultLocale, profile), issues);
|
|
1196
|
+
validateTemplateProfileSkillRoutes(profile, getTemplateFiles(template, template.manifest.defaultLocale, profile), knownSkillNames, issues);
|
|
1183
1197
|
}
|
|
1184
1198
|
}
|
|
1185
1199
|
function listManagedMarkdownDocuments(projectRoot) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const APPROVAL_ACTION_TYPES = [
|
|
2
|
+
'git_commit',
|
|
3
|
+
'git_push',
|
|
4
|
+
'dependency_install',
|
|
5
|
+
'dependency_upgrade',
|
|
6
|
+
'network_access',
|
|
7
|
+
'database_migration',
|
|
8
|
+
'destructive_command',
|
|
9
|
+
'secret_access',
|
|
10
|
+
'release',
|
|
11
|
+
'cross_repository_change',
|
|
12
|
+
];
|
|
13
|
+
export const APPROVAL_ACTION_TYPE_SET = new Set(APPROVAL_ACTION_TYPES);
|
|
@@ -6,6 +6,7 @@ import { COMMAND_PRECONDITION_KINDS } from './command-preconditions.js';
|
|
|
6
6
|
import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
7
7
|
import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage } from './command-output-limits.js';
|
|
8
8
|
import { SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION, successExitCodesAreValid } from './success-exit-codes.js';
|
|
9
|
+
import { APPROVAL_ACTION_TYPE_SET } from './approval-actions.js';
|
|
9
10
|
const COMMAND_INPUT_TYPES = new Set(['path', 'enum', 'boolean', 'integer', 'literal']);
|
|
10
11
|
const COMMAND_INPUT_NAME_PATTERN = /^[a-z][a-z0-9_]*$/u;
|
|
11
12
|
const COMMAND_ARGV_PLACEHOLDER_PATTERN = /^\{([a-z][a-z0-9_]*)\}$/u;
|
|
@@ -59,6 +60,57 @@ function validateStringArrayField(table, key, label, issues) {
|
|
|
59
60
|
issues.push(commandContractIssue(`${label} must be a string array`));
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
function validateApprovalActions(intentName, intent, issues) {
|
|
64
|
+
validateStringArrayField(intent, 'approval_actions', `[commands.intents.${intentName}].approval_actions`, issues);
|
|
65
|
+
const approvalActions = readStringArray(intent, 'approval_actions');
|
|
66
|
+
if (!approvalActions) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const unsupported = [...new Set(approvalActions.filter((action) => !APPROVAL_ACTION_TYPE_SET.has(action)))];
|
|
70
|
+
if (unsupported.length > 0) {
|
|
71
|
+
issues.push(commandContractIssue(`[commands.intents.${intentName}].approval_actions contains unsupported action types: ${unsupported.join(', ')}`));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function readGitSubcommand(argv) {
|
|
75
|
+
if (normalizeCommandExecutableName(argv[0] ?? '') !== 'git') {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const optionsWithSeparateValue = new Set(['-C', '-c', '--exec-path', '--git-dir', '--work-tree', '--namespace']);
|
|
79
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
80
|
+
const argument = argv[index] ?? '';
|
|
81
|
+
if (optionsWithSeparateValue.has(argument)) {
|
|
82
|
+
index += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (argument.startsWith('-')) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
return argument;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function validateSensitiveGitApprovalActions(intentName, intent, issues) {
|
|
93
|
+
if (intent.status !== 'configured' || intent.run_policy !== 'agent_allowed') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const argv = readStringArray(intent, 'argv');
|
|
97
|
+
if (!argv) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const gitSubcommand = readGitSubcommand(argv);
|
|
101
|
+
const requiredAction = gitSubcommand === 'add' || gitSubcommand === 'commit'
|
|
102
|
+
? 'git_commit'
|
|
103
|
+
: gitSubcommand === 'push'
|
|
104
|
+
? 'git_push'
|
|
105
|
+
: null;
|
|
106
|
+
if (!requiredAction) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const approvalActions = readStringArray(intent, 'approval_actions') ?? [];
|
|
110
|
+
if (!approvalActions.includes(requiredAction)) {
|
|
111
|
+
issues.push(commandContractIssue(`Agent-runnable git ${gitSubcommand} intent ${intentName} must include approval_actions = ["${requiredAction}"]`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
62
114
|
function validatePositiveIntegerField(table, key, label, issues) {
|
|
63
115
|
if (hasOwn(table, key) && !isPositiveInteger(table[key])) {
|
|
64
116
|
issues.push(commandContractIssue(`${label} must be a positive integer`));
|
|
@@ -339,6 +391,8 @@ function validateCommandIntent(intentName, intent, allIntents, issues) {
|
|
|
339
391
|
validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
|
|
340
392
|
validateBooleanField(intent, 'allow_shell', `[commands.intents.${intentName}].allow_shell`, issues);
|
|
341
393
|
validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
|
|
394
|
+
validateApprovalActions(intentName, intent, issues);
|
|
395
|
+
validateSensitiveGitApprovalActions(intentName, intent, issues);
|
|
342
396
|
validateBooleanField(intent, ALLOW_ENV_INHERITANCE_RISKS_KEY, `[commands.intents.${intentName}].${ALLOW_ENV_INHERITANCE_RISKS_KEY}`, issues);
|
|
343
397
|
validateBooleanField(intent, ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY, `[commands.intents.${intentName}].${ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY}`, issues);
|
|
344
398
|
validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
|
package/package.json
CHANGED
|
@@ -210,6 +210,24 @@
|
|
|
210
210
|
},
|
|
211
211
|
"network": { "type": "boolean" },
|
|
212
212
|
"destructive": { "type": "boolean" },
|
|
213
|
+
"approval_actions": {
|
|
214
|
+
"type": "array",
|
|
215
|
+
"uniqueItems": true,
|
|
216
|
+
"items": {
|
|
217
|
+
"enum": [
|
|
218
|
+
"git_commit",
|
|
219
|
+
"git_push",
|
|
220
|
+
"dependency_install",
|
|
221
|
+
"dependency_upgrade",
|
|
222
|
+
"network_access",
|
|
223
|
+
"database_migration",
|
|
224
|
+
"destructive_command",
|
|
225
|
+
"secret_access",
|
|
226
|
+
"release",
|
|
227
|
+
"cross_repository_change"
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
},
|
|
213
231
|
"required_after": {
|
|
214
232
|
"type": "array",
|
|
215
233
|
"items": { "type": "string" }
|
|
@@ -379,6 +379,14 @@ required_after = ["before_final_report", "commit_message_suggestion"]
|
|
|
379
379
|
|
|
380
380
|
[intents.git_commit]
|
|
381
381
|
status = "manual_only"
|
|
382
|
-
description = "Create
|
|
383
|
-
reason = "
|
|
384
|
-
agent_action = "
|
|
382
|
+
description = "Create an unscoped Git commit."
|
|
383
|
+
reason = "A generic commit cannot safely choose reviewed paths or a commit message."
|
|
384
|
+
agent_action = "author_bounded_repo_specific_stage_and_commit_intents_after_explicit_user_request"
|
|
385
|
+
approval_actions = ["git_commit"]
|
|
386
|
+
|
|
387
|
+
[intents.git_push]
|
|
388
|
+
status = "manual_only"
|
|
389
|
+
description = "Push an unspecified Git ref."
|
|
390
|
+
reason = "A generic push cannot safely choose the remote and ref."
|
|
391
|
+
agent_action = "author_bounded_repo_specific_push_intent_after_explicit_user_request"
|
|
392
|
+
approval_actions = ["git_push"]
|