@matimo/core 0.1.0-alpha.8 → 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.
Files changed (173) hide show
  1. package/README.md +341 -14
  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 +41 -10
  7. package/dist/core/schema.d.ts.map +1 -1
  8. package/dist/core/schema.js +40 -4
  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/tool-loader.d.ts +3 -1
  27. package/dist/core/tool-loader.d.ts.map +1 -1
  28. package/dist/core/tool-loader.js +33 -10
  29. package/dist/core/tool-loader.js.map +1 -1
  30. package/dist/core/types.d.ts +203 -6
  31. package/dist/core/types.d.ts.map +1 -1
  32. package/dist/encodings/parameter-encoding.d.ts +1 -1
  33. package/dist/encodings/parameter-encoding.d.ts.map +1 -1
  34. package/dist/encodings/parameter-encoding.js +9 -4
  35. package/dist/encodings/parameter-encoding.js.map +1 -1
  36. package/dist/errors/matimo-error.d.ts +11 -2
  37. package/dist/errors/matimo-error.d.ts.map +1 -1
  38. package/dist/errors/matimo-error.js +25 -1
  39. package/dist/errors/matimo-error.js.map +1 -1
  40. package/dist/executors/command-executor.d.ts +9 -2
  41. package/dist/executors/command-executor.d.ts.map +1 -1
  42. package/dist/executors/command-executor.js +29 -5
  43. package/dist/executors/command-executor.js.map +1 -1
  44. package/dist/executors/function-executor.d.ts +10 -3
  45. package/dist/executors/function-executor.d.ts.map +1 -1
  46. package/dist/executors/function-executor.js +44 -24
  47. package/dist/executors/function-executor.js.map +1 -1
  48. package/dist/executors/http-executor.d.ts +79 -4
  49. package/dist/executors/http-executor.d.ts.map +1 -1
  50. package/dist/executors/http-executor.js +232 -28
  51. package/dist/executors/http-executor.js.map +1 -1
  52. package/dist/index.d.ts +25 -3
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +19 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/integrations/langchain.d.ts +55 -0
  57. package/dist/integrations/langchain.d.ts.map +1 -1
  58. package/dist/integrations/langchain.js +71 -4
  59. package/dist/integrations/langchain.js.map +1 -1
  60. package/dist/logging/logger.d.ts +8 -2
  61. package/dist/logging/logger.d.ts.map +1 -1
  62. package/dist/logging/logger.js.map +1 -1
  63. package/dist/logging/winston-logger.d.ts.map +1 -1
  64. package/dist/logging/winston-logger.js +9 -1
  65. package/dist/logging/winston-logger.js.map +1 -1
  66. package/dist/matimo-instance.d.ts +230 -18
  67. package/dist/matimo-instance.d.ts.map +1 -1
  68. package/dist/matimo-instance.js +739 -40
  69. package/dist/matimo-instance.js.map +1 -1
  70. package/dist/mcp/index.d.ts +18 -0
  71. package/dist/mcp/index.d.ts.map +1 -0
  72. package/dist/mcp/index.js +24 -0
  73. package/dist/mcp/index.js.map +1 -0
  74. package/dist/mcp/mcp-server.d.ts +141 -0
  75. package/dist/mcp/mcp-server.d.ts.map +1 -0
  76. package/dist/mcp/mcp-server.js +754 -0
  77. package/dist/mcp/mcp-server.js.map +1 -0
  78. package/dist/mcp/secrets/aws-resolver.d.ts +41 -0
  79. package/dist/mcp/secrets/aws-resolver.d.ts.map +1 -0
  80. package/dist/mcp/secrets/aws-resolver.js +141 -0
  81. package/dist/mcp/secrets/aws-resolver.js.map +1 -0
  82. package/dist/mcp/secrets/dotenv-resolver.d.ts +23 -0
  83. package/dist/mcp/secrets/dotenv-resolver.d.ts.map +1 -0
  84. package/dist/mcp/secrets/dotenv-resolver.js +94 -0
  85. package/dist/mcp/secrets/dotenv-resolver.js.map +1 -0
  86. package/dist/mcp/secrets/env-resolver.d.ts +14 -0
  87. package/dist/mcp/secrets/env-resolver.d.ts.map +1 -0
  88. package/dist/mcp/secrets/env-resolver.js +27 -0
  89. package/dist/mcp/secrets/env-resolver.js.map +1 -0
  90. package/dist/mcp/secrets/index.d.ts +14 -0
  91. package/dist/mcp/secrets/index.d.ts.map +1 -0
  92. package/dist/mcp/secrets/index.js +13 -0
  93. package/dist/mcp/secrets/index.js.map +1 -0
  94. package/dist/mcp/secrets/resolver-chain.d.ts +34 -0
  95. package/dist/mcp/secrets/resolver-chain.d.ts.map +1 -0
  96. package/dist/mcp/secrets/resolver-chain.js +141 -0
  97. package/dist/mcp/secrets/resolver-chain.js.map +1 -0
  98. package/dist/mcp/secrets/types.d.ts +73 -0
  99. package/dist/mcp/secrets/types.d.ts.map +1 -0
  100. package/dist/mcp/secrets/types.js +8 -0
  101. package/dist/mcp/secrets/types.js.map +1 -0
  102. package/dist/mcp/secrets/vault-resolver.d.ts +43 -0
  103. package/dist/mcp/secrets/vault-resolver.d.ts.map +1 -0
  104. package/dist/mcp/secrets/vault-resolver.js +127 -0
  105. package/dist/mcp/secrets/vault-resolver.js.map +1 -0
  106. package/dist/mcp/tool-converter.d.ts +40 -0
  107. package/dist/mcp/tool-converter.d.ts.map +1 -0
  108. package/dist/mcp/tool-converter.js +185 -0
  109. package/dist/mcp/tool-converter.js.map +1 -0
  110. package/dist/policy/approval-manifest.d.ts +76 -0
  111. package/dist/policy/approval-manifest.d.ts.map +1 -0
  112. package/dist/policy/approval-manifest.js +197 -0
  113. package/dist/policy/approval-manifest.js.map +1 -0
  114. package/dist/policy/content-validator.d.ts +19 -0
  115. package/dist/policy/content-validator.d.ts.map +1 -0
  116. package/dist/policy/content-validator.js +196 -0
  117. package/dist/policy/content-validator.js.map +1 -0
  118. package/dist/policy/default-policy.d.ts +46 -0
  119. package/dist/policy/default-policy.d.ts.map +1 -0
  120. package/dist/policy/default-policy.js +241 -0
  121. package/dist/policy/default-policy.js.map +1 -0
  122. package/dist/policy/events.d.ts +71 -0
  123. package/dist/policy/events.d.ts.map +1 -0
  124. package/dist/policy/events.js +8 -0
  125. package/dist/policy/events.js.map +1 -0
  126. package/dist/policy/index.d.ts +13 -0
  127. package/dist/policy/index.d.ts.map +1 -0
  128. package/dist/policy/index.js +9 -0
  129. package/dist/policy/index.js.map +1 -0
  130. package/dist/policy/integrity-tracker.d.ts +62 -0
  131. package/dist/policy/integrity-tracker.d.ts.map +1 -0
  132. package/dist/policy/integrity-tracker.js +79 -0
  133. package/dist/policy/integrity-tracker.js.map +1 -0
  134. package/dist/policy/policy-loader.d.ts +58 -0
  135. package/dist/policy/policy-loader.d.ts.map +1 -0
  136. package/dist/policy/policy-loader.js +156 -0
  137. package/dist/policy/policy-loader.js.map +1 -0
  138. package/dist/policy/risk-classifier.d.ts +18 -0
  139. package/dist/policy/risk-classifier.d.ts.map +1 -0
  140. package/dist/policy/risk-classifier.js +47 -0
  141. package/dist/policy/risk-classifier.js.map +1 -0
  142. package/dist/policy/types.d.ts +131 -0
  143. package/dist/policy/types.d.ts.map +1 -0
  144. package/dist/policy/types.js +8 -0
  145. package/dist/policy/types.js.map +1 -0
  146. package/package.json +22 -6
  147. package/tools/matimo_approve_tool/definition.yaml +36 -0
  148. package/tools/matimo_approve_tool/matimo_approve_tool.ts +90 -0
  149. package/tools/matimo_create_skill/definition.yaml +46 -0
  150. package/tools/matimo_create_skill/matimo_create_skill.ts +75 -0
  151. package/tools/matimo_create_tool/definition.yaml +48 -0
  152. package/tools/matimo_create_tool/matimo_create_tool.ts +137 -0
  153. package/tools/matimo_get_skill/definition.yaml +60 -0
  154. package/tools/matimo_get_skill/matimo_get_skill.ts +182 -0
  155. package/tools/matimo_get_tool/definition.yaml +36 -0
  156. package/tools/matimo_get_tool/matimo_get_tool.ts +56 -0
  157. package/tools/matimo_get_tool_status/definition.yaml +42 -0
  158. package/tools/matimo_get_tool_status/matimo_get_tool_status.ts +101 -0
  159. package/tools/matimo_list_skills/definition.yaml +52 -0
  160. package/tools/matimo_list_skills/matimo_list_skills.ts +138 -0
  161. package/tools/matimo_list_user_tools/definition.yaml +32 -0
  162. package/tools/matimo_list_user_tools/matimo_list_user_tools.ts +74 -0
  163. package/tools/matimo_reload_tools/definition.yaml +35 -0
  164. package/tools/matimo_reload_tools/matimo_reload_tools.ts +29 -0
  165. package/tools/matimo_search_tools/definition.yaml +32 -0
  166. package/tools/matimo_search_tools/matimo_search_tools.ts +82 -0
  167. package/tools/matimo_validate_skill/definition.yaml +43 -0
  168. package/tools/matimo_validate_skill/matimo_validate_skill.ts +137 -0
  169. package/tools/matimo_validate_tool/definition.yaml +34 -0
  170. package/tools/matimo_validate_tool/matimo_validate_tool.ts +168 -0
  171. package/tools/read/read.ts +0 -2
  172. package/tools/shared/skill-validation.ts +335 -0
  173. 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
+ }
@@ -58,8 +58,6 @@ export default async function readTool(params: ReadParams): Promise<ReadResult>
58
58
  });
59
59
  }
60
60
 
61
- // No approval needed for read-only operations
62
-
63
61
  // Get file stats
64
62
  const stats = fs.statSync(resolvedPath);
65
63
  if (!stats.isFile()) {
@@ -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.