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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -262,6 +264,11 @@ agentcore gateway connector get --gateway-id <gatewayId> --id <targetId>
agentcore gateway connector list --gateway-id <gatewayId> --max-results 20
agentcore gateway rule get --gateway-id <gatewayId> --rule-id <ruleId>
agentcore gateway rule list --gateway-id <gatewayId> --max-results 20
agentcore gateway policy generate --gateway-id <gatewayId> --prompt "forbid IAM callers from every tool"
agentcore gateway policy generate --gateway-id <gatewayArn> --prompt file://policy.txt --json
# Pipe the generated Cedar into a project (run inside the project)
agentcore gateway policy generate --gateway-id <gatewayId> --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 <key>
Expand Down
3 changes: 3 additions & 0 deletions src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down
123 changes: 123 additions & 0 deletions src/core/policy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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, 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 };

function resourceIdFromArn(value: string): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: OOS but there must be a shared util function for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOS but a lot of our coreClient function's don't yield helpful messages like in here. We should work on this later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

input: GeneratePolicyInput,
options: CoreOptions,
): AsyncGenerator<ProgressEvent, PolicyGenerationResult> {
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 }));
const gatewayArn = gateway.gatewayArn!;
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: gatewayArn },
content: { rawText: input.prompt },
name: input.name,
}),
);
const policyGenerationId = started.policyGenerationId!;
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(
`the prompt could not be translated into a Cedar policy${findings ? `: ${findings}` : ""}`,
{ source: ERROR_SOURCE.SERVICE, meta },
);
}
return { policyGenerationId, policyEngineId, gatewayArn, policies };
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t",
"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-02T20:03:57.296Z"
},
"updatedAt": {
"$date": "2026-09-02T20:04:08.119Z"
},
"status": "GENERATED",
"statusReasons": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t",
"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-02T20:04:26.137Z"
},
"updatedAt": {
"$date": "2026-09-02T20:04:34.371Z"
},
"status": "GENERATED",
"statusReasons": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t",
"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-02T20:04:08.865Z"
},
"updatedAt": {
"$date": "2026-09-02T20:04:19.851Z"
},
"status": "GENERATED",
"statusReasons": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"policyGenerationAssets": [
{
"policyGenerationAssetId": "golden_forbid_1788379436724-9nhse8m4wg",
"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\");"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"policyGenerationAssets": [
{
"policyGenerationAssetId": "golden_untranslatable_1788379436724-s0kq76o_vl",
"rawTextFragment": "Forbid calling any tool whose name contains delete.",
"findings": [
{
"type": "INVALID",
"description": "Non-translatable: cannot be expressed in Dogwood"
}
]
},
{
"policyGenerationAssetId": "golden_untranslatable_1788379436724-v9e46aqhnp",
"rawTextFragment": "Permit everyone to list tools.",
"findings": [
{
"type": "INVALID",
"description": "Non-translatable: cannot be expressed in Dogwood"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"policyGenerationAssets": [
{
"policyGenerationAssetId": "golden_permit_1788379436724-1jag4xr0sz",
"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\");"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t",
"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-02T20:04:26.137Z"
},
"updatedAt": {
"$date": "2026-09-02T20:04:26.137Z"
},
"status": "GENERATING",
"statusReasons": []
}
Original file line number Diff line number Diff line change
@@ -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}$"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"policyEngineId": "PolicyGenE2E_Guardrails-gn5jf72o3t",
"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-02T20:04:08.865Z"
},
"updatedAt": {
"$date": "2026-09-02T20:04:08.865Z"
},
"status": "GENERATING",
"statusReasons": []
}
Loading
Loading