@aws/ml-container-creator 0.2.5 → 0.3.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/bin/cli.js +45 -4
- package/config/bootstrap-stack.json +14 -0
- package/infra/ci-harness/package-lock.json +22 -9
- package/package.json +7 -8
- package/servers/base-image-picker/index.js +3 -3
- package/servers/base-image-picker/manifest.json +4 -2
- package/servers/instance-sizer/index.js +564 -0
- package/servers/instance-sizer/lib/instance-ranker.js +270 -0
- package/servers/instance-sizer/lib/model-resolver.js +269 -0
- package/servers/instance-sizer/lib/vram-estimator.js +177 -0
- package/servers/instance-sizer/manifest.json +17 -0
- package/servers/instance-sizer/package.json +15 -0
- package/servers/{instance-recommender → lib}/catalogs/instances.json +136 -34
- package/servers/{base-image-picker → lib}/catalogs/model-servers.json +302 -254
- package/servers/lib/catalogs/model-sizes.json +131 -0
- package/servers/lib/catalogs/models.json +632 -0
- package/servers/{model-picker → lib}/catalogs/popular-diffusors.json +32 -10
- package/servers/{model-picker → lib}/catalogs/popular-transformers.json +59 -26
- package/servers/{base-image-picker → lib}/catalogs/python-slim.json +12 -12
- package/servers/lib/schemas/image-catalog.schema.json +6 -12
- package/servers/lib/schemas/instances.schema.json +29 -0
- package/servers/lib/schemas/model-catalog.schema.json +12 -10
- package/servers/lib/schemas/unified-model-catalog.schema.json +129 -0
- package/servers/model-picker/index.js +4 -4
- package/servers/model-picker/manifest.json +2 -3
- package/servers/region-picker/index.js +1 -1
- package/servers/region-picker/manifest.json +1 -1
- package/src/app.js +36 -0
- package/src/lib/architecture-sync.js +171 -0
- package/src/lib/arn-detection.js +22 -0
- package/src/lib/bootstrap-command-handler.js +120 -0
- package/src/lib/cli-handler.js +3 -3
- package/src/lib/config-manager.js +47 -1
- package/src/lib/configuration-manager.js +2 -2
- package/src/lib/cross-cutting-checker.js +460 -0
- package/src/lib/deployment-entry-schema.js +1 -2
- package/src/lib/dry-run-validator.js +78 -0
- package/src/lib/generation-validator.js +102 -0
- package/src/lib/mcp-validator-config.js +89 -0
- package/src/lib/payload-builder.js +153 -0
- package/src/lib/prompt-runner.js +866 -149
- package/src/lib/prompts.js +2 -2
- package/src/lib/registry-command-handler.js +236 -0
- package/src/lib/registry-loader.js +5 -5
- package/src/lib/schema-sync.js +203 -0
- package/src/lib/schema-validation-engine.js +195 -0
- package/src/lib/secret-classification.js +56 -0
- package/src/lib/secrets-command-handler.js +550 -0
- package/src/lib/service-model-parser.js +102 -0
- package/src/lib/validate-runner.js +216 -0
- package/src/lib/validation-report.js +140 -0
- package/src/lib/validators/base-validator.js +36 -0
- package/src/lib/validators/catalog-validator.js +177 -0
- package/src/lib/validators/enum-validator.js +120 -0
- package/src/lib/validators/required-field-validator.js +150 -0
- package/src/lib/validators/type-validator.js +313 -0
- package/src/prompt-adapter.js +3 -2
- package/templates/Dockerfile +1 -1
- package/templates/do/build +37 -5
- package/templates/do/config +15 -3
- package/templates/do/deploy +60 -5
- package/templates/do/logs +18 -3
- package/templates/do/run +15 -1
- package/templates/do/validate +61 -0
- package/servers/instance-recommender/LICENSE +0 -202
- package/servers/instance-recommender/index.js +0 -284
- package/servers/instance-recommender/manifest.json +0 -16
- package/servers/instance-recommender/package.json +0 -15
- /package/servers/{model-picker → lib}/catalogs/jumpstart-public.json +0 -0
- /package/servers/{region-picker → lib}/catalogs/regions.json +0 -0
- /package/servers/{base-image-picker → lib}/catalogs/triton-backends.json +0 -0
- /package/servers/{base-image-picker → lib}/catalogs/triton.json +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validation runner — the Node.js module invoked by do/validate.
|
|
6
|
+
* Loads config, constructs payloads, runs SchemaValidationEngine, and prints a report.
|
|
7
|
+
*
|
|
8
|
+
* - Prints structured report (text format by default)
|
|
9
|
+
* - Exits with code 1 if errors found, 0 if clean
|
|
10
|
+
* - Includes service model version date and fields validated count on success
|
|
11
|
+
* - Supports --smart flag for future MCP validator integration
|
|
12
|
+
*
|
|
13
|
+
* Requirements: 9.2, 9.3, 9.4
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import PayloadBuilder from './payload-builder.js';
|
|
20
|
+
import SchemaValidationEngine from './schema-validation-engine.js';
|
|
21
|
+
import ServiceModelParser from './service-model-parser.js';
|
|
22
|
+
import CrossCuttingChecker from './cross-cutting-checker.js';
|
|
23
|
+
import HuggingFaceClient from './huggingface-client.js';
|
|
24
|
+
import { getRegistryPath, loadManifest } from './schema-sync.js';
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = path.dirname(__filename);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse a do/config shell file into a key-value object.
|
|
31
|
+
* Extracts lines matching: export KEY="value" or export KEY=value
|
|
32
|
+
*
|
|
33
|
+
* @param {string} configPath - Path to the do/config file
|
|
34
|
+
* @returns {Object} Parsed configuration values
|
|
35
|
+
*/
|
|
36
|
+
export function parseDoConfig(configPath) {
|
|
37
|
+
if (!existsSync(configPath)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const content = readFileSync(configPath, 'utf8');
|
|
42
|
+
const config = {};
|
|
43
|
+
|
|
44
|
+
for (const line of content.split('\n')) {
|
|
45
|
+
const match = line.match(/^export\s+([A-Z_][A-Z0-9_]*)=["']?([^"'\n]*)["']?/);
|
|
46
|
+
if (match) {
|
|
47
|
+
const [, key, value] = match;
|
|
48
|
+
config[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run the full validation pipeline.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} options
|
|
59
|
+
* @param {string} [options.configDir] - Path to the do/ directory containing config
|
|
60
|
+
* @param {string} [options.format] - Output format: 'text' (default) or 'json'
|
|
61
|
+
* @param {boolean} [options.smart] - Enable smart-mode validators
|
|
62
|
+
* @param {string} [options.registryPath] - Override schema registry path
|
|
63
|
+
* @param {Object} [options.config] - Pre-parsed config (overrides configDir loading)
|
|
64
|
+
* @returns {Promise<number>} Exit code (0 = pass, 1 = fail, 2 = cannot run)
|
|
65
|
+
*/
|
|
66
|
+
export async function run(options = {}) {
|
|
67
|
+
const format = options.format || 'text';
|
|
68
|
+
const smart = options.smart || false;
|
|
69
|
+
const registryPath = options.registryPath || getRegistryPath();
|
|
70
|
+
|
|
71
|
+
// Check schema registry exists
|
|
72
|
+
if (!existsSync(registryPath) || !existsSync(path.join(registryPath, 'manifest.json'))) {
|
|
73
|
+
console.log('⚠️ Schema registry not found.');
|
|
74
|
+
console.log(' Run: ml-container-creator bootstrap sync-schemas');
|
|
75
|
+
process.exit(2);
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Load config
|
|
80
|
+
let config = options.config;
|
|
81
|
+
if (!config && options.configDir) {
|
|
82
|
+
const configPath = path.join(options.configDir, 'config');
|
|
83
|
+
config = parseDoConfig(configPath);
|
|
84
|
+
if (!config) {
|
|
85
|
+
console.log('❌ Could not load do/config');
|
|
86
|
+
process.exit(2);
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!config) {
|
|
92
|
+
console.log('❌ No configuration provided');
|
|
93
|
+
process.exit(2);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const deploymentTarget = config.DEPLOYMENT_TARGET || 'realtime-inference';
|
|
98
|
+
|
|
99
|
+
// Construct payloads
|
|
100
|
+
const builder = new PayloadBuilder();
|
|
101
|
+
const context = builder.build(config, deploymentTarget);
|
|
102
|
+
|
|
103
|
+
// Load and parse service models from registry
|
|
104
|
+
const parser = new ServiceModelParser();
|
|
105
|
+
const serviceModels = [];
|
|
106
|
+
try {
|
|
107
|
+
const entries = readdirSync(registryPath, { withFileTypes: true });
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
const modelPath = path.join(registryPath, entry.name, 'service-2.json');
|
|
111
|
+
if (existsSync(modelPath)) {
|
|
112
|
+
const rawModel = JSON.parse(readFileSync(modelPath, 'utf8'));
|
|
113
|
+
serviceModels.push(parser.parse(rawModel));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
console.log('⚠️ Could not load service models from registry');
|
|
119
|
+
process.exit(2);
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Run validation engine
|
|
124
|
+
const engine = new SchemaValidationEngine({
|
|
125
|
+
registryPath,
|
|
126
|
+
smartMode: smart,
|
|
127
|
+
serviceModels
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const report = await engine.validate(context);
|
|
131
|
+
|
|
132
|
+
// Run model architecture compatibility check (Requirement 5.1-5.2)
|
|
133
|
+
if (config.MODEL_NAME) {
|
|
134
|
+
try {
|
|
135
|
+
const catalogPath = path.resolve(__dirname, '../../servers/lib/catalogs/model-servers.json');
|
|
136
|
+
if (existsSync(catalogPath)) {
|
|
137
|
+
const modelServersCatalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
|
|
138
|
+
|
|
139
|
+
// Fetch model's config.json from HuggingFace to get model_type
|
|
140
|
+
const hfClient = new HuggingFaceClient({ timeout: 10000 });
|
|
141
|
+
const modelConfig = await hfClient.fetchModelConfig(config.MODEL_NAME);
|
|
142
|
+
const modelType = modelConfig?.model_type || null;
|
|
143
|
+
|
|
144
|
+
if (modelType) {
|
|
145
|
+
// Extract baseImageVersion from BASE_IMAGE (e.g., "vllm/vllm-openai:v0.10.1" → "v0.10.1")
|
|
146
|
+
const baseImage = config.BASE_IMAGE || '';
|
|
147
|
+
const baseImageVersion = baseImage.includes(':') ? baseImage.split(':').pop() : '';
|
|
148
|
+
// Strip leading 'v' to match catalog's framework_version format (e.g., "v0.10.1" → "0.10.1")
|
|
149
|
+
const frameworkVersion = baseImageVersion.replace(/^v/, '');
|
|
150
|
+
|
|
151
|
+
const modelServer = config.MODEL_SERVER || '';
|
|
152
|
+
|
|
153
|
+
// Build context fields for the architecture checker
|
|
154
|
+
const archContext = {
|
|
155
|
+
config: {
|
|
156
|
+
modelType,
|
|
157
|
+
modelServer,
|
|
158
|
+
baseImageVersion: frameworkVersion
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const checker = new CrossCuttingChecker();
|
|
163
|
+
const archFindings = checker.checkModelArchitectureCompatibility(archContext, modelServersCatalog);
|
|
164
|
+
for (const finding of archFindings) {
|
|
165
|
+
report.addFinding(finding);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Graceful degradation: if architecture check fails, continue without it
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const summary = report.getSummary();
|
|
175
|
+
|
|
176
|
+
// Load manifest for version info
|
|
177
|
+
const manifest = loadManifest(registryPath);
|
|
178
|
+
|
|
179
|
+
// Output report
|
|
180
|
+
if (format === 'json') {
|
|
181
|
+
report.metadata.serviceModelVersionDate = manifest?.lastSynced || null;
|
|
182
|
+
const output = report.toJSON();
|
|
183
|
+
console.log(JSON.stringify(output, null, 2));
|
|
184
|
+
} else {
|
|
185
|
+
// Print static results immediately
|
|
186
|
+
const text = report.toText();
|
|
187
|
+
console.log(text);
|
|
188
|
+
|
|
189
|
+
// On success, print version info
|
|
190
|
+
if (summary.errors === 0) {
|
|
191
|
+
const versionDate = manifest?.lastSynced
|
|
192
|
+
? new Date(manifest.lastSynced).toISOString().split('T')[0]
|
|
193
|
+
: 'unknown';
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log('✅ Validation passed');
|
|
196
|
+
console.log(` Service model version: ${versionDate}`);
|
|
197
|
+
console.log(` Fields validated: ${summary.fieldsValidated}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// If smart mode and results are streaming, display them after static
|
|
201
|
+
if (smart && summary.advisory > 0) {
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log('── Smart-mode findings ──');
|
|
204
|
+
for (const finding of report.advisoryFindings) {
|
|
205
|
+
console.log(` ℹ ${finding.fieldPath || finding.operation}: ${finding.remediationHint || ''}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Exit code
|
|
211
|
+
const exitCode = summary.errors > 0 ? 1 : 0;
|
|
212
|
+
process.exit(exitCode);
|
|
213
|
+
return exitCode;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default { run, parseDoConfig };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured validation report that categorizes findings by severity and source.
|
|
3
|
+
* Supports text (color-coded) and JSON output formats.
|
|
4
|
+
*
|
|
5
|
+
* Requirements: 11.1, 11.2, 11.3, 11.4, 11.5
|
|
6
|
+
*/
|
|
7
|
+
export default class ValidationReport {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.schemaErrors = [];
|
|
10
|
+
this.crossCuttingErrors = [];
|
|
11
|
+
this.advisoryFindings = [];
|
|
12
|
+
this.warnings = [];
|
|
13
|
+
this.metadata = {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add a finding to the appropriate category based on finding.source.
|
|
18
|
+
* Smart-mode findings are labeled advisory unless confidence is 'definitive' and severity is 'error'.
|
|
19
|
+
* @param {Object} finding - A Finding object with source, severity, confidence, etc.
|
|
20
|
+
*/
|
|
21
|
+
addFinding(finding) {
|
|
22
|
+
const source = finding.source || '';
|
|
23
|
+
|
|
24
|
+
if (source === 'cross-cutting') {
|
|
25
|
+
// Cross-cutting findings with medium/low confidence are advisory, not errors
|
|
26
|
+
if (finding.confidence === 'medium' || finding.confidence === 'low') {
|
|
27
|
+
this.advisoryFindings.push(finding);
|
|
28
|
+
} else if (finding.severity === 'warning') {
|
|
29
|
+
this.warnings.push(finding);
|
|
30
|
+
} else {
|
|
31
|
+
this.crossCuttingErrors.push(finding);
|
|
32
|
+
}
|
|
33
|
+
} else if (source === 'smart-mode' || source.startsWith('smart:')) {
|
|
34
|
+
// Smart-mode findings are advisory UNLESS confidence is definitive AND severity is error
|
|
35
|
+
if (finding.confidence === 'definitive' && finding.severity === 'error') {
|
|
36
|
+
this.schemaErrors.push(finding);
|
|
37
|
+
} else {
|
|
38
|
+
this.advisoryFindings.push(finding);
|
|
39
|
+
}
|
|
40
|
+
} else if (finding.confidence === 'medium' || finding.confidence === 'low') {
|
|
41
|
+
this.advisoryFindings.push(finding);
|
|
42
|
+
} else if (finding.severity === 'warning') {
|
|
43
|
+
this.warnings.push(finding);
|
|
44
|
+
} else {
|
|
45
|
+
this.schemaErrors.push(finding);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Render report as formatted text with color-coded severity grouping by operation.
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
toText() {
|
|
54
|
+
const lines = [];
|
|
55
|
+
|
|
56
|
+
const groupByOperation = (findings) => {
|
|
57
|
+
const groups = {};
|
|
58
|
+
for (const f of findings) {
|
|
59
|
+
const key = f.operation || 'general';
|
|
60
|
+
if (!groups[key]) groups[key] = [];
|
|
61
|
+
groups[key].push(f);
|
|
62
|
+
}
|
|
63
|
+
return groups;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (this.schemaErrors.length > 0) {
|
|
67
|
+
lines.push('\x1b[31m── Schema Errors ──\x1b[0m');
|
|
68
|
+
const groups = groupByOperation(this.schemaErrors);
|
|
69
|
+
for (const [op, findings] of Object.entries(groups)) {
|
|
70
|
+
lines.push(` ${op}:`);
|
|
71
|
+
for (const f of findings) {
|
|
72
|
+
lines.push(` \x1b[31m✗\x1b[0m ${f.fieldPath}: ${f.invalidValue} (${f.remediationHint || ''})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.crossCuttingErrors.length > 0) {
|
|
78
|
+
lines.push('\x1b[31m── Cross-Cutting Errors ──\x1b[0m');
|
|
79
|
+
const groups = groupByOperation(this.crossCuttingErrors);
|
|
80
|
+
for (const [op, findings] of Object.entries(groups)) {
|
|
81
|
+
lines.push(` ${op}:`);
|
|
82
|
+
for (const f of findings) {
|
|
83
|
+
lines.push(` \x1b[31m✗\x1b[0m ${f.fieldPath}: ${f.remediationHint || ''}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.advisoryFindings.length > 0) {
|
|
89
|
+
lines.push('\x1b[36m── Advisory Findings ──\x1b[0m');
|
|
90
|
+
const groups = groupByOperation(this.advisoryFindings);
|
|
91
|
+
for (const [op, findings] of Object.entries(groups)) {
|
|
92
|
+
lines.push(` ${op}:`);
|
|
93
|
+
for (const f of findings) {
|
|
94
|
+
lines.push(` \x1b[36mℹ\x1b[0m ${f.fieldPath}: ${f.remediationHint || ''}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (this.warnings.length > 0) {
|
|
100
|
+
lines.push('\x1b[33m── Warnings ──\x1b[0m');
|
|
101
|
+
for (const f of this.warnings) {
|
|
102
|
+
lines.push(` \x1b[33m⚠\x1b[0m ${f.fieldPath || f.operation || ''}: ${f.remediationHint || ''}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const summary = this.getSummary();
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push(`Summary: ${summary.errors} error(s), ${summary.warnings} warning(s), ${summary.advisory} advisory, ${summary.fieldsValidated} fields validated`);
|
|
109
|
+
|
|
110
|
+
return lines.join('\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Render report as JSON with full structured object.
|
|
115
|
+
* @returns {Object}
|
|
116
|
+
*/
|
|
117
|
+
toJSON() {
|
|
118
|
+
return {
|
|
119
|
+
schemaErrors: this.schemaErrors,
|
|
120
|
+
crossCuttingErrors: this.crossCuttingErrors,
|
|
121
|
+
advisoryFindings: this.advisoryFindings,
|
|
122
|
+
warnings: this.warnings,
|
|
123
|
+
metadata: this.metadata,
|
|
124
|
+
summary: this.getSummary()
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get summary counts.
|
|
130
|
+
* @returns {{ errors: number, warnings: number, advisory: number, fieldsValidated: number }}
|
|
131
|
+
*/
|
|
132
|
+
getSummary() {
|
|
133
|
+
return {
|
|
134
|
+
errors: this.schemaErrors.length + this.crossCuttingErrors.length,
|
|
135
|
+
warnings: this.warnings.length,
|
|
136
|
+
advisory: this.advisoryFindings.length,
|
|
137
|
+
fieldsValidated: this.metadata.fieldsValidated || 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all validation plugins.
|
|
3
|
+
* Custom validators extend this class and implement the validate method.
|
|
4
|
+
*
|
|
5
|
+
* Requirements: 12.1, 12.3, 12.6
|
|
6
|
+
*/
|
|
7
|
+
export default class BaseValidator {
|
|
8
|
+
/**
|
|
9
|
+
* Plugin name for source attribution in findings.
|
|
10
|
+
* @type {string}
|
|
11
|
+
*/
|
|
12
|
+
get name() {
|
|
13
|
+
return 'base';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* When this validator runs: 'static', 'smart', or 'both'.
|
|
18
|
+
* Static-only plugins run always; smart-only plugins run only when --smart is passed.
|
|
19
|
+
* @type {'static'|'smart'|'both'}
|
|
20
|
+
*/
|
|
21
|
+
get mode() {
|
|
22
|
+
return 'static';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate the context and return findings.
|
|
27
|
+
* @param {Object} context - Full validation context (ValidationContext)
|
|
28
|
+
* @param {Object} options
|
|
29
|
+
* @param {Array} options.priorFindings - Findings from earlier validators
|
|
30
|
+
* @param {Array} options.serviceModels - Parsed service models
|
|
31
|
+
* @returns {Promise<Array>} Array of Finding objects
|
|
32
|
+
*/
|
|
33
|
+
async validate(_context, _options) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog enum validator.
|
|
3
|
+
* Validates that catalog entries (e.g., model-servers.json) contain valid
|
|
4
|
+
* enum values according to the AWS service model.
|
|
5
|
+
*
|
|
6
|
+
* Unlike other validators that check API payloads, this validator reads
|
|
7
|
+
* catalog files and validates their fields against the service model enums.
|
|
8
|
+
*
|
|
9
|
+
* Requirements: 14.1, 14.2, 14.3
|
|
10
|
+
*/
|
|
11
|
+
import BaseValidator from './base-validator.js';
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { resolve, dirname } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CATALOG_PATH = resolve(__dirname, '../../../servers/lib/catalogs/model-servers.json');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Map of catalog field names to their corresponding SageMaker service model shape names.
|
|
23
|
+
*/
|
|
24
|
+
const CATALOG_ENUM_FIELDS = [
|
|
25
|
+
{ field: 'inferenceAmiVersion', shapeName: 'InferenceAmiVersion' }
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export default class CatalogValidator extends BaseValidator {
|
|
29
|
+
get name() {
|
|
30
|
+
return 'catalog';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get mode() {
|
|
34
|
+
return 'static';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate catalog enum fields against the service model.
|
|
39
|
+
* @param {Object} context - ValidationContext (may be empty for catalog-only validation)
|
|
40
|
+
* @param {Object} options
|
|
41
|
+
* @param {Array} options.serviceModels - Parsed ServiceModelIndex objects
|
|
42
|
+
* @param {string} [options.catalogPath] - Path to model-servers.json (defaults to well-known location)
|
|
43
|
+
* @param {Object} [options.catalogData] - Pre-loaded catalog data (for testing)
|
|
44
|
+
* @returns {Promise<Array>} Array of Finding objects
|
|
45
|
+
*/
|
|
46
|
+
async validate(context, options) {
|
|
47
|
+
const findings = [];
|
|
48
|
+
const serviceModels = options.serviceModels || [];
|
|
49
|
+
|
|
50
|
+
if (serviceModels.length === 0) {
|
|
51
|
+
return findings;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Load catalog data
|
|
55
|
+
let catalogData;
|
|
56
|
+
let catalogFilePath;
|
|
57
|
+
|
|
58
|
+
if (options.catalogData) {
|
|
59
|
+
catalogData = options.catalogData;
|
|
60
|
+
catalogFilePath = options.catalogPath || 'servers/lib/catalogs/model-servers.json';
|
|
61
|
+
} else {
|
|
62
|
+
catalogFilePath = options.catalogPath || DEFAULT_CATALOG_PATH;
|
|
63
|
+
|
|
64
|
+
if (!existsSync(catalogFilePath)) {
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
catalogData = JSON.parse(readFileSync(catalogFilePath, 'utf8'));
|
|
70
|
+
} catch {
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Build enum lookup from service models
|
|
76
|
+
const enumMap = this._buildEnumMap(serviceModels);
|
|
77
|
+
|
|
78
|
+
// Validate each server group and its entries
|
|
79
|
+
for (const [serverKey, entries] of Object.entries(catalogData)) {
|
|
80
|
+
if (!Array.isArray(entries)) continue;
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < entries.length; i++) {
|
|
83
|
+
const entry = entries[i];
|
|
84
|
+
const entryKey = `${serverKey}[${i}]`;
|
|
85
|
+
const entryLabel = entry.tag || entry.image || `index ${i}`;
|
|
86
|
+
|
|
87
|
+
this._validateEntry(
|
|
88
|
+
entry, entryKey, entryLabel, catalogFilePath, enumMap, findings
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return findings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build a map of field names to their valid enum values from service models.
|
|
98
|
+
* @param {Array} serviceModels - Parsed ServiceModelIndex objects
|
|
99
|
+
* @returns {Map<string, string[]>} field name → valid enum values
|
|
100
|
+
*/
|
|
101
|
+
_buildEnumMap(serviceModels) {
|
|
102
|
+
const enumMap = new Map();
|
|
103
|
+
|
|
104
|
+
for (const { field, shapeName } of CATALOG_ENUM_FIELDS) {
|
|
105
|
+
for (const model of serviceModels) {
|
|
106
|
+
const shape = model.shapes.get(shapeName);
|
|
107
|
+
if (shape && shape.enum && shape.enum.length > 0) {
|
|
108
|
+
enumMap.set(field, [...shape.enum]);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return enumMap;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate a single catalog entry's enum fields.
|
|
119
|
+
* @param {Object} entry - Catalog entry object
|
|
120
|
+
* @param {string} entryKey - Key identifying the entry (e.g., "vllm[0]")
|
|
121
|
+
* @param {string} entryLabel - Human-readable label for the entry
|
|
122
|
+
* @param {string} catalogFilePath - Path to the catalog file
|
|
123
|
+
* @param {Map<string, string[]>} enumMap - field name → valid enum values
|
|
124
|
+
* @param {Array} findings - Accumulator for findings
|
|
125
|
+
*/
|
|
126
|
+
_validateEntry(entry, entryKey, entryLabel, catalogFilePath, enumMap, findings) {
|
|
127
|
+
// Check top-level fields
|
|
128
|
+
for (const [field, validValues] of enumMap) {
|
|
129
|
+
if (entry[field] !== undefined) {
|
|
130
|
+
this._checkEnumValue(
|
|
131
|
+
entry[field], field, entryKey, catalogFilePath, validValues, findings
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check nested defaults object
|
|
137
|
+
if (entry.defaults && typeof entry.defaults === 'object') {
|
|
138
|
+
for (const [field, validValues] of enumMap) {
|
|
139
|
+
if (entry.defaults[field] !== undefined) {
|
|
140
|
+
this._checkEnumValue(
|
|
141
|
+
entry.defaults[field], field, entryKey, catalogFilePath, validValues, findings
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check a single enum value and add a finding if invalid.
|
|
150
|
+
* @param {string} value - The value to check
|
|
151
|
+
* @param {string} fieldName - The field name
|
|
152
|
+
* @param {string} entryKey - The entry key (e.g., "vllm[0]")
|
|
153
|
+
* @param {string} catalogFilePath - Path to the catalog file
|
|
154
|
+
* @param {string[]} validValues - Array of valid enum values
|
|
155
|
+
* @param {Array} findings - Accumulator for findings
|
|
156
|
+
*/
|
|
157
|
+
_checkEnumValue(value, fieldName, entryKey, catalogFilePath, validValues, findings) {
|
|
158
|
+
if (typeof value !== 'string') return;
|
|
159
|
+
|
|
160
|
+
if (!validValues.includes(value)) {
|
|
161
|
+
findings.push({
|
|
162
|
+
service: 'sagemaker',
|
|
163
|
+
operation: 'catalog-validation',
|
|
164
|
+
fieldPath: `${entryKey}.${fieldName}`,
|
|
165
|
+
invalidValue: value,
|
|
166
|
+
constraint: { type: 'enum', values: validValues },
|
|
167
|
+
severity: 'error',
|
|
168
|
+
confidence: 'definitive',
|
|
169
|
+
source: this.name,
|
|
170
|
+
catalogFile: catalogFilePath,
|
|
171
|
+
entryKey,
|
|
172
|
+
fieldName,
|
|
173
|
+
remediationHint: `Value "${value}" is not a valid ${fieldName}. Allowed values: ${validValues.join(', ')}. Run \`bootstrap sync-schemas\` to update the enum set.`
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enum constraint validator.
|
|
3
|
+
* Validates that payload field values are within the allowed enum set
|
|
4
|
+
* defined in the AWS service model.
|
|
5
|
+
*
|
|
6
|
+
* Requirements: 4.1, 4.2, 4.3, 4.5
|
|
7
|
+
*/
|
|
8
|
+
import BaseValidator from './base-validator.js';
|
|
9
|
+
|
|
10
|
+
export default class EnumValidator extends BaseValidator {
|
|
11
|
+
get name() {
|
|
12
|
+
return 'enum';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get mode() {
|
|
16
|
+
return 'static';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate enum constraints for all payload fields.
|
|
21
|
+
* @param {Object} context - ValidationContext from PayloadBuilder
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {Array} options.serviceModels - Parsed ServiceModelIndex objects
|
|
24
|
+
* @param {Array} options.priorFindings - Findings from earlier validators
|
|
25
|
+
* @returns {Promise<Array>} Array of Finding objects
|
|
26
|
+
*/
|
|
27
|
+
async validate(context, options) {
|
|
28
|
+
const findings = [];
|
|
29
|
+
const serviceModels = options.serviceModels || [];
|
|
30
|
+
|
|
31
|
+
for (const [operationKey, payload] of Object.entries(context.payloads || {})) {
|
|
32
|
+
const [service, operation] = operationKey.split(':');
|
|
33
|
+
|
|
34
|
+
for (const model of serviceModels) {
|
|
35
|
+
const op = model.operations.get(operation);
|
|
36
|
+
if (!op || !op.input) continue;
|
|
37
|
+
|
|
38
|
+
const inputShape = model.shapes.get(op.input);
|
|
39
|
+
if (!inputShape || inputShape.type !== 'structure') continue;
|
|
40
|
+
|
|
41
|
+
this._validateStructure(
|
|
42
|
+
payload, inputShape, model, service, operation, '', findings
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Recursively validate enum constraints in a structure.
|
|
52
|
+
* @param {Object} payload - The payload object to validate
|
|
53
|
+
* @param {Object} shape - The structure shape definition
|
|
54
|
+
* @param {Object} model - The ServiceModelIndex
|
|
55
|
+
* @param {string} service - Service name
|
|
56
|
+
* @param {string} operation - Operation name
|
|
57
|
+
* @param {string} parentPath - Dot-notation path prefix
|
|
58
|
+
* @param {Array} findings - Accumulator for findings
|
|
59
|
+
*/
|
|
60
|
+
_validateStructure(payload, shape, model, service, operation, parentPath, findings) {
|
|
61
|
+
if (!payload || typeof payload !== 'object' || !shape.members) return;
|
|
62
|
+
|
|
63
|
+
for (const [fieldName, value] of Object.entries(payload)) {
|
|
64
|
+
const memberDef = shape.members.get
|
|
65
|
+
? shape.members.get(fieldName)
|
|
66
|
+
: shape.members[fieldName];
|
|
67
|
+
if (!memberDef) continue;
|
|
68
|
+
|
|
69
|
+
const fieldPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
70
|
+
const fieldShape = model.shapes.get(memberDef.shape);
|
|
71
|
+
if (!fieldShape) continue;
|
|
72
|
+
|
|
73
|
+
if (fieldShape.type === 'string' && fieldShape.enum && fieldShape.enum.length > 0) {
|
|
74
|
+
if (typeof value === 'string' && !fieldShape.enum.includes(value)) {
|
|
75
|
+
findings.push({
|
|
76
|
+
service,
|
|
77
|
+
operation,
|
|
78
|
+
fieldPath,
|
|
79
|
+
invalidValue: value,
|
|
80
|
+
constraint: { type: 'enum', values: [...fieldShape.enum] },
|
|
81
|
+
severity: 'error',
|
|
82
|
+
confidence: 'definitive',
|
|
83
|
+
source: this.name,
|
|
84
|
+
remediationHint: `Value "${value}" is not valid. Allowed values: ${fieldShape.enum.join(', ')}. Run \`bootstrap sync-schemas\` to update the enum set.`
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} else if (fieldShape.type === 'structure') {
|
|
88
|
+
this._validateStructure(
|
|
89
|
+
value, fieldShape, model, service, operation, fieldPath, findings
|
|
90
|
+
);
|
|
91
|
+
} else if (fieldShape.type === 'list' && Array.isArray(value) && fieldShape.member) {
|
|
92
|
+
const elementShape = model.shapes.get(fieldShape.member.shape);
|
|
93
|
+
if (elementShape && elementShape.type === 'structure') {
|
|
94
|
+
for (let i = 0; i < value.length; i++) {
|
|
95
|
+
this._validateStructure(
|
|
96
|
+
value[i], elementShape, model, service, operation,
|
|
97
|
+
`${fieldPath}[${i}]`, findings
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
} else if (elementShape && elementShape.type === 'string' && elementShape.enum) {
|
|
101
|
+
for (let i = 0; i < value.length; i++) {
|
|
102
|
+
if (typeof value[i] === 'string' && !elementShape.enum.includes(value[i])) {
|
|
103
|
+
findings.push({
|
|
104
|
+
service,
|
|
105
|
+
operation,
|
|
106
|
+
fieldPath: `${fieldPath}[${i}]`,
|
|
107
|
+
invalidValue: value[i],
|
|
108
|
+
constraint: { type: 'enum', values: [...elementShape.enum] },
|
|
109
|
+
severity: 'error',
|
|
110
|
+
confidence: 'definitive',
|
|
111
|
+
source: this.name,
|
|
112
|
+
remediationHint: `Value "${value[i]}" is not valid. Allowed values: ${elementShape.enum.join(', ')}. Run \`bootstrap sync-schemas\` to update the enum set.`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|