mustflow 2.112.14 → 2.113.0
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/skill.js +13 -0
- package/dist/cli/lib/external-skill-import.js +194 -4
- package/package.json +1 -1
- package/schemas/skill-import-report.schema.json +75 -0
- package/templates/default/i18n.toml +1 -1
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +4 -1
- package/templates/default/manifest.toml +1 -1
|
@@ -16,6 +16,7 @@ const SKILL_OPTIONS = [
|
|
|
16
16
|
{ name: '--dry-run', kind: 'boolean' },
|
|
17
17
|
{ name: '--name', kind: 'string' },
|
|
18
18
|
{ name: '--ref', kind: 'string' },
|
|
19
|
+
{ name: '--trust-scripts', kind: 'boolean' },
|
|
19
20
|
];
|
|
20
21
|
export function getSkillHelp(lang = 'en') {
|
|
21
22
|
return renderHelp({
|
|
@@ -32,6 +33,7 @@ export function getSkillHelp(lang = 'en') {
|
|
|
32
33
|
{ label: '--install', description: 'Install an external skill after previewing the same source' },
|
|
33
34
|
{ label: '--name <slug>', description: 'Override the installed external skill directory name' },
|
|
34
35
|
{ label: '--ref <ref>', description: 'Override the GitHub ref used for import' },
|
|
36
|
+
{ label: '--trust-scripts', description: 'Create command-contract intents for imported scripts; requires --install to write them' },
|
|
35
37
|
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
36
38
|
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
37
39
|
],
|
|
@@ -73,6 +75,7 @@ function parseSkillArgs(args) {
|
|
|
73
75
|
dryRun: hasParsedCliOption(parsed, '--dry-run'),
|
|
74
76
|
name: getParsedCliStringOption(parsed, '--name'),
|
|
75
77
|
ref: getParsedCliStringOption(parsed, '--ref'),
|
|
78
|
+
trustScripts: hasParsedCliOption(parsed, '--trust-scripts'),
|
|
76
79
|
error: parsed.error,
|
|
77
80
|
};
|
|
78
81
|
}
|
|
@@ -138,6 +141,15 @@ function renderSkillImportReport(report) {
|
|
|
138
141
|
lines.push(`- ${file.relative_path} (${file.kind}, ${file.bytes} bytes, ${file.sha256})`);
|
|
139
142
|
}
|
|
140
143
|
}
|
|
144
|
+
if (report.script_trust) {
|
|
145
|
+
lines.push('', 'Script trust', `- requested: ${String(report.script_trust.requested)}`, `- status: ${report.script_trust.status}`, `- command authority: ${String(report.script_trust.grants_command_authority)}`);
|
|
146
|
+
if (report.script_trust.fragment_path) {
|
|
147
|
+
lines.push(`- command fragment: ${report.script_trust.fragment_path}`);
|
|
148
|
+
}
|
|
149
|
+
for (const intent of report.script_trust.intents) {
|
|
150
|
+
lines.push(`- intent: ${intent.intent}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
141
153
|
if (report.warnings.length > 0) {
|
|
142
154
|
lines.push('', 'Warnings', ...report.warnings.map((warning) => `- ${warning}`));
|
|
143
155
|
}
|
|
@@ -183,6 +195,7 @@ export async function runSkill(args, reporter, lang = 'en') {
|
|
|
183
195
|
mode,
|
|
184
196
|
name: parsed.name,
|
|
185
197
|
ref: parsed.ref,
|
|
198
|
+
trustScripts: parsed.trustScripts,
|
|
186
199
|
});
|
|
187
200
|
if (parsed.json) {
|
|
188
201
|
reporter.stdout(JSON.stringify(report, null, 2));
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { existsSync, renameSync, rmSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { readCommandContract, readCommandContractIncludePaths, } from '../../core/config-loading.js';
|
|
5
|
+
import { ensureFileTargetInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
|
|
5
6
|
const EXTERNAL_SKILL_ROOT = '.mustflow/external-skills';
|
|
6
7
|
const PROVENANCE_FILE = 'mustflow-skill-source.json';
|
|
8
|
+
const COMMANDS_CONFIG_PATH = '.mustflow/config/commands.toml';
|
|
9
|
+
const COMMAND_FRAGMENT_DIRECTORY = '.mustflow/config/commands';
|
|
7
10
|
const DEFAULT_GITHUB_REF = 'HEAD';
|
|
8
11
|
const MAX_IMPORTED_FILES = 40;
|
|
9
12
|
const MAX_TOTAL_BYTES = 512 * 1024;
|
|
@@ -130,6 +133,9 @@ function sanitizeSkillName(value) {
|
|
|
130
133
|
.replace(/^-+|-+$/gu, '')
|
|
131
134
|
.replace(/-{2,}/gu, '-');
|
|
132
135
|
}
|
|
136
|
+
function commandSafeSegment(value) {
|
|
137
|
+
return sanitizeSkillName(value).replace(/-/gu, '_');
|
|
138
|
+
}
|
|
133
139
|
function readFrontmatterScalar(content, key) {
|
|
134
140
|
if (!content.startsWith('---')) {
|
|
135
141
|
return null;
|
|
@@ -359,7 +365,172 @@ function createTarget(skillName) {
|
|
|
359
365
|
provenance_path: `${skillDir}/${PROVENANCE_FILE}`,
|
|
360
366
|
};
|
|
361
367
|
}
|
|
362
|
-
function
|
|
368
|
+
function scriptNameForIntent(relativePath) {
|
|
369
|
+
const basename = path.posix.basename(relativePath);
|
|
370
|
+
const withoutExtension = basename.replace(/\.[^.]+$/u, '');
|
|
371
|
+
const sanitized = commandSafeSegment(withoutExtension);
|
|
372
|
+
if (!sanitized) {
|
|
373
|
+
throw new Error(`External script path cannot be converted to a safe intent name: ${relativePath}`);
|
|
374
|
+
}
|
|
375
|
+
return sanitized;
|
|
376
|
+
}
|
|
377
|
+
function trustedScriptArgv(skillName, scriptPath) {
|
|
378
|
+
const extension = path.posix.extname(scriptPath).toLowerCase();
|
|
379
|
+
const projectScriptPath = `${EXTERNAL_SKILL_ROOT}/${skillName}/${scriptPath}`;
|
|
380
|
+
if (['.js', '.mjs', '.cjs'].includes(extension)) {
|
|
381
|
+
return ['node', projectScriptPath];
|
|
382
|
+
}
|
|
383
|
+
if (['.ts', '.mts', '.cts'].includes(extension)) {
|
|
384
|
+
return ['bun', projectScriptPath];
|
|
385
|
+
}
|
|
386
|
+
if (extension === '.ps1') {
|
|
387
|
+
return ['pwsh', '-NoProfile', '-File', projectScriptPath];
|
|
388
|
+
}
|
|
389
|
+
if (extension === '.sh') {
|
|
390
|
+
return ['sh', projectScriptPath];
|
|
391
|
+
}
|
|
392
|
+
throw new Error(`External script import cannot create a trusted command intent for unsupported script type: ${scriptPath}`);
|
|
393
|
+
}
|
|
394
|
+
function createTrustedScriptIntent(skillName, scriptPath) {
|
|
395
|
+
return {
|
|
396
|
+
intent: `external_skill_${commandSafeSegment(skillName)}_${scriptNameForIntent(scriptPath)}`,
|
|
397
|
+
script_path: scriptPath,
|
|
398
|
+
argv: trustedScriptArgv(skillName, scriptPath),
|
|
399
|
+
run_policy: 'agent_allowed',
|
|
400
|
+
network: true,
|
|
401
|
+
destructive: true,
|
|
402
|
+
approval_required: ['network_access', 'destructive_command'],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function createScriptTrust(projectRoot, target, files, trustScripts, mode) {
|
|
406
|
+
const scripts = files.filter((file) => file.kind === 'script');
|
|
407
|
+
if (!trustScripts) {
|
|
408
|
+
return {
|
|
409
|
+
requested: false,
|
|
410
|
+
status: 'not_requested',
|
|
411
|
+
grants_command_authority: false,
|
|
412
|
+
command_contract_path: null,
|
|
413
|
+
include_entry: null,
|
|
414
|
+
fragment_path: null,
|
|
415
|
+
intents: [],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (scripts.length === 0) {
|
|
419
|
+
return {
|
|
420
|
+
requested: true,
|
|
421
|
+
status: 'no_scripts',
|
|
422
|
+
grants_command_authority: false,
|
|
423
|
+
command_contract_path: null,
|
|
424
|
+
include_entry: null,
|
|
425
|
+
fragment_path: null,
|
|
426
|
+
intents: [],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const fragmentName = `external-skills-${target.skill_name}.toml`;
|
|
430
|
+
const includeEntry = `commands/${fragmentName}`;
|
|
431
|
+
const fragmentPath = `${COMMAND_FRAGMENT_DIRECTORY}/${fragmentName}`;
|
|
432
|
+
const intents = scripts.map((script) => createTrustedScriptIntent(target.skill_name, script.relative_path));
|
|
433
|
+
const duplicateIntent = findDuplicate(intents.map((intent) => intent.intent));
|
|
434
|
+
if (duplicateIntent) {
|
|
435
|
+
throw new Error(`External skill trusted script intents contain a duplicate name: ${duplicateIntent}`);
|
|
436
|
+
}
|
|
437
|
+
validateTrustedScriptCommandPlan(projectRoot, includeEntry, fragmentPath, intents);
|
|
438
|
+
return {
|
|
439
|
+
requested: true,
|
|
440
|
+
status: mode === 'install' ? 'trusted' : 'planned',
|
|
441
|
+
grants_command_authority: mode === 'install',
|
|
442
|
+
command_contract_path: COMMANDS_CONFIG_PATH,
|
|
443
|
+
include_entry: includeEntry,
|
|
444
|
+
fragment_path: fragmentPath,
|
|
445
|
+
intents,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function findDuplicate(values) {
|
|
449
|
+
const seen = new Set();
|
|
450
|
+
for (const value of values) {
|
|
451
|
+
if (seen.has(value)) {
|
|
452
|
+
return value;
|
|
453
|
+
}
|
|
454
|
+
seen.add(value);
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
function validateTrustedScriptCommandPlan(projectRoot, includeEntry, fragmentPath, intents) {
|
|
459
|
+
const existingIncludes = new Set(readCommandContractIncludePaths(projectRoot).map((entry) => entry.replace(/^\.mustflow\/config\//u, '')));
|
|
460
|
+
if (existingIncludes.has(includeEntry)) {
|
|
461
|
+
throw new Error(`External skill trusted script command include already exists: ${includeEntry}`);
|
|
462
|
+
}
|
|
463
|
+
if (existsSync(path.join(projectRoot, ...fragmentPath.split('/')))) {
|
|
464
|
+
throw new Error(`External skill trusted script command fragment already exists: ${fragmentPath}`);
|
|
465
|
+
}
|
|
466
|
+
const contract = readCommandContract(projectRoot);
|
|
467
|
+
for (const intent of intents) {
|
|
468
|
+
if (Object.prototype.hasOwnProperty.call(contract.intents, intent.intent)) {
|
|
469
|
+
throw new Error(`External skill trusted script intent already exists in command contract: ${intent.intent}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function renderTomlString(value) {
|
|
474
|
+
return JSON.stringify(value);
|
|
475
|
+
}
|
|
476
|
+
function renderTomlStringArray(values) {
|
|
477
|
+
return `[${values.map(renderTomlString).join(', ')}]`;
|
|
478
|
+
}
|
|
479
|
+
function renderTrustedScriptCommandFragment(target, source, scriptTrust) {
|
|
480
|
+
const lines = [
|
|
481
|
+
`# Generated by mf skill import --trust-scripts for external skill ${JSON.stringify(target.skill_name)}.`,
|
|
482
|
+
`# Source: ${source.source_url}`,
|
|
483
|
+
'# These intents execute imported external script files. They are agent-runnable only through mf run,',
|
|
484
|
+
'# and remain gated by network_access and destructive_command approvals.',
|
|
485
|
+
'',
|
|
486
|
+
];
|
|
487
|
+
for (const intent of scriptTrust.intents) {
|
|
488
|
+
lines.push(`[intents.${intent.intent}]`, 'status = "configured"', 'lifecycle = "oneshot"', 'run_policy = "agent_allowed"', `description = ${renderTomlString(`Run trusted external skill script ${intent.script_path} from ${target.skill_name}.`)}`, `argv = ${renderTomlStringArray(intent.argv)}`, 'cwd = "."', 'timeout_seconds = 300', 'stdin = "closed"', 'success_exit_codes = [0]', 'writes = []', 'network = true', 'destructive = true', 'env_policy = "minimal"', '');
|
|
489
|
+
}
|
|
490
|
+
return lines.join('\n');
|
|
491
|
+
}
|
|
492
|
+
function updateCommandIncludeText(content, includeEntry) {
|
|
493
|
+
const normalizedContent = content.replace(/\r\n?/gu, '\n');
|
|
494
|
+
const lines = normalizedContent.split('\n');
|
|
495
|
+
const includeLineIndex = lines.findIndex((line) => /^\s*\[include\]\s*$/u.test(line));
|
|
496
|
+
const includeLine = ` ${renderTomlString(includeEntry)},`;
|
|
497
|
+
if (includeLineIndex < 0) {
|
|
498
|
+
const separator = normalizedContent.endsWith('\n') ? '' : '\n';
|
|
499
|
+
return `${normalizedContent}${separator}\n[include]\nfiles = [\n${includeLine}\n]\n`;
|
|
500
|
+
}
|
|
501
|
+
const nextSectionIndex = lines.findIndex((line, index) => index > includeLineIndex && /^\s*\[[^\]]+\]\s*$/u.test(line));
|
|
502
|
+
const sectionEnd = nextSectionIndex < 0 ? lines.length : nextSectionIndex;
|
|
503
|
+
const filesLineIndex = lines.findIndex((line, index) => index > includeLineIndex &&
|
|
504
|
+
index < sectionEnd &&
|
|
505
|
+
/^\s*files\s*=\s*\[/u.test(line));
|
|
506
|
+
if (filesLineIndex < 0) {
|
|
507
|
+
lines.splice(includeLineIndex + 1, 0, 'files = [', includeLine, ']');
|
|
508
|
+
return lines.join('\n');
|
|
509
|
+
}
|
|
510
|
+
if (lines[filesLineIndex].includes(']')) {
|
|
511
|
+
const existingValues = [...lines[filesLineIndex].matchAll(/"([^"]+)"/gu)].map((match) => match[1]);
|
|
512
|
+
const values = [...new Set([...existingValues, includeEntry])].sort((left, right) => left.localeCompare(right));
|
|
513
|
+
lines.splice(filesLineIndex, 1, 'files = [', ...values.map((entry) => ` ${renderTomlString(entry)},`), ']');
|
|
514
|
+
return lines.join('\n');
|
|
515
|
+
}
|
|
516
|
+
const closingLineIndex = lines.findIndex((line, index) => index > filesLineIndex && index < sectionEnd && /^\s*\]\s*$/u.test(line));
|
|
517
|
+
if (closingLineIndex < 0) {
|
|
518
|
+
throw new Error(`[include].files in ${COMMANDS_CONFIG_PATH} must be a TOML array`);
|
|
519
|
+
}
|
|
520
|
+
lines.splice(closingLineIndex, 0, includeLine);
|
|
521
|
+
return lines.join('\n');
|
|
522
|
+
}
|
|
523
|
+
function writeTrustedScriptCommandContract(projectRoot, target, source, scriptTrust) {
|
|
524
|
+
if (scriptTrust.status !== 'trusted' || !scriptTrust.fragment_path || !scriptTrust.include_entry) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const fragmentContent = renderTrustedScriptCommandFragment(target, source, scriptTrust);
|
|
528
|
+
writeUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...scriptTrust.fragment_path.split('/')), fragmentContent);
|
|
529
|
+
const commandsPath = path.join(projectRoot, ...COMMANDS_CONFIG_PATH.split('/'));
|
|
530
|
+
const commandsContent = readUtf8FileInsideWithoutSymlinks(projectRoot, commandsPath, { maxBytes: 256 * 1024 });
|
|
531
|
+
writeUtf8FileInsideWithoutSymlinks(projectRoot, commandsPath, updateCommandIncludeText(commandsContent, scriptTrust.include_entry));
|
|
532
|
+
}
|
|
533
|
+
function writeImportedSkillFiles(projectRoot, target, source, files, fileReport, warnings, scriptTrust) {
|
|
363
534
|
const targetPath = path.join(projectRoot, ...target.skill_dir.split('/'));
|
|
364
535
|
const skillPath = path.join(targetPath, 'SKILL.md');
|
|
365
536
|
if (existsSync(targetPath)) {
|
|
@@ -384,6 +555,7 @@ function writeImportedSkillFiles(projectRoot, target, source, files, fileReport,
|
|
|
384
555
|
kind: 'external_skill_source',
|
|
385
556
|
source,
|
|
386
557
|
files: fileReport,
|
|
558
|
+
script_trust: scriptTrust,
|
|
387
559
|
warnings,
|
|
388
560
|
});
|
|
389
561
|
renameSync(tempPath, targetPath);
|
|
@@ -424,14 +596,31 @@ export async function createExternalSkillImportReport(projectRoot, inputUrl, opt
|
|
|
424
596
|
const source = externalSkillSourceFromParsed(inputUrl, parsed);
|
|
425
597
|
const files = normalizeImportedSkillFiles(sourceFiles);
|
|
426
598
|
const reports = fileReports(files);
|
|
599
|
+
const scriptTrust = createScriptTrust(projectRoot, target, reports, options.trustScripts === true, mode);
|
|
427
600
|
const warnings = [
|
|
428
|
-
...(reports.some((file) => file.kind === 'script')
|
|
601
|
+
...(reports.some((file) => file.kind === 'script') && scriptTrust.status !== 'trusted'
|
|
429
602
|
? ['Imported scripts are inert reference files; mustflow does not grant command authority for external scripts.']
|
|
430
603
|
: []),
|
|
604
|
+
...(scriptTrust.status === 'trusted'
|
|
605
|
+
? ['Imported scripts were trusted by request; mustflow created command-contract intents gated by network and destructive approvals.']
|
|
606
|
+
: []),
|
|
607
|
+
...(scriptTrust.status === 'planned'
|
|
608
|
+
? ['Imported scripts would be trusted by request during install; dry-run only reports the command-contract plan.']
|
|
609
|
+
: []),
|
|
431
610
|
'External skills are untrusted until the agent reads and evaluates the selected SKILL.md.',
|
|
432
611
|
];
|
|
433
612
|
if (mode === 'install') {
|
|
434
|
-
|
|
613
|
+
try {
|
|
614
|
+
writeImportedSkillFiles(projectRoot, target, source, files, reports, warnings, scriptTrust);
|
|
615
|
+
writeTrustedScriptCommandContract(projectRoot, target, source, scriptTrust);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
rmSync(path.join(projectRoot, ...target.skill_dir.split('/')), { recursive: true, force: true });
|
|
619
|
+
if (scriptTrust.fragment_path) {
|
|
620
|
+
rmSync(path.join(projectRoot, ...scriptTrust.fragment_path.split('/')), { force: true });
|
|
621
|
+
}
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
435
624
|
}
|
|
436
625
|
return {
|
|
437
626
|
schema_version: '1',
|
|
@@ -444,6 +633,7 @@ export async function createExternalSkillImportReport(projectRoot, inputUrl, opt
|
|
|
444
633
|
source,
|
|
445
634
|
target,
|
|
446
635
|
files: reports,
|
|
636
|
+
script_trust: scriptTrust,
|
|
447
637
|
warnings,
|
|
448
638
|
issues: [],
|
|
449
639
|
wrote_files: mode === 'install',
|
package/package.json
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"type": "array",
|
|
44
44
|
"items": { "$ref": "#/$defs/file" }
|
|
45
45
|
},
|
|
46
|
+
"script_trust": { "$ref": "#/$defs/scriptTrust" },
|
|
46
47
|
"warnings": {
|
|
47
48
|
"type": "array",
|
|
48
49
|
"items": { "type": "string" }
|
|
@@ -92,6 +93,80 @@
|
|
|
92
93
|
"bytes": { "type": "integer", "minimum": 0 },
|
|
93
94
|
"sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
|
|
94
95
|
}
|
|
96
|
+
},
|
|
97
|
+
"scriptTrust": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"additionalProperties": false,
|
|
100
|
+
"required": [
|
|
101
|
+
"requested",
|
|
102
|
+
"status",
|
|
103
|
+
"grants_command_authority",
|
|
104
|
+
"command_contract_path",
|
|
105
|
+
"include_entry",
|
|
106
|
+
"fragment_path",
|
|
107
|
+
"intents"
|
|
108
|
+
],
|
|
109
|
+
"properties": {
|
|
110
|
+
"requested": { "type": "boolean" },
|
|
111
|
+
"status": { "type": "string", "enum": ["not_requested", "no_scripts", "planned", "trusted"] },
|
|
112
|
+
"grants_command_authority": { "type": "boolean" },
|
|
113
|
+
"command_contract_path": {
|
|
114
|
+
"oneOf": [
|
|
115
|
+
{ "const": ".mustflow/config/commands.toml" },
|
|
116
|
+
{ "type": "null" }
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"include_entry": {
|
|
120
|
+
"oneOf": [
|
|
121
|
+
{ "type": "string", "pattern": "^commands/external-skills-[a-z0-9]+(?:-[a-z0-9]+)*\\.toml$" },
|
|
122
|
+
{ "type": "null" }
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
"fragment_path": {
|
|
126
|
+
"oneOf": [
|
|
127
|
+
{ "type": "string", "pattern": "^\\.mustflow/config/commands/external-skills-[a-z0-9]+(?:-[a-z0-9]+)*\\.toml$" },
|
|
128
|
+
{ "type": "null" }
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
"intents": {
|
|
132
|
+
"type": "array",
|
|
133
|
+
"items": { "$ref": "#/$defs/trustedScriptIntent" }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"trustedScriptIntent": {
|
|
138
|
+
"type": "object",
|
|
139
|
+
"additionalProperties": false,
|
|
140
|
+
"required": [
|
|
141
|
+
"intent",
|
|
142
|
+
"script_path",
|
|
143
|
+
"argv",
|
|
144
|
+
"run_policy",
|
|
145
|
+
"network",
|
|
146
|
+
"destructive",
|
|
147
|
+
"approval_required"
|
|
148
|
+
],
|
|
149
|
+
"properties": {
|
|
150
|
+
"intent": { "type": "string", "pattern": "^external_skill_[A-Za-z0-9_-]+$" },
|
|
151
|
+
"script_path": { "type": "string", "pattern": "^scripts/.+$" },
|
|
152
|
+
"argv": {
|
|
153
|
+
"type": "array",
|
|
154
|
+
"minItems": 2,
|
|
155
|
+
"items": { "type": "string" }
|
|
156
|
+
},
|
|
157
|
+
"run_policy": { "const": "agent_allowed" },
|
|
158
|
+
"network": { "const": true },
|
|
159
|
+
"destructive": { "const": true },
|
|
160
|
+
"approval_required": {
|
|
161
|
+
"type": "array",
|
|
162
|
+
"prefixItems": [
|
|
163
|
+
{ "const": "network_access" },
|
|
164
|
+
{ "const": "destructive_command" }
|
|
165
|
+
],
|
|
166
|
+
"minItems": 2,
|
|
167
|
+
"maxItems": 2
|
|
168
|
+
}
|
|
169
|
+
}
|
|
95
170
|
}
|
|
96
171
|
}
|
|
97
172
|
}
|
|
@@ -1164,7 +1164,7 @@ translations = {}
|
|
|
1164
1164
|
[documents."skill.security-privacy-review"]
|
|
1165
1165
|
source = "locales/en/.mustflow/skills/security-privacy-review/SKILL.md"
|
|
1166
1166
|
source_locale = "en"
|
|
1167
|
-
revision =
|
|
1167
|
+
revision = 25
|
|
1168
1168
|
translations = {}
|
|
1169
1169
|
|
|
1170
1170
|
[documents."skill.security-regression-tests"]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
mustflow_doc: skill.security-privacy-review
|
|
3
3
|
locale: en
|
|
4
4
|
canonical: true
|
|
5
|
-
revision:
|
|
5
|
+
revision: 25
|
|
6
6
|
lifecycle: mustflow-owned
|
|
7
7
|
authority: procedure
|
|
8
8
|
name: security-privacy-review
|
|
@@ -200,6 +200,9 @@ Catch security, privacy, and disclosure risks introduced by ordinary code, docum
|
|
|
200
200
|
35. For pinned action references, distinguish tag objects from the commit that implements the tag. Verify pinned SHAs against the action repository so scanner tooling does not report an imposter or non-member commit.
|
|
201
201
|
36. For dependency scanner alerts, separate production dependency manifests from fixtures, examples, generated test repositories, and intentionally vulnerable samples. Narrow the scan scope before treating fixture-only alerts as product vulnerabilities.
|
|
202
202
|
- For lockfile CVEs, inspect the manifest and lockfile together. Identify the direct parent that keeps the vulnerable transitive package in the graph, update the narrowest direct dependency or override needed to reach the fixed range, and confirm the vulnerable package version no longer appears in the resolved graph before claiming the alert is fixed.
|
|
203
|
+
- For object merge, defaulting, or configuration merge advisories, trace whether parsed request bodies, database records, uploaded JSON, repository config, or provider payloads can become the first or highest-priority merge input. Treat `__proto__`, `constructor`, and `prototype` keys, inherited polluted values, and default-overwrite behavior as prototype-pollution sinks; prove the patched dependency is resolved or add a regression payload that cannot override trusted defaults.
|
|
204
|
+
- For ORM or SQL-builder advisories around identifiers, aliases, dynamic sorting, report columns, CTE names, or `.as()`-style APIs, remember that value parameter binding does not protect identifier positions. Runtime input must map through an allowlist of known columns or aliases, and dialect quote delimiters inside identifiers must be escaped by the library before the identifier is wrapped.
|
|
205
|
+
- For serializer or deserializer advisories, treat sparse arrays, giant indexes, deep objects, cyclic references, and attacker-controlled serialized payloads as allocation and CPU sinks. Bound serialized size, array length or highest index, nesting depth, and parse source trust before claiming a parser-only upgrade removes the availability risk.
|
|
203
206
|
- For advisories involving development tools such as Vite, Vitest UI, browser-mode test servers, Storybook, docs preview, asset servers, or framework dev servers, do not dismiss the issue merely because the package is a devDependency. Check whether scripts, docs, Docker, Codespaces, CI previews, tunnels, or config bind the server to a non-localhost host, widen file-serving roots, disable deny lists, or expose privileged read, write, rerun, snapshot, attachment, or execute APIs.
|
|
204
207
|
37. For deployment settings, check debug mode, sample admin accounts, default credentials, public admin panels, open metrics endpoints, public storage, root container users, HTTPS enforcement, and exposed GraphQL or development consoles.
|
|
205
208
|
38. For runtime and framework security updates, check that supported versions are documented, end-of-life versions are rejected, dependency locks exist where appropriate, security patches can be tested and deployed quickly, and rollback or redeploy can happen without manual dashboard memory. Do not treat a fashionable or high-performance runtime as safe unless the patch path is operationally credible.
|