@aws/ml-container-creator 0.2.6 → 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.
@@ -0,0 +1,550 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Secrets Command Handler
6
+ *
7
+ * Handles the `secrets` CLI subcommand tree for managing secrets in
8
+ * AWS Secrets Manager. Follows the same dispatch pattern as
9
+ * BootstrapCommandHandler.
10
+ *
11
+ * Subcommands:
12
+ * create Create a new secret in Secrets Manager
13
+ * list List all mlcc-managed secrets
14
+ * describe <name-or-arn> Show metadata for a specific secret
15
+ */
16
+
17
+ import { execSync } from 'node:child_process';
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { tmpdir } from 'node:os';
21
+ import { runPrompts } from '../prompt-adapter.js';
22
+ import { SECRET_CLASSIFICATIONS } from './secret-classification.js';
23
+ import BootstrapConfig from './bootstrap-config.js';
24
+
25
+ export default class SecretsCommandHandler {
26
+ constructor({ promptFn, execAwsFn } = {}) {
27
+ this._promptFn = promptFn || runPrompts;
28
+ this._execAwsFn = execAwsFn || null;
29
+ this._bootstrapConfig = new BootstrapConfig();
30
+ }
31
+
32
+ /**
33
+ * Dispatch secrets subcommands.
34
+ * @param {string[]} args - Positional args after 'secrets'
35
+ * @param {object} options - Parsed CLI options
36
+ */
37
+ async handle(args, options) {
38
+ if (args.length === 0) {
39
+ this._showHelp();
40
+ return;
41
+ }
42
+
43
+ const subcommand = args[0].toLowerCase();
44
+
45
+ switch (subcommand) {
46
+ case 'create':
47
+ await this._handleCreate(options);
48
+ break;
49
+ case 'list':
50
+ await this._handleList();
51
+ break;
52
+ case 'describe':
53
+ await this._handleDescribe(args[1]);
54
+ break;
55
+ default:
56
+ console.log(`Unknown secrets subcommand: ${subcommand}`);
57
+ this._showHelp();
58
+ break;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Create a new secret in AWS Secrets Manager.
64
+ * Supports three input modes:
65
+ * 1. Interactive — prompts for type, name, and value
66
+ * 2. --json flag — inline JSON or file:// path
67
+ * 3. Individual flags — --type, --name, --secret-value, etc.
68
+ * @param {object} options - Parsed CLI options
69
+ */
70
+ async _handleCreate(options) {
71
+ let secretType, label, secretValue, description, kmsKeyId, userTags;
72
+
73
+ if (options.json) {
74
+ // JSON mode: parse inline JSON or read from file
75
+ const jsonInput = await this._resolveJsonInput(options.json);
76
+ if (!jsonInput) return;
77
+
78
+ secretType = jsonInput.type || jsonInput.secretType;
79
+ label = jsonInput.name || jsonInput.label;
80
+ secretValue = jsonInput.secretValue || jsonInput['secret-value'];
81
+ description = jsonInput.description;
82
+ kmsKeyId = jsonInput.kmsKeyId || jsonInput['kms-key-id'];
83
+ userTags = jsonInput.tags || [];
84
+ } else if (options.type || options.name || options.secretValue) {
85
+ // Flag mode: use individual CLI flags
86
+ secretType = options.type;
87
+ label = options.name;
88
+ secretValue = options.secretValue;
89
+ description = options.description;
90
+ kmsKeyId = options.kmsKeyId;
91
+ userTags = [];
92
+ } else {
93
+ // Interactive mode: prompt for all required fields
94
+ const result = await this._runInteractiveCreate();
95
+ if (!result) return;
96
+ secretType = result.type;
97
+ label = result.name;
98
+ secretValue = result.secretValue;
99
+ description = result.description;
100
+ kmsKeyId = result.kmsKeyId;
101
+ userTags = [];
102
+ }
103
+
104
+ // Validate required fields
105
+ const missing = [];
106
+ if (!secretType) missing.push('--type');
107
+ if (!label) missing.push('--name');
108
+ if (!secretValue) missing.push('--secret-value');
109
+
110
+ if (missing.length > 0) {
111
+ console.log(`❌ Missing required fields: ${missing.join(', ')}`);
112
+ console.log(' Provide all required flags or run without flags for interactive mode.');
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+
117
+ // Validate secret type against registry
118
+ const classification = SECRET_CLASSIFICATIONS.find(c => c.identifier === secretType);
119
+ if (!classification) {
120
+ const validTypes = SECRET_CLASSIFICATIONS.map(c => c.identifier).join(', ');
121
+ console.log(`❌ Unknown secret type: ${secretType}`);
122
+ console.log(` Valid types: ${validTypes}`);
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+
127
+ // Construct the secret name
128
+ const secretName = this._constructSecretName(secretType, label);
129
+
130
+ // Merge tags
131
+ const tags = this._mergeTags(userTags || [], secretType);
132
+
133
+ // Build the create-secret command
134
+ const { profile, region } = this._getActiveBootstrapContext();
135
+ if (!profile) {
136
+ console.log('❌ No active bootstrap profile found.');
137
+ console.log(' Run `ml-container-creator bootstrap` to set up shared infrastructure.');
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+
142
+ // Write secret value to temp file to avoid shell exposure
143
+ const secretValueFile = this._writeJsonTempFile(secretValue, 'secret-value');
144
+
145
+ let command = `secretsmanager create-secret --name ${secretName} --secret-string ${secretValueFile} --tags ${this._formatTagsForCli(tags)} --region ${region}`;
146
+
147
+ if (description) {
148
+ command += ` --description "${description}"`;
149
+ }
150
+ if (kmsKeyId) {
151
+ command += ` --kms-key-id ${kmsKeyId}`;
152
+ }
153
+
154
+ try {
155
+ const result = this._execAws(command, profile);
156
+ const arn = result.ARN || result.Name;
157
+ console.log('✅ Secret created successfully');
158
+ console.log(` Name: ${secretName}`);
159
+ console.log(` ARN: ${arn}`);
160
+ } catch (error) {
161
+ console.log(`❌ Failed to create secret: ${error.message}`);
162
+ process.exitCode = 1;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Run the interactive secret creation flow.
168
+ * Prompts for type, name, and value (password-masked).
169
+ * @returns {object|null} Object with type, name, secretValue, description, kmsKeyId
170
+ */
171
+ async _runInteractiveCreate() {
172
+ console.log('\n🔐 Create a new secret in AWS Secrets Manager\n');
173
+
174
+ // Prompt for secret type
175
+ const typeChoices = SECRET_CLASSIFICATIONS.map(c => ({
176
+ name: `${c.displayName} — ${c.purpose}`,
177
+ value: c.identifier
178
+ }));
179
+
180
+ const { secretType } = await this._promptFn([{
181
+ type: 'list',
182
+ name: 'secretType',
183
+ message: 'Secret type:',
184
+ choices: typeChoices
185
+ }]);
186
+
187
+ // Prompt for label
188
+ const { label } = await this._promptFn([{
189
+ type: 'input',
190
+ name: 'label',
191
+ message: 'Secret name (label):',
192
+ validate: (val) => val && val.trim().length > 0 ? true : 'Name is required'
193
+ }]);
194
+
195
+ // Prompt for secret value (password-masked)
196
+ const { value } = await this._promptFn([{
197
+ type: 'password',
198
+ name: 'value',
199
+ message: 'Secret value:',
200
+ mask: '*',
201
+ validate: (val) => val && val.trim().length > 0 ? true : 'Value is required'
202
+ }]);
203
+
204
+ // Optional: description
205
+ const { description } = await this._promptFn([{
206
+ type: 'input',
207
+ name: 'description',
208
+ message: 'Description (optional):',
209
+ default: ''
210
+ }]);
211
+
212
+ return {
213
+ type: secretType,
214
+ name: label.trim(),
215
+ secretValue: value,
216
+ description: description || undefined,
217
+ kmsKeyId: undefined
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Resolve JSON input from inline string or file:// path.
223
+ * @param {string} jsonOrPath - Inline JSON string or file://path
224
+ * @returns {object|null} Parsed JSON object, or null on error
225
+ */
226
+ async _resolveJsonInput(jsonOrPath) {
227
+ let rawJson;
228
+
229
+ if (jsonOrPath.startsWith('file://')) {
230
+ const filePath = jsonOrPath.slice(7);
231
+ if (!existsSync(filePath)) {
232
+ console.log(`❌ File not found: ${filePath}`);
233
+ process.exitCode = 1;
234
+ return null;
235
+ }
236
+ rawJson = readFileSync(filePath, 'utf8');
237
+ } else {
238
+ rawJson = jsonOrPath;
239
+ }
240
+
241
+ try {
242
+ return JSON.parse(rawJson);
243
+ } catch (error) {
244
+ console.log(`❌ Invalid JSON: ${error.message}`);
245
+ process.exitCode = 1;
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Construct the secret name following the mlcc naming convention.
252
+ * @param {string} type - Secret type identifier (e.g., 'hf-token')
253
+ * @param {string} label - User-provided label
254
+ * @returns {string} Constructed name in format mlcc/<type>/<label>
255
+ */
256
+ _constructSecretName(type, label) {
257
+ return `mlcc/${type}/${label}`;
258
+ }
259
+
260
+ /**
261
+ * Merge user-provided tags with required system tags.
262
+ * System tags always win over user-provided tags with the mlcc: prefix.
263
+ * User tags without the mlcc: prefix are preserved.
264
+ * @param {Array<{Key: string, Value: string}>} userTags - User-provided tags
265
+ * @param {string} secretType - Secret type identifier
266
+ * @returns {Array<{Key: string, Value: string}>} Merged tag array
267
+ */
268
+ _mergeTags(userTags, secretType) {
269
+ const systemTags = [
270
+ { Key: 'mlcc:managed-by', Value: 'ml-container-creator' },
271
+ { Key: 'mlcc:created-by', Value: 'secrets' },
272
+ { Key: 'mlcc:secret-type', Value: secretType }
273
+ ];
274
+
275
+ const systemTagKeys = new Set(systemTags.map(t => t.Key));
276
+
277
+ // Filter user tags: preserve non-mlcc: tags, warn about mlcc: conflicts
278
+ const preservedUserTags = [];
279
+ for (const tag of (userTags || [])) {
280
+ if (!tag || !tag.Key) continue;
281
+ if (tag.Key.startsWith('mlcc:')) {
282
+ if (systemTagKeys.has(tag.Key)) {
283
+ console.log(`⚠️ Tag "${tag.Key}" is reserved and will be overwritten with system value`);
284
+ } else {
285
+ console.log(`⚠️ Tag "${tag.Key}" uses reserved mlcc: prefix and will be removed`);
286
+ }
287
+ } else {
288
+ preservedUserTags.push(tag);
289
+ }
290
+ }
291
+
292
+ return [...systemTags, ...preservedUserTags];
293
+ }
294
+
295
+ /**
296
+ * Get the active bootstrap profile's AWS profile and region.
297
+ * @returns {{ profile: string|null, region: string|null }}
298
+ */
299
+ _getActiveBootstrapContext() {
300
+ const active = this._bootstrapConfig.getActiveProfile();
301
+ if (!active) {
302
+ return { profile: null, region: null };
303
+ }
304
+ return {
305
+ profile: active.config.awsProfile,
306
+ region: active.config.awsRegion
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Execute an AWS CLI command and return parsed JSON output.
312
+ * @param {string} command - AWS CLI command (without 'aws' prefix)
313
+ * @param {string} profile - AWS CLI profile name
314
+ * @returns {object} Parsed JSON output
315
+ */
316
+ _execAws(command, profile) {
317
+ if (this._execAwsFn) {
318
+ return this._execAwsFn(command, profile);
319
+ }
320
+ const fullCommand = `aws ${command} --profile ${profile} --output json`;
321
+ const output = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
322
+ const trimmed = output.trim();
323
+ if (!trimmed) {
324
+ return {};
325
+ }
326
+ return JSON.parse(trimmed);
327
+ }
328
+
329
+ /**
330
+ * Write a JSON value to a temp file and return the file:// path.
331
+ * Used to avoid shell escaping issues with complex values.
332
+ * @param {*} value - Value to serialize (string or object)
333
+ * @param {string} prefix - Filename prefix
334
+ * @returns {string} file:// path to the temp file
335
+ */
336
+ _writeJsonTempFile(value, prefix = 'mlcc-secret') {
337
+ const dir = path.join(tmpdir(), 'mlcc-secrets');
338
+ if (!existsSync(dir)) {
339
+ mkdirSync(dir, { recursive: true });
340
+ }
341
+ const filePath = path.join(dir, `${prefix}-${Date.now()}.json`);
342
+ const content = typeof value === 'string' ? value : JSON.stringify(value);
343
+ writeFileSync(filePath, content);
344
+ return `file://${filePath}`;
345
+ }
346
+
347
+ /**
348
+ * Format tags for the AWS CLI --tags parameter.
349
+ * Writes tags to a temp file and returns the file:// reference.
350
+ * @param {Array<{Key: string, Value: string}>} tags - Tag array
351
+ * @returns {string} file:// path to the tags JSON file
352
+ */
353
+ _formatTagsForCli(tags) {
354
+ return this._writeJsonTempFile(tags, 'tags');
355
+ }
356
+
357
+ /**
358
+ * List all mlcc-managed secrets.
359
+ * Calls list-secrets filtered by the mlcc:managed-by tag and displays
360
+ * name, ARN, secret type, creation date, and last accessed date.
361
+ * Never displays secret values.
362
+ *
363
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5
364
+ */
365
+ async _handleList() {
366
+ const { profile, region } = this._getActiveBootstrapContext();
367
+ if (!profile) {
368
+ console.log('❌ No active bootstrap profile found.');
369
+ console.log(' Run `ml-container-creator bootstrap` to set up shared infrastructure.');
370
+ process.exitCode = 1;
371
+ return;
372
+ }
373
+
374
+ const command = `secretsmanager list-secrets --filters Key=tag-key,Values=mlcc:managed-by Key=tag-value,Values=ml-container-creator --region ${region}`;
375
+
376
+ let result;
377
+ try {
378
+ result = this._execAws(command, profile);
379
+ } catch (error) {
380
+ console.log(`❌ Failed to list secrets: ${error.message}`);
381
+ process.exitCode = 1;
382
+ return;
383
+ }
384
+
385
+ const secrets = result.SecretList || [];
386
+
387
+ if (secrets.length === 0) {
388
+ console.log('\nNo mlcc-managed secrets found.');
389
+ console.log(' Run `ml-container-creator secrets create` to create your first secret.\n');
390
+ return;
391
+ }
392
+
393
+ console.log(`\n🔐 Managed Secrets (${secrets.length})\n`);
394
+ console.log('─'.repeat(80));
395
+
396
+ for (const secret of secrets) {
397
+ const secretType = this._extractTagValue(secret.Tags, 'mlcc:secret-type') || 'unknown';
398
+ const createdDate = secret.CreatedDate ? new Date(secret.CreatedDate).toLocaleDateString() : 'N/A';
399
+ const lastAccessed = secret.LastAccessedDate ? new Date(secret.LastAccessedDate).toLocaleDateString() : 'Never';
400
+
401
+ console.log(` Name: ${secret.Name}`);
402
+ console.log(` ARN: ${secret.ARN}`);
403
+ console.log(` Type: ${secretType}`);
404
+ console.log(` Created: ${createdDate}`);
405
+ console.log(` Last Accessed: ${lastAccessed}`);
406
+ console.log('─'.repeat(80));
407
+ }
408
+
409
+ console.log('');
410
+ }
411
+
412
+ /**
413
+ * Extract a tag value from a Tags array by key.
414
+ * @param {Array<{Key: string, Value: string}>} tags - Tags array
415
+ * @param {string} key - Tag key to find
416
+ * @returns {string|undefined} Tag value or undefined if not found
417
+ */
418
+ _extractTagValue(tags, key) {
419
+ if (!Array.isArray(tags)) return undefined;
420
+ const tag = tags.find(t => t.Key === key);
421
+ return tag ? tag.Value : undefined;
422
+ }
423
+
424
+ /**
425
+ * Describe a specific secret's metadata (never reveals the value).
426
+ * Calls `aws secretsmanager describe-secret` and displays name, ARN,
427
+ * description, tags, creation date, last changed date, last accessed date,
428
+ * and rotation configuration.
429
+ *
430
+ * Never calls GetSecretValue. Displays error if secret not found or
431
+ * not a managed secret.
432
+ *
433
+ * Requirements: 4.1, 4.2, 4.3, 4.4
434
+ *
435
+ * @param {string} nameOrArn - Secret name or ARN to describe
436
+ */
437
+ async _handleDescribe(nameOrArn) {
438
+ if (!nameOrArn) {
439
+ console.log('❌ Missing secret name or ARN.');
440
+ console.log(' Usage: ml-container-creator secrets describe <name-or-arn>');
441
+ process.exitCode = 1;
442
+ return;
443
+ }
444
+
445
+ const { profile, region } = this._getActiveBootstrapContext();
446
+ if (!profile) {
447
+ console.log('❌ No active bootstrap profile found.');
448
+ console.log(' Run `ml-container-creator bootstrap` to set up shared infrastructure.');
449
+ process.exitCode = 1;
450
+ return;
451
+ }
452
+
453
+ const command = `secretsmanager describe-secret --secret-id ${nameOrArn} --region ${region}`;
454
+
455
+ let result;
456
+ try {
457
+ result = this._execAws(command, profile);
458
+ } catch (error) {
459
+ console.log(`❌ Secret not found: ${nameOrArn}`);
460
+ console.log(` ${error.message}`);
461
+ process.exitCode = 1;
462
+ return;
463
+ }
464
+
465
+ // Verify this is a managed secret by checking the mlcc:managed-by tag
466
+ const managedByValue = this._extractTagValue(result.Tags, 'mlcc:managed-by');
467
+ if (managedByValue !== 'ml-container-creator') {
468
+ console.log(`❌ Secret "${nameOrArn}" is not managed by ml-container-creator.`);
469
+ console.log(' Only secrets created with `ml-container-creator secrets create` can be described.');
470
+ process.exitCode = 1;
471
+ return;
472
+ }
473
+
474
+ // Display secret metadata
475
+ const createdDate = result.CreatedDate ? new Date(result.CreatedDate).toLocaleString() : 'N/A';
476
+ const lastChanged = result.LastChangedDate ? new Date(result.LastChangedDate).toLocaleString() : 'N/A';
477
+ const lastAccessed = result.LastAccessedDate ? new Date(result.LastAccessedDate).toLocaleDateString() : 'Never';
478
+ const secretType = this._extractTagValue(result.Tags, 'mlcc:secret-type') || 'unknown';
479
+
480
+ console.log('\n🔐 Secret Details\n');
481
+ console.log('─'.repeat(80));
482
+ console.log(` Name: ${result.Name}`);
483
+ console.log(` ARN: ${result.ARN}`);
484
+ console.log(` Type: ${secretType}`);
485
+ console.log(` Description: ${result.Description || '(none)'}`);
486
+ console.log(` Created: ${createdDate}`);
487
+ console.log(` Last Changed: ${lastChanged}`);
488
+ console.log(` Last Accessed: ${lastAccessed}`);
489
+
490
+ // Rotation configuration
491
+ if (result.RotationEnabled) {
492
+ console.log(' Rotation: Enabled');
493
+ if (result.RotationRules) {
494
+ if (result.RotationRules.AutomaticallyAfterDays) {
495
+ console.log(` Rotation Rule: Every ${result.RotationRules.AutomaticallyAfterDays} days`);
496
+ }
497
+ if (result.RotationRules.Duration) {
498
+ console.log(` Rotation Window: ${result.RotationRules.Duration}`);
499
+ }
500
+ if (result.RotationRules.ScheduleExpression) {
501
+ console.log(` Rotation Schedule: ${result.RotationRules.ScheduleExpression}`);
502
+ }
503
+ }
504
+ } else {
505
+ console.log(' Rotation: Disabled');
506
+ }
507
+
508
+ // Tags
509
+ if (Array.isArray(result.Tags) && result.Tags.length > 0) {
510
+ console.log(' Tags:');
511
+ for (const tag of result.Tags) {
512
+ console.log(` ${tag.Key} = ${tag.Value}`);
513
+ }
514
+ }
515
+
516
+ console.log('─'.repeat(80));
517
+ console.log('');
518
+ }
519
+
520
+ /**
521
+ * Show secrets usage help.
522
+ */
523
+ _showHelp() {
524
+ console.log(`
525
+ Secrets — Manage secrets in AWS Secrets Manager
526
+
527
+ USAGE:
528
+ ml-container-creator secrets <action> [options]
529
+
530
+ ACTIONS:
531
+ create Create a new secret
532
+ list List all mlcc-managed secrets
533
+ describe <name-or-arn> Show metadata for a specific secret
534
+
535
+ CREATE OPTIONS:
536
+ --type <type> Secret type (e.g., hf-token, ngc-token)
537
+ --name <label> Secret label (used in naming convention)
538
+ --secret-value <value> Secret value
539
+ --description <text> Secret description
540
+ --kms-key-id <key> KMS key for encryption
541
+ --json <json-or-path> JSON input (inline or file://path)
542
+
543
+ EXAMPLES:
544
+ ml-container-creator secrets create --type hf-token --name production --secret-value hf_***
545
+ ml-container-creator secrets create --json file://secret.json
546
+ ml-container-creator secrets list
547
+ ml-container-creator secrets describe mlcc/hf-token/production
548
+ `.trim());
549
+ }
550
+ }
@@ -15,11 +15,17 @@
15
15
 
16
16
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
17
17
  import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
18
19
  import PayloadBuilder from './payload-builder.js';
19
20
  import SchemaValidationEngine from './schema-validation-engine.js';
20
21
  import ServiceModelParser from './service-model-parser.js';
22
+ import CrossCuttingChecker from './cross-cutting-checker.js';
23
+ import HuggingFaceClient from './huggingface-client.js';
21
24
  import { getRegistryPath, loadManifest } from './schema-sync.js';
22
25
 
26
+ const __filename = fileURLToPath(import.meta.url);
27
+ const __dirname = path.dirname(__filename);
28
+
23
29
  /**
24
30
  * Parse a do/config shell file into a key-value object.
25
31
  * Extracts lines matching: export KEY="value" or export KEY=value
@@ -122,6 +128,49 @@ export async function run(options = {}) {
122
128
  });
123
129
 
124
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
+
125
174
  const summary = report.getSummary();
126
175
 
127
176
  // Load manifest for version info
@@ -22,7 +22,14 @@ export default class ValidationReport {
22
22
  const source = finding.source || '';
23
23
 
24
24
  if (source === 'cross-cutting') {
25
- this.crossCuttingErrors.push(finding);
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
+ }
26
33
  } else if (source === 'smart-mode' || source.startsWith('smart:')) {
27
34
  // Smart-mode findings are advisory UNLESS confidence is definitive AND severity is error
28
35
  if (finding.confidence === 'definitive' && finding.severity === 'error') {
@@ -1,12 +1,12 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import { select, input, confirm, checkbox, number, Separator } from '@inquirer/prompts';
4
+ import { select, input, confirm, checkbox, number, password, Separator } from '@inquirer/prompts';
5
5
 
6
6
  /**
7
7
  * Maps Yeoman prompt type names to @inquirer/prompts runner functions.
8
8
  */
9
- const runners = { list: select, select, input, confirm, checkbox, number };
9
+ const runners = { list: select, select, input, confirm, checkbox, number, password };
10
10
 
11
11
  /**
12
12
  * Runs a sequence of Yeoman-style prompt definitions using @inquirer/prompts.
@@ -55,6 +55,7 @@ export async function runPrompts(prompts, previousAnswers = {}, options = {}) {
55
55
  if (mappedChoices !== undefined) config.choices = mappedChoices;
56
56
  if (defaultVal !== undefined) config.default = defaultVal;
57
57
  if (prompt.validate) config.validate = prompt.validate;
58
+ if (prompt.mask !== undefined) config.mask = prompt.mask;
58
59
 
59
60
  answers[prompt.name] = await runner(config);
60
61
  }
@@ -27,6 +27,28 @@ fi
27
27
  # fail on SageMaker with CannotStartContainerError.
28
28
  PLATFORM_FLAG="--platform linux/amd64"
29
29
 
30
+ # --- Secrets Manager resolution (build-time) ---
31
+ if [ -n "${HF_TOKEN_ARN:-}" ]; then
32
+ echo "🔐 Resolving HuggingFace token from Secrets Manager..."
33
+ HF_TOKEN=$(aws secretsmanager get-secret-value --secret-id "${HF_TOKEN_ARN}" --query SecretString --output text) || {
34
+ echo "❌ Failed to resolve HuggingFace token from Secrets Manager"
35
+ exit 3
36
+ }
37
+ export HF_TOKEN
38
+ fi
39
+
40
+ if [ -n "${NGC_API_KEY_ARN:-}" ]; then
41
+ echo "🔐 Resolving NGC API key from Secrets Manager..."
42
+ NGC_API_KEY=$(aws secretsmanager get-secret-value --secret-id "${NGC_API_KEY_ARN}" --query SecretString --output text) || {
43
+ echo "❌ Failed to resolve NGC API key from Secrets Manager"
44
+ exit 3
45
+ }
46
+ export NGC_API_KEY
47
+ fi
48
+
49
+ # NOTE: Build-time secrets are passed as --build-arg. The secret value may persist
50
+ # in the image layer. A future improvement will use BuildKit --secret mounts.
51
+
30
52
  # Framework-specific build logic
31
53
  case "${DEPLOYMENT_CONFIG}" in
32
54
  transformers-tensorrt-llm)