Skip to content

Commit 5bed8ed

Browse files
committed
fix(import): block evaluator import when referenced by online eval, use ARN-only references
Evaluators locked by an online eval config cannot be CFN-imported because CloudFormation triggers a post-import TagResource call that the resource handler rejects. Instead of stripping tags from the import template, block the import with a clear error and suggestion to use import online-eval. Online eval config import now always references evaluators by ARN rather than resolving to local names, since the evaluators cannot be imported into the project alongside the config. Constraint: CFN IMPORT triggers TagResource which fails on locked evaluators Rejected: Strip Tags from import template | still fails on some resource types Confidence: high Scope-risk: narrow
1 parent 1cadfc7 commit 5bed8ed

2 files changed

Lines changed: 54 additions & 60 deletions

File tree

src/cli/commands/import/import-evaluator.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import type { Evaluator } from '../../../schema';
22
import type { EvaluatorSummary, GetEvaluatorResult } from '../../aws/agentcore-control';
3-
import { getEvaluator, listAllEvaluators } from '../../aws/agentcore-control';
3+
import {
4+
getEvaluator,
5+
getOnlineEvaluationConfig,
6+
listAllEvaluators,
7+
listAllOnlineEvaluationConfigs,
8+
} from '../../aws/agentcore-control';
49
import { ANSI } from './constants';
5-
import { parseAndValidateArn } from './import-utils';
10+
import { failResult, parseAndValidateArn } from './import-utils';
611
import { executeResourceImport } from './resource-import';
712
import type { ImportResourceOptions, ImportResourceResult, ResourceImportDescriptor } from './types';
813
import type { Command } from '@commander-js/extra-typings';
@@ -77,6 +82,38 @@ const evaluatorDescriptor: ResourceImportDescriptor<GetEvaluatorResult, Evaluato
7782
cfnIdentifierKey: 'EvaluatorId',
7883

7984
buildDeployedStateEntry: (name, id, d) => ({ type: 'evaluator', name, id, arn: d.evaluatorArn }),
85+
86+
beforeConfigWrite: async ({ detail, localName, target, onProgress, logger }) => {
87+
// Check if any online eval config references this evaluator.
88+
// CFN IMPORT of locked evaluators always fails because CFN triggers a
89+
// post-import TagResource call that the resource handler rejects.
90+
logger.startStep('Check for online eval config references');
91+
onProgress('Checking if evaluator is referenced by an online eval config...');
92+
93+
const oecSummaries = await listAllOnlineEvaluationConfigs({ region: target.region });
94+
if (oecSummaries.length > 0) {
95+
const oecDetails = await Promise.all(
96+
oecSummaries.map(s =>
97+
getOnlineEvaluationConfig({ region: target.region, configId: s.onlineEvaluationConfigId })
98+
)
99+
);
100+
101+
const referencingOec = oecDetails.find(oec => oec.evaluatorIds?.includes(detail.evaluatorId));
102+
103+
if (referencingOec) {
104+
return failResult(
105+
logger,
106+
`Evaluator "${localName}" is referenced by online eval config "${referencingOec.configName}" and cannot be imported directly (locked by CloudFormation).\n` +
107+
`To import this evaluator along with its online eval config, run:\n` +
108+
` agentcore import online-eval --arn ${referencingOec.configArn}`,
109+
'evaluator',
110+
localName
111+
);
112+
}
113+
}
114+
115+
logger.endStep('success');
116+
},
80117
};
81118

82119
/**

src/cli/commands/import/import-online-eval.ts

Lines changed: 15 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AgentCoreProjectSpec, DeployedState, OnlineEvalConfig } from '../../../schema';
1+
import type { OnlineEvalConfig } from '../../../schema';
22
import type { GetOnlineEvalConfigResult, OnlineEvalConfigSummary } from '../../aws/agentcore-control';
33
import { getOnlineEvaluationConfig, listAllOnlineEvaluationConfigs } from '../../aws/agentcore-control';
44
import { ANSI } from './constants';
@@ -7,8 +7,6 @@ import { executeResourceImport } from './resource-import';
77
import type { ImportResourceOptions, ImportResourceResult, ResourceImportDescriptor } from './types';
88
import type { Command } from '@commander-js/extra-typings';
99

10-
const ARN_PREFIX = 'arn:';
11-
1210
/**
1311
* Derive the agent name from the online eval config's service names.
1412
* Service names follow the pattern: "{agentName}.DEFAULT"
@@ -28,7 +26,7 @@ export function toOnlineEvalConfigSpec(
2826
detail: GetOnlineEvalConfigResult,
2927
localName: string,
3028
agentName: string,
31-
evaluatorNames: string[]
29+
evaluatorArns: string[]
3230
): OnlineEvalConfig {
3331
if (!detail.samplingPercentage) {
3432
throw new Error(`Online eval config "${detail.configName}" has no sampling configuration. Cannot import.`);
@@ -37,51 +35,28 @@ export function toOnlineEvalConfigSpec(
3735
return {
3836
name: localName,
3937
agent: agentName,
40-
evaluators: evaluatorNames,
38+
evaluators: evaluatorArns,
4139
samplingRate: detail.samplingPercentage,
4240
...(detail.description && { description: detail.description }),
4341
...(detail.executionStatus === 'ENABLED' && { enableOnCreate: true }),
4442
};
4543
}
4644

4745
/**
48-
* Resolve evaluator IDs to local names or ARNs.
49-
* If an evaluator ID matches a local evaluator (by checking deployed state), use the local name.
50-
* Otherwise, construct an ARN so the schema validation passes.
46+
* Build evaluator ARNs from evaluator IDs.
47+
* Online eval configs reference evaluators by ARN rather than importing them,
48+
* since evaluators locked by an online eval config cannot be CFN-imported.
5149
*/
52-
function resolveEvaluatorReferences(
53-
evaluatorIds: string[],
54-
projectSpec: AgentCoreProjectSpec,
55-
deployedEvaluators: Record<string, string>,
56-
region: string,
57-
account: string
58-
): string[] {
59-
const localEvaluators = projectSpec.evaluators ?? [];
60-
61-
return evaluatorIds.map(id => {
62-
// First check deployed state for an exact physical ID → local name match
63-
// This handles imported evaluators where the local name differs from the AWS name
64-
if (deployedEvaluators[id]) {
65-
return deployedEvaluators[id];
66-
}
67-
// Then check if the evaluator ID contains a local evaluator name
68-
// This handles evaluators deployed by the same project (ID pattern: {projectName}_{evaluatorName}-{suffix})
69-
for (const localEval of localEvaluators) {
70-
if (id.includes(localEval.name)) {
71-
return localEval.name;
72-
}
73-
}
74-
// Fall back to ARN format (bypasses schema cross-reference validation)
75-
return `${ARN_PREFIX}aws:bedrock-agentcore:${region}:${account}:evaluator/${id}`;
76-
});
50+
function buildEvaluatorArns(evaluatorIds: string[], region: string, account: string): string[] {
51+
return evaluatorIds.map(id => `arn:aws:bedrock-agentcore:${region}:${account}:evaluator/${id}`);
7752
}
7853

7954
/**
8055
* Create an online-eval descriptor with closed-over state for reference resolution.
8156
*/
8257
function createOnlineEvalDescriptor(): ResourceImportDescriptor<GetOnlineEvalConfigResult, OnlineEvalConfigSummary> {
8358
let resolvedAgentName = '';
84-
let resolvedEvaluatorNames: string[] = [];
59+
let resolvedEvaluatorArns: string[] = [];
8560

8661
return {
8762
resourceType: 'online-eval',
@@ -112,7 +87,7 @@ function createOnlineEvalDescriptor(): ResourceImportDescriptor<GetOnlineEvalCon
11287
getExistingNames: spec => (spec.onlineEvalConfigs ?? []).map(c => c.name),
11388
addToProjectSpec: (detail, localName, spec) => {
11489
(spec.onlineEvalConfigs ??= []).push(
115-
toOnlineEvalConfigSpec(detail, localName, resolvedAgentName, resolvedEvaluatorNames)
90+
toOnlineEvalConfigSpec(detail, localName, resolvedAgentName, resolvedEvaluatorArns)
11691
);
11792
},
11893

@@ -122,7 +97,8 @@ function createOnlineEvalDescriptor(): ResourceImportDescriptor<GetOnlineEvalCon
12297

12398
buildDeployedStateEntry: (name, id, d) => ({ type: 'online-eval', name, id, arn: d.configArn }),
12499

125-
beforeConfigWrite: async ({ detail, localName, projectSpec, ctx, target, onProgress, logger }) => {
100+
// eslint-disable-next-line @typescript-eslint/require-await -- interface requires Promise return type
101+
beforeConfigWrite: async ({ detail, localName, projectSpec, target, onProgress, logger }) => {
126102
logger.startStep('Resolve references');
127103

128104
// Extract agent name from service names
@@ -148,7 +124,7 @@ function createOnlineEvalDescriptor(): ResourceImportDescriptor<GetOnlineEvalCon
148124
);
149125
}
150126

151-
// Resolve evaluator IDs to local names or ARNs
127+
// Resolve evaluator IDs to ARNs
152128
const evaluatorIds = detail.evaluatorIds ?? [];
153129
if (evaluatorIds.length === 0) {
154130
return failResult(
@@ -159,28 +135,9 @@ function createOnlineEvalDescriptor(): ResourceImportDescriptor<GetOnlineEvalCon
159135
);
160136
}
161137

162-
// Build reverse map from deployed state: evaluatorId → localName
163-
const deployedEvaluators: Record<string, string> = {};
164-
const deployedState: DeployedState = await ctx.configIO
165-
.readDeployedState()
166-
.catch((): DeployedState => ({ targets: {} }));
167-
const targetName = target.name ?? 'default';
168-
const evalEntries = deployedState.targets[targetName]?.resources?.evaluators;
169-
if (evalEntries) {
170-
for (const [localEvalName, entry] of Object.entries(evalEntries)) {
171-
deployedEvaluators[entry.evaluatorId] = localEvalName;
172-
}
173-
}
174-
175-
resolvedEvaluatorNames = resolveEvaluatorReferences(
176-
evaluatorIds,
177-
projectSpec,
178-
deployedEvaluators,
179-
target.region,
180-
target.account
181-
);
138+
resolvedEvaluatorArns = buildEvaluatorArns(evaluatorIds, target.region, target.account);
182139
resolvedAgentName = agentName;
183-
onProgress(`Agent: ${agentName}, Evaluators: ${resolvedEvaluatorNames.join(', ')}`);
140+
onProgress(`Agent: ${agentName}, Evaluators: ${resolvedEvaluatorArns.join(', ')}`);
184141
logger.endStep('success');
185142
},
186143
};

0 commit comments

Comments
 (0)