From 89fd944b157e58644b143f05f697adc0a6cee684 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Thu, 15 Oct 2026 21:26:35 +0000 Subject: [PATCH 1/3] fix(runtime,gateway): reject ARNs passed as --id on get/list commands runtime get/version/endpoint and gateway get/target/rule/connector took any string as the ID, so an ARN reached the service and came back as a misleading AccessDeniedException. invoke/exec/logs already used runtimeIdSchema / gatewayIdSchema to reject ARNs client-side; use them here too. --- src/handlers/gateway/connector/get/index.tsx | 3 ++- src/handlers/gateway/connector/list/index.tsx | 3 ++- src/handlers/gateway/gateway.test.tsx | 17 +++++++++++++++++ src/handlers/gateway/get/index.tsx | 4 ++-- src/handlers/gateway/rule/get/index.tsx | 3 ++- src/handlers/gateway/rule/list/index.tsx | 3 ++- src/handlers/gateway/target/get/index.tsx | 3 ++- src/handlers/gateway/target/list/index.tsx | 3 ++- src/handlers/runtime/endpoint/get/index.tsx | 3 ++- src/handlers/runtime/endpoint/list/index.tsx | 3 ++- src/handlers/runtime/get/index.tsx | 4 ++-- src/handlers/runtime/runtime.test.tsx | 12 ++++++++++++ src/handlers/runtime/version/get/index.tsx | 3 ++- src/handlers/runtime/version/list/index.tsx | 3 ++- 14 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/handlers/gateway/connector/get/index.tsx b/src/handlers/gateway/connector/get/index.tsx index 890eed8a4b..0331ca5f4d 100644 --- a/src/handlers/gateway/connector/get/index.tsx +++ b/src/handlers/gateway/connector/get/index.tsx @@ -1,3 +1,4 @@ +import { gatewayIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createGetGatewayConnectorHandler = (core: Core) => name: "get", description: "get a connector-backed Gateway Target", flags: [ - flag("gateway-id", "the ID of the Gateway", z.string().min(1)), + flag("gateway-id", "the ID of the Gateway", gatewayIdSchema.min(1)), flag("id", "the ID of the connector-backed Gateway Target", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/gateway/connector/list/index.tsx b/src/handlers/gateway/connector/list/index.tsx index fd5cdb1b7c..3526de764a 100644 --- a/src/handlers/gateway/connector/list/index.tsx +++ b/src/handlers/gateway/connector/list/index.tsx @@ -1,3 +1,4 @@ +import { gatewayIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createListGatewayConnectorsHandler = (core: Core) => name: "list", description: "list connectors configured for an AgentCore Gateway", flags: [ - flag("gateway-id", "the ID of the Gateway", z.string().min(1)), + flag("gateway-id", "the ID of the Gateway", gatewayIdSchema.min(1)), 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()), ], diff --git a/src/handlers/gateway/gateway.test.tsx b/src/handlers/gateway/gateway.test.tsx index 360309f405..f2ea7f374f 100644 --- a/src/handlers/gateway/gateway.test.tsx +++ b/src/handlers/gateway/gateway.test.tsx @@ -134,6 +134,23 @@ describe("gateway validation", () => { }, ); + test.each([ + ["Gateway get", ["gateway", "get", "--id"]], + ["Target list", ["gateway", "target", "list", "--gateway-id"]], + ["Rule list", ["gateway", "rule", "list", "--gateway-id"]], + ["Connector list", ["gateway", "connector", "list", "--gateway-id"]], + ] as const)("rejects a Gateway ARN for %s before calling Core", async (_name, args) => { + const core = new TestCoreClient(); + const arn = `arn:aws:bedrock-agentcore:${REGION}:123456789012:gateway/${GATEWAY_ID}`; + + await expectError( + run([...args, arn], core), + /must be a Gateway ID, not an ARN/, + InputValidationError, + ); + expect(core.gateway.calls).toEqual([]); + }); + test("rejects a non-numeric max-results value before calling Core", async () => { const core = new TestCoreClient(); diff --git a/src/handlers/gateway/get/index.tsx b/src/handlers/gateway/get/index.tsx index 91f7ecc679..76310a006e 100644 --- a/src/handlers/gateway/get/index.tsx +++ b/src/handlers/gateway/get/index.tsx @@ -1,4 +1,4 @@ -import z from "zod"; +import { gatewayIdSchema } from "../invoke/request"; import { createHandler, flag } from "../../../router"; import { JsonRendererKey } from "../../../tui"; import type { Core } from "../../types"; @@ -8,7 +8,7 @@ export const createGetGatewayHandler = (core: Core) => createHandler({ name: "get", description: "get an AgentCore Gateway", - flags: [flag("id", "the ID of the Gateway", z.string().min(1))], + flags: [flag("id", "the ID of the Gateway", gatewayIdSchema.min(1))], handle: async (ctx, flags) => { ctx .require(JsonRendererKey) diff --git a/src/handlers/gateway/rule/get/index.tsx b/src/handlers/gateway/rule/get/index.tsx index 25552bcc7a..898e1949ef 100644 --- a/src/handlers/gateway/rule/get/index.tsx +++ b/src/handlers/gateway/rule/get/index.tsx @@ -1,3 +1,4 @@ +import { gatewayIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createGetGatewayRuleHandler = (core: Core) => name: "get", description: "get a Gateway Rule", flags: [ - flag("gateway-id", "the ID of the Gateway", z.string().min(1)), + flag("gateway-id", "the ID of the Gateway", gatewayIdSchema.min(1)), flag("rule-id", "the ID of the Gateway Rule", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/gateway/rule/list/index.tsx b/src/handlers/gateway/rule/list/index.tsx index b68a9f66af..2b1e71a89d 100644 --- a/src/handlers/gateway/rule/list/index.tsx +++ b/src/handlers/gateway/rule/list/index.tsx @@ -1,3 +1,4 @@ +import { gatewayIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createListGatewayRulesHandler = (core: Core) => name: "list", description: "list Rules for an AgentCore Gateway", flags: [ - flag("gateway-id", "the ID of the Gateway", z.string().min(1)), + flag("gateway-id", "the ID of the Gateway", gatewayIdSchema.min(1)), 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()), ], diff --git a/src/handlers/gateway/target/get/index.tsx b/src/handlers/gateway/target/get/index.tsx index 8bce43eef1..c84726c8e6 100644 --- a/src/handlers/gateway/target/get/index.tsx +++ b/src/handlers/gateway/target/get/index.tsx @@ -1,3 +1,4 @@ +import { gatewayIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createGetGatewayTargetHandler = (core: Core) => name: "get", description: "get a Gateway Target", flags: [ - flag("gateway-id", "the ID of the Gateway", z.string().min(1)), + flag("gateway-id", "the ID of the Gateway", gatewayIdSchema.min(1)), flag("target-id", "the ID of the Gateway Target", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/gateway/target/list/index.tsx b/src/handlers/gateway/target/list/index.tsx index 3df000a9b2..73122a8896 100644 --- a/src/handlers/gateway/target/list/index.tsx +++ b/src/handlers/gateway/target/list/index.tsx @@ -1,3 +1,4 @@ +import { gatewayIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createListGatewayTargetsHandler = (core: Core) => name: "list", description: "list Targets for an AgentCore Gateway", flags: [ - flag("gateway-id", "the ID of the Gateway", z.string().min(1)), + flag("gateway-id", "the ID of the Gateway", gatewayIdSchema.min(1)), 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()), ], diff --git a/src/handlers/runtime/endpoint/get/index.tsx b/src/handlers/runtime/endpoint/get/index.tsx index ef48ec264d..c2e5abbb65 100644 --- a/src/handlers/runtime/endpoint/get/index.tsx +++ b/src/handlers/runtime/endpoint/get/index.tsx @@ -1,3 +1,4 @@ +import { runtimeIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createGetRuntimeEndpointHandler = (core: Core) => name: "get", description: "get a Runtime endpoint", flags: [ - flag("id", "the ID of the Runtime", z.string().min(1)), + flag("id", "the ID of the Runtime", runtimeIdSchema.min(1)), flag("qualifier", "the endpoint name (qualifier)", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/runtime/endpoint/list/index.tsx b/src/handlers/runtime/endpoint/list/index.tsx index b2798b3cd9..d34d37e6bd 100644 --- a/src/handlers/runtime/endpoint/list/index.tsx +++ b/src/handlers/runtime/endpoint/list/index.tsx @@ -1,3 +1,4 @@ +import { runtimeIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createListRuntimeEndpointsHandler = (core: Core) => name: "list", description: "list a Runtime's endpoints", flags: [ - flag("id", "the ID of the Runtime", z.string().min(1)), + flag("id", "the ID of the Runtime", runtimeIdSchema.min(1)), 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()), ], diff --git a/src/handlers/runtime/get/index.tsx b/src/handlers/runtime/get/index.tsx index 2b3846f2f3..9b12e47879 100644 --- a/src/handlers/runtime/get/index.tsx +++ b/src/handlers/runtime/get/index.tsx @@ -1,4 +1,4 @@ -import z from "zod"; +import { runtimeIdSchema } from "../invoke/request"; import { createHandler, flag } from "../../../router"; import { JsonRendererKey } from "../../../tui"; import type { Core } from "../../types"; @@ -8,7 +8,7 @@ export const createGetRuntimeHandler = (core: Core) => createHandler({ name: "get", description: "get an AgentCore Runtime", - flags: [flag("id", "the ID of the Runtime", z.string().min(1))], + flags: [flag("id", "the ID of the Runtime", runtimeIdSchema.min(1))], handle: async (ctx, flags) => { ctx .require(JsonRendererKey) diff --git a/src/handlers/runtime/runtime.test.tsx b/src/handlers/runtime/runtime.test.tsx index b747d9393a..d9695c62fd 100644 --- a/src/handlers/runtime/runtime.test.tsx +++ b/src/handlers/runtime/runtime.test.tsx @@ -351,6 +351,18 @@ describe("runtime read-only commands", () => { }, ); + test.each([["runtime get"], ["runtime version list"], ["runtime endpoint list"]] as const)( + "rejects a Runtime ARN passed as --id for `%s`", + async (label) => { + const arn = `arn:aws:bedrock-agentcore:${REGION}:123456789012:runtime/${FIXTURE_RUNTIME_ID}`; + await expectError( + run([...label.split(" "), "--id", arn, "--json"]), + /must be a Runtime ID, not an ARN/, + InputValidationError, + ); + }, + ); + test("propagates ResourceNotFoundException from Runtime get", async () => { await expect(run(["runtime", "get", "--id", MISSING_RUNTIME_ID])).rejects.toMatchObject({ name: "ResourceNotFoundException", diff --git a/src/handlers/runtime/version/get/index.tsx b/src/handlers/runtime/version/get/index.tsx index 7ffb2e86ee..64c9073ddf 100644 --- a/src/handlers/runtime/version/get/index.tsx +++ b/src/handlers/runtime/version/get/index.tsx @@ -1,3 +1,4 @@ +import { runtimeIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createGetRuntimeVersionHandler = (core: Core) => name: "get", description: "get a specific Runtime version", flags: [ - flag("id", "the ID of the Runtime", z.string().min(1)), + flag("id", "the ID of the Runtime", runtimeIdSchema.min(1)), flag("version", "the Runtime version to get", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/runtime/version/list/index.tsx b/src/handlers/runtime/version/list/index.tsx index 6d24f9ea9e..726cf7e807 100644 --- a/src/handlers/runtime/version/list/index.tsx +++ b/src/handlers/runtime/version/list/index.tsx @@ -1,3 +1,4 @@ +import { runtimeIdSchema } from "../../invoke/request"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createListRuntimeVersionsHandler = (core: Core) => name: "list", description: "list a Runtime's versions", flags: [ - flag("id", "the ID of the Runtime", z.string().min(1)), + flag("id", "the ID of the Runtime", runtimeIdSchema.min(1)), 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()), ], From 97e86308c80454a6932eec2bdb6c101b0946d9bb Mon Sep 17 00:00:00 2001 From: jariy17 Date: Thu, 15 Oct 2026 21:28:24 +0000 Subject: [PATCH 2/3] fix(memory): reject ARNs passed as --id on memory read commands Same gap as runtime/gateway: a Memory ARN reached the service and surfaced as a misleading AccessDeniedException. Reject it client-side. --- src/handlers/memory/actor/list/index.tsx | 3 ++- src/handlers/memory/event/get/index.tsx | 3 ++- src/handlers/memory/event/list/index.tsx | 3 ++- src/handlers/memory/get/index.tsx | 3 ++- src/handlers/memory/idSchema.ts | 6 ++++++ src/handlers/memory/memory.test.tsx | 12 ++++++++++++ src/handlers/memory/record/get/index.tsx | 3 ++- src/handlers/memory/record/list/index.tsx | 3 ++- src/handlers/memory/session/list/index.tsx | 3 ++- 9 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 src/handlers/memory/idSchema.ts diff --git a/src/handlers/memory/actor/list/index.tsx b/src/handlers/memory/actor/list/index.tsx index 2b08aeeff1..8a59843e62 100644 --- a/src/handlers/memory/actor/list/index.tsx +++ b/src/handlers/memory/actor/list/index.tsx @@ -1,3 +1,4 @@ +import { memoryIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createListMemoryActorsHandler = (core: Core) => name: "list", description: "list actors in an AgentCore Memory", flags: [ - flag("id", "the ID of the Memory", z.string().min(1)), + flag("id", "the ID of the Memory", memoryIdSchema), flag("max-results", "maximum number of actors to return", z.number().optional()), flag("next-token", "pagination token returned by a previous request", z.string().optional()), ], diff --git a/src/handlers/memory/event/get/index.tsx b/src/handlers/memory/event/get/index.tsx index df014ab23a..5397770dec 100644 --- a/src/handlers/memory/event/get/index.tsx +++ b/src/handlers/memory/event/get/index.tsx @@ -1,3 +1,4 @@ +import { memoryIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import type { Core } from "../../../types"; @@ -9,7 +10,7 @@ export const createGetMemoryEventHandler = (core: Core) => name: "get", description: "get an AgentCore Memory Event", flags: [ - flag("id", "the ID of the Memory", z.string().min(1)), + flag("id", "the ID of the Memory", memoryIdSchema), flag("actor-id", "the ID of the actor", z.string().min(1)), flag("session-id", "the session ID", z.string().min(1)), flag("event-id", "the event ID", z.string().min(1)), diff --git a/src/handlers/memory/event/list/index.tsx b/src/handlers/memory/event/list/index.tsx index dc6c853b81..dcfbe59b5f 100644 --- a/src/handlers/memory/event/list/index.tsx +++ b/src/handlers/memory/event/list/index.tsx @@ -1,3 +1,4 @@ +import { memoryIdSchema } from "../../idSchema"; import z from "zod"; import { InputValidationError } from "../../../../errors"; import { createHandler, flag } from "../../../../router"; @@ -11,7 +12,7 @@ export const createListMemoryEventsHandler = (core: Core) => name: "list", description: "list AgentCore Memory events", flags: [ - flag("id", "the ID of the Memory", z.string().min(1)), + flag("id", "the ID of the Memory", memoryIdSchema), flag("actor-id", "the ID of the actor", z.string().min(1)), flag("session-id", "the session ID", z.string().min(1)), flag("include-payloads", "includes event payloads in the response", z.boolean().optional()), diff --git a/src/handlers/memory/get/index.tsx b/src/handlers/memory/get/index.tsx index 69e2517bed..f45449ffed 100644 --- a/src/handlers/memory/get/index.tsx +++ b/src/handlers/memory/get/index.tsx @@ -1,3 +1,4 @@ +import { memoryIdSchema } from "../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../router"; import { JsonRendererKey } from "../../../tui"; @@ -11,7 +12,7 @@ export const createGetMemoryHandler = (core: Core) => name: "get", description: "get an AgentCore Memory", flags: [ - flag("id", "the ID of the Memory", z.string().min(1)), + flag("id", "the ID of the Memory", memoryIdSchema), flag("view", "response view", z.enum(MEMORY_VIEWS).optional()), ], handle: async (ctx, flags) => { diff --git a/src/handlers/memory/idSchema.ts b/src/handlers/memory/idSchema.ts new file mode 100644 index 0000000000..4f2c4d2bb8 --- /dev/null +++ b/src/handlers/memory/idSchema.ts @@ -0,0 +1,6 @@ +import z from "zod"; + +export const memoryIdSchema = z + .string() + .min(1) + .refine((value) => !value.startsWith("arn:"), "must be a Memory ID, not an ARN"); diff --git a/src/handlers/memory/memory.test.tsx b/src/handlers/memory/memory.test.tsx index f7c8c9f586..e2a03e9186 100644 --- a/src/handlers/memory/memory.test.tsx +++ b/src/handlers/memory/memory.test.tsx @@ -238,6 +238,18 @@ describe("memory read-only commands", () => { await expectError(run(["memory", "get", "--json"]), /--id/, InputValidationError); }); + test.each([["get"], ["session list"], ["record list"]] as const)( + "rejects a Memory ARN passed as --id for `memory %s`", + async (sub) => { + const arn = `arn:aws:bedrock-agentcore:${REGION}:123456789012:memory/${FIXTURE_MEMORY_ID}`; + await expectError( + run(["memory", ...sub.split(" "), "--id", arn, "--json"]), + /must be a Memory ID, not an ARN/, + InputValidationError, + ); + }, + ); + test("rejects an unsupported response view", async () => { await expect( run(["memory", "get", "--id", FIXTURE_MEMORY_ID, "--view", "summary"]), diff --git a/src/handlers/memory/record/get/index.tsx b/src/handlers/memory/record/get/index.tsx index cdcd0bfbd6..fe4e0535f4 100644 --- a/src/handlers/memory/record/get/index.tsx +++ b/src/handlers/memory/record/get/index.tsx @@ -1,3 +1,4 @@ +import { memoryIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createGetMemoryRecordHandler = (core: Core) => name: "get", description: "get an AgentCore Memory record", flags: [ - flag("id", "the ID of the Memory", z.string().min(1)), + flag("id", "the ID of the Memory", memoryIdSchema), flag("record-id", "the ID of the Memory record", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/memory/record/list/index.tsx b/src/handlers/memory/record/list/index.tsx index 2ad16e081b..58db5fc1e2 100644 --- a/src/handlers/memory/record/list/index.tsx +++ b/src/handlers/memory/record/list/index.tsx @@ -1,3 +1,4 @@ +import { memoryIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -10,7 +11,7 @@ export const createListMemoryRecordsHandler = (core: Core) => name: "list", description: "list AgentCore Memory records", flags: [ - flag("id", "the ID of the Memory", z.string().min(1)), + flag("id", "the ID of the Memory", memoryIdSchema), flag("namespace", "filter by namespace prefix", z.string().optional()), flag("namespace-path", "filter by namespace hierarchy", z.string().optional()), flag("strategy-id", "filter by Memory strategy ID", z.string().optional()), diff --git a/src/handlers/memory/session/list/index.tsx b/src/handlers/memory/session/list/index.tsx index 3c7ffd43b7..b68e59c7b3 100644 --- a/src/handlers/memory/session/list/index.tsx +++ b/src/handlers/memory/session/list/index.tsx @@ -1,3 +1,4 @@ +import { memoryIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; @@ -9,7 +10,7 @@ export const createListMemorySessionsHandler = (core: Core) => name: "list", description: "list sessions in an AgentCore Memory", flags: [ - flag("id", "the ID of the Memory", z.string().min(1)), + flag("id", "the ID of the Memory", memoryIdSchema), flag("actor-id", "the ID of the actor", z.string().min(1)), flag("max-results", "maximum number of sessions to return", z.number().optional()), flag("next-token", "pagination token returned by a previous request", z.string().optional()), From 65e664bc53bf511404f48980ddccbb76a95857cc Mon Sep 17 00:00:00 2001 From: jariy17 Date: Thu, 15 Oct 2026 21:31:20 +0000 Subject: [PATCH 3/3] fix(harness): accept full-length harness IDs and reject ARNs with a clear error Every harness --id flag capped input at 48 characters, but 48 is the harness name limit; the service's ID is a <=40-char name plus a 10-char suffix (51 max), so valid IDs of 49-51 characters were rejected client-side. Passing an ARN also failed with an opaque length error. Share one schema across harness commands. --- src/handlers/harness/delete/index.tsx | 3 ++- src/handlers/harness/endpoint/create/index.tsx | 3 ++- src/handlers/harness/endpoint/delete/index.tsx | 3 ++- src/handlers/harness/endpoint/get/index.tsx | 3 ++- src/handlers/harness/endpoint/list/index.tsx | 3 ++- src/handlers/harness/endpoint/update/index.tsx | 3 ++- src/handlers/harness/exec/index.tsx | 3 ++- src/handlers/harness/get/index.tsx | 4 ++-- src/handlers/harness/idSchema.test.ts | 18 ++++++++++++++++++ src/handlers/harness/idSchema.ts | 8 ++++++++ src/handlers/harness/invoke/index.tsx | 3 ++- src/handlers/harness/logs/index.tsx | 3 ++- src/handlers/harness/shell/index.tsx | 3 ++- src/handlers/harness/traces/get/index.tsx | 3 ++- src/handlers/harness/traces/list/index.tsx | 3 ++- src/handlers/harness/update/index.tsx | 3 ++- src/handlers/harness/version/get/index.tsx | 3 ++- src/handlers/harness/version/list/index.tsx | 3 ++- 18 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 src/handlers/harness/idSchema.test.ts create mode 100644 src/handlers/harness/idSchema.ts diff --git a/src/handlers/harness/delete/index.tsx b/src/handlers/harness/delete/index.tsx index 86cd01eb90..3bf7062f95 100644 --- a/src/handlers/harness/delete/index.tsx +++ b/src/handlers/harness/delete/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../router"; import type { Core } from "../../types.tsx"; @@ -9,7 +10,7 @@ export const createDeleteHarnessHandler = (core: Core) => name: "delete", description: "delete a harness", flags: [ - flag("id", "the ID of the harness to delete", z.string().min(1).max(48)), + flag("id", "the ID of the harness to delete", harnessIdSchema), flag( "delete-managed-memory", "whether to also delete the managed Memory (default true; pass false to keep it)", diff --git a/src/handlers/harness/endpoint/create/index.tsx b/src/handlers/harness/endpoint/create/index.tsx index 941af30e57..a67aab42e0 100644 --- a/src/handlers/harness/endpoint/create/index.tsx +++ b/src/handlers/harness/endpoint/create/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import type { Core } from "../../../types.tsx"; @@ -9,7 +10,7 @@ export const createCreateEndpointHandler = (core: Core) => name: "create", description: "create a harness endpoint", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("name", "the name of the endpoint", z.string().min(1)), flag( "target-version", diff --git a/src/handlers/harness/endpoint/delete/index.tsx b/src/handlers/harness/endpoint/delete/index.tsx index 601c8a4990..147460b0aa 100644 --- a/src/handlers/harness/endpoint/delete/index.tsx +++ b/src/handlers/harness/endpoint/delete/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import type { Core } from "../../../types.tsx"; @@ -9,7 +10,7 @@ export const createDeleteEndpointHandler = (core: Core) => name: "delete", description: "delete a harness endpoint", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("qualifier", "the endpoint name (qualifier)", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/harness/endpoint/get/index.tsx b/src/handlers/harness/endpoint/get/index.tsx index b2896f6cd4..d2cdbc9427 100644 --- a/src/handlers/harness/endpoint/get/index.tsx +++ b/src/handlers/harness/endpoint/get/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import type { Core } from "../../../types.tsx"; @@ -9,7 +10,7 @@ export const createGetEndpointHandler = (core: Core) => name: "get", description: "get a harness endpoint", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("qualifier", "the endpoint name (qualifier)", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/harness/endpoint/list/index.tsx b/src/handlers/harness/endpoint/list/index.tsx index f593505f74..db742641e7 100644 --- a/src/handlers/harness/endpoint/list/index.tsx +++ b/src/handlers/harness/endpoint/list/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import type { Core } from "../../../types.tsx"; @@ -9,7 +10,7 @@ export const createListEndpointsHandler = (core: Core) => name: "list", description: "list a harness's endpoints", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("next-token", "next token to use on paginated", z.string().optional()), flag("max-results", "max number of items to return", z.number().optional()), ], diff --git a/src/handlers/harness/endpoint/update/index.tsx b/src/handlers/harness/endpoint/update/index.tsx index 2e24c662f8..56e826e669 100644 --- a/src/handlers/harness/endpoint/update/index.tsx +++ b/src/handlers/harness/endpoint/update/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import type { Core } from "../../../types.tsx"; @@ -9,7 +10,7 @@ export const createUpdateEndpointHandler = (core: Core) => name: "update", description: "update a harness endpoint", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("qualifier", "the endpoint name (qualifier)", z.string().min(1)), flag("target-version", "the harness version the endpoint points to", z.string().optional()), ], diff --git a/src/handlers/harness/exec/index.tsx b/src/handlers/harness/exec/index.tsx index bcae691ebe..f243c9d42a 100644 --- a/src/handlers/harness/exec/index.tsx +++ b/src/handlers/harness/exec/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../idSchema"; import z from "zod"; import { createHandler, flag, PathKey } from "../../../router"; import type { AppIO } from "../../../io"; @@ -13,7 +14,7 @@ export const createExecHarnessHandler = (core: Core, io: AppIO) => name: "exec", description: "run a shell command in a harness", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("command", "the shell command to run", z.string().optional()), flag( "session-id", diff --git a/src/handlers/harness/get/index.tsx b/src/handlers/harness/get/index.tsx index ced2c4f4f4..e86dab903b 100644 --- a/src/handlers/harness/get/index.tsx +++ b/src/handlers/harness/get/index.tsx @@ -1,4 +1,4 @@ -import z from "zod"; +import { harnessIdSchema } from "../idSchema"; import { createHandler, flag } from "../../../router"; import type { Core } from "../../types.tsx"; import { coreOptsFromCtx } from "../../utils.tsx"; @@ -8,7 +8,7 @@ export const createGetHarnessHandler = (core: Core) => createHandler({ name: "get", description: "get a harness", - flags: [flag("id", "the ID of the harness", z.string().min(1).max(48))], + flags: [flag("id", "the ID of the harness", harnessIdSchema)], handle: async (ctx, flags) => { const harness = await core.harness.getHarness(flags["id"], coreOptsFromCtx(ctx)); ctx.require(JsonRendererKey).renderJson(harness); diff --git a/src/handlers/harness/idSchema.test.ts b/src/handlers/harness/idSchema.test.ts new file mode 100644 index 0000000000..738ce39fbf --- /dev/null +++ b/src/handlers/harness/idSchema.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test"; +import { harnessIdSchema } from "./idSchema"; + +describe("harnessIdSchema", () => { + test("accepts the longest ID the service issues (40-char name + 10-char suffix)", () => { + const id = `${"a".repeat(40)}-${"B".repeat(10)}`; + expect(id).toHaveLength(51); + expect(harnessIdSchema.safeParse(id).success).toBe(true); + }); + + test("rejects an ARN with an ARN-specific message", () => { + const result = harnessIdSchema.safeParse( + "arn:aws:bedrock-agentcore:us-west-2:123456789012:harness/MyHarness-abcdefghij", + ); + expect(result.success).toBe(false); + expect(result.error?.issues[0]?.message).toBe("must be a harness ID, not an ARN"); + }); +}); diff --git a/src/handlers/harness/idSchema.ts b/src/handlers/harness/idSchema.ts new file mode 100644 index 0000000000..7f70e1e4ba --- /dev/null +++ b/src/handlers/harness/idSchema.ts @@ -0,0 +1,8 @@ +import z from "zod"; + +// The service's harnessId is a <=40-char name plus "-" and a 10-char suffix: 51 max. +export const harnessIdSchema = z + .string() + .min(1) + .refine((value) => !value.startsWith("arn:"), "must be a harness ID, not an ARN") + .max(51); diff --git a/src/handlers/harness/invoke/index.tsx b/src/handlers/harness/invoke/index.tsx index fa448b9602..09c2d1995a 100644 --- a/src/handlers/harness/invoke/index.tsx +++ b/src/handlers/harness/invoke/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../idSchema"; import z from "zod"; import { createHandler, flag, PathKey } from "../../../router"; import type { AppIO } from "../../../io"; @@ -11,7 +12,7 @@ import { invokeHarnessTurn } from "./operation.ts"; import { HarnessInvokeLaunchContextKey } from "./launchContext"; export const invokeHarnessFlags = [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("prompt", "the message to send to the harness", z.string().optional()), flag( "user-id", diff --git a/src/handlers/harness/logs/index.tsx b/src/handlers/harness/logs/index.tsx index 075429f78e..2d24c064e5 100644 --- a/src/handlers/harness/logs/index.tsx +++ b/src/handlers/harness/logs/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../idSchema"; import z from "zod"; import { DEFAULT_ENDPOINT_QUALIFIER, runtimeLogGroup } from "../../../core/observability/index"; import type { AppIO } from "../../../io"; @@ -7,7 +8,7 @@ import type { Core } from "../../types"; import { coreOptsFromCtx } from "../../utils"; const harnessFlags = [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("qualifier", "the harness endpoint qualifier", z.string().min(1).optional()), ] as const; diff --git a/src/handlers/harness/shell/index.tsx b/src/handlers/harness/shell/index.tsx index c0b517b7ca..9e118a940b 100644 --- a/src/handlers/harness/shell/index.tsx +++ b/src/handlers/harness/shell/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../idSchema"; import z from "zod"; import { InputValidationError } from "../../../errors"; import type { AppIO } from "../../../io"; @@ -14,7 +15,7 @@ export const createHarnessShellHandler = (core: Core, io: AppIO) => name: "shell", description: "open an interactive shell in a harness", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("qualifier", "the harness endpoint qualifier", z.string().min(1).optional()), flag("session-id", "the Runtime session ID to use", z.string().min(33).max(256).optional()), flag("bearer-token", "the CUSTOM_JWT bearer token", z.string().optional(), { diff --git a/src/handlers/harness/traces/get/index.tsx b/src/handlers/harness/traces/get/index.tsx index 4133a62004..8e5e4e6f35 100644 --- a/src/handlers/harness/traces/get/index.tsx +++ b/src/handlers/harness/traces/get/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { DEFAULT_ENDPOINT_QUALIFIER, runtimeLogGroup } from "../../../../core/observability/index"; import type { AppIO } from "../../../../io"; @@ -8,7 +9,7 @@ import type { Core } from "../../../types"; import { coreOptsFromCtx } from "../../../utils"; const harnessFlags = [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("qualifier", "the harness endpoint qualifier", z.string().min(1).optional()), ] as const; diff --git a/src/handlers/harness/traces/list/index.tsx b/src/handlers/harness/traces/list/index.tsx index 3d27e0ad3e..53cfea2330 100644 --- a/src/handlers/harness/traces/list/index.tsx +++ b/src/handlers/harness/traces/list/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { DEFAULT_ENDPOINT_QUALIFIER, runtimeLogGroup } from "../../../../core/observability/index"; import type { AppIO } from "../../../../io"; @@ -7,7 +8,7 @@ import type { Core } from "../../../types"; import { coreOptsFromCtx } from "../../../utils"; const harnessFlags = [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("qualifier", "the harness endpoint qualifier", z.string().min(1).optional()), ] as const; diff --git a/src/handlers/harness/update/index.tsx b/src/handlers/harness/update/index.tsx index a48df3a5c8..84b8a67312 100644 --- a/src/handlers/harness/update/index.tsx +++ b/src/handlers/harness/update/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../idSchema"; import z from "zod"; import type { AuthorizerConfiguration, @@ -29,7 +30,7 @@ export const createUpdateHarnessHandler = (core: Core) => name: "update", description: "update a harness (creates a new version)", flags: [ - flag("id", "the ID of the harness to update", z.string().min(1).max(48)), + flag("id", "the ID of the harness to update", harnessIdSchema), flag("execution-role-arn", "IAM role the harness assumes", z.string().optional(), { help: parameterHelp.executionRoleArn, }), diff --git a/src/handlers/harness/version/get/index.tsx b/src/handlers/harness/version/get/index.tsx index c44e1d69e8..a58e61cf34 100644 --- a/src/handlers/harness/version/get/index.tsx +++ b/src/handlers/harness/version/get/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import type { Core } from "../../../types.tsx"; @@ -9,7 +10,7 @@ export const createGetVersionHandler = (core: Core) => name: "get", description: "get a specific version of a harness", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("version", "the harness version to get", z.string().min(1)), ], handle: async (ctx, flags) => { diff --git a/src/handlers/harness/version/list/index.tsx b/src/handlers/harness/version/list/index.tsx index 45a2592e22..4ada65030e 100644 --- a/src/handlers/harness/version/list/index.tsx +++ b/src/handlers/harness/version/list/index.tsx @@ -1,3 +1,4 @@ +import { harnessIdSchema } from "../../idSchema"; import z from "zod"; import { createHandler, flag } from "../../../../router"; import type { Core } from "../../../types.tsx"; @@ -9,7 +10,7 @@ export const createListVersionsHandler = (core: Core) => name: "list", description: "list a harness's versions", flags: [ - flag("id", "the ID of the harness", z.string().min(1).max(48)), + flag("id", "the ID of the harness", harnessIdSchema), flag("next-token", "next token to use on paginated", z.string().optional()), flag("max-results", "max number of items to return", z.number().optional()), ],