@matimo/core 0.1.0-alpha.12.1 → 0.1.0-alpha.14

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.
Files changed (118) hide show
  1. package/README.md +169 -8
  2. package/dist/approval/approval-handler.d.ts +5 -1
  3. package/dist/approval/approval-handler.d.ts.map +1 -1
  4. package/dist/approval/approval-handler.js +6 -0
  5. package/dist/approval/approval-handler.js.map +1 -1
  6. package/dist/core/schema.d.ts +29 -8
  7. package/dist/core/schema.d.ts.map +1 -1
  8. package/dist/core/schema.js +10 -3
  9. package/dist/core/schema.js.map +1 -1
  10. package/dist/core/skill-content-parser.d.ts +91 -0
  11. package/dist/core/skill-content-parser.d.ts.map +1 -0
  12. package/dist/core/skill-content-parser.js +248 -0
  13. package/dist/core/skill-content-parser.js.map +1 -0
  14. package/dist/core/skill-loader.d.ts +46 -0
  15. package/dist/core/skill-loader.d.ts.map +1 -0
  16. package/dist/core/skill-loader.js +310 -0
  17. package/dist/core/skill-loader.js.map +1 -0
  18. package/dist/core/skill-registry.d.ts +131 -0
  19. package/dist/core/skill-registry.d.ts.map +1 -0
  20. package/dist/core/skill-registry.js +316 -0
  21. package/dist/core/skill-registry.js.map +1 -0
  22. package/dist/core/tfidf-embedding.d.ts +45 -0
  23. package/dist/core/tfidf-embedding.d.ts.map +1 -0
  24. package/dist/core/tfidf-embedding.js +199 -0
  25. package/dist/core/tfidf-embedding.js.map +1 -0
  26. package/dist/core/types.d.ts +155 -6
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/errors/matimo-error.d.ts +3 -1
  29. package/dist/errors/matimo-error.d.ts.map +1 -1
  30. package/dist/errors/matimo-error.js +2 -0
  31. package/dist/errors/matimo-error.js.map +1 -1
  32. package/dist/executors/command-executor.d.ts.map +1 -1
  33. package/dist/executors/command-executor.js +13 -2
  34. package/dist/executors/command-executor.js.map +1 -1
  35. package/dist/executors/function-executor.d.ts.map +1 -1
  36. package/dist/executors/function-executor.js +33 -20
  37. package/dist/executors/function-executor.js.map +1 -1
  38. package/dist/index.d.ts +20 -3
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +14 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/integrations/langchain.d.ts +55 -0
  43. package/dist/integrations/langchain.d.ts.map +1 -1
  44. package/dist/integrations/langchain.js +66 -0
  45. package/dist/integrations/langchain.js.map +1 -1
  46. package/dist/logging/winston-logger.d.ts.map +1 -1
  47. package/dist/logging/winston-logger.js +9 -1
  48. package/dist/logging/winston-logger.js.map +1 -1
  49. package/dist/matimo-instance.d.ts +171 -6
  50. package/dist/matimo-instance.d.ts.map +1 -1
  51. package/dist/matimo-instance.js +602 -13
  52. package/dist/matimo-instance.js.map +1 -1
  53. package/dist/mcp/mcp-server.d.ts +25 -0
  54. package/dist/mcp/mcp-server.d.ts.map +1 -1
  55. package/dist/mcp/mcp-server.js +128 -21
  56. package/dist/mcp/mcp-server.js.map +1 -1
  57. package/dist/mcp/tool-converter.d.ts.map +1 -1
  58. package/dist/mcp/tool-converter.js +10 -1
  59. package/dist/mcp/tool-converter.js.map +1 -1
  60. package/dist/policy/approval-manifest.d.ts +74 -0
  61. package/dist/policy/approval-manifest.d.ts.map +1 -0
  62. package/dist/policy/approval-manifest.js +183 -0
  63. package/dist/policy/approval-manifest.js.map +1 -0
  64. package/dist/policy/content-validator.d.ts +19 -0
  65. package/dist/policy/content-validator.d.ts.map +1 -0
  66. package/dist/policy/content-validator.js +196 -0
  67. package/dist/policy/content-validator.js.map +1 -0
  68. package/dist/policy/default-policy.d.ts +46 -0
  69. package/dist/policy/default-policy.d.ts.map +1 -0
  70. package/dist/policy/default-policy.js +241 -0
  71. package/dist/policy/default-policy.js.map +1 -0
  72. package/dist/policy/events.d.ts +71 -0
  73. package/dist/policy/events.d.ts.map +1 -0
  74. package/dist/policy/events.js +8 -0
  75. package/dist/policy/events.js.map +1 -0
  76. package/dist/policy/index.d.ts +13 -0
  77. package/dist/policy/index.d.ts.map +1 -0
  78. package/dist/policy/index.js +9 -0
  79. package/dist/policy/index.js.map +1 -0
  80. package/dist/policy/integrity-tracker.d.ts +62 -0
  81. package/dist/policy/integrity-tracker.d.ts.map +1 -0
  82. package/dist/policy/integrity-tracker.js +79 -0
  83. package/dist/policy/integrity-tracker.js.map +1 -0
  84. package/dist/policy/policy-loader.d.ts +58 -0
  85. package/dist/policy/policy-loader.d.ts.map +1 -0
  86. package/dist/policy/policy-loader.js +153 -0
  87. package/dist/policy/policy-loader.js.map +1 -0
  88. package/dist/policy/risk-classifier.d.ts +18 -0
  89. package/dist/policy/risk-classifier.d.ts.map +1 -0
  90. package/dist/policy/risk-classifier.js +43 -0
  91. package/dist/policy/risk-classifier.js.map +1 -0
  92. package/dist/policy/types.d.ts +126 -0
  93. package/dist/policy/types.d.ts.map +1 -0
  94. package/dist/policy/types.js +8 -0
  95. package/dist/policy/types.js.map +1 -0
  96. package/package.json +5 -5
  97. package/tools/matimo_approve_tool/definition.yaml +36 -0
  98. package/tools/matimo_approve_tool/matimo_approve_tool.ts +90 -0
  99. package/tools/matimo_create_skill/definition.yaml +46 -0
  100. package/tools/matimo_create_skill/matimo_create_skill.ts +75 -0
  101. package/tools/matimo_create_tool/definition.yaml +48 -0
  102. package/tools/matimo_create_tool/matimo_create_tool.ts +137 -0
  103. package/tools/matimo_get_skill/definition.yaml +60 -0
  104. package/tools/matimo_get_skill/matimo_get_skill.ts +182 -0
  105. package/tools/matimo_get_tool_status/definition.yaml +42 -0
  106. package/tools/matimo_get_tool_status/matimo_get_tool_status.ts +101 -0
  107. package/tools/matimo_list_skills/definition.yaml +52 -0
  108. package/tools/matimo_list_skills/matimo_list_skills.ts +138 -0
  109. package/tools/matimo_list_user_tools/definition.yaml +32 -0
  110. package/tools/matimo_list_user_tools/matimo_list_user_tools.ts +74 -0
  111. package/tools/matimo_reload_tools/definition.yaml +35 -0
  112. package/tools/matimo_reload_tools/matimo_reload_tools.ts +29 -0
  113. package/tools/matimo_validate_skill/definition.yaml +43 -0
  114. package/tools/matimo_validate_skill/matimo_validate_skill.ts +137 -0
  115. package/tools/matimo_validate_tool/definition.yaml +34 -0
  116. package/tools/matimo_validate_tool/matimo_validate_tool.ts +168 -0
  117. package/tools/shared/skill-validation.ts +335 -0
  118. package/LICENSE +0 -21
@@ -0,0 +1,138 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { getGlobalMatimoLogger, getGlobalMatimoInstance, extractSkillMetadata, ToolLoader } from '@matimo/core';
4
+
5
+ interface ListSkillsParams {
6
+ skills_dir?: string;
7
+ }
8
+
9
+ interface SkillSummary {
10
+ name: string;
11
+ description: string;
12
+ version?: string;
13
+ license?: string;
14
+ metadata?: Record<string, string>;
15
+ source: 'builtin' | 'user' | 'catalog';
16
+ }
17
+
18
+ interface ListSkillsResult {
19
+ skills: SkillSummary[];
20
+ total: number;
21
+ }
22
+
23
+ /**
24
+ * Helper: Load SKILL.md files from a directory and extract metadata.
25
+ */
26
+ function loadSkillsFromPath(
27
+ skillsPath: string,
28
+ source: 'builtin' | 'user',
29
+ logger: ReturnType<typeof getGlobalMatimoLogger>,
30
+ ): SkillSummary[] {
31
+ const skills: SkillSummary[] = [];
32
+
33
+ if (!fs.existsSync(skillsPath)) return skills;
34
+
35
+ try {
36
+ const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
37
+ for (const entry of entries) {
38
+ if (!entry.isDirectory()) continue;
39
+ const skillFilePath = path.join(skillsPath, entry.name, 'SKILL.md');
40
+ if (!fs.existsSync(skillFilePath)) continue;
41
+
42
+ try {
43
+ const content = fs.readFileSync(skillFilePath, 'utf-8');
44
+ const result = extractSkillMetadata(content, source);
45
+ if (result.success && result.metadata) {
46
+ skills.push(result.metadata);
47
+ }
48
+ } catch (err) {
49
+ logger.debug('matimo_list_skills: failed to extract metadata', {
50
+ skill: entry.name,
51
+ error: (err as Error).message,
52
+ });
53
+ }
54
+ }
55
+ } catch (err) {
56
+ logger.debug('matimo_list_skills: failed to read directory', {
57
+ path: skillsPath,
58
+ error: (err as Error).message,
59
+ });
60
+ }
61
+
62
+ return skills;
63
+ }
64
+
65
+
66
+
67
+ /**
68
+ * List all skills available in the current Matimo instance.
69
+ *
70
+ * Skills are discovered in this order (priority):
71
+ * 1. Global MatimoInstance (if initialized) — includes auto-discovered @matimo/* skills
72
+ * 2. Auto-discover from @matimo/* packages in node_modules (like tools do)
73
+ * 3. Explicit skills_dir if provided
74
+ *
75
+ * Returns METADATA ONLY: name, description, license, version, metadata, source.
76
+ * Full body content is available via matimo_get_skill when explicitly requested.
77
+ *
78
+ * Uses lightweight YAML-only extraction (no body/sections parsing) for efficiency.
79
+ * This avoids the overhead of parsing skill markdown sections and keeps responses small.
80
+ */
81
+ export default async function matimoListSkills(
82
+ params: ListSkillsParams,
83
+ ): Promise<ListSkillsResult> {
84
+ const logger = getGlobalMatimoLogger();
85
+ const allSkills = new Map<string, SkillSummary>();
86
+
87
+ try {
88
+ // Try global MatimoInstance first
89
+ try {
90
+ const matimo = getGlobalMatimoInstance();
91
+ if (matimo) {
92
+ const matimoSkills = matimo.listSkills();
93
+ if (matimoSkills?.length > 0) {
94
+ logger.debug('matimo_list_skills: from MatimoInstance', { count: matimoSkills.length });
95
+ matimoSkills.forEach((s) => allSkills.set(s.name, s));
96
+ return { skills: Array.from(allSkills.values()), total: allSkills.size };
97
+ }
98
+ }
99
+ } catch (err) {
100
+ logger.debug('matimo_list_skills: MatimoInstance unavailable', {
101
+ error: (err as Error).message,
102
+ });
103
+ }
104
+
105
+ // Auto-discover from @matimo/* packages
106
+ try {
107
+ const toolLoader = new ToolLoader();
108
+ const discoveredPaths = toolLoader.autoDiscoverPackages();
109
+
110
+ for (const toolPath of discoveredPaths) {
111
+ const pkgDir = path.dirname(toolPath);
112
+ const skillsPath = path.join(pkgDir, 'skills');
113
+ const discovered = loadSkillsFromPath(skillsPath, 'builtin', logger);
114
+ discovered.forEach((s) => allSkills.set(s.name, s));
115
+ }
116
+ } catch (err) {
117
+ logger.debug('matimo_list_skills: auto-discovery failed', {
118
+ error: (err as Error).message,
119
+ });
120
+ }
121
+
122
+ // Load from explicit skills_dir if provided
123
+ if (params.skills_dir) {
124
+ const skillsDir = path.resolve(params.skills_dir);
125
+ const discovered = loadSkillsFromPath(skillsDir, 'user', logger);
126
+ discovered.forEach((s) => allSkills.set(s.name, s));
127
+ }
128
+
129
+ const results = Array.from(allSkills.values());
130
+ logger.debug('matimo_list_skills: complete', { total: results.length });
131
+ return { skills: results, total: results.length };
132
+ } catch (err) {
133
+ logger.error('matimo_list_skills: failed', {
134
+ error: (err as Error).message,
135
+ });
136
+ return { skills: [], total: 0 };
137
+ }
138
+ }
@@ -0,0 +1,32 @@
1
+ name: matimo_list_user_tools
2
+ version: '1.0.0'
3
+ description: List all user-created tools in a directory with risk classification, approval status, and metadata.
4
+ requires_approval: false
5
+ tags:
6
+ - matimo
7
+ - meta
8
+ - discovery
9
+
10
+ parameters:
11
+ tool_dir:
12
+ type: string
13
+ required: false
14
+ description: Directory to scan for tools (default ./matimo-tools)
15
+ include_drafts:
16
+ type: boolean
17
+ required: false
18
+ description: Whether to include draft tools in the listing (default true)
19
+
20
+ execution:
21
+ type: function
22
+ code: './matimo_list_user_tools.ts'
23
+
24
+ output_schema:
25
+ type: object
26
+ properties:
27
+ tools:
28
+ type: array
29
+ description: List of discovered tools
30
+ total:
31
+ type: number
32
+ description: Total number of tools found
@@ -0,0 +1,74 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import {
5
+ validateToolDefinition,
6
+ classifyRisk,
7
+ getGlobalMatimoLogger,
8
+ } from '@matimo/core';
9
+
10
+ interface ListParams {
11
+ tool_dir?: string;
12
+ include_drafts?: boolean;
13
+ }
14
+
15
+ interface ToolSummary {
16
+ name: string;
17
+ description: string;
18
+ version: string;
19
+ status: string;
20
+ riskLevel: string;
21
+ tags: string[];
22
+ }
23
+
24
+ interface ListResult {
25
+ tools: ToolSummary[];
26
+ total: number;
27
+ }
28
+
29
+ export default async function matimoListUserTools(
30
+ params: ListParams,
31
+ ): Promise<ListResult> {
32
+ const logger = getGlobalMatimoLogger();
33
+ const toolDir = params.tool_dir || './matimo-tools';
34
+ const includeDrafts = params.include_drafts !== false;
35
+
36
+ const tools: ToolSummary[] = [];
37
+
38
+ if (!fs.existsSync(toolDir)) {
39
+ return { tools: [], total: 0 };
40
+ }
41
+
42
+ const entries = fs.readdirSync(toolDir, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ if (!entry.isDirectory()) continue;
45
+
46
+ const defPath = path.join(toolDir, entry.name, 'definition.yaml');
47
+ if (!fs.existsSync(defPath)) continue;
48
+
49
+ try {
50
+ const content = fs.readFileSync(defPath, 'utf-8');
51
+ const parsed = yaml.load(content);
52
+ const tool = validateToolDefinition(parsed);
53
+
54
+ const status = tool.status || 'approved';
55
+ if (!includeDrafts && status === 'draft') continue;
56
+
57
+ tools.push({
58
+ name: tool.name,
59
+ description: tool.description,
60
+ version: tool.version,
61
+ status,
62
+ riskLevel: classifyRisk(tool),
63
+ tags: tool.tags || [],
64
+ });
65
+ } catch (err) {
66
+ logger.warn('matimo_list_user_tools: failed to parse tool', {
67
+ dir: entry.name,
68
+ error: (err as Error).message,
69
+ });
70
+ }
71
+ }
72
+
73
+ return { tools, total: tools.length };
74
+ }
@@ -0,0 +1,35 @@
1
+ name: matimo_reload_tools
2
+ version: '1.0.0'
3
+ description: >-
4
+ Hot-reload all tools from configured toolPaths. Clears the registry, re-reads
5
+ YAML definitions from disk, re-validates untrusted tools against the active
6
+ policy, and returns a summary of loaded, removed, revalidated, and rejected
7
+ tools. Use after matimo_create_tool + matimo_approve_tool to bring new tools
8
+ into the live registry without restarting the process.
9
+ requires_approval: true
10
+ tags:
11
+ - matimo
12
+ - meta
13
+ - lifecycle
14
+
15
+ parameters: {}
16
+
17
+ execution:
18
+ type: function
19
+ code: './matimo_reload_tools.ts'
20
+
21
+ output_schema:
22
+ type: object
23
+ properties:
24
+ success:
25
+ type: boolean
26
+ loaded:
27
+ type: number
28
+ removed:
29
+ type: number
30
+ revalidated:
31
+ type: number
32
+ rejected:
33
+ type: array
34
+ message:
35
+ type: string
@@ -0,0 +1,29 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core';
2
+
3
+ /**
4
+ * matimo_reload_tools — Hot-reload all tools from configured toolPaths.
5
+ *
6
+ * NOTE: This function is a placeholder. The actual reload logic is intercepted
7
+ * and handled directly by MatimoInstance.execute() because reloadTools() is a
8
+ * method on the instance itself (it clears the registry, re-reads YAML from
9
+ * disk, re-validates untrusted tools, etc.). The function executor cannot do
10
+ * this because it doesn't have a reference to the MatimoInstance.
11
+ *
12
+ * If this file is ever reached (e.g., in tests without the interception),
13
+ * it returns an error instructing the caller to use matimo.reloadTools() directly.
14
+ */
15
+ export default async function matimoReloadTools(): Promise<{
16
+ success: boolean;
17
+ message: string;
18
+ }> {
19
+ const logger = getGlobalMatimoLogger();
20
+ logger.warn(
21
+ 'matimo_reload_tools: reached fallback implementation — this should be intercepted by MatimoInstance.execute()',
22
+ );
23
+
24
+ return {
25
+ success: false,
26
+ message:
27
+ 'Reload must be handled by MatimoInstance. If you see this, the interception in execute() is not active.',
28
+ };
29
+ }
@@ -0,0 +1,43 @@
1
+ name: matimo_validate_skill
2
+ version: '1.0.0'
3
+ description: >-
4
+ Validate an existing skill against the Agent Skills specification
5
+ (https://agentskills.io/specification). Checks SKILL.md frontmatter, naming
6
+ rules, directory structure, and best practices.
7
+ requires_approval: false
8
+ tags:
9
+ - matimo
10
+ - meta
11
+ - skill
12
+ - validation
13
+
14
+ parameters:
15
+ name:
16
+ type: string
17
+ required: true
18
+ description: Name of the skill directory to validate
19
+ skills_dir:
20
+ type: string
21
+ required: false
22
+ description: Directory containing skills (default ./matimo-tools/skills)
23
+
24
+ execution:
25
+ type: function
26
+ code: './matimo_validate_skill.ts'
27
+
28
+ output_schema:
29
+ type: object
30
+ properties:
31
+ valid:
32
+ type: boolean
33
+ description: Whether the skill passes all validation checks
34
+ name:
35
+ type: string
36
+ issues:
37
+ type: array
38
+ description: List of validation issues (errors and warnings)
39
+ structure:
40
+ type: object
41
+ description: Skill directory structure analysis
42
+ message:
43
+ type: string
@@ -0,0 +1,137 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getGlobalMatimoLogger } from '@matimo/core';
4
+ import {
5
+ parseSkillContent,
6
+ validateFrontmatter,
7
+ listBundledResources,
8
+ type ValidationIssue,
9
+ type BundledResources,
10
+ } from '../shared/skill-validation';
11
+
12
+ interface ValidateSkillParams {
13
+ /** Name of the skill directory to validate. */
14
+ name: string;
15
+ /** Directory containing skills (default ./matimo-tools/skills). */
16
+ skills_dir?: string;
17
+ }
18
+
19
+ interface ValidateSkillResult {
20
+ valid: boolean;
21
+ name: string;
22
+ issues: ValidationIssue[];
23
+ structure: {
24
+ has_skill_md: boolean;
25
+ resources: BundledResources;
26
+ };
27
+ message: string;
28
+ }
29
+
30
+ /**
31
+ * Validate an existing skill against the Agent Skills specification.
32
+ *
33
+ * Checks:
34
+ * - SKILL.md exists
35
+ * - YAML frontmatter is present and valid
36
+ * - Name follows spec rules (lowercase, hyphens, max 64 chars)
37
+ * - Name matches directory name
38
+ * - Description is present and within limits
39
+ * - Optional fields follow constraints
40
+ * - Lists bundled resources for review
41
+ *
42
+ * @see https://agentskills.io/specification
43
+ */
44
+ export default async function matimoValidateSkill(
45
+ params: ValidateSkillParams,
46
+ ): Promise<ValidateSkillResult> {
47
+ const logger = getGlobalMatimoLogger();
48
+ const skillsDir = params.skills_dir || './matimo-tools/skills';
49
+
50
+ const failResult = (message: string, issues: ValidationIssue[] = []): ValidateSkillResult => ({
51
+ valid: false,
52
+ name: params.name || '',
53
+ issues,
54
+ structure: { has_skill_md: false, resources: { scripts: [], references: [], assets: [], other: [] } },
55
+ message,
56
+ });
57
+
58
+ if (!params.name || params.name.trim().length === 0) {
59
+ return failResult('Skill name is required');
60
+ }
61
+
62
+ const skillDir = path.join(skillsDir, params.name);
63
+ const skillPath = path.join(skillDir, 'SKILL.md');
64
+
65
+ // Check directory exists
66
+ if (!fs.existsSync(skillDir)) {
67
+ return failResult(`Skill directory not found: ${skillDir}`);
68
+ }
69
+
70
+ // Check SKILL.md exists
71
+ if (!fs.existsSync(skillPath)) {
72
+ return failResult(`SKILL.md not found in ${skillDir}`, [
73
+ { field: 'SKILL.md', message: 'Required SKILL.md file is missing', severity: 'error' },
74
+ ]);
75
+ }
76
+
77
+ // Parse content
78
+ const content = fs.readFileSync(skillPath, 'utf-8');
79
+ const parseResult = parseSkillContent(content);
80
+
81
+ if (!parseResult.success) {
82
+ return failResult(parseResult.error!, [
83
+ { field: 'frontmatter', message: parseResult.error!, severity: 'error' },
84
+ ]);
85
+ }
86
+
87
+ const { frontmatter, body } = parseResult.parsed!;
88
+
89
+ // Validate frontmatter (with directory name matching)
90
+ const fmResult = validateFrontmatter(frontmatter, params.name);
91
+
92
+ // Add warnings for best practices
93
+ const allIssues = [...fmResult.issues];
94
+
95
+ // Warn if body is empty
96
+ if (!body || body.trim().length === 0) {
97
+ allIssues.push({
98
+ field: 'body',
99
+ message: 'SKILL.md has no instructions body — add content after the frontmatter',
100
+ severity: 'warning',
101
+ });
102
+ }
103
+
104
+ // Warn if body is too long (spec recommends < 5000 tokens ≈ < 500 lines)
105
+ const lineCount = body.split('\n').length;
106
+ if (lineCount > 500) {
107
+ allIssues.push({
108
+ field: 'body',
109
+ message: `SKILL.md body has ${lineCount} lines — spec recommends < 500 lines. Consider splitting into referenced files.`,
110
+ severity: 'warning',
111
+ });
112
+ }
113
+
114
+ // List bundled resources
115
+ const resources = listBundledResources(skillDir);
116
+
117
+ const valid = allIssues.filter(i => i.severity === 'error').length === 0;
118
+
119
+ logger.info('matimo_validate_skill: validation complete', {
120
+ name: params.name,
121
+ valid,
122
+ issueCount: allIssues.length,
123
+ });
124
+
125
+ return {
126
+ valid,
127
+ name: params.name,
128
+ issues: allIssues,
129
+ structure: {
130
+ has_skill_md: true,
131
+ resources,
132
+ },
133
+ message: valid
134
+ ? `Skill "${params.name}" is valid per the Agent Skills specification.`
135
+ : `Skill "${params.name}" has ${allIssues.filter(i => i.severity === 'error').length} error(s).`,
136
+ };
137
+ }
@@ -0,0 +1,34 @@
1
+ name: matimo_validate_tool
2
+ version: '1.0.0'
3
+ description: Validate a tool definition YAML string against the Matimo schema and policy rules. Returns schema errors, policy violations, and risk classification.
4
+ requires_approval: false
5
+ tags:
6
+ - matimo
7
+ - meta
8
+ - validation
9
+
10
+ parameters:
11
+ yaml_content:
12
+ type: string
13
+ required: true
14
+ description: The YAML content of the tool definition to validate
15
+
16
+ execution:
17
+ type: function
18
+ code: './matimo_validate_tool.ts'
19
+
20
+ output_schema:
21
+ type: object
22
+ properties:
23
+ valid:
24
+ type: boolean
25
+ description: Whether the tool definition is valid
26
+ schemaErrors:
27
+ type: array
28
+ description: Schema validation errors (if any)
29
+ policyViolations:
30
+ type: array
31
+ description: Policy violations found by content validator
32
+ riskLevel:
33
+ type: string
34
+ description: Risk classification (low, medium, high, critical)
@@ -0,0 +1,168 @@
1
+ import yaml from 'js-yaml';
2
+ import {
3
+ validateToolDefinition,
4
+ validateToolContent,
5
+ classifyRisk,
6
+ getGlobalMatimoLogger,
7
+ } from '@matimo/core';
8
+ import type { Violation } from '@matimo/core';
9
+
10
+ interface ValidateParams {
11
+ yaml_content: string;
12
+ }
13
+
14
+ /** Structured schema error with field path and optional valid values. */
15
+ interface SchemaError {
16
+ field: string;
17
+ message: string;
18
+ validOptions?: string[];
19
+ }
20
+
21
+ interface ValidateResult {
22
+ valid: boolean;
23
+ schemaErrors: SchemaError[];
24
+ policyViolations: Array<{ rule: string; severity: string; message: string }>;
25
+ riskLevel: string;
26
+ }
27
+
28
+ const EXECUTION_TYPE_OPTIONS = ['command', 'http', 'function'];
29
+ const PARAMETER_TYPE_OPTIONS = ['string', 'number', 'boolean', 'array', 'object'];
30
+ const HTTP_METHOD_OPTIONS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
31
+ const AUTH_TYPE_OPTIONS = ['api_key', 'basic', 'bearer', 'oauth2', 'custom'];
32
+ const STATUS_OPTIONS = ['draft', 'approved', 'deprecated'];
33
+
34
+ /**
35
+ * Map known field paths to valid option sets so agents get actionable errors.
36
+ */
37
+ const VALID_OPTIONS_BY_PATH: Record<string, string[]> = {
38
+ 'execution.type': EXECUTION_TYPE_OPTIONS,
39
+ 'execution.method': HTTP_METHOD_OPTIONS,
40
+ 'authentication.type': AUTH_TYPE_OPTIONS,
41
+ status: STATUS_OPTIONS,
42
+ };
43
+
44
+ const PARAMETER_TYPE_PATTERN = /^parameters\.[^.]+\.type$/;
45
+ const PARAMETER_ENCODING_BACKOFF = /^error_handling\.backoff_type$/;
46
+
47
+ function getValidOptions(fieldPath: string): string[] | undefined {
48
+ if (VALID_OPTIONS_BY_PATH[fieldPath]) return VALID_OPTIONS_BY_PATH[fieldPath];
49
+ if (PARAMETER_TYPE_PATTERN.test(fieldPath)) return PARAMETER_TYPE_OPTIONS;
50
+ if (PARAMETER_ENCODING_BACKOFF.test(fieldPath)) return ['linear', 'exponential'];
51
+ return undefined;
52
+ }
53
+
54
+ /**
55
+ * Convert a raw Zod issue into a human-readable SchemaError.
56
+ * Handles the most common patterns: missing fields, invalid enums,
57
+ * invalid discriminated union (execution.type).
58
+ */
59
+ function formatZodIssue(issue: { path: (string | number)[]; message: string; code: string; expected?: string; received?: string }): SchemaError {
60
+ const fieldPath = issue.path.length > 0 ? issue.path.join('.') : 'root';
61
+ const validOptions = getValidOptions(fieldPath);
62
+
63
+ let message: string;
64
+
65
+ switch (issue.code) {
66
+ case 'invalid_type':
67
+ if (issue.received === 'undefined') {
68
+ message = `Missing required field: \`${fieldPath}\``;
69
+ if (validOptions) {
70
+ message += ` — must be one of: ${validOptions.map((v) => `'${v}'`).join(', ')}`;
71
+ }
72
+ } else {
73
+ message = `Invalid type for \`${fieldPath}\`: expected ${issue.expected ?? 'unknown'}, got ${issue.received ?? 'unknown'}`;
74
+ }
75
+ break;
76
+
77
+ case 'invalid_literal':
78
+ case 'invalid_enum_value':
79
+ message = `Invalid value for \`${fieldPath}\``;
80
+ if (validOptions) {
81
+ message += ` — must be one of: ${validOptions.map((v) => `'${v}'`).join(', ')}`;
82
+ } else {
83
+ message += ` (${issue.message})`;
84
+ }
85
+ break;
86
+
87
+ case 'invalid_union':
88
+ // Discriminated union failure — most commonly execution.type
89
+ message = `Invalid value for \`${fieldPath}\``;
90
+ if (fieldPath === 'execution' || fieldPath === 'root') {
91
+ message = `Missing or invalid \`execution.type\` — must be one of: ${EXECUTION_TYPE_OPTIONS.map((v) => `'${v}'`).join(', ')}`;
92
+ return { field: 'execution.type', message, validOptions: EXECUTION_TYPE_OPTIONS };
93
+ }
94
+ if (validOptions) {
95
+ message += ` — must be one of: ${validOptions.map((v) => `'${v}'`).join(', ')}`;
96
+ } else {
97
+ message += ` (${issue.message})`;
98
+ }
99
+ break;
100
+
101
+ default:
102
+ message = `\`${fieldPath}\`: ${issue.message}`;
103
+ }
104
+
105
+ return { field: fieldPath, message, ...(validOptions ? { validOptions } : {}) };
106
+ }
107
+
108
+ export default async function matimoValidateTool(
109
+ params: ValidateParams,
110
+ ): Promise<ValidateResult> {
111
+ const logger = getGlobalMatimoLogger();
112
+ const result: ValidateResult = {
113
+ valid: true,
114
+ schemaErrors: [],
115
+ policyViolations: [],
116
+ riskLevel: 'low',
117
+ };
118
+
119
+ // Step 1: Parse YAML
120
+ let parsed: unknown;
121
+ try {
122
+ parsed = yaml.load(params.yaml_content);
123
+ } catch (err) {
124
+ result.valid = false;
125
+ result.schemaErrors.push({
126
+ field: 'root',
127
+ message: `YAML parse error: ${(err as Error).message}`,
128
+ });
129
+ logger.warn('matimo_validate_tool: YAML parse failed', { error: (err as Error).message });
130
+ return result;
131
+ }
132
+
133
+ // Step 2: Validate against ToolDefinition schema
134
+ let tool: ReturnType<typeof validateToolDefinition>;
135
+ try {
136
+ tool = validateToolDefinition(parsed);
137
+ } catch (err) {
138
+ result.valid = false;
139
+ // MatimoError carries raw Zod issues in details.issues — use them for structured output
140
+ const matimoErr = err as { details?: { issues?: unknown[] }; message?: string };
141
+ const rawIssues = matimoErr.details?.issues;
142
+ if (Array.isArray(rawIssues) && rawIssues.length > 0) {
143
+ result.schemaErrors = (rawIssues as { path: (string | number)[]; message: string; code: string; expected?: string; received?: string }[]).map(formatZodIssue);
144
+ } else {
145
+ result.schemaErrors.push({ field: 'root', message: (err as Error).message });
146
+ }
147
+ logger.warn('matimo_validate_tool: schema validation failed', {
148
+ errorCount: result.schemaErrors.length,
149
+ });
150
+ return result;
151
+ }
152
+
153
+ // Step 3: Run content validator (as untrusted source)
154
+ const validation = validateToolContent(tool, { source: 'untrusted' });
155
+ if (!validation.valid) {
156
+ result.valid = false;
157
+ result.policyViolations = validation.violations.map((v: Violation) => ({
158
+ rule: v.rule,
159
+ severity: v.severity,
160
+ message: v.message,
161
+ }));
162
+ }
163
+
164
+ // Step 4: Classify risk
165
+ result.riskLevel = classifyRisk(tool);
166
+
167
+ return result;
168
+ }