From 6452e7687070f63911c3aebb1b341d75a6ce13bc Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Wed, 2 Sep 2026 14:57:18 -0400 Subject: [PATCH 1/5] feat(core): PolicyClient generates Cedar from a prompt against a deployed gateway --- src/core/index.tsx | 3 + src/core/policy.test.ts | 184 ++++++++++++++++++++++++++ src/core/policy.tsx | 138 +++++++++++++++++++ src/handlers/gateway/policy/types.tsx | 36 +++++ src/handlers/types.tsx | 2 + src/testing/TestCoreClient.tsx | 29 ++++ 6 files changed, 392 insertions(+) create mode 100644 src/core/policy.test.ts create mode 100644 src/core/policy.tsx create mode 100644 src/handlers/gateway/policy/types.tsx diff --git a/src/core/index.tsx b/src/core/index.tsx index 7207457fb..ef821e37e 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -7,6 +7,7 @@ import { GatewayClient } from "./gateway"; import { HarnessClient } from "./harness"; import { IdentityClient } from "./identity"; import { MemoryClient } from "./memory"; +import { PolicyClient } from "./policy"; import { ObservabilityClient } from "./observability"; import { RuntimeClient } from "./runtime"; import { FsReadWriteJson } from "../io"; @@ -74,6 +75,7 @@ export class CoreClient implements AwsClients { readonly gateway: GatewayClient; readonly eval: EvalClient; readonly observability: ObservabilityClient; + readonly policy: PolicyClient; readonly projectManager: ProjectManager; readonly describeBedrockAgent: DescribeBedrockAgent; @@ -89,6 +91,7 @@ export class CoreClient implements AwsClients { this.fetch = fetch; this.runtime = new RuntimeClient(this, fetch, this.logger.child({ module: "runtime" })); this.gateway = new GatewayClient(this, fetch, this.logger.child({ module: "gateway" })); + this.policy = new PolicyClient(this, this.logger.child({ module: "policy" })); // EvalClient shares the injected fetch: dataset content is served from a // presigned S3 URL, outside the SDK seam the other operations use. The logger // is used for batch-evaluation result-log diagnostics. diff --git a/src/core/policy.test.ts b/src/core/policy.test.ts new file mode 100644 index 000000000..8c38bd098 --- /dev/null +++ b/src/core/policy.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, test } from "bun:test"; +import { + GetGatewayCommand, + GetPolicyGenerationCommand, + ListPolicyGenerationAssetsCommand, + StartPolicyGenerationCommand, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { InputValidationError, NetworkingError } from "../errors"; +import type { ProgressEvent } from "../tui/progress"; +import { createSilentLogger } from "../testing"; +import { PolicyClient } from "./policy"; +import type { AwsClients } from "./types"; + +const options = { region: "us-west-2" }; +const GATEWAY_ARN = "arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/gw-1"; +const ATTACHED_ENGINE_ARN = + "arn:aws:bedrock-agentcore:us-west-2:111122223333:policy-engine/pe-attached"; +const EXPLICIT_ENGINE_ARN = + "arn:aws:bedrock-agentcore:us-west-2:111122223333:policy-engine/pe-explicit"; +const FORBID = "forbid (principal, action, resource is AgentCore::Gateway);"; +const PERMIT = "permit (principal, action, resource is AgentCore::Gateway);"; + +type Responses = { + getGateway?: unknown; + start?: unknown; + get?: unknown; + assets?: unknown[]; +}; + +const HAPPY: Responses = { + getGateway: { gatewayArn: GATEWAY_ARN, policyEngineConfiguration: { arn: ATTACHED_ENGINE_ARN } }, + start: { policyGenerationId: "gen-1" }, + get: { status: "GENERATED" }, + assets: [ + { + policyGenerationAssets: [ + { + definition: { cedar: { statement: FORBID } }, + findings: [{ type: "DENY_ALL", description: "denies every request" }], + }, + ], + nextToken: "page-2", + }, + { + policyGenerationAssets: [{ definition: { policy: { statement: PERMIT } } }], + }, + ], +}; + +function policyClient(responses: Responses, sent: { input: unknown }[] = []) { + const assetPages = [...(responses.assets ?? [])]; + const control = { + send: async (command: { input: unknown }) => { + sent.push(command); + if (command instanceof GetGatewayCommand) return responses.getGateway; + if (command instanceof StartPolicyGenerationCommand) return responses.start; + if (command instanceof GetPolicyGenerationCommand) return responses.get; + if (command instanceof ListPolicyGenerationAssetsCommand) return assetPages.shift(); + throw new Error(`unexpected command ${command.constructor.name}`); + }, + }; + return new PolicyClient( + { control: () => control } as unknown as AwsClients, + createSilentLogger(), + { maxWaitTime: 2, minDelay: 1, maxDelay: 1 }, + ); +} + +async function drain(generator: AsyncGenerator) { + const steps: string[] = []; + let next = await generator.next(); + while (!next.done) { + if (next.value.type === "step") steps.push(next.value.message); + next = await generator.next(); + } + return { steps, result: next.value }; +} + +describe("PolicyClient.generatePolicy", () => { + test.each([ + ["the attached engine when none is given", undefined, "pe-attached"], + ["an explicit engine ARN over the attached one", EXPLICIT_ENGINE_ARN, "pe-explicit"], + ])("generates against %s", async (_label, policyEngineId, expectedEngineId) => { + const sent: { input: unknown }[] = []; + const { steps, result } = await drain( + policyClient(HAPPY, sent).generatePolicy( + { gatewayId: GATEWAY_ARN, policyEngineId, prompt: "deny everything", name: "gen" }, + options, + ), + ); + + expect(sent[0]!.input).toEqual({ gatewayIdentifier: "gw-1" }); + expect(sent[1]!.input).toEqual({ + policyEngineId: expectedEngineId, + resource: { arn: GATEWAY_ARN }, + content: { rawText: "deny everything" }, + name: "gen", + }); + expect(sent.at(-1)!.input).toEqual({ + policyEngineId: expectedEngineId, + policyGenerationId: "gen-1", + nextToken: "page-2", + }); + expect(steps).toEqual([ + "Resolving gateway gw-1", + "Starting policy generation gen", + "Waiting for generation to complete", + "Reading generated policies", + ]); + expect(result).toEqual({ + policyGenerationId: "gen-1", + policyEngineId: expectedEngineId, + gatewayArn: GATEWAY_ARN, + policies: [ + { + statement: FORBID, + findings: [{ type: "DENY_ALL", description: "denies every request" }], + }, + { statement: PERMIT, findings: [] }, + ], + }); + }); + + test.each([ + [ + "the gateway has no engine attached and none is given", + { ...HAPPY, getGateway: { gatewayArn: GATEWAY_ARN } }, + InputValidationError, + "pass --policy-engine-id", + ], + ["GetGateway returns no ARN", { ...HAPPY, getGateway: {} }, Error, "returned no ARN"], + ["StartPolicyGeneration returns no id", { ...HAPPY, start: {} }, Error, "no generation id"], + [ + "the generation fails", + { ...HAPPY, get: { status: "GENERATE_FAILED", statusReasons: ["bad prompt", "try again"] } }, + Error, + "failed: bad prompt; try again", + ], + [ + "no asset carries a statement", + { + ...HAPPY, + assets: [ + { + policyGenerationAssets: [ + { + rawTextFragment: "do the thing", + findings: [{ type: "INVALID", description: "Non-translatable" }], + }, + ], + }, + ], + }, + Error, + "could not be translated into a Cedar policy: [INVALID] Non-translatable", + ], + [ + "there are no assets", + { ...HAPPY, assets: [{ policyGenerationAssets: [] }] }, + Error, + "produced no policy statement", + ], + ])("fails when %s", async (_label, responses, errorClass, message) => { + const run = drain( + policyClient(responses).generatePolicy( + { gatewayId: "gw-1", prompt: "x", name: "gen" }, + options, + ), + ); + await expect(run).rejects.toBeInstanceOf(errorClass); + await expect(run).rejects.toThrow(message); + }); + + test("times out when the generation keeps running", async () => { + const run = drain( + policyClient({ ...HAPPY, get: { status: "GENERATING" } }).generatePolicy( + { gatewayId: "gw-1", prompt: "x", name: "gen" }, + options, + ), + ); + await expect(run).rejects.toBeInstanceOf(NetworkingError); + await expect(run).rejects.toThrow("did not finish within 2s"); + }, 10_000); +}); diff --git a/src/core/policy.tsx b/src/core/policy.tsx new file mode 100644 index 000000000..0f0a5b451 --- /dev/null +++ b/src/core/policy.tsx @@ -0,0 +1,138 @@ +import { + GetGatewayCommand, + ListPolicyGenerationAssetsCommand, + StartPolicyGenerationCommand, + waitForPolicyGenerationCompleted, + type GetPolicyGenerationCommandOutput, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { WaiterState } from "@smithy/core/client"; +import { + AgentCoreCLIError, + ERROR_SOURCE, + InputValidationError, + MalformedServiceResponseError, + NetworkingError, +} from "../errors"; +import type { + CorePolicyClient, + GeneratedPolicy, + GeneratePolicyInput, + PolicyGenerationResult, +} from "../handlers/gateway/policy/types"; +import type { Logger } from "../logging"; +import type { ProgressEvent } from "../tui/progress"; +import type { AwsClients, CoreOptions } from "./types"; +import { toClientConfig } from "./utils"; + +export type PolicyGenerationWait = { + maxWaitTime: number; + minDelay: number; + maxDelay: number; +}; + +const DEFAULT_WAIT: PolicyGenerationWait = { maxWaitTime: 60, minDelay: 2, maxDelay: 5 }; + +export function resourceIdFromArn(value: string): string { + return value.startsWith("arn:") ? value.slice(value.lastIndexOf("/") + 1) : value; +} + +export class PolicyClient implements CorePolicyClient { + constructor( + private readonly clients: AwsClients, + private readonly logger: Logger, + private readonly wait: PolicyGenerationWait = DEFAULT_WAIT, + ) {} + + async *generatePolicy( + input: GeneratePolicyInput, + options: CoreOptions, + ): AsyncGenerator { + const control = this.clients.control(toClientConfig(options)); + const gatewayId = resourceIdFromArn(input.gatewayId); + + yield { type: "step", message: `Resolving gateway ${gatewayId}` }; + const gateway = await control.send(new GetGatewayCommand({ gatewayIdentifier: gatewayId })); + if (!gateway.gatewayArn) { + throw new MalformedServiceResponseError( + `GetGateway returned no ARN for gateway '${gatewayId}'`, + ); + } + const engine = input.policyEngineId ?? gateway.policyEngineConfiguration?.arn; + if (!engine) { + throw new InputValidationError( + `gateway '${gatewayId}' has no Policy Engine attached; pass --policy-engine-id`, + ); + } + const policyEngineId = resourceIdFromArn(engine); + + yield { type: "step", message: `Starting policy generation ${input.name}` }; + const started = await control.send( + new StartPolicyGenerationCommand({ + policyEngineId, + resource: { arn: gateway.gatewayArn }, + content: { rawText: input.prompt }, + name: input.name, + }), + ); + const policyGenerationId = started.policyGenerationId; + if (!policyGenerationId) { + throw new MalformedServiceResponseError("StartPolicyGeneration returned no generation id"); + } + const meta = { policyGenerationId, policyEngineId }; + + yield { type: "step", message: "Waiting for generation to complete" }; + const waited = await waitForPolicyGenerationCompleted( + { client: control, ...this.wait }, + { policyEngineId, policyGenerationId }, + ); + this.logger.debug(`policy generation ${policyGenerationId} waiter state: ${waited.state}`); + if (waited.state === WaiterState.TIMEOUT) { + throw new NetworkingError( + `policy generation '${policyGenerationId}' did not finish within ${this.wait.maxWaitTime}s; ` + + "it may still complete on the service", + { meta }, + ); + } + if (waited.state !== WaiterState.SUCCESS) { + const reasons = (waited.reason as GetPolicyGenerationCommandOutput | undefined) + ?.statusReasons; + throw new AgentCoreCLIError( + `policy generation '${policyGenerationId}' failed: ${reasons?.join("; ") ?? waited.state}`, + { source: ERROR_SOURCE.SERVICE, meta }, + ); + } + + yield { type: "step", message: "Reading generated policies" }; + const policies: GeneratedPolicy[] = []; + let nextToken: string | undefined; + do { + const page = await control.send( + new ListPolicyGenerationAssetsCommand({ policyEngineId, policyGenerationId, nextToken }), + ); + for (const asset of page.policyGenerationAssets ?? []) { + policies.push({ + statement: asset.definition?.cedar?.statement ?? asset.definition?.policy?.statement, + findings: (asset.findings ?? []).map((finding) => ({ + type: finding.type ?? "UNKNOWN", + description: finding.description ?? "", + })), + }); + } + nextToken = page.nextToken; + } while (nextToken); + + if (!policies.some((policy) => policy.statement)) { + const findings = policies + .flatMap((policy) => policy.findings) + .map((finding) => `[${finding.type}] ${finding.description}`) + .join("; "); + throw new AgentCoreCLIError( + findings + ? `the prompt could not be translated into a Cedar policy: ${findings}` + : "policy generation completed but produced no policy statement", + { source: ERROR_SOURCE.SERVICE, meta }, + ); + } + return { policyGenerationId, policyEngineId, gatewayArn: gateway.gatewayArn, policies }; + } +} diff --git a/src/handlers/gateway/policy/types.tsx b/src/handlers/gateway/policy/types.tsx new file mode 100644 index 000000000..e1193277f --- /dev/null +++ b/src/handlers/gateway/policy/types.tsx @@ -0,0 +1,36 @@ +import type { CoreOptions } from "../../../core/types"; +import type { ProgressEvent } from "../../../tui/progress"; + +export type GeneratePolicyInput = { + /** Gateway ID or ARN. */ + gatewayId: string; + /** Policy Engine ID or ARN. Omitted means the gateway's attached engine. */ + policyEngineId?: string; + prompt: string; + name: string; +}; + +export type GeneratedPolicyFinding = { + type: string; + description: string; +}; + +export type GeneratedPolicy = { + /** Absent when the service could not translate this fragment. */ + statement?: string; + findings: GeneratedPolicyFinding[]; +}; + +export type PolicyGenerationResult = { + policyGenerationId: string; + policyEngineId: string; + gatewayArn: string; + policies: GeneratedPolicy[]; +}; + +export interface CorePolicyClient { + generatePolicy( + input: GeneratePolicyInput, + options: CoreOptions, + ): AsyncGenerator; +} diff --git a/src/handlers/types.tsx b/src/handlers/types.tsx index 570ccb1b0..ff5717ab6 100644 --- a/src/handlers/types.tsx +++ b/src/handlers/types.tsx @@ -1,5 +1,6 @@ import type { CoreEvalClient } from "./eval/types.tsx"; import type { CoreGatewayClient } from "./gateway/types.tsx"; +import type { CorePolicyClient } from "./gateway/policy/types.tsx"; import type { CoreHarnessClient } from "./harness/types.tsx"; import type { CoreIdentityClient } from "./identity/types.tsx"; import type { CoreMemoryClient } from "./memory/types.tsx"; @@ -17,6 +18,7 @@ export interface Core { gateway: CoreGatewayClient; eval: CoreEvalClient; observability: CoreObservabilityClient; + policy: CorePolicyClient; projectManager: ProjectManager; /** Describes a Bedrock Agent + alias for `--type import`. */ describeBedrockAgent: DescribeBedrockAgent; diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 7cbe6b005..45d185db3 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -179,6 +179,12 @@ import { isTerminalStatus } from "../core/batchEvaluationResults"; import { abortable } from "../core/abortable"; import type { CoreFetch, CoreOptions, CreateCloudFormationClient } from "../core/types"; import type { Project, ProjectManager } from "../handlers/project/types"; +import type { + CorePolicyClient, + GeneratePolicyInput, + PolicyGenerationResult, +} from "../handlers/gateway/policy/types"; +import type { ProgressEvent } from "../tui/progress"; import type { Logger } from "../logging"; import type { ReadWriteJson } from "../io"; import { createSilentLogger } from "./logging"; @@ -2377,6 +2383,28 @@ export class TestObservabilityClient implements CoreObservabilityClient { } } +export class TestPolicyClient implements CorePolicyClient { + result: PolicyGenerationResult = { + policyGenerationId: "gen-1", + policyEngineId: "pe-1", + gatewayArn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/gw-1", + policies: [ + { statement: "forbid (principal, action, resource is AgentCore::Gateway);", findings: [] }, + ], + }; + error: Error | undefined; + readonly calls: GeneratePolicyInput[] = []; + + async *generatePolicy( + input: GeneratePolicyInput, + ): AsyncGenerator { + this.calls.push(input); + if (this.error) throw this.error; + yield { type: "step", message: "Generating policy" }; + return this.result; + } +} + // TestCoreClient implements the Core contract with fully controllable sub-clients. export class TestCoreClient implements Core { readonly harness = new TestHarnessClient(); @@ -2386,6 +2414,7 @@ export class TestCoreClient implements Core { readonly gateway = new TestGatewayClient(); readonly eval = new TestEvalClient(); readonly observability = new TestObservabilityClient(); + readonly policy = new TestPolicyClient(); fetch: CoreFetch = (async () => { throw new Error("TestCoreClient.fetch is not configured; set it in the test that needs it"); }) as unknown as CoreFetch; From 6b1f69aff3afcd3bb5b90bca7dc1760020f6fa46 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Wed, 2 Sep 2026 15:02:47 -0400 Subject: [PATCH 2/5] feat(gateway): gateway policy generate turns a prompt into Cedar --- README.md | 13 +- src/handlers/gateway/gateway.test.tsx | 3 + src/handlers/gateway/index.tsx | 4 +- src/handlers/gateway/policy/format.ts | 17 +++ src/handlers/gateway/policy/generate.test.tsx | 123 ++++++++++++++++++ src/handlers/gateway/policy/generate.tsx | 80 ++++++++++++ src/handlers/gateway/policy/index.tsx | 10 ++ 7 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 src/handlers/gateway/policy/format.ts create mode 100644 src/handlers/gateway/policy/generate.test.tsx create mode 100644 src/handlers/gateway/policy/generate.tsx create mode 100644 src/handlers/gateway/policy/index.tsx diff --git a/README.md b/README.md index 84f21fcb5..2f2bee3c7 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,11 @@ agentcore # interactive TUI │ ├── connector │ │ ├── get # get a connector-backed Target │ │ └── list # list connector-backed Targets -│ └── rule -│ ├── get # get a Rule under a Gateway -│ └── list # list Rules under a Gateway +│ ├── rule +│ │ ├── get # get a Rule under a Gateway +│ │ └── list # list Rules under a Gateway +│ └── policy +│ └── generate # generate Cedar for a Gateway from a natural-language prompt ├── eval # evaluate and optimize AgentCore agents │ └── evaluator # manage AgentCore evaluators │ ├── llm-as-a-judge # LLM-as-a-Judge evaluators @@ -262,6 +264,11 @@ agentcore gateway connector get --gateway-id --id agentcore gateway connector list --gateway-id --max-results 20 agentcore gateway rule get --gateway-id --rule-id agentcore gateway rule list --gateway-id --max-results 20 +agentcore gateway policy generate --gateway-id --prompt "forbid IAM callers from every tool" +agentcore gateway policy generate --gateway-id --prompt file://policy.txt --json +# Pipe the generated Cedar into a project (run inside the project) +agentcore gateway policy generate --gateway-id --prompt "..." \ + | agentcore project add policy --engine Guardrails --name Generated --statement - # Manage API key credential providers agentcore identity api-key-credential-provider create --name my-provider --api-key diff --git a/src/handlers/gateway/gateway.test.tsx b/src/handlers/gateway/gateway.test.tsx index bf767c77b..af7ed599c 100644 --- a/src/handlers/gateway/gateway.test.tsx +++ b/src/handlers/gateway/gateway.test.tsx @@ -57,6 +57,7 @@ describe("gateway command hierarchy", () => { const target = gateway?.children().find((child) => child.name() === "target"); const connector = gateway?.children().find((child) => child.name() === "connector"); const rule = gateway?.children().find((child) => child.name() === "rule"); + const policy = gateway?.children().find((child) => child.name() === "policy"); expect(gateway?.flags().map((flag) => flag.name)).not.toContain("interactive"); expect(gateway?.children().map((child) => child.name())).toEqual([ @@ -69,6 +70,7 @@ describe("gateway command hierarchy", () => { "target", "connector", "rule", + "policy", ]); expect(target?.children().map((child) => child.name())).toEqual([ "create", @@ -91,6 +93,7 @@ describe("gateway command hierarchy", () => { "list", "delete", ]); + expect(policy?.children().map((child) => child.name())).toEqual(["generate"]); }); test.each([ diff --git a/src/handlers/gateway/index.tsx b/src/handlers/gateway/index.tsx index c953c3d2c..62b3374b5 100644 --- a/src/handlers/gateway/index.tsx +++ b/src/handlers/gateway/index.tsx @@ -9,6 +9,7 @@ import { createDeleteGatewayHandler } from "./delete"; import { createGetGatewayHandler } from "./get"; import { createInvokeGatewayHandler } from "./invoke"; import { createListGatewaysHandler } from "./list"; +import { createGatewayPolicyHandler } from "./policy"; import { createGatewayRuleHandler } from "./rule"; import { createGatewayTargetHandler } from "./target"; import { createUpdateGatewayHandler } from "./update"; @@ -26,5 +27,6 @@ export function createGatewayHandler(core: Core, io: AppIO): Router { .handler(createInvokeGatewayHandler(core, io)) .handler(createGatewayTargetHandler(core, io)) .handler(createGatewayConnectorHandler(core, io)) - .handler(createGatewayRuleHandler(core, io)); + .handler(createGatewayRuleHandler(core, io)) + .handler(createGatewayPolicyHandler(core, io)); } diff --git a/src/handlers/gateway/policy/format.ts b/src/handlers/gateway/policy/format.ts new file mode 100644 index 000000000..0ed293edc --- /dev/null +++ b/src/handlers/gateway/policy/format.ts @@ -0,0 +1,17 @@ +import type { GeneratedPolicy } from "./types"; + +export function formatStatements(policies: GeneratedPolicy[]): string { + const statements = policies.flatMap((policy) => + policy.statement ? [policy.statement.trimEnd()] : [], + ); + return `${statements.join("\n\n")}\n`; +} + +export function formatFindings(policies: GeneratedPolicy[]): string { + const rows = policies.flatMap((policy, index) => + policy.findings.map( + (finding) => ` policy ${index + 1} [${finding.type}] ${finding.description}`, + ), + ); + return rows.length === 0 ? "" : `Findings:\n${rows.join("\n")}\n`; +} diff --git a/src/handlers/gateway/policy/generate.test.tsx b/src/handlers/gateway/policy/generate.test.tsx new file mode 100644 index 000000000..438324ab4 --- /dev/null +++ b/src/handlers/gateway/policy/generate.test.tsx @@ -0,0 +1,123 @@ +import { describe, expect, test } from "bun:test"; +import { NetworkingError } from "../../../errors"; +import { + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../../../testing"; +import { createRootHandler } from "../../index"; + +const REGION = "us-west-2"; +const GATEWAY_ARN = "arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/gw-1"; +const FORBID = "forbid (principal, action, resource is AgentCore::Gateway);"; +const PERMIT = "permit (principal, action, resource is AgentCore::Gateway);"; + +function subject(stdin?: string) { + const core = new TestCoreClient(); + const io = testIO({ stdin }); + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + return { + core, + io, + route: (args: string[]) => + root.route([ + "node", + "agentcore", + "gateway", + "policy", + "generate", + ...args, + "--region", + REGION, + ]), + }; +} + +describe("gateway policy generate", () => { + test.each([ + ["--gateway-id", []], + ["--prompt", ["--gateway-id", "gw-1"]], + ])("rejects a missing %s before calling the service", async (flag, args) => { + const { core, route } = subject(); + await expect(route(args)).rejects.toThrow(`required option '${flag}`); + expect(core.policy.calls).toEqual([]); + }); + + test("prints the Cedar on stdout and the findings on stderr", async () => { + const { core, io, route } = subject("deny everything\n"); + core.policy.result = { + policyGenerationId: "gen-1", + policyEngineId: "pe-explicit", + gatewayArn: GATEWAY_ARN, + policies: [ + { + statement: FORBID, + findings: [{ type: "DENY_ALL", description: "denies every request" }], + }, + { statement: undefined, findings: [{ type: "INVALID", description: "Non-translatable" }] }, + { statement: PERMIT, findings: [] }, + ], + }; + + await route([ + "--gateway-id", + GATEWAY_ARN, + "--policy-engine-id", + "pe-explicit", + "--prompt", + "-", + "--name", + "my_generation", + ]); + + expect(core.policy.calls).toEqual([ + { + gatewayId: GATEWAY_ARN, + policyEngineId: "pe-explicit", + prompt: "deny everything\n", + name: "my_generation", + }, + ]); + expect(io.stdout()).toBe(`${FORBID}\n\n${PERMIT}`); + expect(io.stderr()).toBe( + [ + "Generating policy", + "Findings:", + " policy 1 [DENY_ALL] denies every request", + " policy 2 [INVALID] Non-translatable", + ].join("\n"), + ); + }); + + test("prints the result object with --json and defaults the generation name", async () => { + const { core, io, route } = subject(); + + await route(["--gateway-id", "gw-1", "--prompt", "deny everything", "--json"]); + + expect(core.policy.calls[0]).toMatchObject({ + gatewayId: "gw-1", + policyEngineId: undefined, + prompt: "deny everything", + }); + expect(core.policy.calls[0]!.name).toMatch(/^cli_generation_\d+$/); + expect(JSON.parse(io.stdout())).toEqual(core.policy.result); + expect(io.stderr()).toBe("Generating policy"); + }); + + test("renders a --json error and writes no Cedar when generation fails", async () => { + const { core, io, route } = subject(); + core.policy.error = new NetworkingError("policy generation 'gen-1' did not finish within 60s"); + + await expect( + route(["--gateway-id", "gw-1", "--prompt", "deny everything", "--json"]), + ).rejects.toThrow("did not finish within 60s"); + expect(JSON.parse(io.stdout())).toEqual({ + error: "policy generation 'gen-1' did not finish within 60s", + }); + }); +}); diff --git a/src/handlers/gateway/policy/generate.tsx b/src/handlers/gateway/policy/generate.tsx new file mode 100644 index 000000000..74ac54bc1 --- /dev/null +++ b/src/handlers/gateway/policy/generate.tsx @@ -0,0 +1,80 @@ +import z from "zod"; +import { InputValidationError } from "../../../errors"; +import { type AppIO, SourceResolver } from "../../../io"; +import { createHandler, flag } from "../../../router"; +import { JsonRendererKey } from "../../../tui"; +import { runWithProgress } from "../../../tui/progress"; +import { JsonKey } from "../../keys"; +import type { Core } from "../../types"; +import { coreOptsFromCtx, renderJsonError } from "../../utils"; +import { formatFindings, formatStatements } from "./format"; +import type { PolicyGenerationResult } from "./types"; + +export const createGeneratePolicyHandler = (core: Core, io: AppIO) => + createHandler({ + name: "generate", + description: "generate a Cedar policy for a Gateway from a natural-language prompt", + flags: [ + flag( + "gateway-id", + "the ID or ARN of the Gateway the policy applies to", + z.string().optional(), + ), + flag( + "policy-engine-id", + "the ID or ARN of the Policy Engine (defaults to the Gateway's attached engine)", + z.string().optional(), + ), + flag( + "prompt", + "what the policy should allow or deny (inline, file://, or - for stdin)", + z.string().optional(), + ), + flag( + "name", + "name of the generation request (defaults to cli_generation_)", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["gateway-id"]) { + throw new InputValidationError("required option '--gateway-id ' not specified"); + } + if (flags.prompt === undefined) { + throw new InputValidationError("required option '--prompt ' not specified"); + } + const prompt = (await new SourceResolver({ stdin: io.stdin }).resolveText( + "prompt", + flags.prompt, + ))!; + const jsonOutput = ctx.require(JsonKey); + + const generation = core.policy.generatePolicy( + { + gatewayId: flags["gateway-id"], + policyEngineId: flags["policy-engine-id"], + prompt, + name: flags.name ?? `cli_generation_${Date.now()}`, + }, + coreOptsFromCtx(ctx), + ); + + let result: PolicyGenerationResult; + try { + result = await runWithProgress(generation, { + io, + interactive: jsonOutput ? false : undefined, + }); + } catch (error) { + if (jsonOutput) renderJsonError(ctx, error); + throw error; + } + + if (jsonOutput) { + ctx.require(JsonRendererKey).renderJson(result); + return; + } + io.stderr.write(formatFindings(result.policies)); + io.stdout.write(formatStatements(result.policies)); + }, + }); diff --git a/src/handlers/gateway/policy/index.tsx b/src/handlers/gateway/policy/index.tsx new file mode 100644 index 000000000..50efe78ec --- /dev/null +++ b/src/handlers/gateway/policy/index.tsx @@ -0,0 +1,10 @@ +import type { AppIO } from "../../../io"; +import { Router } from "../../../router"; +import type { Core } from "../../types"; +import { createGeneratePolicyHandler } from "./generate"; + +export function createGatewayPolicyHandler(core: Core, io: AppIO): Router { + return new Router("policy", "generate Cedar policies for an AgentCore Gateway").handler( + createGeneratePolicyHandler(core, io), + ); +} From 7ad8a2deae0652ce535f970dcb23672de688388e Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Wed, 2 Sep 2026 15:16:27 -0400 Subject: [PATCH 3/5] refactor(policy): inline one-use finding type and collapse the no-statement error --- src/core/policy.test.ts | 2 +- src/core/policy.tsx | 6 ++---- src/handlers/gateway/policy/types.tsx | 7 +------ 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/core/policy.test.ts b/src/core/policy.test.ts index 8c38bd098..724e9f7eb 100644 --- a/src/core/policy.test.ts +++ b/src/core/policy.test.ts @@ -158,7 +158,7 @@ describe("PolicyClient.generatePolicy", () => { "there are no assets", { ...HAPPY, assets: [{ policyGenerationAssets: [] }] }, Error, - "produced no policy statement", + "could not be translated into a Cedar policy", ], ])("fails when %s", async (_label, responses, errorClass, message) => { const run = drain( diff --git a/src/core/policy.tsx b/src/core/policy.tsx index 0f0a5b451..79f0ba181 100644 --- a/src/core/policy.tsx +++ b/src/core/policy.tsx @@ -32,7 +32,7 @@ export type PolicyGenerationWait = { const DEFAULT_WAIT: PolicyGenerationWait = { maxWaitTime: 60, minDelay: 2, maxDelay: 5 }; -export function resourceIdFromArn(value: string): string { +function resourceIdFromArn(value: string): string { return value.startsWith("arn:") ? value.slice(value.lastIndexOf("/") + 1) : value; } @@ -127,9 +127,7 @@ export class PolicyClient implements CorePolicyClient { .map((finding) => `[${finding.type}] ${finding.description}`) .join("; "); throw new AgentCoreCLIError( - findings - ? `the prompt could not be translated into a Cedar policy: ${findings}` - : "policy generation completed but produced no policy statement", + `the prompt could not be translated into a Cedar policy${findings ? `: ${findings}` : ""}`, { source: ERROR_SOURCE.SERVICE, meta }, ); } diff --git a/src/handlers/gateway/policy/types.tsx b/src/handlers/gateway/policy/types.tsx index e1193277f..dabd678fc 100644 --- a/src/handlers/gateway/policy/types.tsx +++ b/src/handlers/gateway/policy/types.tsx @@ -10,15 +10,10 @@ export type GeneratePolicyInput = { name: string; }; -export type GeneratedPolicyFinding = { - type: string; - description: string; -}; - export type GeneratedPolicy = { /** Absent when the service could not translate this fragment. */ statement?: string; - findings: GeneratedPolicyFinding[]; + findings: { type: string; description: string }[]; }; export type PolicyGenerationResult = { From a07936d065fe7bc0ea9034c3ec354e99494a53fa Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Wed, 2 Sep 2026 15:55:52 -0400 Subject: [PATCH 4/5] test(policy): record golden fixtures for gateway policy generate and move waiter cases to gateway.test.tsx --- src/core/policy.test.ts | 184 ------------------ src/core/policy.tsx | 23 +-- .../GetGatewayCommand.5513677d251db507.json | 19 ++ .../GetGatewayCommand.aaf1bce123157e06.json | 23 +++ ...icyGenerationCommand.362175715d99ad29.json | 17 ++ ...icyGenerationCommand.363616a21dff1546.json | 17 ++ ...icyGenerationCommand.c4e5fba54a2144ec.json | 17 ++ ...erationAssetsCommand.362175715d99ad29.json | 19 ++ ...erationAssetsCommand.363616a21dff1546.json | 24 +++ ...erationAssetsCommand.c4e5fba54a2144ec.json | 19 ++ ...icyGenerationCommand.1182c7bafb2ef3d0.json | 6 + ...icyGenerationCommand.1641ea5f6632d799.json | 17 ++ ...icyGenerationCommand.c124c0c5bbf51352.json | 17 ++ ...icyGenerationCommand.fbcab060875e4a02.json | 17 ++ .../policy/generate-json.golden.json | 16 ++ .../__fixtures__/policy/generate.golden.cedar | 1 + .../policy/generate.golden.stderr | 6 + src/handlers/gateway/gateway.policy.test.tsx | 138 +++++++++++++ src/handlers/gateway/gateway.test.tsx | 69 ++++++- src/handlers/gateway/policy/generate.test.tsx | 123 ------------ src/testing/TestCoreClient.tsx | 19 +- 21 files changed, 452 insertions(+), 339 deletions(-) delete mode 100644 src/core/policy.test.ts create mode 100644 src/handlers/gateway/__fixtures__/policy/GetGatewayCommand.5513677d251db507.json create mode 100644 src/handlers/gateway/__fixtures__/policy/GetGatewayCommand.aaf1bce123157e06.json create mode 100644 src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.362175715d99ad29.json create mode 100644 src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.363616a21dff1546.json create mode 100644 src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c4e5fba54a2144ec.json create mode 100644 src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.362175715d99ad29.json create mode 100644 src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.363616a21dff1546.json create mode 100644 src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c4e5fba54a2144ec.json create mode 100644 src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1182c7bafb2ef3d0.json create mode 100644 src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1641ea5f6632d799.json create mode 100644 src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c124c0c5bbf51352.json create mode 100644 src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.fbcab060875e4a02.json create mode 100644 src/handlers/gateway/__fixtures__/policy/generate-json.golden.json create mode 100644 src/handlers/gateway/__fixtures__/policy/generate.golden.cedar create mode 100644 src/handlers/gateway/__fixtures__/policy/generate.golden.stderr create mode 100644 src/handlers/gateway/gateway.policy.test.tsx delete mode 100644 src/handlers/gateway/policy/generate.test.tsx diff --git a/src/core/policy.test.ts b/src/core/policy.test.ts deleted file mode 100644 index 724e9f7eb..000000000 --- a/src/core/policy.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - GetGatewayCommand, - GetPolicyGenerationCommand, - ListPolicyGenerationAssetsCommand, - StartPolicyGenerationCommand, -} from "@aws-sdk/client-bedrock-agentcore-control"; -import { InputValidationError, NetworkingError } from "../errors"; -import type { ProgressEvent } from "../tui/progress"; -import { createSilentLogger } from "../testing"; -import { PolicyClient } from "./policy"; -import type { AwsClients } from "./types"; - -const options = { region: "us-west-2" }; -const GATEWAY_ARN = "arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/gw-1"; -const ATTACHED_ENGINE_ARN = - "arn:aws:bedrock-agentcore:us-west-2:111122223333:policy-engine/pe-attached"; -const EXPLICIT_ENGINE_ARN = - "arn:aws:bedrock-agentcore:us-west-2:111122223333:policy-engine/pe-explicit"; -const FORBID = "forbid (principal, action, resource is AgentCore::Gateway);"; -const PERMIT = "permit (principal, action, resource is AgentCore::Gateway);"; - -type Responses = { - getGateway?: unknown; - start?: unknown; - get?: unknown; - assets?: unknown[]; -}; - -const HAPPY: Responses = { - getGateway: { gatewayArn: GATEWAY_ARN, policyEngineConfiguration: { arn: ATTACHED_ENGINE_ARN } }, - start: { policyGenerationId: "gen-1" }, - get: { status: "GENERATED" }, - assets: [ - { - policyGenerationAssets: [ - { - definition: { cedar: { statement: FORBID } }, - findings: [{ type: "DENY_ALL", description: "denies every request" }], - }, - ], - nextToken: "page-2", - }, - { - policyGenerationAssets: [{ definition: { policy: { statement: PERMIT } } }], - }, - ], -}; - -function policyClient(responses: Responses, sent: { input: unknown }[] = []) { - const assetPages = [...(responses.assets ?? [])]; - const control = { - send: async (command: { input: unknown }) => { - sent.push(command); - if (command instanceof GetGatewayCommand) return responses.getGateway; - if (command instanceof StartPolicyGenerationCommand) return responses.start; - if (command instanceof GetPolicyGenerationCommand) return responses.get; - if (command instanceof ListPolicyGenerationAssetsCommand) return assetPages.shift(); - throw new Error(`unexpected command ${command.constructor.name}`); - }, - }; - return new PolicyClient( - { control: () => control } as unknown as AwsClients, - createSilentLogger(), - { maxWaitTime: 2, minDelay: 1, maxDelay: 1 }, - ); -} - -async function drain(generator: AsyncGenerator) { - const steps: string[] = []; - let next = await generator.next(); - while (!next.done) { - if (next.value.type === "step") steps.push(next.value.message); - next = await generator.next(); - } - return { steps, result: next.value }; -} - -describe("PolicyClient.generatePolicy", () => { - test.each([ - ["the attached engine when none is given", undefined, "pe-attached"], - ["an explicit engine ARN over the attached one", EXPLICIT_ENGINE_ARN, "pe-explicit"], - ])("generates against %s", async (_label, policyEngineId, expectedEngineId) => { - const sent: { input: unknown }[] = []; - const { steps, result } = await drain( - policyClient(HAPPY, sent).generatePolicy( - { gatewayId: GATEWAY_ARN, policyEngineId, prompt: "deny everything", name: "gen" }, - options, - ), - ); - - expect(sent[0]!.input).toEqual({ gatewayIdentifier: "gw-1" }); - expect(sent[1]!.input).toEqual({ - policyEngineId: expectedEngineId, - resource: { arn: GATEWAY_ARN }, - content: { rawText: "deny everything" }, - name: "gen", - }); - expect(sent.at(-1)!.input).toEqual({ - policyEngineId: expectedEngineId, - policyGenerationId: "gen-1", - nextToken: "page-2", - }); - expect(steps).toEqual([ - "Resolving gateway gw-1", - "Starting policy generation gen", - "Waiting for generation to complete", - "Reading generated policies", - ]); - expect(result).toEqual({ - policyGenerationId: "gen-1", - policyEngineId: expectedEngineId, - gatewayArn: GATEWAY_ARN, - policies: [ - { - statement: FORBID, - findings: [{ type: "DENY_ALL", description: "denies every request" }], - }, - { statement: PERMIT, findings: [] }, - ], - }); - }); - - test.each([ - [ - "the gateway has no engine attached and none is given", - { ...HAPPY, getGateway: { gatewayArn: GATEWAY_ARN } }, - InputValidationError, - "pass --policy-engine-id", - ], - ["GetGateway returns no ARN", { ...HAPPY, getGateway: {} }, Error, "returned no ARN"], - ["StartPolicyGeneration returns no id", { ...HAPPY, start: {} }, Error, "no generation id"], - [ - "the generation fails", - { ...HAPPY, get: { status: "GENERATE_FAILED", statusReasons: ["bad prompt", "try again"] } }, - Error, - "failed: bad prompt; try again", - ], - [ - "no asset carries a statement", - { - ...HAPPY, - assets: [ - { - policyGenerationAssets: [ - { - rawTextFragment: "do the thing", - findings: [{ type: "INVALID", description: "Non-translatable" }], - }, - ], - }, - ], - }, - Error, - "could not be translated into a Cedar policy: [INVALID] Non-translatable", - ], - [ - "there are no assets", - { ...HAPPY, assets: [{ policyGenerationAssets: [] }] }, - Error, - "could not be translated into a Cedar policy", - ], - ])("fails when %s", async (_label, responses, errorClass, message) => { - const run = drain( - policyClient(responses).generatePolicy( - { gatewayId: "gw-1", prompt: "x", name: "gen" }, - options, - ), - ); - await expect(run).rejects.toBeInstanceOf(errorClass); - await expect(run).rejects.toThrow(message); - }); - - test("times out when the generation keeps running", async () => { - const run = drain( - policyClient({ ...HAPPY, get: { status: "GENERATING" } }).generatePolicy( - { gatewayId: "gw-1", prompt: "x", name: "gen" }, - options, - ), - ); - await expect(run).rejects.toBeInstanceOf(NetworkingError); - await expect(run).rejects.toThrow("did not finish within 2s"); - }, 10_000); -}); diff --git a/src/core/policy.tsx b/src/core/policy.tsx index 79f0ba181..6ac4b722a 100644 --- a/src/core/policy.tsx +++ b/src/core/policy.tsx @@ -6,13 +6,7 @@ import { type GetPolicyGenerationCommandOutput, } from "@aws-sdk/client-bedrock-agentcore-control"; import { WaiterState } from "@smithy/core/client"; -import { - AgentCoreCLIError, - ERROR_SOURCE, - InputValidationError, - MalformedServiceResponseError, - NetworkingError, -} from "../errors"; +import { AgentCoreCLIError, ERROR_SOURCE, InputValidationError, NetworkingError } from "../errors"; import type { CorePolicyClient, GeneratedPolicy, @@ -52,11 +46,7 @@ export class PolicyClient implements CorePolicyClient { yield { type: "step", message: `Resolving gateway ${gatewayId}` }; const gateway = await control.send(new GetGatewayCommand({ gatewayIdentifier: gatewayId })); - if (!gateway.gatewayArn) { - throw new MalformedServiceResponseError( - `GetGateway returned no ARN for gateway '${gatewayId}'`, - ); - } + const gatewayArn = gateway.gatewayArn!; const engine = input.policyEngineId ?? gateway.policyEngineConfiguration?.arn; if (!engine) { throw new InputValidationError( @@ -69,15 +59,12 @@ export class PolicyClient implements CorePolicyClient { const started = await control.send( new StartPolicyGenerationCommand({ policyEngineId, - resource: { arn: gateway.gatewayArn }, + resource: { arn: gatewayArn }, content: { rawText: input.prompt }, name: input.name, }), ); - const policyGenerationId = started.policyGenerationId; - if (!policyGenerationId) { - throw new MalformedServiceResponseError("StartPolicyGeneration returned no generation id"); - } + const policyGenerationId = started.policyGenerationId!; const meta = { policyGenerationId, policyEngineId }; yield { type: "step", message: "Waiting for generation to complete" }; @@ -131,6 +118,6 @@ export class PolicyClient implements CorePolicyClient { { source: ERROR_SOURCE.SERVICE, meta }, ); } - return { policyGenerationId, policyEngineId, gatewayArn: gateway.gatewayArn, policies }; + return { policyGenerationId, policyEngineId, gatewayArn, policies }; } } diff --git a/src/handlers/gateway/__fixtures__/policy/GetGatewayCommand.5513677d251db507.json b/src/handlers/gateway/__fixtures__/policy/GetGatewayCommand.5513677d251db507.json new file mode 100644 index 000000000..441b7cc4a --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/GetGatewayCommand.5513677d251db507.json @@ -0,0 +1,19 @@ +{ + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-bare-xl0dy3pq5h", + "gatewayId": "policygene2e-bare-xl0dy3pq5h", + "createdAt": { + "$date": "2026-09-02T19:50:44.028Z" + }, + "updatedAt": { + "$date": "2026-09-02T19:50:44.841Z" + }, + "status": "READY", + "name": "PolicyGenE2E-bare", + "authorizerType": "NONE", + "gatewayUrl": "https://policygene2e-bare-xl0dy3pq5h.gateway.bedrock-agentcore.us-west-2.amazonaws.com", + "description": "Gateway for PolicyGenE2E-bare", + "roleArn": "arn:aws:iam::887863153624:role/AgentCore-PolicyGenE2E-de-McpGatewayBareRole58BAE2C-jSYHioxsHFHU", + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:workload-identity-directory/default/workload-identity/policygene2e-bare-xl0dy3pq5h" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/GetGatewayCommand.aaf1bce123157e06.json b/src/handlers/gateway/__fixtures__/policy/GetGatewayCommand.aaf1bce123157e06.json new file mode 100644 index 000000000..8a5387089 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/GetGatewayCommand.aaf1bce123157e06.json @@ -0,0 +1,23 @@ +{ + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z", + "gatewayId": "policygene2e-tools-zhijfh6m5z", + "createdAt": { + "$date": "2026-09-02T19:30:56.123Z" + }, + "updatedAt": { + "$date": "2026-09-02T19:30:56.809Z" + }, + "status": "READY", + "name": "PolicyGenE2E-tools", + "authorizerType": "NONE", + "gatewayUrl": "https://policygene2e-tools-zhijfh6m5z.gateway.bedrock-agentcore.us-west-2.amazonaws.com", + "description": "Gateway for PolicyGenE2E-tools", + "roleArn": "arn:aws:iam::887863153624:role/AgentCore-PolicyGenE2E-de-McpGatewayToolsRole1D55B5-WhvuoXGDoNYC", + "policyEngineConfiguration": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t", + "mode": "LOG_ONLY" + }, + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:workload-identity-directory/default/workload-identity/policygene2e-tools-zhijfh6m5z" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.362175715d99ad29.json b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.362175715d99ad29.json new file mode 100644 index 000000000..c5b552f1e --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.362175715d99ad29.json @@ -0,0 +1,17 @@ +{ + "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", + "policyGenerationId": "golden_permit-62mnrlgenk", + "name": "golden_permit", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_permit-62mnrlgenk", + "resource": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" + }, + "createdAt": { + "$date": "2026-09-02T19:52:59.583Z" + }, + "updatedAt": { + "$date": "2026-09-02T19:53:09.854Z" + }, + "status": "GENERATED", + "statusReasons": [] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.363616a21dff1546.json b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.363616a21dff1546.json new file mode 100644 index 000000000..96580f0b3 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.363616a21dff1546.json @@ -0,0 +1,17 @@ +{ + "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", + "policyGenerationId": "golden_untranslatable-dh214s4x7d", + "name": "golden_untranslatable", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_untranslatable-dh214s4x7d", + "resource": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" + }, + "createdAt": { + "$date": "2026-09-02T19:53:11.658Z" + }, + "updatedAt": { + "$date": "2026-09-02T19:53:20.774Z" + }, + "status": "GENERATED", + "statusReasons": [] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c4e5fba54a2144ec.json b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c4e5fba54a2144ec.json new file mode 100644 index 000000000..dbd4a0b6f --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c4e5fba54a2144ec.json @@ -0,0 +1,17 @@ +{ + "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", + "policyGenerationId": "golden_forbid-yovi81zlhe", + "name": "golden_forbid", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_forbid-yovi81zlhe", + "resource": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" + }, + "createdAt": { + "$date": "2026-09-02T19:52:47.764Z" + }, + "updatedAt": { + "$date": "2026-09-02T19:52:58.063Z" + }, + "status": "GENERATED", + "statusReasons": [] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.362175715d99ad29.json b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.362175715d99ad29.json new file mode 100644 index 000000000..b00d36bff --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.362175715d99ad29.json @@ -0,0 +1,19 @@ +{ + "policyGenerationAssets": [ + { + "policyGenerationAssetId": "golden_permit-o9blndc30w", + "rawTextFragment": "permit IAM principals to call any tool on this gateway", + "findings": [ + { + "type": "ALLOW_ALL", + "description": "Overly Permissive: The generated policy permits all actions for all principals. Confirm that unrestricted access is intended before applying this policy" + } + ], + "definition": { + "policy": { + "statement": "permit (principal is AgentCore::IamEntity, action, resource == AgentCore::Gateway::\"arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z\");" + } + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.363616a21dff1546.json b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.363616a21dff1546.json new file mode 100644 index 000000000..a4f852812 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.363616a21dff1546.json @@ -0,0 +1,24 @@ +{ + "policyGenerationAssets": [ + { + "policyGenerationAssetId": "golden_untranslatable-jk98c0i1qv", + "rawTextFragment": "Permit everyone to list tools.", + "findings": [ + { + "type": "INVALID", + "description": "Non-translatable: cannot be expressed in Dogwood" + } + ] + }, + { + "policyGenerationAssetId": "golden_untranslatable-mt1y937v8n", + "rawTextFragment": "Forbid calling any tool whose name contains delete.", + "findings": [ + { + "type": "INVALID", + "description": "Non-translatable: cannot be expressed in Dogwood" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c4e5fba54a2144ec.json b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c4e5fba54a2144ec.json new file mode 100644 index 000000000..754732c5b --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c4e5fba54a2144ec.json @@ -0,0 +1,19 @@ +{ + "policyGenerationAssets": [ + { + "policyGenerationAssetId": "golden_forbid-8xiwjqekhd", + "rawTextFragment": "forbid IAM principals from calling any tool on this gateway", + "findings": [ + { + "type": "DENY_ALL", + "description": "Overly Restrictive: The generated policy denies all actions for all principals. Confirm that full restriction is intended before applying this policy." + } + ], + "definition": { + "policy": { + "statement": "forbid (principal is AgentCore::IamEntity, action, resource == AgentCore::Gateway::\"arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z\");" + } + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1182c7bafb2ef3d0.json b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1182c7bafb2ef3d0.json new file mode 100644 index 000000000..5c5385db6 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1182c7bafb2ef3d0.json @@ -0,0 +1,6 @@ +{ + "$error": { + "name": "ValidationException", + "message": "1 validation error detected. Value at '/policyEngineId' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[A-Za-z][A-Za-z0-9_]*-[a-z0-9_]{10}$" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1641ea5f6632d799.json b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1641ea5f6632d799.json new file mode 100644 index 000000000..5ab9b457f --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1641ea5f6632d799.json @@ -0,0 +1,17 @@ +{ + "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", + "policyGenerationId": "golden_untranslatable-dh214s4x7d", + "name": "golden_untranslatable", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_untranslatable-dh214s4x7d", + "resource": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" + }, + "createdAt": { + "$date": "2026-09-02T19:53:11.658Z" + }, + "updatedAt": { + "$date": "2026-09-02T19:53:11.658Z" + }, + "status": "GENERATING", + "statusReasons": [] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c124c0c5bbf51352.json b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c124c0c5bbf51352.json new file mode 100644 index 000000000..acd687ae2 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c124c0c5bbf51352.json @@ -0,0 +1,17 @@ +{ + "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", + "policyGenerationId": "golden_permit-62mnrlgenk", + "name": "golden_permit", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_permit-62mnrlgenk", + "resource": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" + }, + "createdAt": { + "$date": "2026-09-02T19:52:59.583Z" + }, + "updatedAt": { + "$date": "2026-09-02T19:52:59.583Z" + }, + "status": "GENERATING", + "statusReasons": [] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.fbcab060875e4a02.json b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.fbcab060875e4a02.json new file mode 100644 index 000000000..9b1a5efd3 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.fbcab060875e4a02.json @@ -0,0 +1,17 @@ +{ + "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", + "policyGenerationId": "golden_forbid-yovi81zlhe", + "name": "golden_forbid", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_forbid-yovi81zlhe", + "resource": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" + }, + "createdAt": { + "$date": "2026-09-02T19:52:47.764Z" + }, + "updatedAt": { + "$date": "2026-09-02T19:52:47.764Z" + }, + "status": "GENERATING", + "statusReasons": [] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/generate-json.golden.json b/src/handlers/gateway/__fixtures__/policy/generate-json.golden.json new file mode 100644 index 000000000..68cac36ed --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/generate-json.golden.json @@ -0,0 +1,16 @@ +{ + "policyGenerationId": "golden_permit-62mnrlgenk", + "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z", + "policies": [ + { + "statement": "permit (principal is AgentCore::IamEntity, action, resource == AgentCore::Gateway::\"arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z\");", + "findings": [ + { + "type": "ALLOW_ALL", + "description": "Overly Permissive: The generated policy permits all actions for all principals. Confirm that unrestricted access is intended before applying this policy" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/generate.golden.cedar b/src/handlers/gateway/__fixtures__/policy/generate.golden.cedar new file mode 100644 index 000000000..cafca29b8 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/generate.golden.cedar @@ -0,0 +1 @@ +forbid (principal is AgentCore::IamEntity, action, resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z"); \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/generate.golden.stderr b/src/handlers/gateway/__fixtures__/policy/generate.golden.stderr new file mode 100644 index 000000000..224011afb --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/generate.golden.stderr @@ -0,0 +1,6 @@ +Resolving gateway policygene2e-tools-zhijfh6m5z +Starting policy generation golden_forbid +Waiting for generation to complete +Reading generated policies +Findings: + policy 1 [DENY_ALL] Overly Restrictive: The generated policy denies all actions for all principals. Confirm that full restriction is intended before applying this policy. \ No newline at end of file diff --git a/src/handlers/gateway/gateway.policy.test.tsx b/src/handlers/gateway/gateway.policy.test.tsx new file mode 100644 index 000000000..8afa7aeae --- /dev/null +++ b/src/handlers/gateway/gateway.policy.test.tsx @@ -0,0 +1,138 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { CoreClient } from "../../core"; +import { + createSilentLogger, + fixtureFactories, + matchGolden, + TestGlobalConfigAccessor, + testIO, +} from "../../testing"; +import { createRootHandler } from "../index"; + +const REGION = "us-west-2"; +const FIXTURES = join(import.meta.dir, "__fixtures__", "policy"); +const GATEWAY_ID = "policygene2e-tools-zhijfh6m5z"; +const GATEWAY_ARN = `arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/${GATEWAY_ID}`; +const ENGINE_ARN = + "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t"; +const BARE_GATEWAY_ID = "policygene2e-bare-xl0dy3pq5h"; +const RECORD_TIMEOUT = 600_000; + +// The fixture graph is the deployed `PolicyGenE2E` project: Gateway `tools` with +// Policy Engine `Guardrails` attached, and Gateway `bare` with no engine. Record with: +// RECORD=1 bun test src/handlers/gateway/gateway.policy.test.tsx +function createFixtureCore(): CoreClient { + const { createControlClient, createDataClient, createIamClient, createLogsClient } = + fixtureFactories(FIXTURES); + return new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + createLogsClient, + logger: createSilentLogger(), + }); +} + +async function run(args: string[]): Promise<{ stdout: string; stderr: string }> { + const io = testIO(); + const root = createRootHandler(createFixtureCore(), { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + await root.route([ + "node", + "agentcore", + "gateway", + "policy", + "generate", + ...args, + "--region", + REGION, + ]); + return { stdout: io.stdout(), stderr: io.stderr() }; +} + +describe("gateway policy generate fixture-backed flows", () => { + test( + "prints the Cedar on stdout and the findings on stderr with the attached engine", + async () => { + const { stdout, stderr } = await run([ + "--gateway-id", + GATEWAY_ID, + "--prompt", + "forbid IAM principals from calling any tool on this gateway", + "--name", + "golden_forbid", + ]); + matchGolden(FIXTURES, "generate.golden.cedar", stdout); + matchGolden(FIXTURES, "generate.golden.stderr", stderr); + expect(stdout).toContain(`resource == AgentCore::Gateway::"${GATEWAY_ARN}"`); + }, + RECORD_TIMEOUT, + ); + + test( + "prints the result object with --json for an ARN and an explicit engine ARN", + async () => { + const { stdout } = await run([ + "--gateway-id", + GATEWAY_ARN, + "--policy-engine-id", + ENGINE_ARN, + "--prompt", + "permit IAM principals to call any tool on this gateway", + "--name", + "golden_permit", + "--json", + ]); + matchGolden(FIXTURES, "generate-json.golden.json", stdout); + expect(JSON.parse(stdout)).toMatchObject({ + policyEngineId: "PolicyGenE2E_Guardrails-gn5jf72o3t", + gatewayArn: GATEWAY_ARN, + }); + }, + RECORD_TIMEOUT, + ); + + test.each([ + [ + "the gateway has no engine attached", + ["--gateway-id", BARE_GATEWAY_ID, "--prompt", "forbid everything", "--name", "golden_bare"], + /has no Policy Engine attached; pass --policy-engine-id/, + ], + [ + "the explicit engine does not exist", + [ + "--gateway-id", + GATEWAY_ID, + "--policy-engine-id", + "pe-does-not-exist", + "--prompt", + "forbid everything", + "--name", + "golden_missing_engine", + ], + /policyEngineId/, + ], + [ + "the prompt cannot be translated", + [ + "--gateway-id", + GATEWAY_ID, + "--prompt", + "permit everyone to list tools but forbid calling any tool whose name contains delete", + "--name", + "golden_untranslatable", + ], + /could not be translated into a Cedar policy: \[INVALID\]/, + ], + ])( + "fails when %s", + async (_label, args, message) => { + await expect(run(args)).rejects.toThrow(message); + }, + RECORD_TIMEOUT, + ); +}); diff --git a/src/handlers/gateway/gateway.test.tsx b/src/handlers/gateway/gateway.test.tsx index af7ed599c..f61218b05 100644 --- a/src/handlers/gateway/gateway.test.tsx +++ b/src/handlers/gateway/gateway.test.tsx @@ -1,4 +1,12 @@ import { describe, expect, test } from "bun:test"; +import { + GetGatewayCommand, + GetPolicyGenerationCommand, + StartPolicyGenerationCommand, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { PolicyClient } from "../../core/policy"; +import type { AwsClients } from "../../core/types"; +import { NetworkingError } from "../../errors"; import { createSilentLogger, TestCoreClient, @@ -7,16 +15,17 @@ import { } from "../../testing"; import { compile, isTuiCommandSupported, ValueContext } from "../../router"; import { createRootHandler } from "../index"; +import type { Core } from "../types"; const REGION = "us-west-2"; const GATEWAY_ID = "gateway-1"; const TARGET_ID = "target-1"; const RULE_ID = "rule-1"; -async function run( +async function run( args: string[], - core = new TestCoreClient(), -): Promise<{ core: TestCoreClient; stdout: string }> { + core: C = new TestCoreClient() as unknown as C, +): Promise<{ core: C; stdout: string }> { const io = testIO(); const root = createRootHandler(core, { io: io.io, @@ -136,6 +145,12 @@ describe("gateway validation", () => { ["Rule get parent", ["gateway", "rule", "get", "--rule-id", RULE_ID], /--gateway-id/], ["Rule get child", ["gateway", "rule", "get", "--gateway-id", GATEWAY_ID], /--rule-id/], ["Rule list", ["gateway", "rule", "list", "--max-results", "1"], /--gateway-id/], + ["Policy generate gateway", ["gateway", "policy", "generate", "--prompt", "x"], /--gateway-id/], + [ + "Policy generate prompt", + ["gateway", "policy", "generate", "--gateway-id", GATEWAY_ID], + /--prompt/, + ], ] as const)( "rejects a missing selector for %s before calling Core", async (_name, args, error) => { @@ -143,6 +158,7 @@ describe("gateway validation", () => { await expect(run([...args], core)).rejects.toThrow(error); expect(core.gateway.calls).toEqual([]); + expect(core.policy.calls).toEqual([]); }, ); @@ -155,3 +171,50 @@ describe("gateway validation", () => { expect(core.gateway.calls).toEqual([]); }); }); + +/** + The waiter outcomes below cannot be recorded against the live service, so the + control plane is faked at .send() while the real PolicyClient and waiter run. +**/ +describe("gateway policy generate against a faked control plane", () => { + function coreWith(status: string, statusReasons?: string[]): Core { + const control = { + send: async (command: unknown) => { + if (command instanceof GetGatewayCommand) { + return { + gatewayArn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/gw-1", + policyEngineConfiguration: { + arn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:policy-engine/pe-1", + }, + }; + } + if (command instanceof StartPolicyGenerationCommand) return { policyGenerationId: "gen-1" }; + if (command instanceof GetPolicyGenerationCommand) return { status, statusReasons }; + throw new Error(`unexpected command ${(command as object).constructor.name}`); + }, + }; + const clients = { control: () => control } as unknown as AwsClients; + return { + ...new TestCoreClient(), + policy: new PolicyClient(clients, createSilentLogger(), { + maxWaitTime: 2, + minDelay: 1, + maxDelay: 1, + }), + }; + } + + const args = ["gateway", "policy", "generate", "--gateway-id", GATEWAY_ID, "--prompt", "x"]; + + test("fails with the service reasons when the generation fails", async () => { + await expect( + run(args, coreWith("GENERATE_FAILED", ["bad prompt", "try again"])), + ).rejects.toThrow("policy generation 'gen-1' failed: bad prompt; try again"); + }); + + test("times out when the generation keeps running", async () => { + const attempt = run(args, coreWith("GENERATING")); + await expect(attempt).rejects.toBeInstanceOf(NetworkingError); + await expect(attempt).rejects.toThrow("did not finish within 2s"); + }, 10_000); +}); diff --git a/src/handlers/gateway/policy/generate.test.tsx b/src/handlers/gateway/policy/generate.test.tsx deleted file mode 100644 index 438324ab4..000000000 --- a/src/handlers/gateway/policy/generate.test.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { NetworkingError } from "../../../errors"; -import { - createSilentLogger, - TestCoreClient, - TestGlobalConfigAccessor, - testIO, -} from "../../../testing"; -import { createRootHandler } from "../../index"; - -const REGION = "us-west-2"; -const GATEWAY_ARN = "arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/gw-1"; -const FORBID = "forbid (principal, action, resource is AgentCore::Gateway);"; -const PERMIT = "permit (principal, action, resource is AgentCore::Gateway);"; - -function subject(stdin?: string) { - const core = new TestCoreClient(); - const io = testIO({ stdin }); - const root = createRootHandler(core, { - io: io.io, - logger: createSilentLogger(), - globalConfigAccessor: new TestGlobalConfigAccessor(), - }); - return { - core, - io, - route: (args: string[]) => - root.route([ - "node", - "agentcore", - "gateway", - "policy", - "generate", - ...args, - "--region", - REGION, - ]), - }; -} - -describe("gateway policy generate", () => { - test.each([ - ["--gateway-id", []], - ["--prompt", ["--gateway-id", "gw-1"]], - ])("rejects a missing %s before calling the service", async (flag, args) => { - const { core, route } = subject(); - await expect(route(args)).rejects.toThrow(`required option '${flag}`); - expect(core.policy.calls).toEqual([]); - }); - - test("prints the Cedar on stdout and the findings on stderr", async () => { - const { core, io, route } = subject("deny everything\n"); - core.policy.result = { - policyGenerationId: "gen-1", - policyEngineId: "pe-explicit", - gatewayArn: GATEWAY_ARN, - policies: [ - { - statement: FORBID, - findings: [{ type: "DENY_ALL", description: "denies every request" }], - }, - { statement: undefined, findings: [{ type: "INVALID", description: "Non-translatable" }] }, - { statement: PERMIT, findings: [] }, - ], - }; - - await route([ - "--gateway-id", - GATEWAY_ARN, - "--policy-engine-id", - "pe-explicit", - "--prompt", - "-", - "--name", - "my_generation", - ]); - - expect(core.policy.calls).toEqual([ - { - gatewayId: GATEWAY_ARN, - policyEngineId: "pe-explicit", - prompt: "deny everything\n", - name: "my_generation", - }, - ]); - expect(io.stdout()).toBe(`${FORBID}\n\n${PERMIT}`); - expect(io.stderr()).toBe( - [ - "Generating policy", - "Findings:", - " policy 1 [DENY_ALL] denies every request", - " policy 2 [INVALID] Non-translatable", - ].join("\n"), - ); - }); - - test("prints the result object with --json and defaults the generation name", async () => { - const { core, io, route } = subject(); - - await route(["--gateway-id", "gw-1", "--prompt", "deny everything", "--json"]); - - expect(core.policy.calls[0]).toMatchObject({ - gatewayId: "gw-1", - policyEngineId: undefined, - prompt: "deny everything", - }); - expect(core.policy.calls[0]!.name).toMatch(/^cli_generation_\d+$/); - expect(JSON.parse(io.stdout())).toEqual(core.policy.result); - expect(io.stderr()).toBe("Generating policy"); - }); - - test("renders a --json error and writes no Cedar when generation fails", async () => { - const { core, io, route } = subject(); - core.policy.error = new NetworkingError("policy generation 'gen-1' did not finish within 60s"); - - await expect( - route(["--gateway-id", "gw-1", "--prompt", "deny everything", "--json"]), - ).rejects.toThrow("did not finish within 60s"); - expect(JSON.parse(io.stdout())).toEqual({ - error: "policy generation 'gen-1' did not finish within 60s", - }); - }); -}); diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 45d185db3..57ff599c4 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -2384,24 +2384,21 @@ export class TestObservabilityClient implements CoreObservabilityClient { } export class TestPolicyClient implements CorePolicyClient { - result: PolicyGenerationResult = { - policyGenerationId: "gen-1", - policyEngineId: "pe-1", - gatewayArn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/gw-1", - policies: [ - { statement: "forbid (principal, action, resource is AgentCore::Gateway);", findings: [] }, - ], - }; - error: Error | undefined; readonly calls: GeneratePolicyInput[] = []; async *generatePolicy( input: GeneratePolicyInput, ): AsyncGenerator { this.calls.push(input); - if (this.error) throw this.error; yield { type: "step", message: "Generating policy" }; - return this.result; + return { + policyGenerationId: "gen-1", + policyEngineId: "pe-1", + gatewayArn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/gw-1", + policies: [ + { statement: "forbid (principal, action, resource is AgentCore::Gateway);", findings: [] }, + ], + }; } } From ce49534fe9476b38ad5d6e6345540f77f1136c97 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Wed, 2 Sep 2026 16:06:17 -0400 Subject: [PATCH 5/5] feat(policy): print only the Cedar document; findings stay in --json --- ...cyGenerationCommand.7ae2ce2725ceedf1.json} | 10 ++++---- ...cyGenerationCommand.81eab1512df1bb7e.json} | 10 ++++---- ...cyGenerationCommand.c3cc22b88924256e.json} | 10 ++++---- ...rationAssetsCommand.7ae2ce2725ceedf1.json} | 2 +- ...rationAssetsCommand.81eab1512df1bb7e.json} | 8 +++---- ...rationAssetsCommand.c3cc22b88924256e.json} | 2 +- ...cyGenerationCommand.3add87bfdfde5e9e.json} | 10 ++++---- ...cyGenerationCommand.c16bc5832065a273.json} | 0 ...cyGenerationCommand.cbe8967379451f9d.json} | 10 ++++---- ...cyGenerationCommand.cf9789a397df7820.json} | 10 ++++---- .../policy/generate-json.golden.json | 2 +- .../policy/generate.golden.stderr | 6 ++--- .../__fixtures__/policy/generation-names.json | 6 +++++ src/handlers/gateway/gateway.policy.test.tsx | 23 +++++++++++++++---- src/handlers/gateway/policy/format.ts | 17 -------------- src/handlers/gateway/policy/generate.tsx | 7 +++--- 16 files changed, 67 insertions(+), 66 deletions(-) rename src/handlers/gateway/__fixtures__/policy/{GetPolicyGenerationCommand.362175715d99ad29.json => GetPolicyGenerationCommand.7ae2ce2725ceedf1.json} (62%) rename src/handlers/gateway/__fixtures__/policy/{GetPolicyGenerationCommand.c4e5fba54a2144ec.json => GetPolicyGenerationCommand.81eab1512df1bb7e.json} (60%) rename src/handlers/gateway/__fixtures__/policy/{GetPolicyGenerationCommand.363616a21dff1546.json => GetPolicyGenerationCommand.c3cc22b88924256e.json} (62%) rename src/handlers/gateway/__fixtures__/policy/{ListPolicyGenerationAssetsCommand.c4e5fba54a2144ec.json => ListPolicyGenerationAssetsCommand.7ae2ce2725ceedf1.json} (89%) rename src/handlers/gateway/__fixtures__/policy/{ListPolicyGenerationAssetsCommand.363616a21dff1546.json => ListPolicyGenerationAssetsCommand.81eab1512df1bb7e.json} (75%) rename src/handlers/gateway/__fixtures__/policy/{ListPolicyGenerationAssetsCommand.362175715d99ad29.json => ListPolicyGenerationAssetsCommand.c3cc22b88924256e.json} (89%) rename src/handlers/gateway/__fixtures__/policy/{StartPolicyGenerationCommand.1641ea5f6632d799.json => StartPolicyGenerationCommand.3add87bfdfde5e9e.json} (60%) rename src/handlers/gateway/__fixtures__/policy/{StartPolicyGenerationCommand.1182c7bafb2ef3d0.json => StartPolicyGenerationCommand.c16bc5832065a273.json} (100%) rename src/handlers/gateway/__fixtures__/policy/{StartPolicyGenerationCommand.c124c0c5bbf51352.json => StartPolicyGenerationCommand.cbe8967379451f9d.json} (62%) rename src/handlers/gateway/__fixtures__/policy/{StartPolicyGenerationCommand.fbcab060875e4a02.json => StartPolicyGenerationCommand.cf9789a397df7820.json} (62%) create mode 100644 src/handlers/gateway/__fixtures__/policy/generation-names.json delete mode 100644 src/handlers/gateway/policy/format.ts diff --git a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.362175715d99ad29.json b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.7ae2ce2725ceedf1.json similarity index 62% rename from src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.362175715d99ad29.json rename to src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.7ae2ce2725ceedf1.json index c5b552f1e..450017f64 100644 --- a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.362175715d99ad29.json +++ b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.7ae2ce2725ceedf1.json @@ -1,16 +1,16 @@ { "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", - "policyGenerationId": "golden_permit-62mnrlgenk", - "name": "golden_permit", - "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_permit-62mnrlgenk", + "policyGenerationId": "golden_forbid_1788379436724-6vfaj00xfb", + "name": "golden_forbid_1788379436724", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_forbid_1788379436724-6vfaj00xfb", "resource": { "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" }, "createdAt": { - "$date": "2026-09-02T19:52:59.583Z" + "$date": "2026-09-02T20:03:57.296Z" }, "updatedAt": { - "$date": "2026-09-02T19:53:09.854Z" + "$date": "2026-09-02T20:04:08.119Z" }, "status": "GENERATED", "statusReasons": [] diff --git a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c4e5fba54a2144ec.json b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.81eab1512df1bb7e.json similarity index 60% rename from src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c4e5fba54a2144ec.json rename to src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.81eab1512df1bb7e.json index dbd4a0b6f..df6619cd6 100644 --- a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c4e5fba54a2144ec.json +++ b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.81eab1512df1bb7e.json @@ -1,16 +1,16 @@ { "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", - "policyGenerationId": "golden_forbid-yovi81zlhe", - "name": "golden_forbid", - "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_forbid-yovi81zlhe", + "policyGenerationId": "golden_untranslatable_1788379436724-zwadwgy5xq", + "name": "golden_untranslatable_1788379436724", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_untranslatable_1788379436724-zwadwgy5xq", "resource": { "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" }, "createdAt": { - "$date": "2026-09-02T19:52:47.764Z" + "$date": "2026-09-02T20:04:26.137Z" }, "updatedAt": { - "$date": "2026-09-02T19:52:58.063Z" + "$date": "2026-09-02T20:04:34.371Z" }, "status": "GENERATED", "statusReasons": [] diff --git a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.363616a21dff1546.json b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c3cc22b88924256e.json similarity index 62% rename from src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.363616a21dff1546.json rename to src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c3cc22b88924256e.json index 96580f0b3..ad311b295 100644 --- a/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.363616a21dff1546.json +++ b/src/handlers/gateway/__fixtures__/policy/GetPolicyGenerationCommand.c3cc22b88924256e.json @@ -1,16 +1,16 @@ { "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", - "policyGenerationId": "golden_untranslatable-dh214s4x7d", - "name": "golden_untranslatable", - "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_untranslatable-dh214s4x7d", + "policyGenerationId": "golden_permit_1788379436724-ps3sdu9w0d", + "name": "golden_permit_1788379436724", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_permit_1788379436724-ps3sdu9w0d", "resource": { "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" }, "createdAt": { - "$date": "2026-09-02T19:53:11.658Z" + "$date": "2026-09-02T20:04:08.865Z" }, "updatedAt": { - "$date": "2026-09-02T19:53:20.774Z" + "$date": "2026-09-02T20:04:19.851Z" }, "status": "GENERATED", "statusReasons": [] diff --git a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c4e5fba54a2144ec.json b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.7ae2ce2725ceedf1.json similarity index 89% rename from src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c4e5fba54a2144ec.json rename to src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.7ae2ce2725ceedf1.json index 754732c5b..f6d2ca92b 100644 --- a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c4e5fba54a2144ec.json +++ b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.7ae2ce2725ceedf1.json @@ -1,7 +1,7 @@ { "policyGenerationAssets": [ { - "policyGenerationAssetId": "golden_forbid-8xiwjqekhd", + "policyGenerationAssetId": "golden_forbid_1788379436724-9nhse8m4wg", "rawTextFragment": "forbid IAM principals from calling any tool on this gateway", "findings": [ { diff --git a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.363616a21dff1546.json b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.81eab1512df1bb7e.json similarity index 75% rename from src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.363616a21dff1546.json rename to src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.81eab1512df1bb7e.json index a4f852812..da2dc1017 100644 --- a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.363616a21dff1546.json +++ b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.81eab1512df1bb7e.json @@ -1,8 +1,8 @@ { "policyGenerationAssets": [ { - "policyGenerationAssetId": "golden_untranslatable-jk98c0i1qv", - "rawTextFragment": "Permit everyone to list tools.", + "policyGenerationAssetId": "golden_untranslatable_1788379436724-s0kq76o_vl", + "rawTextFragment": "Forbid calling any tool whose name contains delete.", "findings": [ { "type": "INVALID", @@ -11,8 +11,8 @@ ] }, { - "policyGenerationAssetId": "golden_untranslatable-mt1y937v8n", - "rawTextFragment": "Forbid calling any tool whose name contains delete.", + "policyGenerationAssetId": "golden_untranslatable_1788379436724-v9e46aqhnp", + "rawTextFragment": "Permit everyone to list tools.", "findings": [ { "type": "INVALID", diff --git a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.362175715d99ad29.json b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c3cc22b88924256e.json similarity index 89% rename from src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.362175715d99ad29.json rename to src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c3cc22b88924256e.json index b00d36bff..3b1a745cd 100644 --- a/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.362175715d99ad29.json +++ b/src/handlers/gateway/__fixtures__/policy/ListPolicyGenerationAssetsCommand.c3cc22b88924256e.json @@ -1,7 +1,7 @@ { "policyGenerationAssets": [ { - "policyGenerationAssetId": "golden_permit-o9blndc30w", + "policyGenerationAssetId": "golden_permit_1788379436724-1jag4xr0sz", "rawTextFragment": "permit IAM principals to call any tool on this gateway", "findings": [ { diff --git a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1641ea5f6632d799.json b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.3add87bfdfde5e9e.json similarity index 60% rename from src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1641ea5f6632d799.json rename to src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.3add87bfdfde5e9e.json index 5ab9b457f..81d32ea2b 100644 --- a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1641ea5f6632d799.json +++ b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.3add87bfdfde5e9e.json @@ -1,16 +1,16 @@ { "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", - "policyGenerationId": "golden_untranslatable-dh214s4x7d", - "name": "golden_untranslatable", - "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_untranslatable-dh214s4x7d", + "policyGenerationId": "golden_untranslatable_1788379436724-zwadwgy5xq", + "name": "golden_untranslatable_1788379436724", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_untranslatable_1788379436724-zwadwgy5xq", "resource": { "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" }, "createdAt": { - "$date": "2026-09-02T19:53:11.658Z" + "$date": "2026-09-02T20:04:26.137Z" }, "updatedAt": { - "$date": "2026-09-02T19:53:11.658Z" + "$date": "2026-09-02T20:04:26.137Z" }, "status": "GENERATING", "statusReasons": [] diff --git a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1182c7bafb2ef3d0.json b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c16bc5832065a273.json similarity index 100% rename from src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.1182c7bafb2ef3d0.json rename to src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c16bc5832065a273.json diff --git a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c124c0c5bbf51352.json b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.cbe8967379451f9d.json similarity index 62% rename from src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c124c0c5bbf51352.json rename to src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.cbe8967379451f9d.json index acd687ae2..60e3372d7 100644 --- a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.c124c0c5bbf51352.json +++ b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.cbe8967379451f9d.json @@ -1,16 +1,16 @@ { "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", - "policyGenerationId": "golden_permit-62mnrlgenk", - "name": "golden_permit", - "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_permit-62mnrlgenk", + "policyGenerationId": "golden_permit_1788379436724-ps3sdu9w0d", + "name": "golden_permit_1788379436724", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_permit_1788379436724-ps3sdu9w0d", "resource": { "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" }, "createdAt": { - "$date": "2026-09-02T19:52:59.583Z" + "$date": "2026-09-02T20:04:08.865Z" }, "updatedAt": { - "$date": "2026-09-02T19:52:59.583Z" + "$date": "2026-09-02T20:04:08.865Z" }, "status": "GENERATING", "statusReasons": [] diff --git a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.fbcab060875e4a02.json b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.cf9789a397df7820.json similarity index 62% rename from src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.fbcab060875e4a02.json rename to src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.cf9789a397df7820.json index 9b1a5efd3..e30f2591d 100644 --- a/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.fbcab060875e4a02.json +++ b/src/handlers/gateway/__fixtures__/policy/StartPolicyGenerationCommand.cf9789a397df7820.json @@ -1,16 +1,16 @@ { "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", - "policyGenerationId": "golden_forbid-yovi81zlhe", - "name": "golden_forbid", - "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_forbid-yovi81zlhe", + "policyGenerationId": "golden_forbid_1788379436724-6vfaj00xfb", + "name": "golden_forbid_1788379436724", + "policyGenerationArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:policy-engine/PolicyGenE2E_Guardrails-gn5jf72o3t/policy-generation/golden_forbid_1788379436724-6vfaj00xfb", "resource": { "arn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z" }, "createdAt": { - "$date": "2026-09-02T19:52:47.764Z" + "$date": "2026-09-02T20:03:57.296Z" }, "updatedAt": { - "$date": "2026-09-02T19:52:47.764Z" + "$date": "2026-09-02T20:03:57.296Z" }, "status": "GENERATING", "statusReasons": [] diff --git a/src/handlers/gateway/__fixtures__/policy/generate-json.golden.json b/src/handlers/gateway/__fixtures__/policy/generate-json.golden.json index 68cac36ed..c60a2a3d8 100644 --- a/src/handlers/gateway/__fixtures__/policy/generate-json.golden.json +++ b/src/handlers/gateway/__fixtures__/policy/generate-json.golden.json @@ -1,5 +1,5 @@ { - "policyGenerationId": "golden_permit-62mnrlgenk", + "policyGenerationId": "golden_permit_1788379436724-ps3sdu9w0d", "policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t", "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:887863153624:gateway/policygene2e-tools-zhijfh6m5z", "policies": [ diff --git a/src/handlers/gateway/__fixtures__/policy/generate.golden.stderr b/src/handlers/gateway/__fixtures__/policy/generate.golden.stderr index 224011afb..d8d8090e1 100644 --- a/src/handlers/gateway/__fixtures__/policy/generate.golden.stderr +++ b/src/handlers/gateway/__fixtures__/policy/generate.golden.stderr @@ -1,6 +1,4 @@ Resolving gateway policygene2e-tools-zhijfh6m5z -Starting policy generation golden_forbid +Starting policy generation golden_forbid_1788379436724 Waiting for generation to complete -Reading generated policies -Findings: - policy 1 [DENY_ALL] Overly Restrictive: The generated policy denies all actions for all principals. Confirm that full restriction is intended before applying this policy. \ No newline at end of file +Reading generated policies \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/policy/generation-names.json b/src/handlers/gateway/__fixtures__/policy/generation-names.json new file mode 100644 index 000000000..14e0dc2e9 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/policy/generation-names.json @@ -0,0 +1,6 @@ +{ + "forbid": "golden_forbid_1788379436724", + "permit": "golden_permit_1788379436724", + "missingEngine": "golden_missing_engine_1788379436724", + "untranslatable": "golden_untranslatable_1788379436724" +} \ No newline at end of file diff --git a/src/handlers/gateway/gateway.policy.test.tsx b/src/handlers/gateway/gateway.policy.test.tsx index 8afa7aeae..2a6fe9a60 100644 --- a/src/handlers/gateway/gateway.policy.test.tsx +++ b/src/handlers/gateway/gateway.policy.test.tsx @@ -7,6 +7,7 @@ import { matchGolden, TestGlobalConfigAccessor, testIO, + uniquePerRecording, } from "../../testing"; import { createRootHandler } from "../index"; @@ -19,6 +20,18 @@ const ENGINE_ARN = const BARE_GATEWAY_ID = "policygene2e-bare-xl0dy3pq5h"; const RECORD_TIMEOUT = 600_000; +// Generation names are unique per engine on the service, so each recording needs +// fresh ones while replays reuse the recorded set. +const NAMES = uniquePerRecording(FIXTURES, "generation-names", () => { + const stamp = Date.now(); + return { + forbid: `golden_forbid_${stamp}`, + permit: `golden_permit_${stamp}`, + missingEngine: `golden_missing_engine_${stamp}`, + untranslatable: `golden_untranslatable_${stamp}`, + }; +}); + // The fixture graph is the deployed `PolicyGenE2E` project: Gateway `tools` with // Policy Engine `Guardrails` attached, and Gateway `bare` with no engine. Record with: // RECORD=1 bun test src/handlers/gateway/gateway.policy.test.tsx @@ -64,7 +77,7 @@ describe("gateway policy generate fixture-backed flows", () => { "--prompt", "forbid IAM principals from calling any tool on this gateway", "--name", - "golden_forbid", + NAMES.forbid, ]); matchGolden(FIXTURES, "generate.golden.cedar", stdout); matchGolden(FIXTURES, "generate.golden.stderr", stderr); @@ -84,7 +97,7 @@ describe("gateway policy generate fixture-backed flows", () => { "--prompt", "permit IAM principals to call any tool on this gateway", "--name", - "golden_permit", + NAMES.permit, "--json", ]); matchGolden(FIXTURES, "generate-json.golden.json", stdout); @@ -99,7 +112,7 @@ describe("gateway policy generate fixture-backed flows", () => { test.each([ [ "the gateway has no engine attached", - ["--gateway-id", BARE_GATEWAY_ID, "--prompt", "forbid everything", "--name", "golden_bare"], + ["--gateway-id", BARE_GATEWAY_ID, "--prompt", "forbid everything"], /has no Policy Engine attached; pass --policy-engine-id/, ], [ @@ -112,7 +125,7 @@ describe("gateway policy generate fixture-backed flows", () => { "--prompt", "forbid everything", "--name", - "golden_missing_engine", + NAMES.missingEngine, ], /policyEngineId/, ], @@ -124,7 +137,7 @@ describe("gateway policy generate fixture-backed flows", () => { "--prompt", "permit everyone to list tools but forbid calling any tool whose name contains delete", "--name", - "golden_untranslatable", + NAMES.untranslatable, ], /could not be translated into a Cedar policy: \[INVALID\]/, ], diff --git a/src/handlers/gateway/policy/format.ts b/src/handlers/gateway/policy/format.ts deleted file mode 100644 index 0ed293edc..000000000 --- a/src/handlers/gateway/policy/format.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { GeneratedPolicy } from "./types"; - -export function formatStatements(policies: GeneratedPolicy[]): string { - const statements = policies.flatMap((policy) => - policy.statement ? [policy.statement.trimEnd()] : [], - ); - return `${statements.join("\n\n")}\n`; -} - -export function formatFindings(policies: GeneratedPolicy[]): string { - const rows = policies.flatMap((policy, index) => - policy.findings.map( - (finding) => ` policy ${index + 1} [${finding.type}] ${finding.description}`, - ), - ); - return rows.length === 0 ? "" : `Findings:\n${rows.join("\n")}\n`; -} diff --git a/src/handlers/gateway/policy/generate.tsx b/src/handlers/gateway/policy/generate.tsx index 74ac54bc1..3557c71fa 100644 --- a/src/handlers/gateway/policy/generate.tsx +++ b/src/handlers/gateway/policy/generate.tsx @@ -7,7 +7,6 @@ import { runWithProgress } from "../../../tui/progress"; import { JsonKey } from "../../keys"; import type { Core } from "../../types"; import { coreOptsFromCtx, renderJsonError } from "../../utils"; -import { formatFindings, formatStatements } from "./format"; import type { PolicyGenerationResult } from "./types"; export const createGeneratePolicyHandler = (core: Core, io: AppIO) => @@ -74,7 +73,9 @@ export const createGeneratePolicyHandler = (core: Core, io: AppIO) => ctx.require(JsonRendererKey).renderJson(result); return; } - io.stderr.write(formatFindings(result.policies)); - io.stdout.write(formatStatements(result.policies)); + const statements = result.policies.flatMap((policy) => + policy.statement ? [policy.statement.trimEnd()] : [], + ); + io.stdout.write(`${statements.join("\n\n")}\n`); }, });