Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion src/core/configBundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
UpdateConfigurationBundleCommand,
type BedrockAgentCoreControlClient,
} from "@aws-sdk/client-bedrock-agentcore-control";
import { NetworkingError } from "../errors";
import { InputValidationError, NetworkingError } from "../errors";
import { EvalClient } from "./eval";
import type { AwsClients, ClientConfig } from "./types";

Expand Down Expand Up @@ -130,4 +130,39 @@ describe("EvalClient configuration bundles", () => {
branchName: "mainline",
});
});

test("rejects a full ARN, telling the caller to pass the bare id", async () => {
// `project status` prints bare ids; a full ARN in --id would reach the service
// as a path segment whose slashes break path→operation parsing (a misleading
// AccessDenied), so it is rejected up front, before any request.
const arn = "arn:aws:bedrock-agentcore:us-west-2:123456789012:configuration-bundle/b-1";
const sent: unknown[] = [];
const { client } = subject(async (command) => {
sent.push(command);
return { versionId: "v-9" };
});

const rejects = /must be a bare resource id, not an ARN/;
await expect(
client.getConfigurationBundle(arn, undefined, "mainline", OPTIONS),
).rejects.toThrow(rejects);
await expect(
client.listConfigurationBundleVersions(arn, undefined, undefined, OPTIONS),
).rejects.toThrow(rejects);
await expect(client.deleteConfigurationBundle(arn, OPTIONS)).rejects.toBeInstanceOf(
InputValidationError,
);
await expect(
client.updateConfigurationBundle(
arn,
{ branchName: "mainline", components: {}, commitMessage: "update" },
OPTIONS,
),
).rejects.toThrow(rejects);

// Rejected before any SDK call; a bare id still works.
expect(sent).toEqual([]);
await client.getConfigurationBundle("b-1", undefined, "mainline", OPTIONS);
expect((sent[0] as GetConfigurationBundleCommand).input).toMatchObject({ bundleId: "b-1" });
});
});
36 changes: 32 additions & 4 deletions src/core/eval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ import type { AwsClients, CoreFetch, CoreOptions } from "./types";
import type { Logger } from "../logging";
import { FilteredPaginator } from "./filteredPaginator";
import { toClientConfig } from "./utils";
import { parseArn } from "./arn";
import { grantOnlineEvalScope } from "./onlineEvalExecutionRole";
import { accountIdFromArn, deleteAbTestRole, provisionAbTestRole } from "./abTestExecutionRole";
import { harnessRuntimeFromResponse } from "./harness";
Expand All @@ -188,6 +189,19 @@ const RETRYABLE_DATASET_STATUSES: ReadonlySet<DatasetStatus> = new Set(["CREATIN
// The shared, account-level OTel span log group.
const SPANS_LOG_GROUP = "aws/spans";

// These commands address a resource by its bare id, which the service places in
// the request path. A full ARN's slashes make the service misparse the path and
// return a misleading AccessDenied, so reject it up front with an actionable
// message rather than sending it. `project status` also prints bare ids for this.
function requireBareId(value: string, option: string): string {
if (parseArn(value) !== undefined) {
throw new InputValidationError(
`--${option} must be a bare resource id, not an ARN (pass the id, e.g. the segment after the last '/').`,
);
}
return value;
}

// Default discovery window when no explicit --start/--end or --lookback-days is
// given. Mirrors the batch service's now-7d default.
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
Expand Down Expand Up @@ -520,6 +534,7 @@ export class EvalClient implements CoreEvalClient {
}) => CreateABTestRequest,
options: CoreOptions,
): Promise<CreateABTestResponse> {
gateway = requireBareId(gateway, "gateway");
const control = this.clients.control(toClientConfig(options));
const gatewayArn = (await control.send(new GetGatewayCommand({ gatewayIdentifier: gateway })))
.gatewayArn!;
Expand Down Expand Up @@ -1189,6 +1204,7 @@ export class EvalClient implements CoreEvalClient {
update: UpdateOnlineEvalInput,
options: CoreOptions,
): Promise<{ response: UpdateOnlineEvaluationConfigResponse }> {
id = requireBareId(id, "id");
const control = this.clients.control(toClientConfig(options));
const current = await control.send(
new GetOnlineEvaluationConfigCommand({
Expand Down Expand Up @@ -1261,6 +1277,7 @@ export class EvalClient implements CoreEvalClient {
id: string,
options: CoreOptions,
): Promise<GetOnlineEvaluationConfigResponse> {
id = requireBareId(id, "id");
return this.clients
.control(toClientConfig(options))
.send(new GetOnlineEvaluationConfigCommand({ onlineEvaluationConfigId: id }));
Expand All @@ -1281,6 +1298,7 @@ export class EvalClient implements CoreEvalClient {
executionStatus: "ENABLED" | "DISABLED",
options: CoreOptions,
): Promise<UpdateOnlineEvaluationConfigResponse> {
id = requireBareId(id, "id");
return this.clients
.control(toClientConfig(options))
.send(
Expand All @@ -1292,6 +1310,7 @@ export class EvalClient implements CoreEvalClient {
id: string,
options: CoreOptions,
): Promise<DeleteOnlineEvaluationConfigResponse> {
id = requireBareId(id, "id");
return this.clients
.control(toClientConfig(options))
.send(new DeleteOnlineEvaluationConfigCommand({ onlineEvaluationConfigId: id }));
Expand All @@ -1312,6 +1331,10 @@ export class EvalClient implements CoreEvalClient {
branchName: string,
options: CoreOptions,
): Promise<GetConfigurationBundleResponse | GetConfigurationBundleVersionResponse> {
// Accept a full ARN (as `project status` prints) and use its bare id: the id
// is a path segment, and an ARN's slashes would make the service parse the
// path as an unknown operation and return a misleading AccessDenied.
id = requireBareId(id, "id");
const control = this.clients.control(toClientConfig(options));
return version === undefined
? control.send(new GetConfigurationBundleCommand({ bundleId: id, branchName }))
Expand All @@ -1335,6 +1358,7 @@ export class EvalClient implements CoreEvalClient {
update: UpdateConfigurationBundleInput,
options: CoreOptions,
): Promise<UpdateConfigurationBundleResponse> {
id = requireBareId(id, "id");
const control = this.clients.control(toClientConfig(options));
const current = await control.send(
new GetConfigurationBundleCommand({ bundleId: id, branchName: update.branchName }),
Expand Down Expand Up @@ -1364,7 +1388,7 @@ export class EvalClient implements CoreEvalClient {
): Promise<DeleteConfigurationBundleResponse> {
return this.clients
.control(toClientConfig(options))
.send(new DeleteConfigurationBundleCommand({ bundleId: id }));
.send(new DeleteConfigurationBundleCommand({ bundleId: requireBareId(id, "id") }));
}

async listConfigurationBundleVersions(
Expand All @@ -1373,9 +1397,13 @@ export class EvalClient implements CoreEvalClient {
maxResults: number | undefined,
options: CoreOptions,
): Promise<ListConfigurationBundleVersionsResponse> {
return this.clients
.control(toClientConfig(options))
.send(new ListConfigurationBundleVersionsCommand({ bundleId: id, nextToken, maxResults }));
return this.clients.control(toClientConfig(options)).send(
new ListConfigurationBundleVersionsCommand({
bundleId: requireBareId(id, "id"),
nextToken,
maxResults,
}),
);
}

async createDataset(
Expand Down
Loading