diff --git a/src/core/eval.tsx b/src/core/eval.tsx index 517a2c68e..d1a295565 100644 --- a/src/core/eval.tsx +++ b/src/core/eval.tsx @@ -1,21 +1,53 @@ import { CreateEvaluatorCommand, + CreateOnlineEvaluationConfigCommand, DeleteEvaluatorCommand, + DeleteOnlineEvaluationConfigCommand, + GetAgentRuntimeCommand, GetEvaluatorCommand, + GetHarnessCommand, + GetOnlineEvaluationConfigCommand, ListEvaluatorsCommand, + ListOnlineEvaluationConfigsCommand, UpdateEvaluatorCommand, + UpdateOnlineEvaluationConfigCommand, type CreateEvaluatorRequest, type CreateEvaluatorResponse, + type CreateOnlineEvaluationConfigResponse, type DeleteEvaluatorResponse, + type DeleteOnlineEvaluationConfigResponse, type EvaluatorConfig, type GetEvaluatorResponse, + type GetOnlineEvaluationConfigResponse, type ListEvaluatorsResponse, + type DataSourceConfig, + type ListOnlineEvaluationConfigsResponse, + type Rule, type UpdateEvaluatorResponse, + type UpdateOnlineEvaluationConfigResponse, + type BedrockAgentCoreControlClient, } from "@aws-sdk/client-bedrock-agentcore-control"; import { InputValidationError } from "../errors"; -import type { CodeBasedUpdate, CoreEvalClient, LlmAsAJudgeUpdate } from "../handlers/eval/types"; +import type { + CodeBasedUpdate, + RoleScopeWarning, + CoreEvalClient, + CreateOnlineEvalInput, + LlmAsAJudgeUpdate, + UpdateOnlineEvalInput, +} from "../handlers/eval/types"; import type { AwsClients, CoreOptions } from "./types"; import { toClientConfig } from "./utils"; +import { + accountIdFromRoleArn, + executionPolicy, + grantOnlineEvalScope, + onlineEvalExecutionRoleName, + revokeOnlineEvalScope, + scopePolicyName, +} from "./onlineEvalExecutionRole"; + +const DEFAULT_ENDPOINT_QUALIFIER = "DEFAULT"; export class EvalClient implements CoreEvalClient { constructor(private readonly clients: AwsClients) {} @@ -153,4 +185,415 @@ export class EvalClient implements CoreEvalClient { .control(toClientConfig(options)) .send(new DeleteEvaluatorCommand({ evaluatorId: id })); } + + async createOnlineEvaluationConfig( + input: CreateOnlineEvalInput, + options: CoreOptions, + ): Promise { + // `--agent` derives the CloudWatch source from the agent's default trace + // path; an explicit dataSourceConfig passes straight through, which is how an + // agent emitting under a custom OTel service name is pointed at its log groups. + const dataSourceConfig = + input.agent !== undefined + ? await agentDataSource(input.agent, input.endpoint, this.clients, options) + : input.dataSourceConfig; + const control = this.clients.control(toClientConfig(options)); + + // The service validates at create time that the role can query the log groups + // it was pointed at, and the required policy is not obvious, so provision a + // default role scoped to them unless the caller brought their own. + const evaluationExecutionRoleArn = + input.evaluationExecutionRoleArn ?? + ( + await grantOnlineEvalScope( + // IAM is a global service; the region only selects the endpoint, and the + // agentcore endpoint override must not leak onto it. + this.clients.iam({ region: options.region }), + input.name, + options.region, + logGroupNamesOf(dataSourceConfig), + await evaluatorKmsKeys(input.evaluatorIds ?? [], control), + ) + ).roleArn; + + const command = new CreateOnlineEvaluationConfigCommand({ + onlineEvaluationConfigName: input.name, + description: input.description, + rule: toRule(input.samplingRate, input.sessionTimeoutMinutes, input.filters), + dataSourceConfig, + evaluators: input.evaluatorIds?.map((evaluatorId) => ({ evaluatorId })), + evaluationExecutionRoleArn, + enableOnCreate: input.enableOnCreate ?? true, + }); + + // A role provisioned moments ago may not be assumable yet (IAM is eventually + // consistent), and the service rejects the create rather than retrying. Only + // worth retrying when we just created the role; a caller-supplied one that + // cannot be assumed is a real misconfiguration and fails immediately. + return input.evaluationExecutionRoleArn + ? control.send(command) + : retryWhileRolePropagates(() => control.send(command)); + } + + // updateOnlineEvaluationConfig fetches the current config and merges the + // provided fields over it, because UpdateOnlineEvaluationConfig replaces the + // whole `rule` (and, when endpoint changes, `dataSourceConfig`) rather than + // patching individual fields. + async updateOnlineEvaluationConfig( + id: string, + update: UpdateOnlineEvalInput, + options: CoreOptions, + ): Promise<{ + response: UpdateOnlineEvaluationConfigResponse; + roleScopeWarning?: RoleScopeWarning; + }> { + const control = this.clients.control(toClientConfig(options)); + const current = await control.send( + new GetOnlineEvaluationConfigCommand({ + onlineEvaluationConfigId: id, + }), + ); + + const samplingPercentage = + update.samplingRate ?? current.rule?.samplingConfig?.samplingPercentage; + const sessionTimeoutMinutes = + update.sessionTimeoutMinutes ?? current.rule?.sessionConfig?.sessionTimeoutMinutes; + const filters = update.filters ?? current.rule?.filters; + + const evaluators = + update.evaluatorIds !== undefined + ? update.evaluatorIds.map((evaluatorId) => ({ evaluatorId })) + : current.evaluators; + + // Repointing the evaluation, in precedence order: an explicit + // dataSourceConfig replaces the source outright; --agent re-derives it from + // that agent; --endpoint/--clear-endpoint alone re-scope the agent this config + // was already built from, which means recovering its runtime id first. + let dataSourceConfig = current.dataSourceConfig; + if (update.dataSourceConfig !== undefined) { + dataSourceConfig = update.dataSourceConfig; + } else if (update.agent !== undefined) { + dataSourceConfig = await agentDataSource( + update.agent, + update.clearEndpoint ? DEFAULT_ENDPOINT_QUALIFIER : update.endpoint, + this.clients, + options, + ); + } else if (update.clearEndpoint || update.endpoint !== undefined) { + // The runtime id only survives inside the stored log group path, so an + // endpoint change has to recover it from there. + const currentLogGroup = + current.dataSourceConfig && "cloudWatchLogs" in current.dataSourceConfig + ? current.dataSourceConfig.cloudWatchLogs?.logGroupNames?.[0] + : undefined; + const runtimeId = currentLogGroup ? runtimeIdFromLogGroup(currentLogGroup) : undefined; + if (!runtimeId) { + throw new InputValidationError( + `Online evaluation config "${id}" was not created from an agent; ` + + `pass --agent or --data-source-config to repoint it`, + { meta: { onlineEvaluationConfigId: id } }, + ); + } + const endpoint = update.clearEndpoint ? DEFAULT_ENDPOINT_QUALIFIER : update.endpoint; + dataSourceConfig = await agentDataSource(runtimeId, endpoint, this.clients, options); + } + + // Moving the data source invalidates the execution role's scope: its policy + // grants query access to the previous log groups only. A role the caller named + // via --role-arn is theirs to manage and is never edited; a CLI-provisioned one + // (identified by its derived name) is re-scoped unless the caller declines. + // Either way, skipping the refresh is reported so the caller can be told. + let roleScopeWarning: RoleScopeWarning | undefined; + const movedTo = + dataSourceConfig !== undefined && dataSourceConfig !== current.dataSourceConfig + ? dataSourceConfig + : undefined; + + const configName = current.onlineEvaluationConfigName; + const roleArn = update.evaluationExecutionRoleArn ?? current.evaluationExecutionRoleArn; + const managedRoleName = + configName !== undefined && + update.evaluationExecutionRoleArn === undefined && + roleArn?.endsWith(`/${onlineEvalExecutionRoleName(configName)}`) === true + ? configName + : undefined; + const refreshManagedRole = movedTo !== undefined && managedRoleName !== undefined; + + if (movedTo !== undefined && managedRoleName === undefined && roleArn) { + roleScopeWarning = { + reason: "custom-role", + roleArn, + logGroupNames: logGroupNamesOf(movedTo), + }; + } else if (movedTo !== undefined && !refreshManagedRole && roleArn) { + // managed role, but the caller declined the refresh + roleScopeWarning = { + reason: "update-declined", + roleArn, + logGroupNames: logGroupNamesOf(movedTo), + }; + } + + if (refreshManagedRole && update.updateRole !== false) { + const iam = this.clients.iam({ region: options.region }); + const newLogGroups = logGroupNamesOf(movedTo); + const oldLogGroups = current.dataSourceConfig + ? logGroupNamesOf(current.dataSourceConfig) + : []; + // The evaluator list may have changed alongside the data source, so + // re-resolve the keys rather than reusing the ones from create. + const kmsKeys = await evaluatorKmsKeys( + update.evaluatorIds ?? + (current.evaluators ?? []) + .map((e) => ("evaluatorId" in e ? e.evaluatorId : undefined)) + .filter((id): id is string => id !== undefined), + control, + ); + + // Grant the new scope as its own inline policy before the update, then + // revoke the superseded one only once the update has landed. IAM unions + // Allows across a role's inline policies, so both scopes are granted in + // between — and because each scope is a separate policy, a failed update + // leaves the one backing the current data source exactly as it was. + const { roleArn: managedRoleArn, policyName: newPolicyName } = await grantOnlineEvalScope( + iam, + managedRoleName, + options.region, + newLogGroups, + kmsKeys, + ); + const oldPolicyName = scopePolicyName( + executionPolicy( + options.region, + accountIdFromRoleArn(managedRoleArn), + oldLogGroups, + kmsKeys, + ), + ); + + const response = await control.send( + new UpdateOnlineEvaluationConfigCommand({ + onlineEvaluationConfigId: id, + rule: toRule(samplingPercentage, sessionTimeoutMinutes, filters), + dataSourceConfig, + evaluators, + }), + ); + + if (newPolicyName !== oldPolicyName) { + try { + await revokeOnlineEvalScope(iam, managedRoleName, oldPolicyName); + } catch { + // The config is already correct; the role just still grants a data + // source it no longer uses. + roleScopeWarning = { + reason: "stale-scope", + roleArn: roleArn!, + logGroupNames: oldLogGroups, + }; + } + } + return { response, roleScopeWarning }; + } + + const response = await control.send( + new UpdateOnlineEvaluationConfigCommand({ + onlineEvaluationConfigId: id, + rule: toRule(samplingPercentage, sessionTimeoutMinutes, filters), + dataSourceConfig, + evaluators, + evaluationExecutionRoleArn: update.evaluationExecutionRoleArn, + }), + ); + return { response, roleScopeWarning }; + } + + async getOnlineEvaluationConfig( + id: string, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new GetOnlineEvaluationConfigCommand({ onlineEvaluationConfigId: id })); + } + + async listOnlineEvaluationConfigs( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new ListOnlineEvaluationConfigsCommand({ nextToken, maxResults })); + } + + async setOnlineEvaluationExecutionStatus( + id: string, + executionStatus: "ENABLED" | "DISABLED", + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send( + new UpdateOnlineEvaluationConfigCommand({ onlineEvaluationConfigId: id, executionStatus }), + ); + } + + async deleteOnlineEvaluationConfig( + id: string, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new DeleteOnlineEvaluationConfigCommand({ onlineEvaluationConfigId: id })); + } +} + +// runtimeLogGroup mirrors the old CLI's derivation (src/cli/aws/cloudwatch.ts): +// AgentCore always writes a runtime endpoint's traces to this fixed path, keyed +// by the runtime *id*. +function runtimeLogGroup(runtimeId: string, endpoint: string): string { + return `/aws/bedrock-agentcore/runtimes/${runtimeId}-${endpoint}`; +} + +// runtimeServiceName derives the CloudWatch trace service name that scopes a +// CreateOnlineEvaluationConfig data source to one runtime endpoint's sessions: +// `{runtimeName}.{endpoint}`, keyed by the runtime *name* (verified against +// production configs — this does NOT match the log group's runtime id). +function runtimeServiceName(runtimeName: string, endpoint: string): string { + return `${runtimeName}.${endpoint}`; +} + +// resolveAgentToRuntime resolves `--agent ` to its underlying runtime id + +// name. A harness is itself implemented as an AgentCore Runtime under the +// hood, so a plain runtime id resolves directly via GetAgentRuntime; a harness +// id 404s there and resolves instead via GetHarness, reading the underlying +// runtime out of `harness.environment.agentCoreRuntimeEnvironment`. Verified +// against real harnesses/runtimes in a live account before relying on it. +async function resolveAgentToRuntime( + agent: string, + clients: AwsClients, + options: CoreOptions, +): Promise<{ runtimeId: string; runtimeName: string }> { + const control = clients.control(toClientConfig(options)); + try { + const runtime = await control.send(new GetAgentRuntimeCommand({ agentRuntimeId: agent })); + if (runtime.agentRuntimeName) { + return { runtimeId: agent, runtimeName: runtime.agentRuntimeName }; + } + } catch (error) { + if ((error as Error).name !== "ResourceNotFoundException") throw error; + } + + const harness = await control.send(new GetHarnessCommand({ harnessId: agent })); + const environment = harness.harness?.environment; + const runtimeEnv = + environment && "agentCoreRuntimeEnvironment" in environment + ? environment.agentCoreRuntimeEnvironment + : undefined; + if (!runtimeEnv?.agentRuntimeId || !runtimeEnv?.agentRuntimeName) { + throw new InputValidationError(`"${agent}" does not exist as a runtime or a harness`, { + meta: { agent }, + }); + } + return { runtimeId: runtimeEnv.agentRuntimeId, runtimeName: runtimeEnv.agentRuntimeName }; +} + +// agentDataSource builds the CloudWatch data source for an agent id, resolving it +// to its underlying runtime first (the log group is keyed by the runtime id, the +// service name by the runtime name). +async function agentDataSource( + agent: string, + endpoint: string | undefined, + clients: AwsClients, + options: CoreOptions, +): Promise { + const qualifier = endpoint ?? DEFAULT_ENDPOINT_QUALIFIER; + const { runtimeId, runtimeName } = await resolveAgentToRuntime(agent, clients, options); + return { + cloudWatchLogs: { + logGroupNames: [runtimeLogGroup(runtimeId, qualifier)], + serviceNames: [runtimeServiceName(runtimeName, qualifier)], + }, + }; +} + +// A just-written role or inline policy is not visible to the service immediately +// (IAM is eventually consistent), and the service validates both when the config +// is created. It surfaces as one of two messages depending on which part has not +// propagated yet. +const ROLE_NOT_PROPAGATED = + /role cannot be assumed|does not have permissions to (create log group|access the specified log groups)/i; + +// retryWhileRolePropagates retries `send` while the service reports the execution +// role as unusable, which is how a not-yet-propagated role or policy surfaces. +// Bounded and short: propagation is normally a few seconds, and a role that is +// genuinely misconfigured should fail fast rather than hang. +async function retryWhileRolePropagates(send: () => Promise): Promise { + const delaysMs = [1_000, 2_000, 4_000, 8_000]; + for (const delay of delaysMs) { + try { + return await send(); + } catch (error) { + if (!ROLE_NOT_PROPAGATED.test((error as Error).message)) throw error; + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + return send(); +} + +// evaluatorKmsKeys collects the customer managed KMS keys of the referenced +// evaluators. The service validates that the execution role can decrypt them when +// the config is created, so a provisioned role has to grant kms:Decrypt on exactly +// these keys. Builtins carry no key, so the common case resolves to nothing. A +// GetEvaluator failure propagates as-is: the SDK's error already names the +// operation and the evaluator, and it is not the caller's input at fault. +async function evaluatorKmsKeys( + evaluatorIds: string[], + control: BedrockAgentCoreControlClient, +): Promise { + const keys = await Promise.all( + evaluatorIds.map(async (evaluatorId) => { + const evaluator = await control.send(new GetEvaluatorCommand({ evaluatorId })); + return evaluator.kmsKeyArn; + }), + ); + return [...new Set(keys.filter((key): key is string => key !== undefined))]; +} + +// logGroupNamesOf reads the log groups out of a resolved dataSourceConfig, for +// scoping the default execution role. cloudWatchLogs is the only arm the API +// defines today; an unrecognized one yields no groups rather than throwing, so a +// future arm degrades to a role the caller can still override with --role-arn. +function logGroupNamesOf(dataSourceConfig: DataSourceConfig): string[] { + return "cloudWatchLogs" in dataSourceConfig + ? (dataSourceConfig.cloudWatchLogs?.logGroupNames ?? []) + : []; +} + +// runtimeIdFromLogGroup recovers the runtime id embedded in a log group path +// produced by runtimeLogGroup, so an update can re-derive dataSourceConfig for a +// new --endpoint without the caller passing --agent again. Returns undefined for +// a path that does not follow the convention, i.e. a config pointed at custom log +// groups, which carries no runtime id to recover. +// +// Splitting on the *last* hyphen is unambiguous: endpoint names are constrained +// to [a-zA-Z][a-zA-Z0-9_]{0,47}, so they never contain one. +function runtimeIdFromLogGroup(logGroupName: string): string | undefined { + const match = logGroupName.match(/^\/aws\/bedrock-agentcore\/runtimes\/(.+)-[^-]+$/); + return match?.[1]; +} + +function toRule( + samplingRate: number | undefined, + sessionTimeoutMinutes: number | undefined, + filters?: Rule["filters"], +): Rule { + return { + samplingConfig: { samplingPercentage: samplingRate }, + // sessionConfig is optional on Rule and the service does not backfill it, so + // omit it when unset rather than materializing the service's own default. + ...(sessionTimeoutMinutes !== undefined ? { sessionConfig: { sessionTimeoutMinutes } } : {}), + filters, + }; } diff --git a/src/core/onlineEvalExecutionRole.test.ts b/src/core/onlineEvalExecutionRole.test.ts new file mode 100644 index 000000000..594de9eac --- /dev/null +++ b/src/core/onlineEvalExecutionRole.test.ts @@ -0,0 +1,86 @@ +import { test, expect } from "bun:test"; +import { + executionPolicy, + onlineEvalExecutionRoleName, + scopePolicyName, +} from "./onlineEvalExecutionRole"; + +const REGION = "us-west-2"; +const ACCOUNT = "123456789012"; +const LOG_GROUPS = ["/aws/bedrock-agentcore/runtimes/orders-agent-abc123-DEFAULT"]; + +function statements(policy: string): { Sid?: string; Action?: unknown; Resource?: unknown }[] { + return JSON.parse(policy).Statement; +} + +// The service validates at create time that the role can decrypt any evaluator +// encrypted with a customer managed key, so the statement has to be present and +// scoped to exactly those keys. +test("grants kms:Decrypt scoped to the referenced evaluator keys", () => { + const keys = [ + "arn:aws:kms:us-west-2:123456789012:key/aaaaaaaa-1111", + "arn:aws:kms:us-west-2:123456789012:key/bbbbbbbb-2222", + ]; + const decrypt = statements(executionPolicy(REGION, ACCOUNT, LOG_GROUPS, keys)).find( + (s) => s.Sid === "DecryptEvaluatorKeys", + ); + + expect(decrypt).toBeDefined(); + expect(decrypt?.Action).toEqual(["kms:Decrypt", "kms:DescribeKey"]); + expect(decrypt?.Resource).toEqual(keys); +}); + +// No wildcard when nothing is encrypted: the builtin evaluators carry no key, so +// the common case must not widen the role. +test("omits the KMS statement when no evaluator is encrypted", () => { + const sids = statements(executionPolicy(REGION, ACCOUNT, LOG_GROUPS, [])).map((s) => s.Sid); + expect(sids).not.toContain("DecryptEvaluatorKeys"); +}); + +// Query access is scoped to the runtime prefix, not one endpoint's log group: the +// service validates at the runtime level and rejects a narrower policy. +test("scopes trace queries to the runtime prefix and aws/spans", () => { + const query = statements(executionPolicy(REGION, ACCOUNT, LOG_GROUPS, [])).find( + (s) => s.Sid === "QuerySampledTraces", + ); + expect(query?.Resource).toEqual([ + `arn:aws:logs:${REGION}:${ACCOUNT}:log-group:aws/spans*`, + `arn:aws:logs:${REGION}:${ACCOUNT}:log-group:/aws/bedrock-agentcore/runtimes/orders-agent-abc123*`, + ]); +}); + +// IAM caps role names at 64 chars. Truncating alone would let two configs share a +// role, and provisioning is idempotent by name, so the second create would +// re-scope the first's policy. +test("keeps role names within 64 characters and distinct", () => { + const a = onlineEvalExecutionRoleName("x".repeat(44) + "AAAA"); + const b = onlineEvalExecutionRoleName("x".repeat(44) + "BBBB"); + expect(a.length).toBeLessThanOrEqual(64); + expect(b.length).toBeLessThanOrEqual(64); + expect(a).not.toBe(b); + expect(onlineEvalExecutionRoleName("short")).toBe("AgentCoreOnlineEval-short"); +}); + +// Each scope must map to its own policy name. Granting a new scope writes a new +// policy rather than overwriting the current one, which is what lets an update +// keep the old scope intact until the config change has landed. +test("gives policies with different contents different names", () => { + const orders = scopePolicyName(executionPolicy(REGION, ACCOUNT, ["/orders*"], [])); + const checkout = scopePolicyName(executionPolicy(REGION, ACCOUNT, ["/checkout*"], [])); + expect(orders).not.toBe(checkout); +}); + +// The name is a fingerprint of the scope, so re-granting an unchanged scope is a +// no-op rewrite of the same policy rather than an accumulating new one — and the +// update path can compare names to know whether anything needs revoking. +test("gives identical policies the same name", () => { + const keys = ["arn:aws:kms:us-west-2:123456789012:key/aaaa"]; + const doc = executionPolicy(REGION, ACCOUNT, ["/a*", "/b*"], keys); + expect(scopePolicyName(doc)).toBe( + scopePolicyName(executionPolicy(REGION, ACCOUNT, ["/a*", "/b*"], keys)), + ); + // A differing KMS key renders a different document, hence a different name. + expect(scopePolicyName(doc)).not.toBe( + scopePolicyName(executionPolicy(REGION, ACCOUNT, ["/a*", "/b*"], [])), + ); +}); diff --git a/src/core/onlineEvalExecutionRole.tsx b/src/core/onlineEvalExecutionRole.tsx new file mode 100644 index 000000000..2d3b6f0e0 --- /dev/null +++ b/src/core/onlineEvalExecutionRole.tsx @@ -0,0 +1,256 @@ +import { + CreateRoleCommand, + DeleteRolePolicyCommand, + GetRoleCommand, + PutRolePolicyCommand, + type IAMClient, +} from "@aws-sdk/client-iam"; + +// Default online-evaluation execution role provisioning, mirroring +// core/executionRole.tsx's pattern for harnesses: CreateOnlineEvaluationConfig +// requires an IAM role the service assumes to read the target CloudWatch log +// group, invoke Bedrock models for LLM-as-a-Judge evaluators, and write +// evaluation results back to CloudWatch. When the caller doesn't bring one, +// OnlineEvalClient provisions a per-config default here, scoped to the log +// group(s) being sampled. Idempotent: an existing role is reused. +// +// Each scope is stored as its own inline policy, named after a fingerprint of the +// scope, so granting a new scope never overwrites the policy backing the current +// one. IAM unions Allows across a role's inline policies, which lets an update +// grant the new scope before changing the config and drop the old scope only once +// the change has landed. + +const POLICY_PREFIX = "AgentCoreOnlineEvalExecutionPolicy"; + +const ROLE_NAME_PREFIX = "AgentCoreOnlineEval-"; +const ROLE_NAME_MAX = 64; +const NAME_HASH_LENGTH = 8; + +// onlineEvalExecutionRoleName derives the default role's name from the online eval +// config name. IAM caps role names at 64 characters, which leaves only 44 for the +// config name — while config names run to 100 — so a name that would overflow is +// truncated and given a hash suffix. Truncating alone would let two configs share +// one role, and because provisioning is idempotent by name the second create would +// silently re-scope the first's policy to a different runtime. +export function onlineEvalExecutionRoleName(configName: string): string { + const full = `${ROLE_NAME_PREFIX}${configName}`; + if (full.length <= ROLE_NAME_MAX) return full; + + const hash = Bun.hash(configName) + .toString(16) + .padStart(NAME_HASH_LENGTH, "0") + .slice(-NAME_HASH_LENGTH); + const room = ROLE_NAME_MAX - ROLE_NAME_PREFIX.length - NAME_HASH_LENGTH - 1; + return `${ROLE_NAME_PREFIX}${configName.slice(0, room)}-${hash}`; +} + +function trustPolicy(): string { + return JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: { Service: "bedrock-agentcore.amazonaws.com" }, + Action: "sts:AssumeRole", + }, + ], + }); +} + +// runtimeLogGroupPrefix strips the trailing `-` qualifier from an +// AgentCore runtime log group name, yielding the runtime-level prefix the +// service expects the execution role to be scoped to. A log group that does not +// follow the runtime naming convention (e.g. a caller-supplied custom group) is +// returned unchanged. +function runtimeLogGroupPrefix(logGroupName: string): string { + const match = logGroupName.match(/^(\/aws\/bedrock-agentcore\/runtimes\/.+)-[^-]+$/); + return match?.[1] ?? logGroupName; +} + +// executionPolicy grants the permissions CreateOnlineEvaluationConfig validates +// at creation time. Exported for assertion: the policy body is not observable +// through the recorded IAM fixtures, whose responses are empty. +// +// at creation time: Logs Insights query access over the sampled log groups plus +// the `aws/spans` group that carries the actual trace spans, Bedrock model +// invocation for LLM-as-a-Judge evaluators, Lambda invocation for code-based +// ones, and permission to write results back to CloudWatch. Modeled on the +// policy the CDK-deployed online evaluations use, since the service rejects a +// role that cannot query the log groups it was pointed at. +export function executionPolicy( + region: string, + accountId: string, + logGroupNames: string[], + kmsKeyArns: string[], +): string { + const logs = `arn:aws:logs:${region}:${accountId}:log-group`; + const spansArn = `${logs}:aws/spans`; + // Scope to the runtime prefix rather than the exact endpoint log group: the + // service validates query access at the runtime level (all of a runtime's + // endpoints share the `...--` naming), and a policy + // pinned to one endpoint is rejected as insufficient. + const sampledArns = logGroupNames.map((name) => `${logs}:${runtimeLogGroupPrefix(name)}*`); + return JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Sid: "DiscoverLogGroups", + Effect: "Allow", + Action: [ + "cloudwatch:GenerateQuery", + "cloudwatch:GenerateQueryResultsSummary", + "logs:DescribeLogGroups", + ], + Resource: "*", + }, + { + // Spans live in `aws/spans`; the runtime's own log group carries the + // session logs. Both are queried when sampling sessions. + Sid: "QuerySampledTraces", + Effect: "Allow", + Action: [ + "logs:DescribeLogStreams", + "logs:FilterLogEvents", + "logs:GetLogEvents", + "logs:GetQueryResults", + "logs:StartQuery", + ], + Resource: [`${spansArn}*`, ...sampledArns], + }, + { + Sid: "WriteEvaluationResults", + Effect: "Allow", + Action: [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + ], + Resource: `${logs}:/aws/bedrock-agentcore/evaluations/*`, + }, + { + Sid: "IndexSpans", + Effect: "Allow", + Action: ["logs:DescribeIndexPolicies", "logs:PutIndexPolicy"], + Resource: spansArn, + }, + { + Sid: "BedrockModelInvocation", + Effect: "Allow", + Action: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], + Resource: [ + "arn:aws:bedrock:*::foundation-model/*", + `arn:aws:bedrock:${region}:${accountId}:inference-profile/*`, + ], + }, + { + // Code-based evaluators are Lambda-backed, so the role that runs an + // online evaluation must be able to invoke them. + Sid: "InvokeCodeBasedEvaluators", + Effect: "Allow", + Action: ["lambda:GetFunction", "lambda:InvokeFunction"], + Resource: `arn:aws:lambda:${region}:${accountId}:function:*`, + }, + // Evaluators encrypted with a customer managed key need kms:Decrypt on that + // key, which the service validates when the config is created. Scoped to the + // referenced keys, and omitted when no evaluator is encrypted. + ...(kmsKeyArns.length > 0 + ? [ + { + Sid: "DecryptEvaluatorKeys", + Effect: "Allow", + Action: ["kms:Decrypt", "kms:DescribeKey"], + Resource: kmsKeyArns, + }, + ] + : []), + ], + }); +} + +export function accountIdFromRoleArn(arn: string): string { + const accountId = arn.split(":")[4]; + if (!accountId) { + throw new Error(`Cannot extract an account id from role ARN "${arn}"`); + } + return accountId; +} + +// scopePolicyName derives the inline-policy name from a fingerprint of the whole +// rendered policy document. Keying the name on the policy's exact contents means +// any change to what the policy grants yields a new name, so writing one scope's +// policy can never clobber another's — a superseded scope stays intact until it +// is explicitly revoked. +export function scopePolicyName(policyDocument: string): string { + const fingerprint = Bun.hash(policyDocument) + .toString(16) + .padStart(NAME_HASH_LENGTH, "0") + .slice(-NAME_HASH_LENGTH); + return `${POLICY_PREFIX}-${fingerprint}`; +} + +// grantOnlineEvalScope creates the execution role for `configName` if it does not +// exist and attaches the inline policy for this scope, returning the role ARN and +// the policy name written. The caller revokes the superseded scope once whatever +// change prompted the new one has succeeded. +export async function grantOnlineEvalScope( + iam: IAMClient, + configName: string, + region: string, + logGroupNames: string[], + kmsKeyArns: string[] = [], +): Promise<{ roleArn: string; policyName: string }> { + const roleName = onlineEvalExecutionRoleName(configName); + + let roleArn: string; + try { + const existing = await iam.send(new GetRoleCommand({ RoleName: roleName })); + roleArn = existing.Role!.Arn!; + } catch (error) { + if ((error as Error).name !== "NoSuchEntityException") throw error; + const created = await iam.send( + new CreateRoleCommand({ + RoleName: roleName, + AssumeRolePolicyDocument: trustPolicy(), + Description: `Default execution role for the AgentCore online evaluation config "${configName}" (created by the agentcore CLI)`, + }), + ); + roleArn = created.Role!.Arn!; + } + + const policyDocument = executionPolicy( + region, + accountIdFromRoleArn(roleArn), + logGroupNames, + kmsKeyArns, + ); + const policyName = scopePolicyName(policyDocument); + await iam.send( + new PutRolePolicyCommand({ + RoleName: roleName, + PolicyName: policyName, + PolicyDocument: policyDocument, + }), + ); + + return { roleArn, policyName }; +} + +// revokeOnlineEvalScope detaches a scope's inline policy, dropping the access it +// granted. A scope that is already absent is treated as revoked. +export async function revokeOnlineEvalScope( + iam: IAMClient, + configName: string, + policyName: string, +): Promise { + try { + await iam.send( + new DeleteRolePolicyCommand({ + RoleName: onlineEvalExecutionRoleName(configName), + PolicyName: policyName, + }), + ); + } catch (error) { + if ((error as Error).name !== "NoSuchEntityException") throw error; + } +} diff --git a/src/handlers/eval/index.tsx b/src/handlers/eval/index.tsx index 788a92b43..1b14b201d 100644 --- a/src/handlers/eval/index.tsx +++ b/src/handlers/eval/index.tsx @@ -3,9 +3,11 @@ import type { AppIO } from "../../io"; import type { Core } from "../types"; import { createHelpDefault } from "../help"; import { createEvaluatorHandler } from "./evaluator"; +import { createOnlineEvalHandler } from "./online-eval"; export function createEvalHandler(core: Core, io: AppIO): Router { return new Router("eval", "evaluate and optimize AgentCore agents") .default(createHelpDefault(io)) - .handler(createEvaluatorHandler(core, io)); + .handler(createEvaluatorHandler(core, io)) + .handler(createOnlineEvalHandler(core, io)); } diff --git a/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.5e70e8cf174a7bc6.json b/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.5e70e8cf174a7bc6.json new file mode 100644 index 000000000..9a38e2ba6 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.5e70e8cf174a7bc6.json @@ -0,0 +1,14 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "createdAt": { + "$date": "2026-08-03T21:29:57.112Z" + }, + "status": "CREATING", + "executionStatus": "DISABLED", + "outputConfig": { + "cloudWatchConfig": { + "logGroupName": "/aws/bedrock-agentcore/evaluations/results/agentcore_cli_online_eval_role_warn-2KNTGuGVDl" + } + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.6b79e5be866896d6.json b/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.6b79e5be866896d6.json new file mode 100644 index 000000000..53629d6dd --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.6b79e5be866896d6.json @@ -0,0 +1,14 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_kms-bG4DUW3Ua5", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_kms-bG4DUW3Ua5", + "createdAt": { + "$date": "2026-08-03T21:29:51.133Z" + }, + "status": "CREATING", + "executionStatus": "DISABLED", + "outputConfig": { + "cloudWatchConfig": { + "logGroupName": "/aws/bedrock-agentcore/evaluations/results/agentcore_cli_online_eval_kms-bG4DUW3Ua5" + } + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.bdca68c635390f67.json b/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.bdca68c635390f67.json new file mode 100644 index 000000000..c4f767ed4 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/CreateOnlineEvaluationConfigCommand.bdca68c635390f67.json @@ -0,0 +1,14 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "createdAt": { + "$date": "2026-08-03T21:29:43.069Z" + }, + "status": "CREATING", + "executionStatus": "DISABLED", + "outputConfig": { + "cloudWatchConfig": { + "logGroupName": "/aws/bedrock-agentcore/evaluations/results/agentcore_cli_online_eval_fixture-vYkaD93sFk" + } + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.77430112d0720f6.json b/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.77430112d0720f6.json new file mode 100644 index 000000000..420108a61 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.77430112d0720f6.json @@ -0,0 +1,5 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_kms-bG4DUW3Ua5", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_kms-bG4DUW3Ua5", + "status": "DELETING" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.a952ff6787c06557.json b/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.a952ff6787c06557.json new file mode 100644 index 000000000..da16e6717 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.a952ff6787c06557.json @@ -0,0 +1,5 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "status": "DELETING" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.fa01de9ac60dc685.json b/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.fa01de9ac60dc685.json new file mode 100644 index 000000000..2feaa4398 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/DeleteOnlineEvaluationConfigCommand.fa01de9ac60dc685.json @@ -0,0 +1,5 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "status": "DELETING" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetAgentRuntimeCommand.80bb567edcaa4bb1.json b/src/handlers/eval/online-eval/__fixtures__/GetAgentRuntimeCommand.80bb567edcaa4bb1.json new file mode 100644 index 000000000..5e8b39035 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetAgentRuntimeCommand.80bb567edcaa4bb1.json @@ -0,0 +1,46 @@ +{ + "agentRuntimeArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:runtime/testAgent_Agent-wm9hYBD93Y", + "agentRuntimeName": "testAgent_Agent", + "agentRuntimeId": "testAgent_Agent-wm9hYBD93Y", + "agentRuntimeVersion": "3", + "createdAt": { + "$date": "2026-03-24T16:02:00.301Z" + }, + "lastUpdatedAt": { + "$date": "2026-03-24T16:13:08.198Z" + }, + "roleArn": "arn:aws:iam::725476964917:role/AgentCore-myimport-defaul-ApplicationAgentTestAgent-tJFjyd6jLIOn", + "networkConfiguration": { + "networkMode": "PUBLIC" + }, + "status": "READY", + "lifecycleConfiguration": { + "idleRuntimeSessionTimeout": 900, + "maxLifetime": 28800 + }, + "description": "AgentCore Runtime: myimport_testAgent_Agent", + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:workload-identity-directory/default/workload-identity/testAgent_Agent-wm9hYBD93Y" + }, + "agentRuntimeArtifact": { + "codeConfiguration": { + "code": { + "s3": { + "bucket": "cdk-hnb659fds-assets-725476964917-us-west-2", + "prefix": "5f58024b207272eeb8560702468b402cb3aeea1c8ee36d3c5f348fcb471bb58f.zip" + } + }, + "runtime": "PYTHON_3_10", + "entryPoint": [ + "opentelemetry-instrument", + "main.py" + ] + } + }, + "environmentVariables": { + "MEMORY_TESTAGENT_AGENT_MEM_ID": "testAgent_Agent_mem-17a3Lg8yrL" + }, + "metadataConfiguration": { + "requireMMDSV2": true + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetAgentRuntimeCommand.d729967662bebc9f.json b/src/handlers/eval/online-eval/__fixtures__/GetAgentRuntimeCommand.d729967662bebc9f.json new file mode 100644 index 000000000..46ce84be3 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetAgentRuntimeCommand.d729967662bebc9f.json @@ -0,0 +1,47 @@ +{ + "agentRuntimeArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:runtime/ABVfyLatest_ABVfyLatest-PFLr353QVA", + "agentRuntimeName": "ABVfyLatest_ABVfyLatest", + "agentRuntimeId": "ABVfyLatest_ABVfyLatest-PFLr353QVA", + "agentRuntimeVersion": "2", + "createdAt": { + "$date": "2026-06-17T22:07:40.934Z" + }, + "lastUpdatedAt": { + "$date": "2026-06-17T22:09:21.093Z" + }, + "roleArn": "arn:aws:iam::725476964917:role/AgentCore-ABVfyLatest-def-ApplicationAgentABVfyLate-b6b570G88FJ3", + "networkConfiguration": { + "networkMode": "PUBLIC" + }, + "status": "READY", + "lifecycleConfiguration": { + "idleRuntimeSessionTimeout": 900, + "maxLifetime": 28800 + }, + "description": "AgentCore Runtime: ABVfyLatest_ABVfyLatest", + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:workload-identity-directory/default/workload-identity/ABVfyLatest_ABVfyLatest-PFLr353QVA" + }, + "agentRuntimeArtifact": { + "codeConfiguration": { + "code": { + "s3": { + "bucket": "cdk-hnb659fds-assets-725476964917-us-west-2", + "prefix": "0dbb193c5cb61825fc348e0c1061726313e0f66c20bd82bc462e3e21daed2a38.zip" + } + }, + "runtime": "PYTHON_3_14", + "entryPoint": [ + "opentelemetry-instrument", + "main.py" + ] + } + }, + "environmentVariables": { + "AGENTCORE_GATEWAY_ABGATEWAY_AUTH_TYPE": "NONE", + "AGENTCORE_GATEWAY_ABGATEWAY_URL": "https://abvfylatest-abgateway-t4w4fdbovi.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp" + }, + "metadataConfiguration": { + "requireMMDSV2": true + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetEvaluatorCommand.5ee939ef1fda1ca7.json b/src/handlers/eval/online-eval/__fixtures__/GetEvaluatorCommand.5ee939ef1fda1ca7.json new file mode 100644 index 000000000..47000de38 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetEvaluatorCommand.5ee939ef1fda1ca7.json @@ -0,0 +1,14 @@ +{ + "evaluatorArn": "arn:aws:bedrock-agentcore:::evaluator/Builtin.Correctness", + "evaluatorId": "Builtin.Correctness", + "evaluatorName": "Builtin.Correctness", + "level": "TRACE", + "status": "ACTIVE", + "createdAt": { + "$date": "2024-10-22T00:00:00.000Z" + }, + "updatedAt": { + "$date": "2024-10-22T00:00:00.000Z" + }, + "kmsKeyArn": "arn:aws:kms:us-west-2:725476964917:key/31a2dd2f-c8a0-42b8-9f52-12e5ecb22468" +} diff --git a/src/handlers/eval/online-eval/__fixtures__/GetEvaluatorCommand.716589b0884f35c0.json b/src/handlers/eval/online-eval/__fixtures__/GetEvaluatorCommand.716589b0884f35c0.json new file mode 100644 index 000000000..1825df228 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetEvaluatorCommand.716589b0884f35c0.json @@ -0,0 +1,51 @@ +{ + "evaluatorArn": "arn:aws:bedrock-agentcore:::evaluator/Builtin.Helpfulness", + "evaluatorId": "Builtin.Helpfulness", + "evaluatorName": "Builtin.Helpfulness", + "evaluatorConfig": { + "llmAsAJudge": { + "ratingScale": { + "numerical": [ + { + "value": 0, + "label": "Not helpful at all" + }, + { + "value": 1, + "label": "Very unhelpful" + }, + { + "value": 2, + "label": "Somewhat unhelpful" + }, + { + "value": 3, + "label": "Neutral/Mixed" + }, + { + "value": 4, + "label": "Somewhat helpful" + }, + { + "value": 5, + "label": "Very helpful" + }, + { + "value": 6, + "label": "Above and beyond" + } + ] + } + } + }, + "level": "TRACE", + "status": "ACTIVE", + "createdAt": { + "$date": "2024-10-22T00:00:00.000Z" + }, + "updatedAt": { + "$date": "2024-10-22T00:00:00.000Z" + }, + "description": "Response Quality Metric. Evaluates from user's perspective how useful and valuable the agent's response is", + "lockedForModification": true +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.77430112d0720f6.json b/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.77430112d0720f6.json new file mode 100644 index 000000000..cbb739904 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.77430112d0720f6.json @@ -0,0 +1,42 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_kms-bG4DUW3Ua5", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_kms-bG4DUW3Ua5", + "onlineEvaluationConfigName": "agentcore_cli_online_eval_kms", + "rule": { + "samplingConfig": { + "samplingPercentage": 10 + } + }, + "dataSourceConfig": { + "cloudWatchLogs": { + "logGroupNames": [ + "/aws/bedrock-agentcore/runtimes/testAgent_Agent-wm9hYBD93Y-DEFAULT" + ], + "serviceNames": [ + "testAgent_Agent.DEFAULT" + ] + } + }, + "status": "ACTIVE", + "executionStatus": "DISABLED", + "createdAt": { + "$date": "2026-08-03T21:29:51.133Z" + }, + "updatedAt": { + "$date": "2026-08-03T21:29:51.288Z" + }, + "evaluators": [ + { + "evaluatorId": "Builtin.Helpfulness" + }, + { + "evaluatorId": "Builtin.Correctness" + } + ], + "outputConfig": { + "cloudWatchConfig": { + "logGroupName": "/aws/bedrock-agentcore/evaluations/results/agentcore_cli_online_eval_kms-bG4DUW3Ua5" + } + }, + "evaluationExecutionRoleArn": "arn:aws:iam::725476964917:role/AgentCoreOnlineEval-agentcore_cli_online_eval_kms" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.859097834b7e85cf.json b/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.859097834b7e85cf.json new file mode 100644 index 000000000..845409c3b --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.859097834b7e85cf.json @@ -0,0 +1,6 @@ +{ + "$error": { + "name": "ResourceNotFoundException", + "message": "Online evaluation configuration not found" + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.a952ff6787c06557.json b/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.a952ff6787c06557.json new file mode 100644 index 000000000..5355a73df --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.a952ff6787c06557.json @@ -0,0 +1,42 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigName": "agentcore_cli_online_eval_fixture", + "rule": { + "samplingConfig": { + "samplingPercentage": 25 + }, + "sessionConfig": { + "sessionTimeoutMinutes": 30 + } + }, + "dataSourceConfig": { + "cloudWatchLogs": { + "logGroupNames": [ + "/aws/bedrock-agentcore/runtimes/testAgent_Agent-wm9hYBD93Y-DEFAULT" + ], + "serviceNames": [ + "testAgent_Agent.DEFAULT" + ] + } + }, + "status": "ACTIVE", + "executionStatus": "DISABLED", + "createdAt": { + "$date": "2026-08-03T21:29:43.069Z" + }, + "updatedAt": { + "$date": "2026-08-03T21:29:44.358Z" + }, + "evaluators": [ + { + "evaluatorId": "Builtin.Helpfulness" + } + ], + "outputConfig": { + "cloudWatchConfig": { + "logGroupName": "/aws/bedrock-agentcore/evaluations/results/agentcore_cli_online_eval_fixture-vYkaD93sFk" + } + }, + "evaluationExecutionRoleArn": "arn:aws:iam::725476964917:role/AgentCoreOnlineEval-agentcore_cli_online_eval_fixture" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.fa01de9ac60dc685.json b/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.fa01de9ac60dc685.json new file mode 100644 index 000000000..791fac7d0 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetOnlineEvaluationConfigCommand.fa01de9ac60dc685.json @@ -0,0 +1,39 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "onlineEvaluationConfigName": "agentcore_cli_online_eval_role_warn", + "rule": { + "samplingConfig": { + "samplingPercentage": 10 + } + }, + "dataSourceConfig": { + "cloudWatchLogs": { + "logGroupNames": [ + "/aws/bedrock-agentcore/runtimes/ABVfyLatest_ABVfyLatest-PFLr353QVA-DEFAULT" + ], + "serviceNames": [ + "ABVfyLatest_ABVfyLatest.DEFAULT" + ] + } + }, + "status": "ACTIVE", + "executionStatus": "DISABLED", + "createdAt": { + "$date": "2026-08-03T21:29:57.112Z" + }, + "updatedAt": { + "$date": "2026-08-03T21:30:02.741Z" + }, + "evaluators": [ + { + "evaluatorId": "Builtin.Helpfulness" + } + ], + "outputConfig": { + "cloudWatchConfig": { + "logGroupName": "/aws/bedrock-agentcore/evaluations/results/agentcore_cli_online_eval_role_warn-2KNTGuGVDl" + } + }, + "evaluationExecutionRoleArn": "arn:aws:iam::725476964917:role/AgentCoreEvalsSDK-us-west-2-a6864eb339" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetRoleCommand.1ba024c017a5c69d.json b/src/handlers/eval/online-eval/__fixtures__/GetRoleCommand.1ba024c017a5c69d.json new file mode 100644 index 000000000..6f9666e79 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetRoleCommand.1ba024c017a5c69d.json @@ -0,0 +1,20 @@ +{ + "Role": { + "Path": "/", + "RoleName": "AgentCoreOnlineEval-agentcore_cli_online_eval_kms", + "RoleId": "AROA2R2OL7I2VZ4GXJX3B", + "Arn": "arn:aws:iam::725476964917:role/AgentCoreOnlineEval-agentcore_cli_online_eval_kms", + "CreateDate": { + "$date": "2026-08-03T15:40:00.000Z" + }, + "AssumeRolePolicyDocument": "%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22bedrock-agentcore.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D", + "Description": "Default execution role for the AgentCore online evaluation config \"agentcore_cli_online_eval_kms\" (created by the agentcore CLI)", + "MaxSessionDuration": 3600, + "RoleLastUsed": { + "LastUsedDate": { + "$date": "2026-08-03T19:57:02.000Z" + }, + "Region": "us-west-2" + } + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/GetRoleCommand.f532eb8675abbed5.json b/src/handlers/eval/online-eval/__fixtures__/GetRoleCommand.f532eb8675abbed5.json new file mode 100644 index 000000000..a9222a5c0 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/GetRoleCommand.f532eb8675abbed5.json @@ -0,0 +1,20 @@ +{ + "Role": { + "Path": "/", + "RoleName": "AgentCoreOnlineEval-agentcore_cli_online_eval_fixture", + "RoleId": "AROA2R2OL7I2UHCNEMCPZ", + "Arn": "arn:aws:iam::725476964917:role/AgentCoreOnlineEval-agentcore_cli_online_eval_fixture", + "CreateDate": { + "$date": "2026-07-30T22:23:38.000Z" + }, + "AssumeRolePolicyDocument": "%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22bedrock-agentcore.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D", + "Description": "Default execution role for the AgentCore online evaluation config \"agentcore_cli_online_eval_fixture\" (created by the agentcore CLI)", + "MaxSessionDuration": 3600, + "RoleLastUsed": { + "LastUsedDate": { + "$date": "2026-08-03T19:56:55.000Z" + }, + "Region": "us-west-2" + } + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.23f97c9dcdd6350b.json b/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.23f97c9dcdd6350b.json new file mode 100644 index 000000000..d56c1eb32 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.23f97c9dcdd6350b.json @@ -0,0 +1,442 @@ +{ + "onlineEvaluationConfigs": [ + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyLatest_ProdEval-2vqlCb2UiG", + "onlineEvaluationConfigId": "ABVfyLatest_ProdEval-2vqlCb2UiG", + "onlineEvaluationConfigName": "ABVfyLatest_ProdEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-17T22:09:59.115Z" + }, + "updatedAt": { + "$date": "2026-06-17T22:10:11.792Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyLatest_StagingEval-4utSyp3pE9", + "onlineEvaluationConfigId": "ABVfyLatest_StagingEval-4utSyp3pE9", + "onlineEvaluationConfigName": "ABVfyLatest_StagingEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-17T22:09:59.058Z" + }, + "updatedAt": { + "$date": "2026-06-17T22:10:12.095Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyPrerelease_ProdEval-MtuOYB2GZP", + "onlineEvaluationConfigId": "ABVfyPrerelease_ProdEval-MtuOYB2GZP", + "onlineEvaluationConfigName": "ABVfyPrerelease_ProdEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-17T20:28:10.738Z" + }, + "updatedAt": { + "$date": "2026-06-17T20:28:26.527Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyPrerelease_StagingEval-fH26hNBHL6", + "onlineEvaluationConfigId": "ABVfyPrerelease_StagingEval-fH26hNBHL6", + "onlineEvaluationConfigName": "ABVfyPrerelease_StagingEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-17T20:28:09.163Z" + }, + "updatedAt": { + "$date": "2026-06-17T20:28:26.799Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/EvoBugBash_abOnlineEval-Q0Iv1k2cdo", + "onlineEvaluationConfigId": "EvoBugBash_abOnlineEval-Q0Iv1k2cdo", + "onlineEvaluationConfigName": "EvoBugBash_abOnlineEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-12T22:27:27.466Z" + }, + "updatedAt": { + "$date": "2026-06-12T22:27:40.636Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/GwFilterVfy_ctrlEval-T97o5LAtRS", + "onlineEvaluationConfigId": "GwFilterVfy_ctrlEval-T97o5LAtRS", + "onlineEvaluationConfigName": "GwFilterVfy_ctrlEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T18:27:10.496Z" + }, + "updatedAt": { + "$date": "2026-06-15T18:27:26.982Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/GwFilterVfy_treatEval-bTv4u14rUQ", + "onlineEvaluationConfigId": "GwFilterVfy_treatEval-bTv4u14rUQ", + "onlineEvaluationConfigName": "GwFilterVfy_treatEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T18:27:10.370Z" + }, + "updatedAt": { + "$date": "2026-06-15T18:27:27.240Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/OhioTripPlanner_ProdEvalStagin-8wGIBCFzOE", + "onlineEvaluationConfigId": "OhioTripPlanner_ProdEvalStagin-8wGIBCFzOE", + "onlineEvaluationConfigName": "OhioTripPlanner_ProdEvalStagin", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-16T00:09:44.834Z" + }, + "updatedAt": { + "$date": "2026-06-16T00:09:56.722Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/OhioTripPlanner_StagingOnly-A3o7OsFV5a", + "onlineEvaluationConfigId": "OhioTripPlanner_StagingOnly-A3o7OsFV5a", + "onlineEvaluationConfigName": "OhioTripPlanner_StagingOnly", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-16T00:22:22.611Z" + }, + "updatedAt": { + "$date": "2026-06-16T00:22:44.675Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeBundle-UekzTY3B12", + "onlineEvaluationConfigId": "PromoteT2_oeBundle-UekzTY3B12", + "onlineEvaluationConfigName": "PromoteT2_oeBundle", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T17:06:17.370Z" + }, + "updatedAt": { + "$date": "2026-06-15T17:06:45.741Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeCtrl-Vwz1F69rtu", + "onlineEvaluationConfigId": "PromoteT2_oeCtrl-Vwz1F69rtu", + "onlineEvaluationConfigName": "PromoteT2_oeCtrl", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T17:06:17.504Z" + }, + "updatedAt": { + "$date": "2026-06-15T17:06:44.609Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeT2c-2CHeFHBYfA", + "onlineEvaluationConfigId": "PromoteT2_oeT2c-2CHeFHBYfA", + "onlineEvaluationConfigName": "PromoteT2_oeT2c", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T17:06:17.603Z" + }, + "updatedAt": { + "$date": "2026-06-15T17:06:45.174Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeT2t-pKLwdZDXyw", + "onlineEvaluationConfigId": "PromoteT2_oeT2t-pKLwdZDXyw", + "onlineEvaluationConfigName": "PromoteT2_oeT2t", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T17:06:17.784Z" + }, + "updatedAt": { + "$date": "2026-06-15T17:06:45.466Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeTreat-bC8FNM8MEN", + "onlineEvaluationConfigId": "PromoteT2_oeTreat-bC8FNM8MEN", + "onlineEvaluationConfigName": "PromoteT2_oeTreat", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T17:06:24.185Z" + }, + "updatedAt": { + "$date": "2026-06-15T17:06:44.903Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/TEMPAGENTTJAB_MyOnlineEval_prod-n6GVQ2Atnw", + "onlineEvaluationConfigId": "TEMPAGENTTJAB_MyOnlineEval_prod-n6GVQ2Atnw", + "onlineEvaluationConfigName": "TEMPAGENTTJAB_MyOnlineEval_prod", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-05-05T19:22:26.291Z" + }, + "updatedAt": { + "$date": "2026-05-05T19:22:57.162Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/TEMPAGENTTJAB_MyOnlineEval_staging-vPcBdKCji0", + "onlineEvaluationConfigId": "TEMPAGENTTJAB_MyOnlineEval_staging-vPcBdKCji0", + "onlineEvaluationConfigName": "TEMPAGENTTJAB_MyOnlineEval_staging", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-05-05T19:22:33.950Z" + }, + "updatedAt": { + "$date": "2026-05-05T19:22:57.441Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/abdemo_evalconfig-ntDb6Q5PLh", + "onlineEvaluationConfigId": "abdemo_evalconfig-ntDb6Q5PLh", + "onlineEvaluationConfigName": "abdemo_evalconfig", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-05-27T22:10:42.270Z" + }, + "updatedAt": { + "$date": "2026-05-27T22:10:51.516Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/abtestdemo_eval_config-Kp7uUyBqZB", + "onlineEvaluationConfigId": "abtestdemo_eval_config-Kp7uUyBqZB", + "onlineEvaluationConfigName": "abtestdemo_eval_config", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-05-26T19:13:52.461Z" + }, + "updatedAt": { + "$date": "2026-05-26T19:14:02.417Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/abtestval_cs_control_eval-seHjGZ7TeH", + "onlineEvaluationConfigId": "abtestval_cs_control_eval-seHjGZ7TeH", + "onlineEvaluationConfigName": "abtestval_cs_control_eval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-24T22:58:24.637Z" + }, + "updatedAt": { + "$date": "2026-06-24T22:58:38.138Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/abtestval_cs_treatment_eval-AdSjkL4CHu", + "onlineEvaluationConfigId": "abtestval_cs_treatment_eval-AdSjkL4CHu", + "onlineEvaluationConfigName": "abtestval_cs_treatment_eval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-24T22:58:24.648Z" + }, + "updatedAt": { + "$date": "2026-06-24T22:58:38.393Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ac570c_myonlineeval-6IRBzU3dkS", + "onlineEvaluationConfigId": "ac570c_myonlineeval-6IRBzU3dkS", + "onlineEvaluationConfigName": "ac570c_myonlineeval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-07-06T18:21:13.033Z" + }, + "updatedAt": { + "$date": "2026-07-06T18:21:28.312Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigName": "agentcore_cli_online_eval_fixture", + "status": "CREATING", + "executionStatus": "DISABLED", + "createdAt": { + "$date": "2026-08-03T21:29:43.069Z" + }, + "updatedAt": { + "$date": "2026-08-03T21:29:43.069Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/bugbashagent_abtesteval-53zYJX8x4X", + "onlineEvaluationConfigId": "bugbashagent_abtesteval-53zYJX8x4X", + "onlineEvaluationConfigName": "bugbashagent_abtesteval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T18:19:55.627Z" + }, + "updatedAt": { + "$date": "2026-06-15T18:20:08.431Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/bugbashagent_lambda_ctrl_eval-EY3GWNCDgH", + "onlineEvaluationConfigId": "bugbashagent_lambda_ctrl_eval-EY3GWNCDgH", + "onlineEvaluationConfigName": "bugbashagent_lambda_ctrl_eval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T19:41:54.132Z" + }, + "updatedAt": { + "$date": "2026-06-15T19:42:10.231Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/bugbashagent_lambda_trtm_eval-IlN5S34Bc5", + "onlineEvaluationConfigId": "bugbashagent_lambda_trtm_eval-IlN5S34Bc5", + "onlineEvaluationConfigName": "bugbashagent_lambda_trtm_eval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-15T19:41:54.111Z" + }, + "updatedAt": { + "$date": "2026-06-15T19:42:10.335Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/demoEval_onlineEval1-QErY9BEWT5", + "onlineEvaluationConfigId": "demoEval_onlineEval1-QErY9BEWT5", + "onlineEvaluationConfigName": "demoEval_onlineEval1", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-07-16T17:26:21.325Z" + }, + "updatedAt": { + "$date": "2026-07-16T17:26:36.933Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/demoEval_onlineEvalBoto3_20260716180853-tbD9Ob7VUX", + "onlineEvaluationConfigId": "demoEval_onlineEvalBoto3_20260716180853-tbD9Ob7VUX", + "onlineEvaluationConfigName": "demoEval_onlineEvalBoto3_20260716180853", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-07-16T18:08:54.221Z" + }, + "updatedAt": { + "$date": "2026-07-16T18:08:54.477Z" + }, + "description": "boto3-created: onlineEvalBoto3" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/demoEval_onlineInsights1-QE6As2GTTB", + "onlineEvaluationConfigId": "demoEval_onlineInsights1-QE6As2GTTB", + "onlineEvaluationConfigName": "demoEval_onlineInsights1", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-07-16T17:26:21.625Z" + }, + "updatedAt": { + "$date": "2026-07-16T17:26:37.218Z" + }, + "insights": [ + { + "insightId": "Builtin.Insight.FailureAnalysis" + } + ], + "clusteringConfig": { + "frequencies": [ + "DAILY" + ] + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/demoEval_onlineInsightsBoto3_20260716180854-aTLOAI48Jr", + "onlineEvaluationConfigId": "demoEval_onlineInsightsBoto3_20260716180854-aTLOAI48Jr", + "onlineEvaluationConfigName": "demoEval_onlineInsightsBoto3_20260716180854", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-07-16T18:08:54.762Z" + }, + "updatedAt": { + "$date": "2026-07-16T18:08:54.954Z" + }, + "description": "boto3-created: onlineInsightsBoto3", + "insights": [ + { + "insightId": "Builtin.Insight.UserIntent" + } + ], + "clusteringConfig": { + "frequencies": [ + "DAILY" + ] + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/deploytest_myeval-JMv0ihHd30", + "onlineEvaluationConfigId": "deploytest_myeval-JMv0ihHd30", + "onlineEvaluationConfigName": "deploytest_myeval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-04-21T22:47:43.896Z" + }, + "updatedAt": { + "$date": "2026-04-21T22:47:52.237Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/temp-5Dz9Ox6yMC", + "onlineEvaluationConfigId": "temp-5Dz9Ox6yMC", + "onlineEvaluationConfigName": "temp", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2025-12-02T21:09:17.495Z" + }, + "updatedAt": { + "$date": "2025-12-02T21:09:17.831Z" + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/tjHarnessDerivTest-8rZzgXEHiM", + "onlineEvaluationConfigId": "tjHarnessDerivTest-8rZzgXEHiM", + "onlineEvaluationConfigName": "tjHarnessDerivTest", + "status": "ACTIVE", + "executionStatus": "DISABLED", + "createdAt": { + "$date": "2026-07-27T17:56:04.618Z" + }, + "updatedAt": { + "$date": "2026-07-27T17:56:04.805Z" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.7d2e22c637f6b633.json b/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.7d2e22c637f6b633.json new file mode 100644 index 000000000..7001a2c94 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.7d2e22c637f6b633.json @@ -0,0 +1,18 @@ +{ + "onlineEvaluationConfigs": [ + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyLatest_ProdEval-2vqlCb2UiG", + "onlineEvaluationConfigId": "ABVfyLatest_ProdEval-2vqlCb2UiG", + "onlineEvaluationConfigName": "ABVfyLatest_ProdEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-17T22:09:59.115Z" + }, + "updatedAt": { + "$date": "2026-06-17T22:10:11.792Z" + } + } + ], + "nextToken": "AQICAHg005/7fkIBR7tGFhppNaSDRXSe8ajCw8sa0ZnYTqa1SAHs3Tc/JEuHsHfRfV7OwMsqAAABKjCCASYGCSqGSIb3DQEHBqCCARcwggETAgEAMIIBDAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAxvGMxcYVwGUaMn490CARCAgd6Grki8j/DTGYrCjoz0pPponIAeCwB4DOt6kzm9O+CUSYZRSXfx2z9ik6NeWb7yunR708ju0A0Ub4UZP+cJzOIh1MfdU79KTB5BOad/OwSGPMEMLQ9yKvXLUhXwA4p8fiRkO6fk49N+foUK54IBCF+B/mLuG2S1a/8rNByu0qD5xA/oRv2GNJdOnf+8bS8jNEiyQyEhiQR7e6dNO8karHz6HQx3ziBj7AG6peN8YxWGa1b60/P76eg0GP1IYZ0Yq2jGXyzmKG4AXAxkzq7opAOm8uASql2sxR++fXDrleM=" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.e001e6ee1696fc1.json b/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.e001e6ee1696fc1.json new file mode 100644 index 000000000..2fae4108e --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/ListOnlineEvaluationConfigsCommand.e001e6ee1696fc1.json @@ -0,0 +1,18 @@ +{ + "onlineEvaluationConfigs": [ + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyLatest_StagingEval-4utSyp3pE9", + "onlineEvaluationConfigId": "ABVfyLatest_StagingEval-4utSyp3pE9", + "onlineEvaluationConfigName": "ABVfyLatest_StagingEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": { + "$date": "2026-06-17T22:09:59.058Z" + }, + "updatedAt": { + "$date": "2026-06-17T22:10:12.095Z" + } + } + ], + "nextToken": "AQICAHg005/7fkIBR7tGFhppNaSDRXSe8ajCw8sa0ZnYTqa1SAEv6LZkoBF2ff9apGPQDI1QAAABLTCCASkGCSqGSIb3DQEHBqCCARowggEWAgEAMIIBDwYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAzndfN1G5lOwU0OoKcCARCAgeFptcFMzCJHdqqbuwga6ImIwdP9YJyQW82pmhDStQefIKBE5DNOnEre+0wrAPnlIrMkImcj2K+ToMSe6Sy+j3NkrDY//zdwlE9izmkzoP9qFzD6nmtoLSVqOBKb/1o6I2019xZmJETD4SMO8FBO8Xw/jPKFyeopIrn5cAQ8/HcV2VhQ1InbeRBErVARDbXLWy/zzFXNQXM5+yXHmpJNKhafJ402KhznsNFfMN3HI+HcTNw64EiqX8ChbSoGW0JBpXaUJ7ZKHr+pocmVVdJpbNgxw4rHJK3WWENG5YTdWAXbNZs=" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.5e12cb8fd951e3d2.json b/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.5e12cb8fd951e3d2.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.5e12cb8fd951e3d2.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.789111680399c05c.json b/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.789111680399c05c.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.789111680399c05c.json @@ -0,0 +1 @@ +{} diff --git a/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.999eb3ded7d6f95.json b/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.999eb3ded7d6f95.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/PutRolePolicyCommand.999eb3ded7d6f95.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.1c674dc74c2a961c.json b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.1c674dc74c2a961c.json new file mode 100644 index 000000000..ef9b0b88d --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.1c674dc74c2a961c.json @@ -0,0 +1,9 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "updatedAt": { + "$date": "2026-08-03T21:30:08.342Z" + }, + "status": "ACTIVE", + "executionStatus": "DISABLED" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.29bcd2abed379bc9.json b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.29bcd2abed379bc9.json new file mode 100644 index 000000000..2743b289c --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.29bcd2abed379bc9.json @@ -0,0 +1,9 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "updatedAt": { + "$date": "2026-08-03T21:29:44.625Z" + }, + "status": "UPDATING", + "executionStatus": "ENABLED" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.2cc6a186c2d4961e.json b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.2cc6a186c2d4961e.json new file mode 100644 index 000000000..7a0e96308 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.2cc6a186c2d4961e.json @@ -0,0 +1,9 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "updatedAt": { + "$date": "2026-08-03T21:29:49.831Z" + }, + "status": "UPDATING", + "executionStatus": "DISABLED" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.4ba036918b84f090.json b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.4ba036918b84f090.json new file mode 100644 index 000000000..bf173abca --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.4ba036918b84f090.json @@ -0,0 +1,9 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "updatedAt": { + "$date": "2026-08-03T21:29:44.358Z" + }, + "status": "ACTIVE", + "executionStatus": "DISABLED" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.d88366abe9f2729b.json b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.d88366abe9f2729b.json new file mode 100644 index 000000000..08c6ab0c2 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/UpdateOnlineEvaluationConfigCommand.d88366abe9f2729b.json @@ -0,0 +1,9 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_role_warn-2KNTGuGVDl", + "updatedAt": { + "$date": "2026-08-03T21:30:02.741Z" + }, + "status": "ACTIVE", + "executionStatus": "DISABLED" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/create.golden.json b/src/handlers/eval/online-eval/__fixtures__/create.golden.json new file mode 100644 index 000000000..edfd4d2e5 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/create.golden.json @@ -0,0 +1,12 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "createdAt": "2026-08-03T21:29:43.069Z", + "status": "CREATING", + "executionStatus": "DISABLED", + "outputConfig": { + "cloudWatchConfig": { + "logGroupName": "/aws/bedrock-agentcore/evaluations/results/agentcore_cli_online_eval_fixture-vYkaD93sFk" + } + } +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/delete.golden.json b/src/handlers/eval/online-eval/__fixtures__/delete.golden.json new file mode 100644 index 000000000..da16e6717 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/delete.golden.json @@ -0,0 +1,5 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "status": "DELETING" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/get.golden.json b/src/handlers/eval/online-eval/__fixtures__/get.golden.json new file mode 100644 index 000000000..574ee3501 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/get.golden.json @@ -0,0 +1,38 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigName": "agentcore_cli_online_eval_fixture", + "rule": { + "samplingConfig": { + "samplingPercentage": 25 + }, + "sessionConfig": { + "sessionTimeoutMinutes": 30 + } + }, + "dataSourceConfig": { + "cloudWatchLogs": { + "logGroupNames": [ + "/aws/bedrock-agentcore/runtimes/testAgent_Agent-wm9hYBD93Y-DEFAULT" + ], + "serviceNames": [ + "testAgent_Agent.DEFAULT" + ] + } + }, + "status": "ACTIVE", + "executionStatus": "DISABLED", + "createdAt": "2026-08-03T21:29:43.069Z", + "updatedAt": "2026-08-03T21:29:44.358Z", + "evaluators": [ + { + "evaluatorId": "Builtin.Helpfulness" + } + ], + "outputConfig": { + "cloudWatchConfig": { + "logGroupName": "/aws/bedrock-agentcore/evaluations/results/agentcore_cli_online_eval_fixture-vYkaD93sFk" + } + }, + "evaluationExecutionRoleArn": "arn:aws:iam::725476964917:role/AgentCoreOnlineEval-agentcore_cli_online_eval_fixture" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/list-page-1.golden.json b/src/handlers/eval/online-eval/__fixtures__/list-page-1.golden.json new file mode 100644 index 000000000..50e4154d1 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/list-page-1.golden.json @@ -0,0 +1,14 @@ +{ + "onlineEvaluationConfigs": [ + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyLatest_ProdEval-2vqlCb2UiG", + "onlineEvaluationConfigId": "ABVfyLatest_ProdEval-2vqlCb2UiG", + "onlineEvaluationConfigName": "ABVfyLatest_ProdEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-17T22:09:59.115Z", + "updatedAt": "2026-06-17T22:10:11.792Z" + } + ], + "nextToken": "AQICAHg005/7fkIBR7tGFhppNaSDRXSe8ajCw8sa0ZnYTqa1SAHs3Tc/JEuHsHfRfV7OwMsqAAABKjCCASYGCSqGSIb3DQEHBqCCARcwggETAgEAMIIBDAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAxvGMxcYVwGUaMn490CARCAgd6Grki8j/DTGYrCjoz0pPponIAeCwB4DOt6kzm9O+CUSYZRSXfx2z9ik6NeWb7yunR708ju0A0Ub4UZP+cJzOIh1MfdU79KTB5BOad/OwSGPMEMLQ9yKvXLUhXwA4p8fiRkO6fk49N+foUK54IBCF+B/mLuG2S1a/8rNByu0qD5xA/oRv2GNJdOnf+8bS8jNEiyQyEhiQR7e6dNO8karHz6HQx3ziBj7AG6peN8YxWGa1b60/P76eg0GP1IYZ0Yq2jGXyzmKG4AXAxkzq7opAOm8uASql2sxR++fXDrleM=" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/list-page-2.golden.json b/src/handlers/eval/online-eval/__fixtures__/list-page-2.golden.json new file mode 100644 index 000000000..9f430050c --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/list-page-2.golden.json @@ -0,0 +1,14 @@ +{ + "onlineEvaluationConfigs": [ + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyLatest_StagingEval-4utSyp3pE9", + "onlineEvaluationConfigId": "ABVfyLatest_StagingEval-4utSyp3pE9", + "onlineEvaluationConfigName": "ABVfyLatest_StagingEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-17T22:09:59.058Z", + "updatedAt": "2026-06-17T22:10:12.095Z" + } + ], + "nextToken": "AQICAHg005/7fkIBR7tGFhppNaSDRXSe8ajCw8sa0ZnYTqa1SAEv6LZkoBF2ff9apGPQDI1QAAABLTCCASkGCSqGSIb3DQEHBqCCARowggEWAgEAMIIBDwYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAzndfN1G5lOwU0OoKcCARCAgeFptcFMzCJHdqqbuwga6ImIwdP9YJyQW82pmhDStQefIKBE5DNOnEre+0wrAPnlIrMkImcj2K+ToMSe6Sy+j3NkrDY//zdwlE9izmkzoP9qFzD6nmtoLSVqOBKb/1o6I2019xZmJETD4SMO8FBO8Xw/jPKFyeopIrn5cAQ8/HcV2VhQ1InbeRBErVARDbXLWy/zzFXNQXM5+yXHmpJNKhafJ402KhznsNFfMN3HI+HcTNw64EiqX8ChbSoGW0JBpXaUJ7ZKHr+pocmVVdJpbNgxw4rHJK3WWENG5YTdWAXbNZs=" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/list.golden.json b/src/handlers/eval/online-eval/__fixtures__/list.golden.json new file mode 100644 index 000000000..e94eab176 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/list.golden.json @@ -0,0 +1,314 @@ +{ + "onlineEvaluationConfigs": [ + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyLatest_ProdEval-2vqlCb2UiG", + "onlineEvaluationConfigId": "ABVfyLatest_ProdEval-2vqlCb2UiG", + "onlineEvaluationConfigName": "ABVfyLatest_ProdEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-17T22:09:59.115Z", + "updatedAt": "2026-06-17T22:10:11.792Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyLatest_StagingEval-4utSyp3pE9", + "onlineEvaluationConfigId": "ABVfyLatest_StagingEval-4utSyp3pE9", + "onlineEvaluationConfigName": "ABVfyLatest_StagingEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-17T22:09:59.058Z", + "updatedAt": "2026-06-17T22:10:12.095Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyPrerelease_ProdEval-MtuOYB2GZP", + "onlineEvaluationConfigId": "ABVfyPrerelease_ProdEval-MtuOYB2GZP", + "onlineEvaluationConfigName": "ABVfyPrerelease_ProdEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-17T20:28:10.738Z", + "updatedAt": "2026-06-17T20:28:26.527Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ABVfyPrerelease_StagingEval-fH26hNBHL6", + "onlineEvaluationConfigId": "ABVfyPrerelease_StagingEval-fH26hNBHL6", + "onlineEvaluationConfigName": "ABVfyPrerelease_StagingEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-17T20:28:09.163Z", + "updatedAt": "2026-06-17T20:28:26.799Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/EvoBugBash_abOnlineEval-Q0Iv1k2cdo", + "onlineEvaluationConfigId": "EvoBugBash_abOnlineEval-Q0Iv1k2cdo", + "onlineEvaluationConfigName": "EvoBugBash_abOnlineEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-12T22:27:27.466Z", + "updatedAt": "2026-06-12T22:27:40.636Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/GwFilterVfy_ctrlEval-T97o5LAtRS", + "onlineEvaluationConfigId": "GwFilterVfy_ctrlEval-T97o5LAtRS", + "onlineEvaluationConfigName": "GwFilterVfy_ctrlEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T18:27:10.496Z", + "updatedAt": "2026-06-15T18:27:26.982Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/GwFilterVfy_treatEval-bTv4u14rUQ", + "onlineEvaluationConfigId": "GwFilterVfy_treatEval-bTv4u14rUQ", + "onlineEvaluationConfigName": "GwFilterVfy_treatEval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T18:27:10.370Z", + "updatedAt": "2026-06-15T18:27:27.240Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/OhioTripPlanner_ProdEvalStagin-8wGIBCFzOE", + "onlineEvaluationConfigId": "OhioTripPlanner_ProdEvalStagin-8wGIBCFzOE", + "onlineEvaluationConfigName": "OhioTripPlanner_ProdEvalStagin", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-16T00:09:44.834Z", + "updatedAt": "2026-06-16T00:09:56.722Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/OhioTripPlanner_StagingOnly-A3o7OsFV5a", + "onlineEvaluationConfigId": "OhioTripPlanner_StagingOnly-A3o7OsFV5a", + "onlineEvaluationConfigName": "OhioTripPlanner_StagingOnly", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-16T00:22:22.611Z", + "updatedAt": "2026-06-16T00:22:44.675Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeBundle-UekzTY3B12", + "onlineEvaluationConfigId": "PromoteT2_oeBundle-UekzTY3B12", + "onlineEvaluationConfigName": "PromoteT2_oeBundle", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T17:06:17.370Z", + "updatedAt": "2026-06-15T17:06:45.741Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeCtrl-Vwz1F69rtu", + "onlineEvaluationConfigId": "PromoteT2_oeCtrl-Vwz1F69rtu", + "onlineEvaluationConfigName": "PromoteT2_oeCtrl", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T17:06:17.504Z", + "updatedAt": "2026-06-15T17:06:44.609Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeT2c-2CHeFHBYfA", + "onlineEvaluationConfigId": "PromoteT2_oeT2c-2CHeFHBYfA", + "onlineEvaluationConfigName": "PromoteT2_oeT2c", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T17:06:17.603Z", + "updatedAt": "2026-06-15T17:06:45.174Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeT2t-pKLwdZDXyw", + "onlineEvaluationConfigId": "PromoteT2_oeT2t-pKLwdZDXyw", + "onlineEvaluationConfigName": "PromoteT2_oeT2t", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T17:06:17.784Z", + "updatedAt": "2026-06-15T17:06:45.466Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/PromoteT2_oeTreat-bC8FNM8MEN", + "onlineEvaluationConfigId": "PromoteT2_oeTreat-bC8FNM8MEN", + "onlineEvaluationConfigName": "PromoteT2_oeTreat", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T17:06:24.185Z", + "updatedAt": "2026-06-15T17:06:44.903Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/TEMPAGENTTJAB_MyOnlineEval_prod-n6GVQ2Atnw", + "onlineEvaluationConfigId": "TEMPAGENTTJAB_MyOnlineEval_prod-n6GVQ2Atnw", + "onlineEvaluationConfigName": "TEMPAGENTTJAB_MyOnlineEval_prod", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-05-05T19:22:26.291Z", + "updatedAt": "2026-05-05T19:22:57.162Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/TEMPAGENTTJAB_MyOnlineEval_staging-vPcBdKCji0", + "onlineEvaluationConfigId": "TEMPAGENTTJAB_MyOnlineEval_staging-vPcBdKCji0", + "onlineEvaluationConfigName": "TEMPAGENTTJAB_MyOnlineEval_staging", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-05-05T19:22:33.950Z", + "updatedAt": "2026-05-05T19:22:57.441Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/abdemo_evalconfig-ntDb6Q5PLh", + "onlineEvaluationConfigId": "abdemo_evalconfig-ntDb6Q5PLh", + "onlineEvaluationConfigName": "abdemo_evalconfig", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-05-27T22:10:42.270Z", + "updatedAt": "2026-05-27T22:10:51.516Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/abtestdemo_eval_config-Kp7uUyBqZB", + "onlineEvaluationConfigId": "abtestdemo_eval_config-Kp7uUyBqZB", + "onlineEvaluationConfigName": "abtestdemo_eval_config", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-05-26T19:13:52.461Z", + "updatedAt": "2026-05-26T19:14:02.417Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/abtestval_cs_control_eval-seHjGZ7TeH", + "onlineEvaluationConfigId": "abtestval_cs_control_eval-seHjGZ7TeH", + "onlineEvaluationConfigName": "abtestval_cs_control_eval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-24T22:58:24.637Z", + "updatedAt": "2026-06-24T22:58:38.138Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/abtestval_cs_treatment_eval-AdSjkL4CHu", + "onlineEvaluationConfigId": "abtestval_cs_treatment_eval-AdSjkL4CHu", + "onlineEvaluationConfigName": "abtestval_cs_treatment_eval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-24T22:58:24.648Z", + "updatedAt": "2026-06-24T22:58:38.393Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/ac570c_myonlineeval-6IRBzU3dkS", + "onlineEvaluationConfigId": "ac570c_myonlineeval-6IRBzU3dkS", + "onlineEvaluationConfigName": "ac570c_myonlineeval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-07-06T18:21:13.033Z", + "updatedAt": "2026-07-06T18:21:28.312Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigName": "agentcore_cli_online_eval_fixture", + "status": "CREATING", + "executionStatus": "DISABLED", + "createdAt": "2026-08-03T21:29:43.069Z", + "updatedAt": "2026-08-03T21:29:43.069Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/bugbashagent_abtesteval-53zYJX8x4X", + "onlineEvaluationConfigId": "bugbashagent_abtesteval-53zYJX8x4X", + "onlineEvaluationConfigName": "bugbashagent_abtesteval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T18:19:55.627Z", + "updatedAt": "2026-06-15T18:20:08.431Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/bugbashagent_lambda_ctrl_eval-EY3GWNCDgH", + "onlineEvaluationConfigId": "bugbashagent_lambda_ctrl_eval-EY3GWNCDgH", + "onlineEvaluationConfigName": "bugbashagent_lambda_ctrl_eval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T19:41:54.132Z", + "updatedAt": "2026-06-15T19:42:10.231Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/bugbashagent_lambda_trtm_eval-IlN5S34Bc5", + "onlineEvaluationConfigId": "bugbashagent_lambda_trtm_eval-IlN5S34Bc5", + "onlineEvaluationConfigName": "bugbashagent_lambda_trtm_eval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-06-15T19:41:54.111Z", + "updatedAt": "2026-06-15T19:42:10.335Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/demoEval_onlineEval1-QErY9BEWT5", + "onlineEvaluationConfigId": "demoEval_onlineEval1-QErY9BEWT5", + "onlineEvaluationConfigName": "demoEval_onlineEval1", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-07-16T17:26:21.325Z", + "updatedAt": "2026-07-16T17:26:36.933Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/demoEval_onlineEvalBoto3_20260716180853-tbD9Ob7VUX", + "onlineEvaluationConfigId": "demoEval_onlineEvalBoto3_20260716180853-tbD9Ob7VUX", + "onlineEvaluationConfigName": "demoEval_onlineEvalBoto3_20260716180853", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-07-16T18:08:54.221Z", + "updatedAt": "2026-07-16T18:08:54.477Z", + "description": "boto3-created: onlineEvalBoto3" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/demoEval_onlineInsights1-QE6As2GTTB", + "onlineEvaluationConfigId": "demoEval_onlineInsights1-QE6As2GTTB", + "onlineEvaluationConfigName": "demoEval_onlineInsights1", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-07-16T17:26:21.625Z", + "updatedAt": "2026-07-16T17:26:37.218Z", + "insights": [ + { + "insightId": "Builtin.Insight.FailureAnalysis" + } + ], + "clusteringConfig": { + "frequencies": [ + "DAILY" + ] + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/demoEval_onlineInsightsBoto3_20260716180854-aTLOAI48Jr", + "onlineEvaluationConfigId": "demoEval_onlineInsightsBoto3_20260716180854-aTLOAI48Jr", + "onlineEvaluationConfigName": "demoEval_onlineInsightsBoto3_20260716180854", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-07-16T18:08:54.762Z", + "updatedAt": "2026-07-16T18:08:54.954Z", + "description": "boto3-created: onlineInsightsBoto3", + "insights": [ + { + "insightId": "Builtin.Insight.UserIntent" + } + ], + "clusteringConfig": { + "frequencies": [ + "DAILY" + ] + } + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/deploytest_myeval-JMv0ihHd30", + "onlineEvaluationConfigId": "deploytest_myeval-JMv0ihHd30", + "onlineEvaluationConfigName": "deploytest_myeval", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2026-04-21T22:47:43.896Z", + "updatedAt": "2026-04-21T22:47:52.237Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/temp-5Dz9Ox6yMC", + "onlineEvaluationConfigId": "temp-5Dz9Ox6yMC", + "onlineEvaluationConfigName": "temp", + "status": "ACTIVE", + "executionStatus": "ENABLED", + "createdAt": "2025-12-02T21:09:17.495Z", + "updatedAt": "2025-12-02T21:09:17.831Z" + }, + { + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/tjHarnessDerivTest-8rZzgXEHiM", + "onlineEvaluationConfigId": "tjHarnessDerivTest-8rZzgXEHiM", + "onlineEvaluationConfigName": "tjHarnessDerivTest", + "status": "ACTIVE", + "executionStatus": "DISABLED", + "createdAt": "2026-07-27T17:56:04.618Z", + "updatedAt": "2026-07-27T17:56:04.805Z" + } + ] +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/pause.golden.json b/src/handlers/eval/online-eval/__fixtures__/pause.golden.json new file mode 100644 index 000000000..49df9ed01 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/pause.golden.json @@ -0,0 +1,7 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "updatedAt": "2026-08-03T21:29:49.831Z", + "status": "UPDATING", + "executionStatus": "DISABLED" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/resume.golden.json b/src/handlers/eval/online-eval/__fixtures__/resume.golden.json new file mode 100644 index 000000000..7b4f0fc12 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/resume.golden.json @@ -0,0 +1,7 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "updatedAt": "2026-08-03T21:29:44.625Z", + "status": "UPDATING", + "executionStatus": "ENABLED" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/__fixtures__/update.golden.json b/src/handlers/eval/online-eval/__fixtures__/update.golden.json new file mode 100644 index 000000000..6fa397d72 --- /dev/null +++ b/src/handlers/eval/online-eval/__fixtures__/update.golden.json @@ -0,0 +1,7 @@ +{ + "onlineEvaluationConfigArn": "arn:aws:bedrock-agentcore:us-west-2:725476964917:online-evaluation-config/agentcore_cli_online_eval_fixture-vYkaD93sFk", + "onlineEvaluationConfigId": "agentcore_cli_online_eval_fixture-vYkaD93sFk", + "updatedAt": "2026-08-03T21:29:44.358Z", + "status": "ACTIVE", + "executionStatus": "DISABLED" +} \ No newline at end of file diff --git a/src/handlers/eval/online-eval/create/index.tsx b/src/handlers/eval/online-eval/create/index.tsx new file mode 100644 index 000000000..1c0a73736 --- /dev/null +++ b/src/handlers/eval/online-eval/create/index.tsx @@ -0,0 +1,116 @@ +import z from "zod"; +import type { DataSourceConfig, Filter } from "@aws-sdk/client-bedrock-agentcore-control"; +import { createHandler, flag } from "../../../../router"; +import { InputValidationError } from "../../../../errors"; +import { JsonRendererKey } from "../../../../tui"; +import { SourceResolver, type AppIO } from "../../../../io"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx, parseJsonFlag } from "../../../utils"; + +export const createCreateOnlineEvalHandler = (core: Core, io: AppIO) => + createHandler({ + name: "create", + description: "create an online evaluation config", + flags: [ + flag("name", "the name of the online evaluation config", z.string().optional()), + flag("agent", "harness ID or runtime ID whose traffic to sample", z.string().optional()), + flag( + "endpoint", + "the agent endpoint qualifier to scope monitoring to (default DEFAULT)", + z.string().optional(), + ), + flag( + "data-source-config", + "the traces to evaluate (JSON DataSourceConfig; inline, file://, or - for stdin), as an alternative to --agent", + z.string().optional(), + ), + flag("evaluator", "the ID(s) of the evaluators to apply", z.array(z.string()).optional()), + flag( + "sampling-rate", + "percentage of sessions to sample (0.01-100)", + z.number().min(0.01).max(100).optional(), + ), + flag( + "session-timeout-minutes", + "minutes of inactivity before a session is considered complete (1-1440, default 15)", + z.number().int().min(1).max(1440).optional(), + ), + flag( + "filters", + "trace filters (JSON Filter[]; inline, file://, or - for stdin)", + z.string().optional(), + ), + flag( + "role-arn", + "IAM role the online evaluation assumes (default: auto-provisioned)", + z.string().optional(), + ), + flag( + "enable-on-create", + "whether to enable evaluation immediately (default true; pass false to create it paused)", + z.enum(["true", "false"]).optional(), + ), + flag( + "description", + "a description of the config's monitoring purpose", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["name"]) + throw new InputValidationError("required option '--name ' not specified"); + if (!flags["sampling-rate"]) { + throw new InputValidationError( + "required option '--sampling-rate ' not specified", + ); + } + if (!flags["evaluator"] || flags["evaluator"].length === 0) { + throw new InputValidationError( + "required option '--evaluator ' not specified", + ); + } + + const hasAgent = flags["agent"] !== undefined; + const hasDataSource = flags["data-source-config"] !== undefined; + if (hasAgent === hasDataSource) { + throw new InputValidationError( + "specify exactly one of '--agent' or '--data-source-config'", + ); + } + if (hasDataSource && flags["endpoint"]) { + throw new InputValidationError("'--endpoint' can only be used with '--agent'"); + } + + const source = new SourceResolver({ stdin: io.stdin }); + const common = { + name: flags["name"], + description: flags["description"], + samplingRate: flags["sampling-rate"], + sessionTimeoutMinutes: flags["session-timeout-minutes"], + filters: parseJsonFlag( + "filters", + await source.resolveText("filters", flags["filters"]), + ), + evaluatorIds: flags["evaluator"], + evaluationExecutionRoleArn: flags["role-arn"], + enableOnCreate: + flags["enable-on-create"] === undefined + ? undefined + : flags["enable-on-create"] === "true", + }; + + const response = await core.eval.createOnlineEvaluationConfig( + hasAgent + ? { ...common, agent: flags["agent"]!, endpoint: flags["endpoint"] } + : { + ...common, + dataSourceConfig: parseJsonFlag( + "data-source-config", + await source.resolveText("data-source-config", flags["data-source-config"]), + )!, + }, + coreOptsFromCtx(ctx), + ); + ctx.require(JsonRendererKey).renderJson(response); + }, + }); diff --git a/src/handlers/eval/online-eval/delete/index.tsx b/src/handlers/eval/online-eval/delete/index.tsx new file mode 100644 index 000000000..af4237354 --- /dev/null +++ b/src/handlers/eval/online-eval/delete/index.tsx @@ -0,0 +1,22 @@ +import z from "zod"; +import { createHandler, flag } from "../../../../router"; +import { InputValidationError } from "../../../../errors"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createDeleteOnlineEvalHandler = (core: Core) => + createHandler({ + name: "delete", + description: "delete an online evaluation config by id", + flags: [flag("id", "the ID of the online evaluation config to delete", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags["id"]) throw new InputValidationError("required option '--id ' not specified"); + + ctx + .require(JsonRendererKey) + .renderJson( + await core.eval.deleteOnlineEvaluationConfig(flags["id"], coreOptsFromCtx(ctx)), + ); + }, + }); diff --git a/src/handlers/eval/online-eval/get/index.tsx b/src/handlers/eval/online-eval/get/index.tsx new file mode 100644 index 000000000..ddfc5f692 --- /dev/null +++ b/src/handlers/eval/online-eval/get/index.tsx @@ -0,0 +1,20 @@ +import z from "zod"; +import { createHandler, flag } from "../../../../router"; +import { InputValidationError } from "../../../../errors"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createGetOnlineEvalHandler = (core: Core) => + createHandler({ + name: "get", + description: "get an online evaluation config by id", + flags: [flag("id", "the ID of the online evaluation config", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags["id"]) throw new InputValidationError("required option '--id ' not specified"); + + ctx + .require(JsonRendererKey) + .renderJson(await core.eval.getOnlineEvaluationConfig(flags["id"], coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/eval/online-eval/index.tsx b/src/handlers/eval/online-eval/index.tsx new file mode 100644 index 000000000..b37d07662 --- /dev/null +++ b/src/handlers/eval/online-eval/index.tsx @@ -0,0 +1,23 @@ +import { Router } from "../../../router"; +import type { AppIO } from "../../../io"; +import type { Core } from "../../types"; +import { createHelpDefault } from "../../help"; +import { createCreateOnlineEvalHandler } from "./create"; +import { createGetOnlineEvalHandler } from "./get"; +import { createListOnlineEvalHandler } from "./list"; +import { createUpdateOnlineEvalHandler } from "./update"; +import { createPauseOnlineEvalHandler } from "./pause"; +import { createResumeOnlineEvalHandler } from "./resume"; +import { createDeleteOnlineEvalHandler } from "./delete"; + +export function createOnlineEvalHandler(core: Core, io: AppIO): Router { + return new Router("online-eval", "manage AgentCore online evaluation configs") + .default(createHelpDefault(io)) + .handler(createCreateOnlineEvalHandler(core, io)) + .handler(createGetOnlineEvalHandler(core)) + .handler(createListOnlineEvalHandler(core)) + .handler(createUpdateOnlineEvalHandler(core, io)) + .handler(createPauseOnlineEvalHandler(core)) + .handler(createResumeOnlineEvalHandler(core)) + .handler(createDeleteOnlineEvalHandler(core)); +} diff --git a/src/handlers/eval/online-eval/list/index.tsx b/src/handlers/eval/online-eval/list/index.tsx new file mode 100644 index 000000000..7e2286ea8 --- /dev/null +++ b/src/handlers/eval/online-eval/list/index.tsx @@ -0,0 +1,23 @@ +import z from "zod"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createListOnlineEvalHandler = (core: Core) => + createHandler({ + name: "list", + description: "list online evaluation configs", + flags: [ + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + const response = await core.eval.listOnlineEvaluationConfigs( + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ); + ctx.require(JsonRendererKey).renderJson(response); + }, + }); diff --git a/src/handlers/eval/online-eval/online-eval.test.tsx b/src/handlers/eval/online-eval/online-eval.test.tsx new file mode 100644 index 000000000..8b9659d0a --- /dev/null +++ b/src/handlers/eval/online-eval/online-eval.test.tsx @@ -0,0 +1,487 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { CoreClient } from "../../../core"; +import { + createSilentLogger, + fixtureFactories, + matchGolden, + settle, + TestGlobalConfigAccessor, + testIO, +} from "../../../testing"; +import { createRootHandler } from "../../index"; + +const REGION = "us-west-2"; +const FIXTURES = join(import.meta.dir, "__fixtures__"); + +// Record with RECORD=1 bun test src/handlers/eval/online-eval/online-eval.test.tsx +// The RECORD run creates one online evaluation config against a real runtime, +// exercises get/list/update/pause/resume against it, then deletes it, so a +// recording leaves no residue. The id the service assigns is captured from the +// recorded create response, which keeps the dependent fixtures (keyed by request +// input) stable on replay. +const CONFIG_NAME = "agentcore_cli_online_eval_fixture"; + +// CreateOnlineEvaluationConfig validates the referenced evaluator, so recording +// uses a builtin that needs no setup. +const FIXTURE_EVALUATOR_ID = "Builtin.Helpfulness"; + +// The runtime whose traffic the recorded config samples. `--agent` resolves it to +// the CloudWatch log group / service name pair, so the id must exist in the +// fixture account. It is only referenced, never invoked. +const FIXTURE_AGENT_ID = "testAgent_Agent-wm9hYBD93Y"; +const FIXTURE_AGENT_NAME = "testAgent_Agent"; + +// An explicit execution role, to record the --role-arn override path. Omitting +// the flag provisions a default role instead, which the create test below covers. +const FIXTURE_ROLE_ARN = "arn:aws:iam::725476964917:role/AgentCoreEvalsSDK-us-west-2-a6864eb339"; + +// Online evaluation config ids match `[a-zA-Z][a-zA-Z0-9-_]{0,99}-[a-zA-Z0-9]{10}`. +// An id failing that pattern is rejected as a ValidationException before any +// lookup, so this one is well-formed and simply absent, to reach the not-found path. +const MISSING_CONFIG_ID = "missing-online-0000000000"; + +function createFixtureCore(): CoreClient { + const { createControlClient, createDataClient, createIamClient } = fixtureFactories(FIXTURES); + return new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + logger: createSilentLogger(), + }); +} + +// run drives the real router (parsing → middleware → handler → CoreClient) against +// the fixture-backed SDK clients and returns captured stdout. +async function run(args: string[]): Promise { + const io = testIO(); + const root = createRootHandler(createFixtureCore(), { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + await root.route(["node", "agentcore", ...args, "--region", REGION]); + return io.stdout(); +} + +// The id assigned by CreateOnlineEvaluationConfig, shared by the tests below. +let configId: string; + +describe("eval online-eval command hierarchy", () => { + test("registers the eval → online-eval command tree", () => { + const root = createRootHandler(createFixtureCore(), { + io: testIO().io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + const onlineEval = root + .children() + .find((c) => c.name() === "eval") + ?.children() + .find((c) => c.name() === "online-eval"); + + expect(onlineEval?.children().map((c) => c.name())).toEqual([ + "create", + "get", + "list", + "update", + "pause", + "resume", + "delete", + ]); + }); + + test("prints help for bare `eval online-eval` without an SDK call", async () => { + const stdout = await run(["eval", "online-eval"]); + expect(stdout).toContain("Usage: agentcore eval online-eval"); + expect(stdout).toContain("Commands:"); + }); +}); + +describe("online-eval CRUDL", () => { + test("creates an online evaluation config from an agent", async () => { + const stdout = await run([ + "eval", + "online-eval", + "create", + "--name", + CONFIG_NAME, + "--agent", + FIXTURE_AGENT_ID, + "--evaluator", + FIXTURE_EVALUATOR_ID, + "--sampling-rate", + "10", + "--session-timeout-minutes", + "30", + "--enable-on-create", + "false", + ]); + + matchGolden(FIXTURES, "create.golden.json", stdout); + configId = JSON.parse(stdout).onlineEvaluationConfigId; + expect(configId).toBeString(); + }); + + test("lists online evaluation configs", async () => { + const stdout = await run(["eval", "online-eval", "list"]); + + matchGolden(FIXTURES, "list.golden.json", stdout); + expect(JSON.parse(stdout).onlineEvaluationConfigs).toBeArray(); + }); + + test("paginates the list with --max-results and --next-token", async () => { + const firstPage = await run(["eval", "online-eval", "list", "--max-results", "1"]); + matchGolden(FIXTURES, "list-page-1.golden.json", firstPage); + + const first = JSON.parse(firstPage); + expect(first.onlineEvaluationConfigs).toHaveLength(1); + expect(first.nextToken).toBeString(); + + const secondPage = await run([ + "eval", + "online-eval", + "list", + "--max-results", + "1", + "--next-token", + first.nextToken, + ]); + matchGolden(FIXTURES, "list-page-2.golden.json", secondPage); + expect(JSON.parse(secondPage).onlineEvaluationConfigs).toHaveLength(1); + }); + + // update merges over the current config because UpdateOnlineEvaluationConfig + // replaces the whole `rule`; this asserts the unset fields survive the round trip. + test("updates only the sampling rate, preserving the session timeout", async () => { + const stdout = await run([ + "eval", + "online-eval", + "update", + "--id", + configId, + "--sampling-rate", + "25", + ]); + + matchGolden(FIXTURES, "update.golden.json", stdout); + + // `get` is asserted here rather than in its own test: fixtures are keyed by + // request input, so a second `get` of this id would share (and disagree with) + // this one's recording. + const getStdout = await run(["eval", "online-eval", "get", "--id", configId]); + matchGolden(FIXTURES, "get.golden.json", getStdout); + + const after = JSON.parse(getStdout); + expect(after.onlineEvaluationConfigName).toBe(CONFIG_NAME); + expect(after.rule.samplingConfig.samplingPercentage).toBe(25); + // `update` replaces the whole `rule`, and --session-timeout-minutes was never + // passed to it, so the value set at create must survive the merge. + expect(after.rule.sessionConfig.sessionTimeoutMinutes).toBe(30); + // `--agent` derives the log group from the runtime *id* and the service name + // from the runtime *name*; the two are not interchangeable. + const cloudWatchLogs = after.dataSourceConfig.cloudWatchLogs; + expect(cloudWatchLogs.logGroupNames).toEqual([ + `/aws/bedrock-agentcore/runtimes/${FIXTURE_AGENT_ID}-DEFAULT`, + ]); + expect(cloudWatchLogs.serviceNames).toEqual([`${FIXTURE_AGENT_NAME}.DEFAULT`]); + }); + + // resume and pause are asserted in one test because the service rejects an + // update while the previous one is still settling (ConflictException, state + // UPDATING). Recording therefore waits for the config to leave UPDATING between + // the two calls; on replay the fixtures are served instantly and the wait is a + // no-op, so the test stays fast and deterministic. + test("resumes then pauses the config, toggling execution status", async () => { + const resumeStdout = await run(["eval", "online-eval", "resume", "--id", configId]); + matchGolden(FIXTURES, "resume.golden.json", resumeStdout); + expect(JSON.parse(resumeStdout).executionStatus).toBe("ENABLED"); + + await settle(); + + const pauseStdout = await run(["eval", "online-eval", "pause", "--id", configId]); + matchGolden(FIXTURES, "pause.golden.json", pauseStdout); + expect(JSON.parse(pauseStdout).executionStatus).toBe("DISABLED"); + // Extended timeout so the settle() wait fits while recording; it is a no-op on replay. + }, 60_000); + + test("deletes the online evaluation config", async () => { + const stdout = await run(["eval", "online-eval", "delete", "--id", configId]); + matchGolden(FIXTURES, "delete.golden.json", stdout); + }); + + test("propagates ResourceNotFoundException from get", async () => { + await expect( + run(["eval", "online-eval", "get", "--id", MISSING_CONFIG_ID]), + ).rejects.toMatchObject({ name: "ResourceNotFoundException" }); + }); +}); + +// Flag parsing never reaches the SDK, so these need no fixtures. +describe("flag validation", () => { + test("create requires a data source", async () => { + await expect( + run([ + "eval", + "online-eval", + "create", + "--name", + CONFIG_NAME, + "--evaluator", + FIXTURE_EVALUATOR_ID, + "--sampling-rate", + "10", + "--role-arn", + FIXTURE_ROLE_ARN, + ]), + ).rejects.toThrow(/exactly one of '--agent' or '--data-source-config'/); + }); + + test("create rejects both --agent and --data-source-config", async () => { + await expect( + run([ + "eval", + "online-eval", + "create", + "--name", + CONFIG_NAME, + "--agent", + FIXTURE_AGENT_ID, + "--data-source-config", + '{"cloudWatchLogs":{"logGroupNames":["/custom"],"serviceNames":["svc"]}}', + "--evaluator", + FIXTURE_EVALUATOR_ID, + "--sampling-rate", + "10", + "--role-arn", + FIXTURE_ROLE_ARN, + ]), + ).rejects.toThrow(/exactly one of '--agent' or '--data-source-config'/); + }); + + test("create rejects --endpoint without --agent", async () => { + await expect( + run([ + "eval", + "online-eval", + "create", + "--name", + CONFIG_NAME, + "--data-source-config", + '{"cloudWatchLogs":{"logGroupNames":["/custom"],"serviceNames":["svc"]}}', + "--endpoint", + "prod", + "--evaluator", + FIXTURE_EVALUATOR_ID, + "--sampling-rate", + "10", + "--role-arn", + FIXTURE_ROLE_ARN, + ]), + ).rejects.toThrow(/'--endpoint' can only be used with '--agent'/); + }); + + test("create rejects malformed --data-source-config JSON", async () => { + await expect( + run([ + "eval", + "online-eval", + "create", + "--name", + CONFIG_NAME, + "--data-source-config", + "not-json", + "--evaluator", + FIXTURE_EVALUATOR_ID, + "--sampling-rate", + "10", + "--role-arn", + FIXTURE_ROLE_ARN, + ]), + ).rejects.toThrow(/Invalid JSON for option '--data-source-config'/); + }); + + test("update rejects --endpoint together with --clear-endpoint", async () => { + await expect( + run([ + "eval", + "online-eval", + "update", + "--id", + "some-config-0000000000", + "--endpoint", + "staging", + "--clear-endpoint", + "true", + ]), + ).rejects.toThrow(/mutually exclusive/); + }); + + test("update rejects --agent together with --data-source-config", async () => { + await expect( + run([ + "eval", + "online-eval", + "update", + "--id", + "some-config-0000000000", + "--agent", + FIXTURE_AGENT_ID, + "--data-source-config", + '{"cloudWatchLogs":{"logGroupNames":["/custom"],"serviceNames":["svc"]}}', + ]), + ).rejects.toThrow(/'--agent' and '--data-source-config' are mutually exclusive/); + }); + + test("update rejects --endpoint together with --data-source-config", async () => { + await expect( + run([ + "eval", + "online-eval", + "update", + "--id", + "some-config-0000000000", + "--endpoint", + "prod", + "--data-source-config", + '{"cloudWatchLogs":{"logGroupNames":["/custom"],"serviceNames":["svc"]}}', + ]), + ).rejects.toThrow(/'--endpoint' cannot be combined with '--data-source-config'/); + }); + + test.each(["get", "update", "pause", "resume", "delete"])("%s requires --id", async (command) => { + await expect(run(["eval", "online-eval", command])).rejects.toThrow( + /required option '--id ' not specified/, + ); + }); +}); + +// A separate config, so this never perturbs the CRUDL sequence above: inserting an +// extra update there shifts which recording each of its calls keys to. +// Provisioning has to grant kms:Decrypt on the keys of any customer-managed-key +// evaluator the config references, because the service validates that permission +// when the config is created. Resolution reads GetEvaluator.kmsKeyArn, which the +// service currently only reports for ~2 minutes after an evaluator is created +// (P484740478), so the encrypted evaluator here is backed by a hand-authored +// fixture representing the documented behavior rather than a live recording. +describe("execution role KMS scoping", () => { + const KMS_CONFIG_NAME = "agentcore_cli_online_eval_kms"; + + test("provisions a role for a config referencing an encrypted evaluator", async () => { + const stdout = await run([ + "eval", + "online-eval", + "create", + "--name", + KMS_CONFIG_NAME, + "--agent", + FIXTURE_AGENT_ID, + // Builtin.Correctness is backed by the hand-authored fixture carrying a + // kmsKeyArn; Builtin.Helpfulness carries none, so this covers both arms of + // the resolution in one create. + "--evaluator", + FIXTURE_EVALUATOR_ID, + "--evaluator", + "Builtin.Correctness", + "--sampling-rate", + "10", + "--enable-on-create", + "false", + ]); + + const created = JSON.parse(stdout); + expect(created.onlineEvaluationConfigId).toBeString(); + // No --role-arn, so the role is the provisioned one named after the config. + // Asserting the ARN mirrors how harness covers its default role. + const detail = JSON.parse( + await run(["eval", "online-eval", "get", "--id", created.onlineEvaluationConfigId]), + ); + expect(detail.evaluationExecutionRoleArn).toContain(`AgentCoreOnlineEval-${KMS_CONFIG_NAME}`); + + await settle(); + await run(["eval", "online-eval", "delete", "--id", created.onlineEvaluationConfigId]); + }, 90_000); +}); + +describe("execution role scoping on update", () => { + const WARN_CONFIG_NAME = "agentcore_cli_online_eval_role_warn"; + + test("warns when a custom role is left scoped to the old log groups", async () => { + const created = await run([ + "eval", + "online-eval", + "create", + "--name", + WARN_CONFIG_NAME, + "--agent", + FIXTURE_AGENT_ID, + "--evaluator", + FIXTURE_EVALUATOR_ID, + "--sampling-rate", + "10", + "--role-arn", + FIXTURE_ROLE_ARN, + "--enable-on-create", + "false", + ]); + const warnConfigId = JSON.parse(created).onlineEvaluationConfigId; + await settle(); + + // Repointing at a different agent moves the log groups, but the role came from + // --role-arn, so the CLI must not touch its permissions — only report it. + const io = testIO(); + const root = createRootHandler(createFixtureCore(), { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + await root.route([ + "node", + "agentcore", + "eval", + "online-eval", + "update", + "--id", + warnConfigId, + "--agent", + "ABVfyLatest_ABVfyLatest-PFLr353QVA", + "--region", + REGION, + ]); + + // Human-readable mode: the advisory goes to stderr, leaving stdout alone. + expect(io.stderr()).toContain("not managed by the CLI"); + expect(io.stderr()).toContain(FIXTURE_ROLE_ARN); + + await settle(); + + // --json suppresses the advisory, matching runtime/invoke's summary: a scripted + // caller gets machine-readable stdout and an empty stderr. + const jsonIo = testIO(); + const jsonRoot = createRootHandler(createFixtureCore(), { + io: jsonIo.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + await jsonRoot.route([ + "node", + "agentcore", + "eval", + "online-eval", + "update", + "--id", + warnConfigId, + "--agent", + FIXTURE_AGENT_ID, + "--region", + REGION, + "--json", + ]); + expect(jsonIo.stderr()).toBe(""); + expect(JSON.parse(jsonIo.stdout()).onlineEvaluationConfigId).toBe(warnConfigId); + + await settle(); + await run(["eval", "online-eval", "delete", "--id", warnConfigId]); + }, 90_000); +}); diff --git a/src/handlers/eval/online-eval/pause/index.tsx b/src/handlers/eval/online-eval/pause/index.tsx new file mode 100644 index 000000000..448a99c60 --- /dev/null +++ b/src/handlers/eval/online-eval/pause/index.tsx @@ -0,0 +1,26 @@ +import z from "zod"; +import { createHandler, flag } from "../../../../router"; +import { InputValidationError } from "../../../../errors"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createPauseOnlineEvalHandler = (core: Core) => + createHandler({ + name: "pause", + description: "pause an online evaluation config", + flags: [flag("id", "the ID of the online evaluation config to pause", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags["id"]) throw new InputValidationError("required option '--id ' not specified"); + + ctx + .require(JsonRendererKey) + .renderJson( + await core.eval.setOnlineEvaluationExecutionStatus( + flags["id"], + "DISABLED", + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/eval/online-eval/resume/index.tsx b/src/handlers/eval/online-eval/resume/index.tsx new file mode 100644 index 000000000..aea33e220 --- /dev/null +++ b/src/handlers/eval/online-eval/resume/index.tsx @@ -0,0 +1,26 @@ +import z from "zod"; +import { createHandler, flag } from "../../../../router"; +import { InputValidationError } from "../../../../errors"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createResumeOnlineEvalHandler = (core: Core) => + createHandler({ + name: "resume", + description: "resume a paused online evaluation config", + flags: [flag("id", "the ID of the online evaluation config to resume", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags["id"]) throw new InputValidationError("required option '--id ' not specified"); + + ctx + .require(JsonRendererKey) + .renderJson( + await core.eval.setOnlineEvaluationExecutionStatus( + flags["id"], + "ENABLED", + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/eval/online-eval/update/index.tsx b/src/handlers/eval/online-eval/update/index.tsx new file mode 100644 index 000000000..89b2833ca --- /dev/null +++ b/src/handlers/eval/online-eval/update/index.tsx @@ -0,0 +1,131 @@ +import z from "zod"; +import type { DataSourceConfig, Filter } from "@aws-sdk/client-bedrock-agentcore-control"; +import { createHandler, flag } from "../../../../router"; +import { InputValidationError } from "../../../../errors"; +import { JsonKey } from "../../../keys"; +import { JsonRendererKey } from "../../../../tui"; +import { SourceResolver, type AppIO } from "../../../../io"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx, parseJsonFlag } from "../../../utils"; + +export const createUpdateOnlineEvalHandler = (core: Core, io: AppIO) => + createHandler({ + name: "update", + description: "update an online evaluation config", + flags: [ + flag("id", "the ID of the online evaluation config to update", z.string().optional()), + flag( + "sampling-rate", + "percentage of sessions to sample (0.01-100)", + z.number().min(0.01).max(100).optional(), + ), + flag( + "session-timeout-minutes", + "minutes of inactivity before a session is considered complete (1-1440)", + z.number().int().min(1).max(1440).optional(), + ), + flag( + "filters", + "trace filters (JSON Filter[]; inline, file://, or - for stdin)", + z.string().optional(), + ), + flag( + "evaluator", + "the ID(s) of the evaluators to apply (replaces the existing list)", + z.array(z.string()).optional(), + ), + flag("agent", "repoint at a different harness ID or runtime ID", z.string().optional()), + flag( + "endpoint", + "re-scope monitoring to a different agent endpoint qualifier", + z.string().optional(), + ), + flag( + "clear-endpoint", + "reset the endpoint scope to the default qualifier (pass true)", + z.enum(["true", "false"]).optional(), + ), + flag( + "data-source-config", + "replace the traces to evaluate (JSON DataSourceConfig; inline, file://, or - for stdin)", + z.string().optional(), + ), + flag("role-arn", "replace the IAM role the online evaluation assumes", z.string().optional()), + flag( + "update-role", + "whether to re-scope an auto-provisioned execution role when the data source changes (default true)", + z.enum(["true", "false"]).optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["id"]) throw new InputValidationError("required option '--id ' not specified"); + if (flags["endpoint"] && flags["clear-endpoint"] === "true") { + throw new InputValidationError( + "'--endpoint' and '--clear-endpoint' are mutually exclusive", + ); + } + if (flags["data-source-config"] && flags["agent"]) { + throw new InputValidationError( + "'--agent' and '--data-source-config' are mutually exclusive", + ); + } + if ( + flags["data-source-config"] && + (flags["endpoint"] || flags["clear-endpoint"] === "true") + ) { + throw new InputValidationError( + "'--endpoint' cannot be combined with '--data-source-config'", + ); + } + + const source = new SourceResolver({ stdin: io.stdin }); + const { response, roleScopeWarning } = await core.eval.updateOnlineEvaluationConfig( + flags["id"], + { + samplingRate: flags["sampling-rate"], + sessionTimeoutMinutes: flags["session-timeout-minutes"], + filters: parseJsonFlag( + "filters", + await source.resolveText("filters", flags["filters"]), + ), + evaluatorIds: flags["evaluator"], + agent: flags["agent"], + endpoint: flags["endpoint"], + clearEndpoint: flags["clear-endpoint"] === "true", + dataSourceConfig: parseJsonFlag( + "data-source-config", + await source.resolveText("data-source-config", flags["data-source-config"]), + ), + evaluationExecutionRoleArn: flags["role-arn"], + updateRole: + flags["update-role"] === undefined ? undefined : flags["update-role"] === "true", + }, + coreOptsFromCtx(ctx), + ); + // Suppressed under --json, matching runtime/invoke's advisory summary: a + // scripted caller gets a machine-readable stdout and nothing else. + if (roleScopeWarning && !ctx.require(JsonKey)) { + const { reason, roleArn, logGroupNames } = roleScopeWarning; + if (reason === "stale-scope") { + // The update succeeded and the role grants the new data source; the + // policy for the superseded one just could not be detached. + io.stderr.write( + `warning: the execution role still grants access to the previous data source.\n` + + ` role: ${roleArn}\n` + + ` detach the inline policy covering: ${logGroupNames.join(", ")}\n`, + ); + } else { + const detail = + reason === "custom-role" + ? "it is not managed by the CLI" + : "re-scoping was declined via --update-role false"; + io.stderr.write( + `warning: the data source moved but the execution role was not re-scoped because ${detail}.\n` + + ` role: ${roleArn}\n` + + ` ensure it grants logs:StartQuery and logs:GetQueryResults on: ${logGroupNames.join(", ")}\n`, + ); + } + } + ctx.require(JsonRendererKey).renderJson(response); + }, + }); diff --git a/src/handlers/eval/types.tsx b/src/handlers/eval/types.tsx index 4dc54cce2..7ceb8f967 100644 --- a/src/handlers/eval/types.tsx +++ b/src/handlers/eval/types.tsx @@ -1,11 +1,18 @@ import type { CreateEvaluatorRequest, CreateEvaluatorResponse, + CreateOnlineEvaluationConfigResponse, DeleteEvaluatorResponse, + DeleteOnlineEvaluationConfigResponse, GetEvaluatorResponse, + GetOnlineEvaluationConfigResponse, ListEvaluatorsResponse, + ListOnlineEvaluationConfigsResponse, + DataSourceConfig, RatingScale, + Rule, UpdateEvaluatorResponse, + UpdateOnlineEvaluationConfigResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; import type { CoreOptions } from "../../core/types"; @@ -32,9 +39,62 @@ export type CodeBasedUpdate = { clientToken?: string; }; -// CoreEvalClient is the evaluator surface the eval handlers depend on. It is -// declared here, next to the handlers that consume it, and implemented by -// src/core/eval.tsx (dependency inversion: handlers own the abstraction). +// CreateOnlineEvalInput mirrors CreateOnlineEvaluationConfigRequest but lets the +// caller identify the traffic to sample either by an existing agent — a plain +// AgentCore Runtime ID or a Harness ID, both resolved to the same underlying +// runtime by Core — or by supplying the API's dataSourceConfig directly. The +// execution role is optional: when omitted, Core provisions a default one scoped +// to the resolved log groups. +export type CreateOnlineEvalInput = { + name: string; + description?: string; + samplingRate: number; + sessionTimeoutMinutes?: number; + filters?: Rule["filters"]; + evaluatorIds?: string[]; + evaluationExecutionRoleArn?: string; + enableOnCreate?: boolean; +} & ( + | { agent: string; endpoint?: string; dataSourceConfig?: undefined } + | { agent?: undefined; endpoint?: undefined; dataSourceConfig: DataSourceConfig } +); + +// UpdateOnlineEvalInput carries the fields a caller may change on an online +// evaluation config. Undefined fields are left untouched by Core (merged over +// the current config, since UpdateOnlineEvaluationConfig replaces the whole +// `rule` object); `clearEndpoint` nulls out the endpoint scope, falling back to +// the agent's default log group. +export type UpdateOnlineEvalInput = { + samplingRate?: number; + sessionTimeoutMinutes?: number; + filters?: Rule["filters"]; + evaluatorIds?: string[]; + // Repoint the evaluation at different traces: `agent` re-derives the source + // from that agent (optionally at `endpoint`), `dataSourceConfig` replaces it + // outright, and `endpoint`/`clearEndpoint` alone re-scope the agent the config + // was already built from. + agent?: string; + endpoint?: string; + clearEndpoint?: boolean; + dataSourceConfig?: DataSourceConfig; + // Replaces the execution role. The CLI never edits the permissions of a role the + // caller names here — it is theirs to manage. + evaluationExecutionRoleArn?: string; + // Whether to re-scope a CLI-provisioned role when the data source moves + // (default true). Only meaningful for a managed role: the old policy grants + // query access to the previous log groups only. + updateRole?: boolean; +}; + +// RoleScopeWarning reports that an execution role was left scoped to log groups +// the config no longer samples, so the caller can surface it. Returned rather +// than logged from Core so the handler owns how it is presented. +export type RoleScopeWarning = { + reason: "custom-role" | "update-declined" | "stale-scope"; + roleArn: string; + logGroupNames: string[]; +}; + export interface CoreEvalClient { createEvaluator( request: CreateEvaluatorRequest, @@ -59,4 +119,37 @@ export interface CoreEvalClient { options: CoreOptions, ): Promise; deleteEvaluator(id: string, options: CoreOptions): Promise; + + createOnlineEvaluationConfig( + input: CreateOnlineEvalInput, + options: CoreOptions, + ): Promise; + // Returns the service response plus an optional warning when the execution + // role was left scoped to log groups the config no longer samples. + updateOnlineEvaluationConfig( + id: string, + update: UpdateOnlineEvalInput, + options: CoreOptions, + ): Promise<{ + response: UpdateOnlineEvaluationConfigResponse; + roleScopeWarning?: RoleScopeWarning; + }>; + getOnlineEvaluationConfig( + id: string, + options: CoreOptions, + ): Promise; + listOnlineEvaluationConfigs( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; + setOnlineEvaluationExecutionStatus( + id: string, + executionStatus: "ENABLED" | "DISABLED", + options: CoreOptions, + ): Promise; + deleteOnlineEvaluationConfig( + id: string, + options: CoreOptions, + ): Promise; } diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 54ef05cdb..7d704215d 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -24,11 +24,16 @@ import type { ListHarnessVersionsResponse, CreateEvaluatorRequest, CreateEvaluatorResponse, + CreateOnlineEvaluationConfigResponse, DeleteEvaluatorResponse, + DeleteOnlineEvaluationConfigResponse, GetEvaluatorResponse, + GetOnlineEvaluationConfigResponse, ListEvaluatorsResponse, + ListOnlineEvaluationConfigsResponse, MemoryView, UpdateEvaluatorResponse, + UpdateOnlineEvaluationConfigResponse, UpdateApiKeyCredentialProviderResponse, UpdateHarnessEndpointRequest, UpdateHarnessEndpointResponse, @@ -56,7 +61,13 @@ import type { RuntimeInvokeRequest, RuntimeInvokeResponse, } from "../handlers/runtime/types"; -import type { CodeBasedUpdate, CoreEvalClient, LlmAsAJudgeUpdate } from "../handlers/eval/types"; +import type { + CodeBasedUpdate, + CoreEvalClient, + CreateOnlineEvalInput, + LlmAsAJudgeUpdate, + UpdateOnlineEvalInput, +} from "../handlers/eval/types"; import { abortable } from "../core/abortable"; import type { CoreOptions } from "../core/types"; import type { ProjectManager } from "../handlers/project/types"; @@ -123,6 +134,10 @@ const DEFAULT_UPDATE_EVALUATOR_RESPONSE = {} as UpdateEvaluatorResponse; const DEFAULT_GET_EVALUATOR_RESPONSE = {} as GetEvaluatorResponse; const DEFAULT_LIST_EVALUATORS_RESPONSE: ListEvaluatorsResponse = { evaluators: [] }; const DEFAULT_DELETE_EVALUATOR_RESPONSE = {} as DeleteEvaluatorResponse; +const DEFAULT_CREATE_ONLINE_EVAL_RESPONSE = {} as CreateOnlineEvaluationConfigResponse; +const DEFAULT_UPDATE_ONLINE_EVAL_RESPONSE = {} as UpdateOnlineEvaluationConfigResponse; +const DEFAULT_GET_ONLINE_EVAL_RESPONSE = {} as GetOnlineEvaluationConfigResponse; +const DEFAULT_DELETE_ONLINE_EVAL_RESPONSE = {} as DeleteOnlineEvaluationConfigResponse; // events wraps canned events as a one-shot AsyncIterable. async function* events(items: T[]): AsyncGenerator { @@ -691,6 +706,20 @@ export class TestEvalClient implements CoreEvalClient { private updateResponse: UpdateEvaluatorResponse = DEFAULT_UPDATE_EVALUATOR_RESPONSE; private getResponse: GetEvaluatorResponse = DEFAULT_GET_EVALUATOR_RESPONSE; private deleteResponse: DeleteEvaluatorResponse = DEFAULT_DELETE_EVALUATOR_RESPONSE; + // Online-eval responses, keyed the same way: listOnlineEvaluationConfigs pages + // by nextToken, the rest are single canned values. + private onlineEvalListResponses = new Map< + string | undefined, + ListOnlineEvaluationConfigsResponse + >(); + private onlineEvalCreateResponse: CreateOnlineEvaluationConfigResponse = + DEFAULT_CREATE_ONLINE_EVAL_RESPONSE; + private onlineEvalUpdateResponse: UpdateOnlineEvaluationConfigResponse = + DEFAULT_UPDATE_ONLINE_EVAL_RESPONSE; + private onlineEvalGetResponse: GetOnlineEvaluationConfigResponse = + DEFAULT_GET_ONLINE_EVAL_RESPONSE; + private onlineEvalDeleteResponse: DeleteOnlineEvaluationConfigResponse = + DEFAULT_DELETE_ONLINE_EVAL_RESPONSE; private error?: Error; // setListResponse sets what listEvaluators resolves to (when not erroring). @@ -726,6 +755,44 @@ export class TestEvalClient implements CoreEvalClient { return this; } + // setOnlineEvalListResponse sets what listOnlineEvaluationConfigs resolves to + // (when not erroring). Pass `forNextToken` to serve a later page. + setOnlineEvalListResponse( + response: ListOnlineEvaluationConfigsResponse, + forNextToken?: string, + ): this { + this.onlineEvalListResponses.set(forNextToken, response); + return this; + } + + // setOnlineEvalCreateResponse sets what createOnlineEvaluationConfig resolves + // to (when not erroring). + setOnlineEvalCreateResponse(response: CreateOnlineEvaluationConfigResponse): this { + this.onlineEvalCreateResponse = response; + return this; + } + + // setOnlineEvalUpdateResponse sets what updateOnlineEvaluationConfig and + // setOnlineEvaluationExecutionStatus resolve to (when not erroring). + setOnlineEvalUpdateResponse(response: UpdateOnlineEvaluationConfigResponse): this { + this.onlineEvalUpdateResponse = response; + return this; + } + + // setOnlineEvalGetResponse sets what getOnlineEvaluationConfig resolves to + // (when not erroring). + setOnlineEvalGetResponse(response: GetOnlineEvaluationConfigResponse): this { + this.onlineEvalGetResponse = response; + return this; + } + + // setOnlineEvalDeleteResponse sets what deleteOnlineEvaluationConfig resolves + // to (when not erroring). + setOnlineEvalDeleteResponse(response: DeleteOnlineEvaluationConfigResponse): this { + this.onlineEvalDeleteResponse = response; + return this; + } + // setError makes every subsequent call reject with `error`. Pass undefined to // clear it. setError(error: Error | undefined): this { @@ -787,6 +854,69 @@ export class TestEvalClient implements CoreEvalClient { if (this.error) throw this.error; return this.deleteResponse; } + + async createOnlineEvaluationConfig( + input: CreateOnlineEvalInput, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "createOnlineEvaluationConfig", args: [input, options] }); + if (this.error) throw this.error; + return this.onlineEvalCreateResponse; + } + + async updateOnlineEvaluationConfig( + id: string, + update: UpdateOnlineEvalInput, + options: CoreOptions, + ): Promise<{ response: UpdateOnlineEvaluationConfigResponse }> { + this.calls.push({ method: "updateOnlineEvaluationConfig", args: [id, update, options] }); + if (this.error) throw this.error; + return { response: this.onlineEvalUpdateResponse }; + } + + async getOnlineEvaluationConfig( + id: string, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "getOnlineEvaluationConfig", args: [id, options] }); + if (this.error) throw this.error; + return this.onlineEvalGetResponse; + } + + async listOnlineEvaluationConfigs( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ + method: "listOnlineEvaluationConfigs", + args: [nextToken, maxResults, options], + }); + if (this.error) throw this.error; + return this.onlineEvalListResponses.get(nextToken) ?? { onlineEvaluationConfigs: [] }; + } + + async setOnlineEvaluationExecutionStatus( + id: string, + executionStatus: "ENABLED" | "DISABLED", + options: CoreOptions, + ): Promise { + this.calls.push({ + method: "setOnlineEvaluationExecutionStatus", + args: [id, executionStatus, options], + }); + if (this.error) throw this.error; + return this.onlineEvalUpdateResponse; + } + + async deleteOnlineEvaluationConfig( + id: string, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "deleteOnlineEvaluationConfig", args: [id, options] }); + if (this.error) throw this.error; + return this.onlineEvalDeleteResponse; + } } // TestCoreClient implements the Core contract with fully controllable sub-clients. diff --git a/src/testing/fixtures.tsx b/src/testing/fixtures.tsx index 9b04b260c..d294ce50d 100644 --- a/src/testing/fixtures.tsx +++ b/src/testing/fixtures.tsx @@ -32,6 +32,16 @@ export function isRecording(): boolean { return v === "1" || v === "true"; } +// settle waits out a service-side state transition between two calls that cannot +// overlap (e.g. AgentCore rejects an update while the resource is still UPDATING). +// It only sleeps while recording: on replay the fixtures are served from disk, so +// there is no state machine to wait for and the test stays fast and deterministic. +// Give the enclosing test a timeout that accommodates the wait. +export async function settle(ms = 5_000): Promise { + if (!isRecording()) return; + await new Promise((resolve) => setTimeout(resolve, ms)); +} + // An AWS SDK command as seen at the `.send()` boundary: its class carries the // operation name and it holds the request `input`. We only read these. interface SdkCommand { diff --git a/src/testing/index.tsx b/src/testing/index.tsx index 7ea3ce891..133607e5e 100644 --- a/src/testing/index.tsx +++ b/src/testing/index.tsx @@ -1,5 +1,5 @@ export { parse, stringify } from "./serialization"; -export { fixtureFactories, isRecording, matchGolden } from "./fixtures"; +export { fixtureFactories, isRecording, matchGolden, settle } from "./fixtures"; export { testIO, type TestIO } from "./testIO"; export { tick, waitFor } from "./timing"; export {