@matimo/core 0.1.0-alpha.12 → 0.1.0-alpha.13
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/README.md +169 -8
- package/dist/approval/approval-handler.d.ts +5 -1
- package/dist/approval/approval-handler.d.ts.map +1 -1
- package/dist/approval/approval-handler.js +6 -0
- package/dist/approval/approval-handler.js.map +1 -1
- package/dist/core/schema.d.ts +29 -8
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +10 -3
- package/dist/core/schema.js.map +1 -1
- package/dist/core/skill-content-parser.d.ts +91 -0
- package/dist/core/skill-content-parser.d.ts.map +1 -0
- package/dist/core/skill-content-parser.js +248 -0
- package/dist/core/skill-content-parser.js.map +1 -0
- package/dist/core/skill-loader.d.ts +46 -0
- package/dist/core/skill-loader.d.ts.map +1 -0
- package/dist/core/skill-loader.js +310 -0
- package/dist/core/skill-loader.js.map +1 -0
- package/dist/core/skill-registry.d.ts +131 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +316 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/tfidf-embedding.d.ts +45 -0
- package/dist/core/tfidf-embedding.d.ts.map +1 -0
- package/dist/core/tfidf-embedding.js +199 -0
- package/dist/core/tfidf-embedding.js.map +1 -0
- package/dist/core/types.d.ts +192 -6
- package/dist/core/types.d.ts.map +1 -1
- package/dist/encodings/parameter-encoding.d.ts.map +1 -1
- package/dist/encodings/parameter-encoding.js +6 -2
- package/dist/encodings/parameter-encoding.js.map +1 -1
- package/dist/errors/matimo-error.d.ts +3 -1
- package/dist/errors/matimo-error.d.ts.map +1 -1
- package/dist/errors/matimo-error.js +2 -0
- package/dist/errors/matimo-error.js.map +1 -1
- package/dist/executors/command-executor.d.ts +9 -2
- package/dist/executors/command-executor.d.ts.map +1 -1
- package/dist/executors/command-executor.js +14 -2
- package/dist/executors/command-executor.js.map +1 -1
- package/dist/executors/function-executor.d.ts +10 -3
- package/dist/executors/function-executor.d.ts.map +1 -1
- package/dist/executors/function-executor.js +11 -4
- package/dist/executors/function-executor.js.map +1 -1
- package/dist/executors/http-executor.d.ts +16 -2
- package/dist/executors/http-executor.d.ts.map +1 -1
- package/dist/executors/http-executor.js +22 -6
- package/dist/executors/http-executor.js.map +1 -1
- package/dist/index.d.ts +20 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/langchain.d.ts +55 -0
- package/dist/integrations/langchain.d.ts.map +1 -1
- package/dist/integrations/langchain.js +66 -0
- package/dist/integrations/langchain.js.map +1 -1
- package/dist/logging/winston-logger.d.ts.map +1 -1
- package/dist/logging/winston-logger.js +9 -1
- package/dist/logging/winston-logger.js.map +1 -1
- package/dist/matimo-instance.d.ts +210 -18
- package/dist/matimo-instance.d.ts.map +1 -1
- package/dist/matimo-instance.js +704 -40
- package/dist/matimo-instance.js.map +1 -1
- package/dist/mcp/mcp-server.d.ts +23 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +119 -8
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/tool-converter.d.ts.map +1 -1
- package/dist/mcp/tool-converter.js +10 -1
- package/dist/mcp/tool-converter.js.map +1 -1
- package/dist/policy/approval-manifest.d.ts +74 -0
- package/dist/policy/approval-manifest.d.ts.map +1 -0
- package/dist/policy/approval-manifest.js +178 -0
- package/dist/policy/approval-manifest.js.map +1 -0
- package/dist/policy/content-validator.d.ts +19 -0
- package/dist/policy/content-validator.d.ts.map +1 -0
- package/dist/policy/content-validator.js +196 -0
- package/dist/policy/content-validator.js.map +1 -0
- package/dist/policy/default-policy.d.ts +46 -0
- package/dist/policy/default-policy.d.ts.map +1 -0
- package/dist/policy/default-policy.js +241 -0
- package/dist/policy/default-policy.js.map +1 -0
- package/dist/policy/events.d.ts +71 -0
- package/dist/policy/events.d.ts.map +1 -0
- package/dist/policy/events.js +8 -0
- package/dist/policy/events.js.map +1 -0
- package/dist/policy/index.d.ts +13 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +9 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/integrity-tracker.d.ts +62 -0
- package/dist/policy/integrity-tracker.d.ts.map +1 -0
- package/dist/policy/integrity-tracker.js +79 -0
- package/dist/policy/integrity-tracker.js.map +1 -0
- package/dist/policy/policy-loader.d.ts +58 -0
- package/dist/policy/policy-loader.d.ts.map +1 -0
- package/dist/policy/policy-loader.js +153 -0
- package/dist/policy/policy-loader.js.map +1 -0
- package/dist/policy/risk-classifier.d.ts +18 -0
- package/dist/policy/risk-classifier.d.ts.map +1 -0
- package/dist/policy/risk-classifier.js +43 -0
- package/dist/policy/risk-classifier.js.map +1 -0
- package/dist/policy/types.d.ts +126 -0
- package/dist/policy/types.d.ts.map +1 -0
- package/dist/policy/types.js +8 -0
- package/dist/policy/types.js.map +1 -0
- package/package.json +2 -2
- package/tools/matimo_approve_tool/definition.yaml +36 -0
- package/tools/matimo_approve_tool/matimo_approve_tool.ts +90 -0
- package/tools/matimo_create_skill/definition.yaml +46 -0
- package/tools/matimo_create_skill/matimo_create_skill.ts +75 -0
- package/tools/matimo_create_tool/definition.yaml +48 -0
- package/tools/matimo_create_tool/matimo_create_tool.ts +137 -0
- package/tools/matimo_get_skill/definition.yaml +60 -0
- package/tools/matimo_get_skill/matimo_get_skill.ts +182 -0
- package/tools/matimo_get_tool_status/definition.yaml +42 -0
- package/tools/matimo_get_tool_status/matimo_get_tool_status.ts +101 -0
- package/tools/matimo_list_skills/definition.yaml +52 -0
- package/tools/matimo_list_skills/matimo_list_skills.ts +138 -0
- package/tools/matimo_list_user_tools/definition.yaml +32 -0
- package/tools/matimo_list_user_tools/matimo_list_user_tools.ts +74 -0
- package/tools/matimo_reload_tools/definition.yaml +35 -0
- package/tools/matimo_reload_tools/matimo_reload_tools.ts +29 -0
- package/tools/matimo_validate_skill/definition.yaml +43 -0
- package/tools/matimo_validate_skill/matimo_validate_skill.ts +137 -0
- package/tools/matimo_validate_tool/definition.yaml +34 -0
- package/tools/matimo_validate_tool/matimo_validate_tool.ts +168 -0
- package/tools/shared/skill-validation.ts +335 -0
|
@@ -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
|
+
}
|