@matimo/core 0.1.0-alpha.9 → 0.1.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/README.md +341 -14
- 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 +41 -10
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +40 -4
- 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/tool-loader.d.ts +3 -1
- package/dist/core/tool-loader.d.ts.map +1 -1
- package/dist/core/tool-loader.js +33 -10
- package/dist/core/tool-loader.js.map +1 -1
- package/dist/core/types.d.ts +203 -6
- package/dist/core/types.d.ts.map +1 -1
- package/dist/encodings/parameter-encoding.d.ts +1 -1
- package/dist/encodings/parameter-encoding.d.ts.map +1 -1
- package/dist/encodings/parameter-encoding.js +9 -4
- package/dist/encodings/parameter-encoding.js.map +1 -1
- package/dist/errors/matimo-error.d.ts +11 -2
- package/dist/errors/matimo-error.d.ts.map +1 -1
- package/dist/errors/matimo-error.js +25 -1
- 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 +29 -5
- 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 +44 -24
- package/dist/executors/function-executor.js.map +1 -1
- package/dist/executors/http-executor.d.ts +79 -4
- package/dist/executors/http-executor.d.ts.map +1 -1
- package/dist/executors/http-executor.js +232 -28
- package/dist/executors/http-executor.js.map +1 -1
- package/dist/index.d.ts +25 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -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 +71 -4
- 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 +230 -18
- package/dist/matimo-instance.d.ts.map +1 -1
- package/dist/matimo-instance.js +739 -40
- package/dist/matimo-instance.js.map +1 -1
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +141 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +754 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/mcp/secrets/aws-resolver.d.ts +41 -0
- package/dist/mcp/secrets/aws-resolver.d.ts.map +1 -0
- package/dist/mcp/secrets/aws-resolver.js +141 -0
- package/dist/mcp/secrets/aws-resolver.js.map +1 -0
- package/dist/mcp/secrets/dotenv-resolver.d.ts +23 -0
- package/dist/mcp/secrets/dotenv-resolver.d.ts.map +1 -0
- package/dist/mcp/secrets/dotenv-resolver.js +94 -0
- package/dist/mcp/secrets/dotenv-resolver.js.map +1 -0
- package/dist/mcp/secrets/env-resolver.d.ts +14 -0
- package/dist/mcp/secrets/env-resolver.d.ts.map +1 -0
- package/dist/mcp/secrets/env-resolver.js +27 -0
- package/dist/mcp/secrets/env-resolver.js.map +1 -0
- package/dist/mcp/secrets/index.d.ts +14 -0
- package/dist/mcp/secrets/index.d.ts.map +1 -0
- package/dist/mcp/secrets/index.js +13 -0
- package/dist/mcp/secrets/index.js.map +1 -0
- package/dist/mcp/secrets/resolver-chain.d.ts +34 -0
- package/dist/mcp/secrets/resolver-chain.d.ts.map +1 -0
- package/dist/mcp/secrets/resolver-chain.js +141 -0
- package/dist/mcp/secrets/resolver-chain.js.map +1 -0
- package/dist/mcp/secrets/types.d.ts +73 -0
- package/dist/mcp/secrets/types.d.ts.map +1 -0
- package/dist/mcp/secrets/types.js +8 -0
- package/dist/mcp/secrets/types.js.map +1 -0
- package/dist/mcp/secrets/vault-resolver.d.ts +43 -0
- package/dist/mcp/secrets/vault-resolver.d.ts.map +1 -0
- package/dist/mcp/secrets/vault-resolver.js +127 -0
- package/dist/mcp/secrets/vault-resolver.js.map +1 -0
- package/dist/mcp/tool-converter.d.ts +40 -0
- package/dist/mcp/tool-converter.d.ts.map +1 -0
- package/dist/mcp/tool-converter.js +185 -0
- package/dist/mcp/tool-converter.js.map +1 -0
- package/dist/policy/approval-manifest.d.ts +76 -0
- package/dist/policy/approval-manifest.d.ts.map +1 -0
- package/dist/policy/approval-manifest.js +197 -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 +156 -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 +47 -0
- package/dist/policy/risk-classifier.js.map +1 -0
- package/dist/policy/types.d.ts +131 -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 +22 -6
- 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/definition.yaml +36 -0
- package/tools/matimo_get_tool/matimo_get_tool.ts +56 -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_search_tools/definition.yaml +32 -0
- package/tools/matimo_search_tools/matimo_search_tools.ts +82 -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
- package/LICENSE +0 -21
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared validation utilities for Agent Skills — aligned with the official
|
|
3
|
+
* Agent Skills specification (https://agentskills.io/specification).
|
|
4
|
+
*
|
|
5
|
+
* Covers: name rules, description rules, frontmatter parsing/validation,
|
|
6
|
+
* and bundled resource discovery.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
// ── Name Validation ──────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** Spec: lowercase letters, numbers, hyphens only. */
|
|
14
|
+
const VALID_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
15
|
+
|
|
16
|
+
/** Consecutive hyphens are not allowed. */
|
|
17
|
+
const CONSECUTIVE_HYPHENS = /--/;
|
|
18
|
+
|
|
19
|
+
/** Max length for skill name. */
|
|
20
|
+
const MAX_NAME_LENGTH = 64;
|
|
21
|
+
|
|
22
|
+
/** Max length for description. */
|
|
23
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
24
|
+
|
|
25
|
+
/** Max length for compatibility field. */
|
|
26
|
+
const MAX_COMPATIBILITY_LENGTH = 500;
|
|
27
|
+
|
|
28
|
+
/** Path traversal detection — kept for defense-in-depth. */
|
|
29
|
+
const UNSAFE_NAME_PATTERN = /[/\\]|\.\.|[\x00-\x1f]/;
|
|
30
|
+
|
|
31
|
+
export interface NameValidationResult {
|
|
32
|
+
valid: boolean;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate a skill name against the Agent Skills spec.
|
|
38
|
+
*
|
|
39
|
+
* Rules:
|
|
40
|
+
* - 1–64 characters
|
|
41
|
+
* - Lowercase letters, numbers, hyphens only
|
|
42
|
+
* - Must not start or end with a hyphen
|
|
43
|
+
* - Must not contain consecutive hyphens (--)
|
|
44
|
+
* - Must not contain path traversal characters
|
|
45
|
+
*/
|
|
46
|
+
export function validateSkillName(name: string): NameValidationResult {
|
|
47
|
+
if (!name || name.trim().length === 0) {
|
|
48
|
+
return { valid: false, error: 'Skill name is required' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (UNSAFE_NAME_PATTERN.test(name)) {
|
|
52
|
+
return { valid: false, error: 'Skill name contains invalid characters' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
56
|
+
return { valid: false, error: `Skill name must be at most ${MAX_NAME_LENGTH} characters (got ${name.length})` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!VALID_NAME_PATTERN.test(name)) {
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
error: 'Skill name must contain only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (CONSECUTIVE_HYPHENS.test(name)) {
|
|
67
|
+
return { valid: false, error: 'Skill name must not contain consecutive hyphens (--)' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { valid: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Frontmatter Parsing ──────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export interface SkillFrontmatter {
|
|
76
|
+
name: string;
|
|
77
|
+
description: string;
|
|
78
|
+
license?: string;
|
|
79
|
+
compatibility?: string;
|
|
80
|
+
'allowed-tools'?: string | string[];
|
|
81
|
+
metadata?: Record<string, string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ParsedSkill {
|
|
85
|
+
frontmatter: SkillFrontmatter;
|
|
86
|
+
body: string;
|
|
87
|
+
raw: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ParseResult {
|
|
91
|
+
success: boolean;
|
|
92
|
+
error?: string;
|
|
93
|
+
parsed?: ParsedSkill;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse YAML frontmatter from SKILL.md content.
|
|
98
|
+
*
|
|
99
|
+
* Handles the spec's required fields (name, description) and optional fields
|
|
100
|
+
* (license, compatibility, metadata, allowed-tools).
|
|
101
|
+
*/
|
|
102
|
+
export function parseSkillContent(content: string): ParseResult {
|
|
103
|
+
if (!content || !content.startsWith('---')) {
|
|
104
|
+
return { success: false, error: 'Skill content must start with YAML frontmatter (---)' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const endIndex = content.indexOf('---', 3);
|
|
108
|
+
if (endIndex === -1) {
|
|
109
|
+
return { success: false, error: 'Skill content must have closing YAML frontmatter (---)' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const frontmatterBlock = content.substring(3, endIndex).trim();
|
|
113
|
+
const body = content.substring(endIndex + 3).trim();
|
|
114
|
+
|
|
115
|
+
// Parse frontmatter lines
|
|
116
|
+
const fields: Record<string, unknown> = {};
|
|
117
|
+
let currentMetadata: Record<string, string> | null = null;
|
|
118
|
+
let currentArray: string[] | null = null;
|
|
119
|
+
let currentArrayKey: string | null = null;
|
|
120
|
+
|
|
121
|
+
for (const line of frontmatterBlock.split('\n')) {
|
|
122
|
+
// Detect array elements (prefixed with "- ")
|
|
123
|
+
if (currentArray !== null && /^\s*- /.test(line)) {
|
|
124
|
+
const item = line.trim().substring(2).trim();
|
|
125
|
+
if (item) {
|
|
126
|
+
currentArray.push(item.replace(/^["']|["']$/g, ''));
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Detect metadata sub-keys (indented with spaces under "metadata:")
|
|
132
|
+
if (currentMetadata !== null && /^\s+\S/.test(line)) {
|
|
133
|
+
const colonIndex = line.indexOf(':');
|
|
134
|
+
if (colonIndex !== -1) {
|
|
135
|
+
const key = line.substring(0, colonIndex).trim();
|
|
136
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
137
|
+
if (key && value) {
|
|
138
|
+
currentMetadata[key] = value.replace(/^["']|["']$/g, '');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Top-level keys
|
|
145
|
+
currentMetadata = null;
|
|
146
|
+
currentArray = null;
|
|
147
|
+
currentArrayKey = null;
|
|
148
|
+
const colonIndex = line.indexOf(':');
|
|
149
|
+
if (colonIndex === -1) continue;
|
|
150
|
+
|
|
151
|
+
const key = line.substring(0, colonIndex).trim();
|
|
152
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
153
|
+
|
|
154
|
+
if (key === 'metadata' && !value) {
|
|
155
|
+
// Start collecting metadata sub-keys
|
|
156
|
+
currentMetadata = {};
|
|
157
|
+
fields['__metadata__'] = 'MAP';
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check for array start (value is empty, array items follow on next lines)
|
|
162
|
+
if (key === 'allowed-tools' && !value) {
|
|
163
|
+
currentArray = [];
|
|
164
|
+
currentArrayKey = 'allowed-tools';
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (key && value) {
|
|
169
|
+
// Strip surrounding quotes (YAML style)
|
|
170
|
+
fields[key] = value.replace(/^["']|["']$/g, '');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Store any pending array
|
|
175
|
+
if (currentArray !== null && currentArrayKey) {
|
|
176
|
+
fields[currentArrayKey] = currentArray;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Build frontmatter object
|
|
180
|
+
const frontmatter: SkillFrontmatter = {
|
|
181
|
+
name: fields.name as string || '',
|
|
182
|
+
description: fields.description as string || '',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (fields.license) frontmatter.license = fields.license as string;
|
|
186
|
+
if (fields.compatibility) frontmatter.compatibility = fields.compatibility as string;
|
|
187
|
+
if (fields['allowed-tools']) {
|
|
188
|
+
frontmatter['allowed-tools'] = Array.isArray(fields['allowed-tools'])
|
|
189
|
+
? (fields['allowed-tools'] as string[])
|
|
190
|
+
: (fields['allowed-tools'] as string);
|
|
191
|
+
}
|
|
192
|
+
if (currentMetadata && Object.keys(currentMetadata).length > 0) {
|
|
193
|
+
frontmatter.metadata = currentMetadata;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
success: true,
|
|
198
|
+
parsed: { frontmatter, body, raw: content },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Frontmatter Validation ───────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export interface ValidationIssue {
|
|
205
|
+
field: string;
|
|
206
|
+
message: string;
|
|
207
|
+
severity: 'error' | 'warning';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface ValidationResult {
|
|
211
|
+
valid: boolean;
|
|
212
|
+
issues: ValidationIssue[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Validate parsed frontmatter against the Agent Skills spec.
|
|
217
|
+
*
|
|
218
|
+
* Checks: name rules, description rules, optional field constraints,
|
|
219
|
+
* and name/directory consistency (if directoryName provided).
|
|
220
|
+
*/
|
|
221
|
+
export function validateFrontmatter(
|
|
222
|
+
frontmatter: SkillFrontmatter,
|
|
223
|
+
directoryName?: string,
|
|
224
|
+
): ValidationResult {
|
|
225
|
+
const issues: ValidationIssue[] = [];
|
|
226
|
+
|
|
227
|
+
// Required: name
|
|
228
|
+
if (!frontmatter.name) {
|
|
229
|
+
issues.push({ field: 'name', message: 'YAML frontmatter must include a "name" field', severity: 'error' });
|
|
230
|
+
} else {
|
|
231
|
+
const nameResult = validateSkillName(frontmatter.name);
|
|
232
|
+
if (!nameResult.valid) {
|
|
233
|
+
issues.push({ field: 'name', message: nameResult.error!, severity: 'error' });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Required: description
|
|
238
|
+
if (!frontmatter.description) {
|
|
239
|
+
issues.push({ field: 'description', message: 'YAML frontmatter must include a "description" field', severity: 'error' });
|
|
240
|
+
} else if (frontmatter.description.length > MAX_DESCRIPTION_LENGTH) {
|
|
241
|
+
issues.push({
|
|
242
|
+
field: 'description',
|
|
243
|
+
message: `Description must be at most ${MAX_DESCRIPTION_LENGTH} characters (got ${frontmatter.description.length})`,
|
|
244
|
+
severity: 'error',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Optional: compatibility
|
|
249
|
+
if (frontmatter.compatibility && frontmatter.compatibility.length > MAX_COMPATIBILITY_LENGTH) {
|
|
250
|
+
issues.push({
|
|
251
|
+
field: 'compatibility',
|
|
252
|
+
message: `Compatibility must be at most ${MAX_COMPATIBILITY_LENGTH} characters (got ${frontmatter.compatibility.length})`,
|
|
253
|
+
severity: 'error',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// name must match directory name (if provided)
|
|
258
|
+
if (directoryName && frontmatter.name && frontmatter.name !== directoryName) {
|
|
259
|
+
issues.push({
|
|
260
|
+
field: 'name',
|
|
261
|
+
message: `Skill name "${frontmatter.name}" must match its directory name "${directoryName}"`,
|
|
262
|
+
severity: 'error',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
valid: issues.filter(i => i.severity === 'error').length === 0,
|
|
268
|
+
issues,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Bundled Resources (Level 3) ──────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
export interface BundledResources {
|
|
275
|
+
scripts: string[];
|
|
276
|
+
references: string[];
|
|
277
|
+
assets: string[];
|
|
278
|
+
other: string[];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* List bundled resources in a skill directory.
|
|
283
|
+
*
|
|
284
|
+
* The spec recognizes three standard subdirectories:
|
|
285
|
+
* - scripts/ — executable code
|
|
286
|
+
* - references/ — additional documentation
|
|
287
|
+
* - assets/ — templates, resources
|
|
288
|
+
*
|
|
289
|
+
* Any other files (besides SKILL.md) are listed under "other".
|
|
290
|
+
*/
|
|
291
|
+
export function listBundledResources(skillDir: string): BundledResources {
|
|
292
|
+
const resources: BundledResources = {
|
|
293
|
+
scripts: [],
|
|
294
|
+
references: [],
|
|
295
|
+
assets: [],
|
|
296
|
+
other: [],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (!fs.existsSync(skillDir)) return resources;
|
|
300
|
+
|
|
301
|
+
const KNOWN_DIRS: Record<string, keyof Pick<BundledResources, 'scripts' | 'references' | 'assets'>> = {
|
|
302
|
+
scripts: 'scripts',
|
|
303
|
+
references: 'references',
|
|
304
|
+
assets: 'assets',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const entries = fs.readdirSync(skillDir, { withFileTypes: true });
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (entry.name === 'SKILL.md') continue;
|
|
310
|
+
|
|
311
|
+
if (entry.isDirectory()) {
|
|
312
|
+
const category = KNOWN_DIRS[entry.name];
|
|
313
|
+
if (category) {
|
|
314
|
+
// List files inside the known subdirectory
|
|
315
|
+
const subDir = path.join(skillDir, entry.name);
|
|
316
|
+
const subEntries = fs.readdirSync(subDir);
|
|
317
|
+
for (const sub of subEntries) {
|
|
318
|
+
resources[category].push(`${entry.name}/${sub}`);
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
// Unknown directory — list contents under "other"
|
|
322
|
+
const subDir = path.join(skillDir, entry.name);
|
|
323
|
+
const subEntries = fs.readdirSync(subDir);
|
|
324
|
+
for (const sub of subEntries) {
|
|
325
|
+
resources.other.push(`${entry.name}/${sub}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
// Top-level file (not SKILL.md)
|
|
330
|
+
resources.other.push(entry.name);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return resources;
|
|
335
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 tallclub
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|