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.
@@ -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 { ensureFileTargetInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
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 writeImportedSkillFiles(projectRoot, target, source, files, fileReport, warnings) {
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
- writeImportedSkillFiles(projectRoot, target, source, files, reports, warnings);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.112.14",
3
+ "version": "2.113.0",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -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 = 24
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: 24
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.
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.112.14"
3
+ version = "2.113.0"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"