Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33c4b00
feat: add policy engine and policy support with full deploy pipeline
jesseturner21 Mar 11, 2026
a80a005
feat: use composite key for policy removal to handle cross-engine nam…
jesseturner21 Mar 13, 2026
0bc9914
fix: sync package-lock.json for npm@10 compatibility
jesseturner21 Mar 13, 2026
b0197e0
fix: resolve lint and formatting issues for CI
jesseturner21 Mar 13, 2026
e118d1e
fix: make --statement, --source, --generate mutually exclusive in add…
jesseturner21 Mar 19, 2026
c61bfce
feat: add policy engine and policy support to TUI remove flow
jesseturner21 Mar 19, 2026
f5c37ea
fix: write both CLIENT_ID and CLIENT_SECRET env vars for managed OAut…
jesseturner21 Mar 19, 2026
d642bd7
feat: add PolicyEngineConfiguration support for gateways
Hweinstock Mar 19, 2026
0486995
feat: add policy engine selection to gateway TUI wizard
Hweinstock Mar 19, 2026
c9c86ca
fix: shorten disabled policy generate description to prevent truncation
jesseturner21 Mar 20, 2026
7bfc445
fix: prevent infinite loop when pressing Escape on policy generation …
jesseturner21 Mar 20, 2026
17997d6
chore: remove legacy McpGateway output pattern from gateway parser
jesseturner21 Mar 23, 2026
2f90d2b
test: add integ tests for --statement/--source/--generate mutual excl…
jesseturner21 Mar 23, 2026
d03d32c
fix: remove unnecessary sourceFile existence check from validate
jesseturner21 Mar 23, 2026
62a73a1
fix: respect --json flag in policy remove command
jesseturner21 Mar 23, 2026
900810a
chore: co-locate hasPolicyEngines with other has* checks in preflight
jesseturner21 Mar 23, 2026
d0a4df2
fix: address PR review comments for policy support
jesseturner21 Mar 23, 2026
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
469 changes: 469 additions & 0 deletions integ-tests/add-remove-policy.test.ts

Large diffs are not rendered by default.

2,033 changes: 247 additions & 1,786 deletions package-lock.json

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -374,6 +374,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
1 change: 1 addition & 0 deletions src/assets/cdk/test/cdk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
8 changes: 8 additions & 0 deletions src/cli/aws/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,14 @@ export {
} from './agentcore-control';
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
export { enableTransactionSearch, type TransactionSearchEnableResult } from './transaction-search';
export {
startPolicyGeneration,
getPolicyGeneration,
type StartPolicyGenerationOptions,
type StartPolicyGenerationResult,
type GetPolicyGenerationOptions,
type GetPolicyGenerationResult,
} from './policy-generation';
export {
DEFAULT_RUNTIME_USER_ID,
invokeA2ARuntime,
Expand Down
116 changes: 116 additions & 0 deletions src/cli/aws/policy-generation.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { getCredentialProvider } from './account';
import {
BedrockAgentCoreControlClient,
GetPolicyGenerationCommand,
ListPolicyGenerationAssetsCommand,
StartPolicyGenerationCommand,
waitUntilPolicyGenerationCompleted,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { WaiterState } from '@smithy/util-waiter';

export interface StartPolicyGenerationOptions {
policyEngineId: string;
description: string;
region: string;
resourceArn: string;
}

export interface StartPolicyGenerationResult {
generationId: string;
}

export interface GetPolicyGenerationOptions {
generationId: string;
policyEngineId: string;
region: string;
}

export interface GetPolicyGenerationResult {
status: string;
statement: string;
}

export async function startPolicyGeneration(
options: StartPolicyGenerationOptions
): Promise<StartPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

const command = new StartPolicyGenerationCommand({
policyEngineId: options.policyEngineId,
resource: { arn: options.resourceArn },
content: {
rawText: options.description,
},
name: `cli_generation_${Date.now()}`,
});

const response = await client.send(command);

if (!response.policyGenerationId) {
throw new Error('No generation ID returned from StartPolicyGeneration');
}

return { generationId: response.policyGenerationId };
}

export async function getPolicyGeneration(options: GetPolicyGenerationOptions): Promise<GetPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

// Use the SDK waiter to poll until generation completes
const waiterResult = await waitUntilPolicyGenerationCompleted(
{ client, maxWaitTime: 120, minDelay: 2, maxDelay: 5 },
{ policyGenerationId: options.generationId, policyEngineId: options.policyEngineId }
);

Comment thread
jesseturner21 marked this conversation as resolved.
if (waiterResult.state !== WaiterState.SUCCESS) {
throw new Error(
`Policy generation did not complete within the timeout period (state: ${waiterResult.state}). ` +
`Generation ID: ${options.generationId}`
);
}

// Check the final status
const getCommand = new GetPolicyGenerationCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const statusResponse = await client.send(getCommand);

if (statusResponse.status === 'GENERATE_FAILED') {
const reasons = statusResponse.statusReasons?.join(', ') ?? 'Unknown reason';
throw new Error(`Policy generation failed: ${reasons}`);
}

// Fetch the generated assets
const assetsCommand = new ListPolicyGenerationAssetsCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const assetsResponse = await client.send(assetsCommand);
const assets = assetsResponse.policyGenerationAssets ?? [];

if (assets.length === 0) {
throw new Error('Policy generation completed but no assets were returned');
}

// Get the Cedar statement from the first asset
const firstAsset = assets[0]!;
Comment thread
jesseturner21 marked this conversation as resolved.
const cedarStatement = firstAsset.definition?.cedar?.statement;

if (!cedarStatement) {
throw new Error('Policy generation completed but no Cedar policy statement was found in the assets');
}

return {
status: statusResponse.status ?? 'GENERATED',
statement: cedarStatement,
};
}
186 changes: 185 additions & 1 deletion src/cli/cloudformation/__tests__/outputs.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
import { buildDeployedState, parseGatewayOutputs, parseMemoryOutputs } from '../outputs';
import {
buildDeployedState,
parseGatewayOutputs,
parseMemoryOutputs,
parsePolicyEngineOutputs,
parsePolicyOutputs,
} from '../outputs';
import { describe, expect, it } from 'vitest';

describe('buildDeployedState', () => {
Expand DownExpand Up@@ -285,3 +291,181 @@ describe('parseMemoryOutputs', () => {
expect(result).toEqual({});
});
});

describe('parsePolicyEngineOutputs', () => {
it('extracts policy engine outputs matching pattern', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
ApplicationPolicyEngineMyEngineArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
});
});

it('handles multiple policy engines', () => {
const outputs = {
ApplicationPolicyEngineFirstEngineIdOutput123: 'pe-1',
ApplicationPolicyEngineFirstEngineArnOutput123: 'arn:pe-1',
ApplicationPolicyEngineSecondEngineIdOutput456: 'pe-2',
ApplicationPolicyEngineSecondEngineArnOutput456: 'arn:pe-2',
};

const result = parsePolicyEngineOutputs(outputs, ['FirstEngine', 'SecondEngine']);

expect(Object.keys(result)).toHaveLength(2);
expect(result.FirstEngine?.policyEngineId).toBe('pe-1');
expect(result.SecondEngine?.policyEngineId).toBe('pe-2');
});

it('returns empty record when no policy engine outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});

it('skips incomplete policy engine outputs (missing ARN)', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});
});

describe('parsePolicyOutputs', () => {
it('extracts policy outputs matching pattern', () => {
const outputs = {
ApplicationPolicyMyEngineDenyAllIdOutputABC123: 'pol-123',
ApplicationPolicyMyEngineDenyAllArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
});
});

it('handles multiple policies across engines', () => {
const outputs = {
ApplicationPolicyEngine1Policy1IdOutput123: 'pol-1',
ApplicationPolicyEngine1Policy1ArnOutput123: 'arn:pol-1',
ApplicationPolicyEngine1Policy2IdOutput456: 'pol-2',
ApplicationPolicyEngine1Policy2ArnOutput456: 'arn:pol-2',
};

const result = parsePolicyOutputs(outputs, [
{ engineName: 'Engine1', policyName: 'Policy1' },
{ engineName: 'Engine1', policyName: 'Policy2' },
]);

expect(Object.keys(result)).toHaveLength(2);
expect(result['Engine1/Policy1']?.policyId).toBe('pol-1');
expect(result['Engine1/Policy2']?.policyId).toBe('pol-2');
});

it('returns empty record when no policy outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({});
});
});

describe('buildDeployedState with policy data', () => {
it('includes policyEngines in deployed state when provided', () => {
const policyEngines = {
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines,
});

expect(result.targets.default!.resources?.policyEngines).toEqual(policyEngines);
});

it('includes policies in deployed state when provided', () => {
const policies = {
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies,
});

expect(result.targets.default!.resources?.policies).toEqual(policies);
});

it('omits policyEngines field when policyEngines is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});

it('omits policies field when policies is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies: {},
});

expect(result.targets.default!.resources?.policies).toBeUndefined();
});

it('omits policyEngines field when not provided', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33c4b00
feat: add policy engine and policy support with full deploy pipeline
jesseturner21 Mar 11, 2026
a80a005
feat: use composite key for policy removal to handle cross-engine nam…
jesseturner21 Mar 13, 2026
0bc9914
fix: sync package-lock.json for npm@10 compatibility
jesseturner21 Mar 13, 2026
b0197e0
fix: resolve lint and formatting issues for CI
jesseturner21 Mar 13, 2026
e118d1e
fix: make --statement, --source, --generate mutually exclusive in add…
jesseturner21 Mar 19, 2026
c61bfce
feat: add policy engine and policy support to TUI remove flow
jesseturner21 Mar 19, 2026
f5c37ea
fix: write both CLIENT_ID and CLIENT_SECRET env vars for managed OAut…
jesseturner21 Mar 19, 2026
d642bd7
feat: add PolicyEngineConfiguration support for gateways
Hweinstock Mar 19, 2026
0486995
feat: add policy engine selection to gateway TUI wizard
Hweinstock Mar 19, 2026
c9c86ca
fix: shorten disabled policy generate description to prevent truncation
jesseturner21 Mar 20, 2026
7bfc445
fix: prevent infinite loop when pressing Escape on policy generation …
jesseturner21 Mar 20, 2026
17997d6
chore: remove legacy McpGateway output pattern from gateway parser
jesseturner21 Mar 23, 2026
2f90d2b
test: add integ tests for --statement/--source/--generate mutual excl…
jesseturner21 Mar 23, 2026
d03d32c
fix: remove unnecessary sourceFile existence check from validate
jesseturner21 Mar 23, 2026
62a73a1
fix: respect --json flag in policy remove command
jesseturner21 Mar 23, 2026
900810a
chore: co-locate hasPolicyEngines with other has* checks in preflight
jesseturner21 Mar 23, 2026
d0a4df2
fix: address PR review comments for policy support
jesseturner21 Mar 23, 2026
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
469 changes: 469 additions & 0 deletions integ-tests/add-remove-policy.test.ts

Large diffs are not rendered by default.

2,033 changes: 247 additions & 1,786 deletions package-lock.json

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -374,6 +374,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
1 change: 1 addition & 0 deletions src/assets/cdk/test/cdk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
8 changes: 8 additions & 0 deletions src/cli/aws/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,14 @@ export {
} from './agentcore-control';
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
export { enableTransactionSearch, type TransactionSearchEnableResult } from './transaction-search';
export {
startPolicyGeneration,
getPolicyGeneration,
type StartPolicyGenerationOptions,
type StartPolicyGenerationResult,
type GetPolicyGenerationOptions,
type GetPolicyGenerationResult,
} from './policy-generation';
export {
DEFAULT_RUNTIME_USER_ID,
invokeA2ARuntime,
Expand Down
116 changes: 116 additions & 0 deletions src/cli/aws/policy-generation.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { getCredentialProvider } from './account';
import {
BedrockAgentCoreControlClient,
GetPolicyGenerationCommand,
ListPolicyGenerationAssetsCommand,
StartPolicyGenerationCommand,
waitUntilPolicyGenerationCompleted,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { WaiterState } from '@smithy/util-waiter';

export interface StartPolicyGenerationOptions {
policyEngineId: string;
description: string;
region: string;
resourceArn: string;
}

export interface StartPolicyGenerationResult {
generationId: string;
}

export interface GetPolicyGenerationOptions {
generationId: string;
policyEngineId: string;
region: string;
}

export interface GetPolicyGenerationResult {
status: string;
statement: string;
}

export async function startPolicyGeneration(
options: StartPolicyGenerationOptions
): Promise<StartPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

const command = new StartPolicyGenerationCommand({
policyEngineId: options.policyEngineId,
resource: { arn: options.resourceArn },
content: {
rawText: options.description,
},
name: `cli_generation_${Date.now()}`,
});

const response = await client.send(command);

if (!response.policyGenerationId) {
throw new Error('No generation ID returned from StartPolicyGeneration');
}

return { generationId: response.policyGenerationId };
}

export async function getPolicyGeneration(options: GetPolicyGenerationOptions): Promise<GetPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

// Use the SDK waiter to poll until generation completes
const waiterResult = await waitUntilPolicyGenerationCompleted(
{ client, maxWaitTime: 120, minDelay: 2, maxDelay: 5 },
{ policyGenerationId: options.generationId, policyEngineId: options.policyEngineId }
);

Comment thread
jesseturner21 marked this conversation as resolved.
if (waiterResult.state !== WaiterState.SUCCESS) {
throw new Error(
`Policy generation did not complete within the timeout period (state: ${waiterResult.state}). ` +
`Generation ID: ${options.generationId}`
);
}

// Check the final status
const getCommand = new GetPolicyGenerationCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const statusResponse = await client.send(getCommand);

if (statusResponse.status === 'GENERATE_FAILED') {
const reasons = statusResponse.statusReasons?.join(', ') ?? 'Unknown reason';
throw new Error(`Policy generation failed: ${reasons}`);
}

// Fetch the generated assets
const assetsCommand = new ListPolicyGenerationAssetsCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const assetsResponse = await client.send(assetsCommand);
const assets = assetsResponse.policyGenerationAssets ?? [];

if (assets.length === 0) {
throw new Error('Policy generation completed but no assets were returned');
}

// Get the Cedar statement from the first asset
const firstAsset = assets[0]!;
Comment thread
jesseturner21 marked this conversation as resolved.
const cedarStatement = firstAsset.definition?.cedar?.statement;

if (!cedarStatement) {
throw new Error('Policy generation completed but no Cedar policy statement was found in the assets');
}

return {
status: statusResponse.status ?? 'GENERATED',
statement: cedarStatement,
};
}
186 changes: 185 additions & 1 deletion src/cli/cloudformation/__tests__/outputs.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
import { buildDeployedState, parseGatewayOutputs, parseMemoryOutputs } from '../outputs';
import {
buildDeployedState,
parseGatewayOutputs,
parseMemoryOutputs,
parsePolicyEngineOutputs,
parsePolicyOutputs,
} from '../outputs';
import { describe, expect, it } from 'vitest';

describe('buildDeployedState', () => {
Expand DownExpand Up@@ -285,3 +291,181 @@ describe('parseMemoryOutputs', () => {
expect(result).toEqual({});
});
});

describe('parsePolicyEngineOutputs', () => {
it('extracts policy engine outputs matching pattern', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
ApplicationPolicyEngineMyEngineArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
});
});

it('handles multiple policy engines', () => {
const outputs = {
ApplicationPolicyEngineFirstEngineIdOutput123: 'pe-1',
ApplicationPolicyEngineFirstEngineArnOutput123: 'arn:pe-1',
ApplicationPolicyEngineSecondEngineIdOutput456: 'pe-2',
ApplicationPolicyEngineSecondEngineArnOutput456: 'arn:pe-2',
};

const result = parsePolicyEngineOutputs(outputs, ['FirstEngine', 'SecondEngine']);

expect(Object.keys(result)).toHaveLength(2);
expect(result.FirstEngine?.policyEngineId).toBe('pe-1');
expect(result.SecondEngine?.policyEngineId).toBe('pe-2');
});

it('returns empty record when no policy engine outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});

it('skips incomplete policy engine outputs (missing ARN)', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});
});

describe('parsePolicyOutputs', () => {
it('extracts policy outputs matching pattern', () => {
const outputs = {
ApplicationPolicyMyEngineDenyAllIdOutputABC123: 'pol-123',
ApplicationPolicyMyEngineDenyAllArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
});
});

it('handles multiple policies across engines', () => {
const outputs = {
ApplicationPolicyEngine1Policy1IdOutput123: 'pol-1',
ApplicationPolicyEngine1Policy1ArnOutput123: 'arn:pol-1',
ApplicationPolicyEngine1Policy2IdOutput456: 'pol-2',
ApplicationPolicyEngine1Policy2ArnOutput456: 'arn:pol-2',
};

const result = parsePolicyOutputs(outputs, [
{ engineName: 'Engine1', policyName: 'Policy1' },
{ engineName: 'Engine1', policyName: 'Policy2' },
]);

expect(Object.keys(result)).toHaveLength(2);
expect(result['Engine1/Policy1']?.policyId).toBe('pol-1');
expect(result['Engine1/Policy2']?.policyId).toBe('pol-2');
});

it('returns empty record when no policy outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({});
});
});

describe('buildDeployedState with policy data', () => {
it('includes policyEngines in deployed state when provided', () => {
const policyEngines = {
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines,
});

expect(result.targets.default!.resources?.policyEngines).toEqual(policyEngines);
});

it('includes policies in deployed state when provided', () => {
const policies = {
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies,
});

expect(result.targets.default!.resources?.policies).toEqual(policies);
});

it('omits policyEngines field when policyEngines is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});

it('omits policies field when policies is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies: {},
});

expect(result.targets.default!.resources?.policies).toBeUndefined();
});

it('omits policyEngines field when not provided', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33c4b00
feat: add policy engine and policy support with full deploy pipeline
jesseturner21 Mar 11, 2026
a80a005
feat: use composite key for policy removal to handle cross-engine nam…
jesseturner21 Mar 13, 2026
0bc9914
fix: sync package-lock.json for npm@10 compatibility
jesseturner21 Mar 13, 2026
b0197e0
fix: resolve lint and formatting issues for CI
jesseturner21 Mar 13, 2026
e118d1e
fix: make --statement, --source, --generate mutually exclusive in add…
jesseturner21 Mar 19, 2026
c61bfce
feat: add policy engine and policy support to TUI remove flow
jesseturner21 Mar 19, 2026
f5c37ea
fix: write both CLIENT_ID and CLIENT_SECRET env vars for managed OAut…
jesseturner21 Mar 19, 2026
d642bd7
feat: add PolicyEngineConfiguration support for gateways
Hweinstock Mar 19, 2026
0486995
feat: add policy engine selection to gateway TUI wizard
Hweinstock Mar 19, 2026
c9c86ca
fix: shorten disabled policy generate description to prevent truncation
jesseturner21 Mar 20, 2026
7bfc445
fix: prevent infinite loop when pressing Escape on policy generation …
jesseturner21 Mar 20, 2026
17997d6
chore: remove legacy McpGateway output pattern from gateway parser
jesseturner21 Mar 23, 2026
2f90d2b
test: add integ tests for --statement/--source/--generate mutual excl…
jesseturner21 Mar 23, 2026
d03d32c
fix: remove unnecessary sourceFile existence check from validate
jesseturner21 Mar 23, 2026
62a73a1
fix: respect --json flag in policy remove command
jesseturner21 Mar 23, 2026
900810a
chore: co-locate hasPolicyEngines with other has* checks in preflight
jesseturner21 Mar 23, 2026
d0a4df2
fix: address PR review comments for policy support
jesseturner21 Mar 23, 2026
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
469 changes: 469 additions & 0 deletions integ-tests/add-remove-policy.test.ts

Large diffs are not rendered by default.

2,033 changes: 247 additions & 1,786 deletions package-lock.json

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -374,6 +374,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
1 change: 1 addition & 0 deletions src/assets/cdk/test/cdk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
8 changes: 8 additions & 0 deletions src/cli/aws/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,14 @@ export {
} from './agentcore-control';
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
export { enableTransactionSearch, type TransactionSearchEnableResult } from './transaction-search';
export {
startPolicyGeneration,
getPolicyGeneration,
type StartPolicyGenerationOptions,
type StartPolicyGenerationResult,
type GetPolicyGenerationOptions,
type GetPolicyGenerationResult,
} from './policy-generation';
export {
DEFAULT_RUNTIME_USER_ID,
invokeA2ARuntime,
Expand Down
116 changes: 116 additions & 0 deletions src/cli/aws/policy-generation.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { getCredentialProvider } from './account';
import {
BedrockAgentCoreControlClient,
GetPolicyGenerationCommand,
ListPolicyGenerationAssetsCommand,
StartPolicyGenerationCommand,
waitUntilPolicyGenerationCompleted,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { WaiterState } from '@smithy/util-waiter';

export interface StartPolicyGenerationOptions {
policyEngineId: string;
description: string;
region: string;
resourceArn: string;
}

export interface StartPolicyGenerationResult {
generationId: string;
}

export interface GetPolicyGenerationOptions {
generationId: string;
policyEngineId: string;
region: string;
}

export interface GetPolicyGenerationResult {
status: string;
statement: string;
}

export async function startPolicyGeneration(
options: StartPolicyGenerationOptions
): Promise<StartPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

const command = new StartPolicyGenerationCommand({
policyEngineId: options.policyEngineId,
resource: { arn: options.resourceArn },
content: {
rawText: options.description,
},
name: `cli_generation_${Date.now()}`,
});

const response = await client.send(command);

if (!response.policyGenerationId) {
throw new Error('No generation ID returned from StartPolicyGeneration');
}

return { generationId: response.policyGenerationId };
}

export async function getPolicyGeneration(options: GetPolicyGenerationOptions): Promise<GetPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

// Use the SDK waiter to poll until generation completes
const waiterResult = await waitUntilPolicyGenerationCompleted(
{ client, maxWaitTime: 120, minDelay: 2, maxDelay: 5 },
{ policyGenerationId: options.generationId, policyEngineId: options.policyEngineId }
);

Comment thread
jesseturner21 marked this conversation as resolved.
if (waiterResult.state !== WaiterState.SUCCESS) {
throw new Error(
`Policy generation did not complete within the timeout period (state: ${waiterResult.state}). ` +
`Generation ID: ${options.generationId}`
);
}

// Check the final status
const getCommand = new GetPolicyGenerationCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const statusResponse = await client.send(getCommand);

if (statusResponse.status === 'GENERATE_FAILED') {
const reasons = statusResponse.statusReasons?.join(', ') ?? 'Unknown reason';
throw new Error(`Policy generation failed: ${reasons}`);
}

// Fetch the generated assets
const assetsCommand = new ListPolicyGenerationAssetsCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const assetsResponse = await client.send(assetsCommand);
const assets = assetsResponse.policyGenerationAssets ?? [];

if (assets.length === 0) {
throw new Error('Policy generation completed but no assets were returned');
}

// Get the Cedar statement from the first asset
const firstAsset = assets[0]!;
Comment thread
jesseturner21 marked this conversation as resolved.
const cedarStatement = firstAsset.definition?.cedar?.statement;

if (!cedarStatement) {
throw new Error('Policy generation completed but no Cedar policy statement was found in the assets');
}

return {
status: statusResponse.status ?? 'GENERATED',
statement: cedarStatement,
};
}
186 changes: 185 additions & 1 deletion src/cli/cloudformation/__tests__/outputs.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
import { buildDeployedState, parseGatewayOutputs, parseMemoryOutputs } from '../outputs';
import {
buildDeployedState,
parseGatewayOutputs,
parseMemoryOutputs,
parsePolicyEngineOutputs,
parsePolicyOutputs,
} from '../outputs';
import { describe, expect, it } from 'vitest';

describe('buildDeployedState', () => {
Expand DownExpand Up@@ -285,3 +291,181 @@ describe('parseMemoryOutputs', () => {
expect(result).toEqual({});
});
});

describe('parsePolicyEngineOutputs', () => {
it('extracts policy engine outputs matching pattern', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
ApplicationPolicyEngineMyEngineArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
});
});

it('handles multiple policy engines', () => {
const outputs = {
ApplicationPolicyEngineFirstEngineIdOutput123: 'pe-1',
ApplicationPolicyEngineFirstEngineArnOutput123: 'arn:pe-1',
ApplicationPolicyEngineSecondEngineIdOutput456: 'pe-2',
ApplicationPolicyEngineSecondEngineArnOutput456: 'arn:pe-2',
};

const result = parsePolicyEngineOutputs(outputs, ['FirstEngine', 'SecondEngine']);

expect(Object.keys(result)).toHaveLength(2);
expect(result.FirstEngine?.policyEngineId).toBe('pe-1');
expect(result.SecondEngine?.policyEngineId).toBe('pe-2');
});

it('returns empty record when no policy engine outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});

it('skips incomplete policy engine outputs (missing ARN)', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});
});

describe('parsePolicyOutputs', () => {
it('extracts policy outputs matching pattern', () => {
const outputs = {
ApplicationPolicyMyEngineDenyAllIdOutputABC123: 'pol-123',
ApplicationPolicyMyEngineDenyAllArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
});
});

it('handles multiple policies across engines', () => {
const outputs = {
ApplicationPolicyEngine1Policy1IdOutput123: 'pol-1',
ApplicationPolicyEngine1Policy1ArnOutput123: 'arn:pol-1',
ApplicationPolicyEngine1Policy2IdOutput456: 'pol-2',
ApplicationPolicyEngine1Policy2ArnOutput456: 'arn:pol-2',
};

const result = parsePolicyOutputs(outputs, [
{ engineName: 'Engine1', policyName: 'Policy1' },
{ engineName: 'Engine1', policyName: 'Policy2' },
]);

expect(Object.keys(result)).toHaveLength(2);
expect(result['Engine1/Policy1']?.policyId).toBe('pol-1');
expect(result['Engine1/Policy2']?.policyId).toBe('pol-2');
});

it('returns empty record when no policy outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({});
});
});

describe('buildDeployedState with policy data', () => {
it('includes policyEngines in deployed state when provided', () => {
const policyEngines = {
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines,
});

expect(result.targets.default!.resources?.policyEngines).toEqual(policyEngines);
});

it('includes policies in deployed state when provided', () => {
const policies = {
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies,
});

expect(result.targets.default!.resources?.policies).toEqual(policies);
});

it('omits policyEngines field when policyEngines is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});

it('omits policies field when policies is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies: {},
});

expect(result.targets.default!.resources?.policies).toBeUndefined();
});

it('omits policyEngines field when not provided', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33c4b00
feat: add policy engine and policy support with full deploy pipeline
jesseturner21 Mar 11, 2026
a80a005
feat: use composite key for policy removal to handle cross-engine nam…
jesseturner21 Mar 13, 2026
0bc9914
fix: sync package-lock.json for npm@10 compatibility
jesseturner21 Mar 13, 2026
b0197e0
fix: resolve lint and formatting issues for CI
jesseturner21 Mar 13, 2026
e118d1e
fix: make --statement, --source, --generate mutually exclusive in add…
jesseturner21 Mar 19, 2026
c61bfce
feat: add policy engine and policy support to TUI remove flow
jesseturner21 Mar 19, 2026
f5c37ea
fix: write both CLIENT_ID and CLIENT_SECRET env vars for managed OAut…
jesseturner21 Mar 19, 2026
d642bd7
feat: add PolicyEngineConfiguration support for gateways
Hweinstock Mar 19, 2026
0486995
feat: add policy engine selection to gateway TUI wizard
Hweinstock Mar 19, 2026
c9c86ca
fix: shorten disabled policy generate description to prevent truncation
jesseturner21 Mar 20, 2026
7bfc445
fix: prevent infinite loop when pressing Escape on policy generation …
jesseturner21 Mar 20, 2026
17997d6
chore: remove legacy McpGateway output pattern from gateway parser
jesseturner21 Mar 23, 2026
2f90d2b
test: add integ tests for --statement/--source/--generate mutual excl…
jesseturner21 Mar 23, 2026
d03d32c
fix: remove unnecessary sourceFile existence check from validate
jesseturner21 Mar 23, 2026
62a73a1
fix: respect --json flag in policy remove command
jesseturner21 Mar 23, 2026
900810a
chore: co-locate hasPolicyEngines with other has* checks in preflight
jesseturner21 Mar 23, 2026
d0a4df2
fix: address PR review comments for policy support
jesseturner21 Mar 23, 2026
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
469 changes: 469 additions & 0 deletions integ-tests/add-remove-policy.test.ts

Large diffs are not rendered by default.

2,033 changes: 247 additions & 1,786 deletions package-lock.json

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -374,6 +374,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
1 change: 1 addition & 0 deletions src/assets/cdk/test/cdk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
8 changes: 8 additions & 0 deletions src/cli/aws/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,14 @@ export {
} from './agentcore-control';
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
export { enableTransactionSearch, type TransactionSearchEnableResult } from './transaction-search';
export {
startPolicyGeneration,
getPolicyGeneration,
type StartPolicyGenerationOptions,
type StartPolicyGenerationResult,
type GetPolicyGenerationOptions,
type GetPolicyGenerationResult,
} from './policy-generation';
export {
DEFAULT_RUNTIME_USER_ID,
invokeA2ARuntime,
Expand Down
116 changes: 116 additions & 0 deletions src/cli/aws/policy-generation.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { getCredentialProvider } from './account';
import {
BedrockAgentCoreControlClient,
GetPolicyGenerationCommand,
ListPolicyGenerationAssetsCommand,
StartPolicyGenerationCommand,
waitUntilPolicyGenerationCompleted,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { WaiterState } from '@smithy/util-waiter';

export interface StartPolicyGenerationOptions {
policyEngineId: string;
description: string;
region: string;
resourceArn: string;
}

export interface StartPolicyGenerationResult {
generationId: string;
}

export interface GetPolicyGenerationOptions {
generationId: string;
policyEngineId: string;
region: string;
}

export interface GetPolicyGenerationResult {
status: string;
statement: string;
}

export async function startPolicyGeneration(
options: StartPolicyGenerationOptions
): Promise<StartPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

const command = new StartPolicyGenerationCommand({
policyEngineId: options.policyEngineId,
resource: { arn: options.resourceArn },
content: {
rawText: options.description,
},
name: `cli_generation_${Date.now()}`,
});

const response = await client.send(command);

if (!response.policyGenerationId) {
throw new Error('No generation ID returned from StartPolicyGeneration');
}

return { generationId: response.policyGenerationId };
}

export async function getPolicyGeneration(options: GetPolicyGenerationOptions): Promise<GetPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

// Use the SDK waiter to poll until generation completes
const waiterResult = await waitUntilPolicyGenerationCompleted(
{ client, maxWaitTime: 120, minDelay: 2, maxDelay: 5 },
{ policyGenerationId: options.generationId, policyEngineId: options.policyEngineId }
);

Comment thread
jesseturner21 marked this conversation as resolved.
if (waiterResult.state !== WaiterState.SUCCESS) {
throw new Error(
`Policy generation did not complete within the timeout period (state: ${waiterResult.state}). ` +
`Generation ID: ${options.generationId}`
);
}

// Check the final status
const getCommand = new GetPolicyGenerationCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const statusResponse = await client.send(getCommand);

if (statusResponse.status === 'GENERATE_FAILED') {
const reasons = statusResponse.statusReasons?.join(', ') ?? 'Unknown reason';
throw new Error(`Policy generation failed: ${reasons}`);
}

// Fetch the generated assets
const assetsCommand = new ListPolicyGenerationAssetsCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const assetsResponse = await client.send(assetsCommand);
const assets = assetsResponse.policyGenerationAssets ?? [];

if (assets.length === 0) {
throw new Error('Policy generation completed but no assets were returned');
}

// Get the Cedar statement from the first asset
const firstAsset = assets[0]!;
Comment thread
jesseturner21 marked this conversation as resolved.
const cedarStatement = firstAsset.definition?.cedar?.statement;

if (!cedarStatement) {
throw new Error('Policy generation completed but no Cedar policy statement was found in the assets');
}

return {
status: statusResponse.status ?? 'GENERATED',
statement: cedarStatement,
};
}
186 changes: 185 additions & 1 deletion src/cli/cloudformation/__tests__/outputs.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
import { buildDeployedState, parseGatewayOutputs, parseMemoryOutputs } from '../outputs';
import {
buildDeployedState,
parseGatewayOutputs,
parseMemoryOutputs,
parsePolicyEngineOutputs,
parsePolicyOutputs,
} from '../outputs';
import { describe, expect, it } from 'vitest';

describe('buildDeployedState', () => {
Expand DownExpand Up@@ -285,3 +291,181 @@ describe('parseMemoryOutputs', () => {
expect(result).toEqual({});
});
});

describe('parsePolicyEngineOutputs', () => {
it('extracts policy engine outputs matching pattern', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
ApplicationPolicyEngineMyEngineArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
});
});

it('handles multiple policy engines', () => {
const outputs = {
ApplicationPolicyEngineFirstEngineIdOutput123: 'pe-1',
ApplicationPolicyEngineFirstEngineArnOutput123: 'arn:pe-1',
ApplicationPolicyEngineSecondEngineIdOutput456: 'pe-2',
ApplicationPolicyEngineSecondEngineArnOutput456: 'arn:pe-2',
};

const result = parsePolicyEngineOutputs(outputs, ['FirstEngine', 'SecondEngine']);

expect(Object.keys(result)).toHaveLength(2);
expect(result.FirstEngine?.policyEngineId).toBe('pe-1');
expect(result.SecondEngine?.policyEngineId).toBe('pe-2');
});

it('returns empty record when no policy engine outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});

it('skips incomplete policy engine outputs (missing ARN)', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});
});

describe('parsePolicyOutputs', () => {
it('extracts policy outputs matching pattern', () => {
const outputs = {
ApplicationPolicyMyEngineDenyAllIdOutputABC123: 'pol-123',
ApplicationPolicyMyEngineDenyAllArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
});
});

it('handles multiple policies across engines', () => {
const outputs = {
ApplicationPolicyEngine1Policy1IdOutput123: 'pol-1',
ApplicationPolicyEngine1Policy1ArnOutput123: 'arn:pol-1',
ApplicationPolicyEngine1Policy2IdOutput456: 'pol-2',
ApplicationPolicyEngine1Policy2ArnOutput456: 'arn:pol-2',
};

const result = parsePolicyOutputs(outputs, [
{ engineName: 'Engine1', policyName: 'Policy1' },
{ engineName: 'Engine1', policyName: 'Policy2' },
]);

expect(Object.keys(result)).toHaveLength(2);
expect(result['Engine1/Policy1']?.policyId).toBe('pol-1');
expect(result['Engine1/Policy2']?.policyId).toBe('pol-2');
});

it('returns empty record when no policy outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({});
});
});

describe('buildDeployedState with policy data', () => {
it('includes policyEngines in deployed state when provided', () => {
const policyEngines = {
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines,
});

expect(result.targets.default!.resources?.policyEngines).toEqual(policyEngines);
});

it('includes policies in deployed state when provided', () => {
const policies = {
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies,
});

expect(result.targets.default!.resources?.policies).toEqual(policies);
});

it('omits policyEngines field when policyEngines is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});

it('omits policies field when policies is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies: {},
});

expect(result.targets.default!.resources?.policies).toBeUndefined();
});

it('omits policyEngines field when not provided', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33c4b00
feat: add policy engine and policy support with full deploy pipeline
jesseturner21 Mar 11, 2026
a80a005
feat: use composite key for policy removal to handle cross-engine nam…
jesseturner21 Mar 13, 2026
0bc9914
fix: sync package-lock.json for npm@10 compatibility
jesseturner21 Mar 13, 2026
b0197e0
fix: resolve lint and formatting issues for CI
jesseturner21 Mar 13, 2026
e118d1e
fix: make --statement, --source, --generate mutually exclusive in add…
jesseturner21 Mar 19, 2026
c61bfce
feat: add policy engine and policy support to TUI remove flow
jesseturner21 Mar 19, 2026
f5c37ea
fix: write both CLIENT_ID and CLIENT_SECRET env vars for managed OAut…
jesseturner21 Mar 19, 2026
d642bd7
feat: add PolicyEngineConfiguration support for gateways
Hweinstock Mar 19, 2026
0486995
feat: add policy engine selection to gateway TUI wizard
Hweinstock Mar 19, 2026
c9c86ca
fix: shorten disabled policy generate description to prevent truncation
jesseturner21 Mar 20, 2026
7bfc445
fix: prevent infinite loop when pressing Escape on policy generation …
jesseturner21 Mar 20, 2026
17997d6
chore: remove legacy McpGateway output pattern from gateway parser
jesseturner21 Mar 23, 2026
2f90d2b
test: add integ tests for --statement/--source/--generate mutual excl…
jesseturner21 Mar 23, 2026
d03d32c
fix: remove unnecessary sourceFile existence check from validate
jesseturner21 Mar 23, 2026
62a73a1
fix: respect --json flag in policy remove command
jesseturner21 Mar 23, 2026
900810a
chore: co-locate hasPolicyEngines with other has* checks in preflight
jesseturner21 Mar 23, 2026
d0a4df2
fix: address PR review comments for policy support
jesseturner21 Mar 23, 2026
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
469 changes: 469 additions & 0 deletions integ-tests/add-remove-policy.test.ts

Large diffs are not rendered by default.

2,033 changes: 247 additions & 1,786 deletions package-lock.json

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -374,6 +374,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
1 change: 1 addition & 0 deletions src/assets/cdk/test/cdk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
8 changes: 8 additions & 0 deletions src/cli/aws/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,14 @@ export {
} from './agentcore-control';
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
export { enableTransactionSearch, type TransactionSearchEnableResult } from './transaction-search';
export {
startPolicyGeneration,
getPolicyGeneration,
type StartPolicyGenerationOptions,
type StartPolicyGenerationResult,
type GetPolicyGenerationOptions,
type GetPolicyGenerationResult,
} from './policy-generation';
export {
DEFAULT_RUNTIME_USER_ID,
invokeA2ARuntime,
Expand Down
116 changes: 116 additions & 0 deletions src/cli/aws/policy-generation.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { getCredentialProvider } from './account';
import {
BedrockAgentCoreControlClient,
GetPolicyGenerationCommand,
ListPolicyGenerationAssetsCommand,
StartPolicyGenerationCommand,
waitUntilPolicyGenerationCompleted,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { WaiterState } from '@smithy/util-waiter';

export interface StartPolicyGenerationOptions {
policyEngineId: string;
description: string;
region: string;
resourceArn: string;
}

export interface StartPolicyGenerationResult {
generationId: string;
}

export interface GetPolicyGenerationOptions {
generationId: string;
policyEngineId: string;
region: string;
}

export interface GetPolicyGenerationResult {
status: string;
statement: string;
}

export async function startPolicyGeneration(
options: StartPolicyGenerationOptions
): Promise<StartPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

const command = new StartPolicyGenerationCommand({
policyEngineId: options.policyEngineId,
resource: { arn: options.resourceArn },
content: {
rawText: options.description,
},
name: `cli_generation_${Date.now()}`,
});

const response = await client.send(command);

if (!response.policyGenerationId) {
throw new Error('No generation ID returned from StartPolicyGeneration');
}

return { generationId: response.policyGenerationId };
}

export async function getPolicyGeneration(options: GetPolicyGenerationOptions): Promise<GetPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

// Use the SDK waiter to poll until generation completes
const waiterResult = await waitUntilPolicyGenerationCompleted(
{ client, maxWaitTime: 120, minDelay: 2, maxDelay: 5 },
{ policyGenerationId: options.generationId, policyEngineId: options.policyEngineId }
);

Comment thread
jesseturner21 marked this conversation as resolved.
if (waiterResult.state !== WaiterState.SUCCESS) {
throw new Error(
`Policy generation did not complete within the timeout period (state: ${waiterResult.state}). ` +
`Generation ID: ${options.generationId}`
);
}

// Check the final status
const getCommand = new GetPolicyGenerationCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const statusResponse = await client.send(getCommand);

if (statusResponse.status === 'GENERATE_FAILED') {
const reasons = statusResponse.statusReasons?.join(', ') ?? 'Unknown reason';
throw new Error(`Policy generation failed: ${reasons}`);
}

// Fetch the generated assets
const assetsCommand = new ListPolicyGenerationAssetsCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const assetsResponse = await client.send(assetsCommand);
const assets = assetsResponse.policyGenerationAssets ?? [];

if (assets.length === 0) {
throw new Error('Policy generation completed but no assets were returned');
}

// Get the Cedar statement from the first asset
const firstAsset = assets[0]!;
Comment thread
jesseturner21 marked this conversation as resolved.
const cedarStatement = firstAsset.definition?.cedar?.statement;

if (!cedarStatement) {
throw new Error('Policy generation completed but no Cedar policy statement was found in the assets');
}

return {
status: statusResponse.status ?? 'GENERATED',
statement: cedarStatement,
};
}
186 changes: 185 additions & 1 deletion src/cli/cloudformation/__tests__/outputs.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
import { buildDeployedState, parseGatewayOutputs, parseMemoryOutputs } from '../outputs';
import {
buildDeployedState,
parseGatewayOutputs,
parseMemoryOutputs,
parsePolicyEngineOutputs,
parsePolicyOutputs,
} from '../outputs';
import { describe, expect, it } from 'vitest';

describe('buildDeployedState', () => {
Expand DownExpand Up@@ -285,3 +291,181 @@ describe('parseMemoryOutputs', () => {
expect(result).toEqual({});
});
});

describe('parsePolicyEngineOutputs', () => {
it('extracts policy engine outputs matching pattern', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
ApplicationPolicyEngineMyEngineArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
});
});

it('handles multiple policy engines', () => {
const outputs = {
ApplicationPolicyEngineFirstEngineIdOutput123: 'pe-1',
ApplicationPolicyEngineFirstEngineArnOutput123: 'arn:pe-1',
ApplicationPolicyEngineSecondEngineIdOutput456: 'pe-2',
ApplicationPolicyEngineSecondEngineArnOutput456: 'arn:pe-2',
};

const result = parsePolicyEngineOutputs(outputs, ['FirstEngine', 'SecondEngine']);

expect(Object.keys(result)).toHaveLength(2);
expect(result.FirstEngine?.policyEngineId).toBe('pe-1');
expect(result.SecondEngine?.policyEngineId).toBe('pe-2');
});

it('returns empty record when no policy engine outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});

it('skips incomplete policy engine outputs (missing ARN)', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});
});

describe('parsePolicyOutputs', () => {
it('extracts policy outputs matching pattern', () => {
const outputs = {
ApplicationPolicyMyEngineDenyAllIdOutputABC123: 'pol-123',
ApplicationPolicyMyEngineDenyAllArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
});
});

it('handles multiple policies across engines', () => {
const outputs = {
ApplicationPolicyEngine1Policy1IdOutput123: 'pol-1',
ApplicationPolicyEngine1Policy1ArnOutput123: 'arn:pol-1',
ApplicationPolicyEngine1Policy2IdOutput456: 'pol-2',
ApplicationPolicyEngine1Policy2ArnOutput456: 'arn:pol-2',
};

const result = parsePolicyOutputs(outputs, [
{ engineName: 'Engine1', policyName: 'Policy1' },
{ engineName: 'Engine1', policyName: 'Policy2' },
]);

expect(Object.keys(result)).toHaveLength(2);
expect(result['Engine1/Policy1']?.policyId).toBe('pol-1');
expect(result['Engine1/Policy2']?.policyId).toBe('pol-2');
});

it('returns empty record when no policy outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({});
});
});

describe('buildDeployedState with policy data', () => {
it('includes policyEngines in deployed state when provided', () => {
const policyEngines = {
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines,
});

expect(result.targets.default!.resources?.policyEngines).toEqual(policyEngines);
});

it('includes policies in deployed state when provided', () => {
const policies = {
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies,
});

expect(result.targets.default!.resources?.policies).toEqual(policies);
});

it('omits policyEngines field when policyEngines is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});

it('omits policies field when policies is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies: {},
});

expect(result.targets.default!.resources?.policies).toBeUndefined();
});

it('omits policyEngines field when not provided', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33c4b00
feat: add policy engine and policy support with full deploy pipeline
jesseturner21 Mar 11, 2026
a80a005
feat: use composite key for policy removal to handle cross-engine nam…
jesseturner21 Mar 13, 2026
0bc9914
fix: sync package-lock.json for npm@10 compatibility
jesseturner21 Mar 13, 2026
b0197e0
fix: resolve lint and formatting issues for CI
jesseturner21 Mar 13, 2026
e118d1e
fix: make --statement, --source, --generate mutually exclusive in add…
jesseturner21 Mar 19, 2026
c61bfce
feat: add policy engine and policy support to TUI remove flow
jesseturner21 Mar 19, 2026
f5c37ea
fix: write both CLIENT_ID and CLIENT_SECRET env vars for managed OAut…
jesseturner21 Mar 19, 2026
d642bd7
feat: add PolicyEngineConfiguration support for gateways
Hweinstock Mar 19, 2026
0486995
feat: add policy engine selection to gateway TUI wizard
Hweinstock Mar 19, 2026
c9c86ca
fix: shorten disabled policy generate description to prevent truncation
jesseturner21 Mar 20, 2026
7bfc445
fix: prevent infinite loop when pressing Escape on policy generation …
jesseturner21 Mar 20, 2026
17997d6
chore: remove legacy McpGateway output pattern from gateway parser
jesseturner21 Mar 23, 2026
2f90d2b
test: add integ tests for --statement/--source/--generate mutual excl…
jesseturner21 Mar 23, 2026
d03d32c
fix: remove unnecessary sourceFile existence check from validate
jesseturner21 Mar 23, 2026
62a73a1
fix: respect --json flag in policy remove command
jesseturner21 Mar 23, 2026
900810a
chore: co-locate hasPolicyEngines with other has* checks in preflight
jesseturner21 Mar 23, 2026
d0a4df2
fix: address PR review comments for policy support
jesseturner21 Mar 23, 2026
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
469 changes: 469 additions & 0 deletions integ-tests/add-remove-policy.test.ts

Large diffs are not rendered by default.

2,033 changes: 247 additions & 1,786 deletions package-lock.json

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -374,6 +374,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
1 change: 1 addition & 0 deletions src/assets/cdk/test/cdk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
8 changes: 8 additions & 0 deletions src/cli/aws/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,14 @@ export {
} from './agentcore-control';
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
export { enableTransactionSearch, type TransactionSearchEnableResult } from './transaction-search';
export {
startPolicyGeneration,
getPolicyGeneration,
type StartPolicyGenerationOptions,
type StartPolicyGenerationResult,
type GetPolicyGenerationOptions,
type GetPolicyGenerationResult,
} from './policy-generation';
export {
DEFAULT_RUNTIME_USER_ID,
invokeA2ARuntime,
Expand Down
116 changes: 116 additions & 0 deletions src/cli/aws/policy-generation.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { getCredentialProvider } from './account';
import {
BedrockAgentCoreControlClient,
GetPolicyGenerationCommand,
ListPolicyGenerationAssetsCommand,
StartPolicyGenerationCommand,
waitUntilPolicyGenerationCompleted,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { WaiterState } from '@smithy/util-waiter';

export interface StartPolicyGenerationOptions {
policyEngineId: string;
description: string;
region: string;
resourceArn: string;
}

export interface StartPolicyGenerationResult {
generationId: string;
}

export interface GetPolicyGenerationOptions {
generationId: string;
policyEngineId: string;
region: string;
}

export interface GetPolicyGenerationResult {
status: string;
statement: string;
}

export async function startPolicyGeneration(
options: StartPolicyGenerationOptions
): Promise<StartPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

const command = new StartPolicyGenerationCommand({
policyEngineId: options.policyEngineId,
resource: { arn: options.resourceArn },
content: {
rawText: options.description,
},
name: `cli_generation_${Date.now()}`,
});

const response = await client.send(command);

if (!response.policyGenerationId) {
throw new Error('No generation ID returned from StartPolicyGeneration');
}

return { generationId: response.policyGenerationId };
}

export async function getPolicyGeneration(options: GetPolicyGenerationOptions): Promise<GetPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

// Use the SDK waiter to poll until generation completes
const waiterResult = await waitUntilPolicyGenerationCompleted(
{ client, maxWaitTime: 120, minDelay: 2, maxDelay: 5 },
{ policyGenerationId: options.generationId, policyEngineId: options.policyEngineId }
);

Comment thread
jesseturner21 marked this conversation as resolved.
if (waiterResult.state !== WaiterState.SUCCESS) {
throw new Error(
`Policy generation did not complete within the timeout period (state: ${waiterResult.state}). ` +
`Generation ID: ${options.generationId}`
);
}

// Check the final status
const getCommand = new GetPolicyGenerationCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const statusResponse = await client.send(getCommand);

if (statusResponse.status === 'GENERATE_FAILED') {
const reasons = statusResponse.statusReasons?.join(', ') ?? 'Unknown reason';
throw new Error(`Policy generation failed: ${reasons}`);
}

// Fetch the generated assets
const assetsCommand = new ListPolicyGenerationAssetsCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const assetsResponse = await client.send(assetsCommand);
const assets = assetsResponse.policyGenerationAssets ?? [];

if (assets.length === 0) {
throw new Error('Policy generation completed but no assets were returned');
}

// Get the Cedar statement from the first asset
const firstAsset = assets[0]!;
Comment thread
jesseturner21 marked this conversation as resolved.
const cedarStatement = firstAsset.definition?.cedar?.statement;

if (!cedarStatement) {
throw new Error('Policy generation completed but no Cedar policy statement was found in the assets');
}

return {
status: statusResponse.status ?? 'GENERATED',
statement: cedarStatement,
};
}
186 changes: 185 additions & 1 deletion src/cli/cloudformation/__tests__/outputs.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
import { buildDeployedState, parseGatewayOutputs, parseMemoryOutputs } from '../outputs';
import {
buildDeployedState,
parseGatewayOutputs,
parseMemoryOutputs,
parsePolicyEngineOutputs,
parsePolicyOutputs,
} from '../outputs';
import { describe, expect, it } from 'vitest';

describe('buildDeployedState', () => {
Expand DownExpand Up@@ -285,3 +291,181 @@ describe('parseMemoryOutputs', () => {
expect(result).toEqual({});
});
});

describe('parsePolicyEngineOutputs', () => {
it('extracts policy engine outputs matching pattern', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
ApplicationPolicyEngineMyEngineArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
});
});

it('handles multiple policy engines', () => {
const outputs = {
ApplicationPolicyEngineFirstEngineIdOutput123: 'pe-1',
ApplicationPolicyEngineFirstEngineArnOutput123: 'arn:pe-1',
ApplicationPolicyEngineSecondEngineIdOutput456: 'pe-2',
ApplicationPolicyEngineSecondEngineArnOutput456: 'arn:pe-2',
};

const result = parsePolicyEngineOutputs(outputs, ['FirstEngine', 'SecondEngine']);

expect(Object.keys(result)).toHaveLength(2);
expect(result.FirstEngine?.policyEngineId).toBe('pe-1');
expect(result.SecondEngine?.policyEngineId).toBe('pe-2');
});

it('returns empty record when no policy engine outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});

it('skips incomplete policy engine outputs (missing ARN)', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});
});

describe('parsePolicyOutputs', () => {
it('extracts policy outputs matching pattern', () => {
const outputs = {
ApplicationPolicyMyEngineDenyAllIdOutputABC123: 'pol-123',
ApplicationPolicyMyEngineDenyAllArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
});
});

it('handles multiple policies across engines', () => {
const outputs = {
ApplicationPolicyEngine1Policy1IdOutput123: 'pol-1',
ApplicationPolicyEngine1Policy1ArnOutput123: 'arn:pol-1',
ApplicationPolicyEngine1Policy2IdOutput456: 'pol-2',
ApplicationPolicyEngine1Policy2ArnOutput456: 'arn:pol-2',
};

const result = parsePolicyOutputs(outputs, [
{ engineName: 'Engine1', policyName: 'Policy1' },
{ engineName: 'Engine1', policyName: 'Policy2' },
]);

expect(Object.keys(result)).toHaveLength(2);
expect(result['Engine1/Policy1']?.policyId).toBe('pol-1');
expect(result['Engine1/Policy2']?.policyId).toBe('pol-2');
});

it('returns empty record when no policy outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({});
});
});

describe('buildDeployedState with policy data', () => {
it('includes policyEngines in deployed state when provided', () => {
const policyEngines = {
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines,
});

expect(result.targets.default!.resources?.policyEngines).toEqual(policyEngines);
});

it('includes policies in deployed state when provided', () => {
const policies = {
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies,
});

expect(result.targets.default!.resources?.policies).toEqual(policies);
});

it('omits policyEngines field when policyEngines is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});

it('omits policies field when policies is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies: {},
});

expect(result.targets.default!.resources?.policies).toBeUndefined();
});

it('omits policyEngines field when not provided', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33c4b00
feat: add policy engine and policy support with full deploy pipeline
jesseturner21 Mar 11, 2026
a80a005
feat: use composite key for policy removal to handle cross-engine nam…
jesseturner21 Mar 13, 2026
0bc9914
fix: sync package-lock.json for npm@10 compatibility
jesseturner21 Mar 13, 2026
b0197e0
fix: resolve lint and formatting issues for CI
jesseturner21 Mar 13, 2026
e118d1e
fix: make --statement, --source, --generate mutually exclusive in add…
jesseturner21 Mar 19, 2026
c61bfce
feat: add policy engine and policy support to TUI remove flow
jesseturner21 Mar 19, 2026
f5c37ea
fix: write both CLIENT_ID and CLIENT_SECRET env vars for managed OAut…
jesseturner21 Mar 19, 2026
d642bd7
feat: add PolicyEngineConfiguration support for gateways
Hweinstock Mar 19, 2026
0486995
feat: add policy engine selection to gateway TUI wizard
Hweinstock Mar 19, 2026
c9c86ca
fix: shorten disabled policy generate description to prevent truncation
jesseturner21 Mar 20, 2026
7bfc445
fix: prevent infinite loop when pressing Escape on policy generation …
jesseturner21 Mar 20, 2026
17997d6
chore: remove legacy McpGateway output pattern from gateway parser
jesseturner21 Mar 23, 2026
2f90d2b
test: add integ tests for --statement/--source/--generate mutual excl…
jesseturner21 Mar 23, 2026
d03d32c
fix: remove unnecessary sourceFile existence check from validate
jesseturner21 Mar 23, 2026
62a73a1
fix: respect --json flag in policy remove command
jesseturner21 Mar 23, 2026
900810a
chore: co-locate hasPolicyEngines with other has* checks in preflight
jesseturner21 Mar 23, 2026
d0a4df2
fix: address PR review comments for policy support
jesseturner21 Mar 23, 2026
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
469 changes: 469 additions & 0 deletions integ-tests/add-remove-policy.test.ts

Large diffs are not rendered by default.

2,033 changes: 247 additions & 1,786 deletions package-lock.json

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -374,6 +374,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
1 change: 1 addition & 0 deletions src/assets/cdk/test/cdk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
8 changes: 8 additions & 0 deletions src/cli/aws/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,14 @@ export {
} from './agentcore-control';
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
export { enableTransactionSearch, type TransactionSearchEnableResult } from './transaction-search';
export {
startPolicyGeneration,
getPolicyGeneration,
type StartPolicyGenerationOptions,
type StartPolicyGenerationResult,
type GetPolicyGenerationOptions,
type GetPolicyGenerationResult,
} from './policy-generation';
export {
DEFAULT_RUNTIME_USER_ID,
invokeA2ARuntime,
Expand Down
116 changes: 116 additions & 0 deletions src/cli/aws/policy-generation.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { getCredentialProvider } from './account';
import {
BedrockAgentCoreControlClient,
GetPolicyGenerationCommand,
ListPolicyGenerationAssetsCommand,
StartPolicyGenerationCommand,
waitUntilPolicyGenerationCompleted,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { WaiterState } from '@smithy/util-waiter';

export interface StartPolicyGenerationOptions {
policyEngineId: string;
description: string;
region: string;
resourceArn: string;
}

export interface StartPolicyGenerationResult {
generationId: string;
}

export interface GetPolicyGenerationOptions {
generationId: string;
policyEngineId: string;
region: string;
}

export interface GetPolicyGenerationResult {
status: string;
statement: string;
}

export async function startPolicyGeneration(
options: StartPolicyGenerationOptions
): Promise<StartPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

const command = new StartPolicyGenerationCommand({
policyEngineId: options.policyEngineId,
resource: { arn: options.resourceArn },
content: {
rawText: options.description,
},
name: `cli_generation_${Date.now()}`,
});

const response = await client.send(command);

if (!response.policyGenerationId) {
throw new Error('No generation ID returned from StartPolicyGeneration');
}

return { generationId: response.policyGenerationId };
}

export async function getPolicyGeneration(options: GetPolicyGenerationOptions): Promise<GetPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

// Use the SDK waiter to poll until generation completes
const waiterResult = await waitUntilPolicyGenerationCompleted(
{ client, maxWaitTime: 120, minDelay: 2, maxDelay: 5 },
{ policyGenerationId: options.generationId, policyEngineId: options.policyEngineId }
);

Comment thread
jesseturner21 marked this conversation as resolved.
if (waiterResult.state !== WaiterState.SUCCESS) {
throw new Error(
`Policy generation did not complete within the timeout period (state: ${waiterResult.state}). ` +
`Generation ID: ${options.generationId}`
);
}

// Check the final status
const getCommand = new GetPolicyGenerationCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const statusResponse = await client.send(getCommand);

if (statusResponse.status === 'GENERATE_FAILED') {
const reasons = statusResponse.statusReasons?.join(', ') ?? 'Unknown reason';
throw new Error(`Policy generation failed: ${reasons}`);
}

// Fetch the generated assets
const assetsCommand = new ListPolicyGenerationAssetsCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const assetsResponse = await client.send(assetsCommand);
const assets = assetsResponse.policyGenerationAssets ?? [];

if (assets.length === 0) {
throw new Error('Policy generation completed but no assets were returned');
}

// Get the Cedar statement from the first asset
const firstAsset = assets[0]!;
Comment thread
jesseturner21 marked this conversation as resolved.
const cedarStatement = firstAsset.definition?.cedar?.statement;

if (!cedarStatement) {
throw new Error('Policy generation completed but no Cedar policy statement was found in the assets');
}

return {
status: statusResponse.status ?? 'GENERATED',
statement: cedarStatement,
};
}
186 changes: 185 additions & 1 deletion src/cli/cloudformation/__tests__/outputs.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
import { buildDeployedState, parseGatewayOutputs, parseMemoryOutputs } from '../outputs';
import {
buildDeployedState,
parseGatewayOutputs,
parseMemoryOutputs,
parsePolicyEngineOutputs,
parsePolicyOutputs,
} from '../outputs';
import { describe, expect, it } from 'vitest';

describe('buildDeployedState', () => {
Expand DownExpand Up@@ -285,3 +291,181 @@ describe('parseMemoryOutputs', () => {
expect(result).toEqual({});
});
});

describe('parsePolicyEngineOutputs', () => {
it('extracts policy engine outputs matching pattern', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
ApplicationPolicyEngineMyEngineArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
});
});

it('handles multiple policy engines', () => {
const outputs = {
ApplicationPolicyEngineFirstEngineIdOutput123: 'pe-1',
ApplicationPolicyEngineFirstEngineArnOutput123: 'arn:pe-1',
ApplicationPolicyEngineSecondEngineIdOutput456: 'pe-2',
ApplicationPolicyEngineSecondEngineArnOutput456: 'arn:pe-2',
};

const result = parsePolicyEngineOutputs(outputs, ['FirstEngine', 'SecondEngine']);

expect(Object.keys(result)).toHaveLength(2);
expect(result.FirstEngine?.policyEngineId).toBe('pe-1');
expect(result.SecondEngine?.policyEngineId).toBe('pe-2');
});

it('returns empty record when no policy engine outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});

it('skips incomplete policy engine outputs (missing ARN)', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});
});

describe('parsePolicyOutputs', () => {
it('extracts policy outputs matching pattern', () => {
const outputs = {
ApplicationPolicyMyEngineDenyAllIdOutputABC123: 'pol-123',
ApplicationPolicyMyEngineDenyAllArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
});
});

it('handles multiple policies across engines', () => {
const outputs = {
ApplicationPolicyEngine1Policy1IdOutput123: 'pol-1',
ApplicationPolicyEngine1Policy1ArnOutput123: 'arn:pol-1',
ApplicationPolicyEngine1Policy2IdOutput456: 'pol-2',
ApplicationPolicyEngine1Policy2ArnOutput456: 'arn:pol-2',
};

const result = parsePolicyOutputs(outputs, [
{ engineName: 'Engine1', policyName: 'Policy1' },
{ engineName: 'Engine1', policyName: 'Policy2' },
]);

expect(Object.keys(result)).toHaveLength(2);
expect(result['Engine1/Policy1']?.policyId).toBe('pol-1');
expect(result['Engine1/Policy2']?.policyId).toBe('pol-2');
});

it('returns empty record when no policy outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({});
});
});

describe('buildDeployedState with policy data', () => {
it('includes policyEngines in deployed state when provided', () => {
const policyEngines = {
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines,
});

expect(result.targets.default!.resources?.policyEngines).toEqual(policyEngines);
});

it('includes policies in deployed state when provided', () => {
const policies = {
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies,
});

expect(result.targets.default!.resources?.policies).toEqual(policies);
});

it('omits policyEngines field when policyEngines is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});

it('omits policies field when policies is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies: {},
});

expect(result.targets.default!.resources?.policies).toBeUndefined();
});

it('omits policyEngines field when not provided', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33c4b00
feat: add policy engine and policy support with full deploy pipeline
jesseturner21 Mar 11, 2026
a80a005
feat: use composite key for policy removal to handle cross-engine nam…
jesseturner21 Mar 13, 2026
0bc9914
fix: sync package-lock.json for npm@10 compatibility
jesseturner21 Mar 13, 2026
b0197e0
fix: resolve lint and formatting issues for CI
jesseturner21 Mar 13, 2026
e118d1e
fix: make --statement, --source, --generate mutually exclusive in add…
jesseturner21 Mar 19, 2026
c61bfce
feat: add policy engine and policy support to TUI remove flow
jesseturner21 Mar 19, 2026
f5c37ea
fix: write both CLIENT_ID and CLIENT_SECRET env vars for managed OAut…
jesseturner21 Mar 19, 2026
d642bd7
feat: add PolicyEngineConfiguration support for gateways
Hweinstock Mar 19, 2026
0486995
feat: add policy engine selection to gateway TUI wizard
Hweinstock Mar 19, 2026
c9c86ca
fix: shorten disabled policy generate description to prevent truncation
jesseturner21 Mar 20, 2026
7bfc445
fix: prevent infinite loop when pressing Escape on policy generation …
jesseturner21 Mar 20, 2026
17997d6
chore: remove legacy McpGateway output pattern from gateway parser
jesseturner21 Mar 23, 2026
2f90d2b
test: add integ tests for --statement/--source/--generate mutual excl…
jesseturner21 Mar 23, 2026
d03d32c
fix: remove unnecessary sourceFile existence check from validate
jesseturner21 Mar 23, 2026
62a73a1
fix: respect --json flag in policy remove command
jesseturner21 Mar 23, 2026
900810a
chore: co-locate hasPolicyEngines with other has* checks in preflight
jesseturner21 Mar 23, 2026
d0a4df2
fix: address PR review comments for policy support
jesseturner21 Mar 23, 2026
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
469 changes: 469 additions & 0 deletions integ-tests/add-remove-policy.test.ts

Large diffs are not rendered by default.

2,033 changes: 247 additions & 1,786 deletions package-lock.json

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -374,6 +374,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
1 change: 1 addition & 0 deletions src/assets/cdk/test/cdk.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ test('AgentCoreStack synthesizes with empty spec', () => {
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
policyEngines: [],
},
});
const template = Template.fromStack(stack);
Expand Down
8 changes: 8 additions & 0 deletions src/cli/aws/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,14 @@ export {
} from './agentcore-control';
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
export { enableTransactionSearch, type TransactionSearchEnableResult } from './transaction-search';
export {
startPolicyGeneration,
getPolicyGeneration,
type StartPolicyGenerationOptions,
type StartPolicyGenerationResult,
type GetPolicyGenerationOptions,
type GetPolicyGenerationResult,
} from './policy-generation';
export {
DEFAULT_RUNTIME_USER_ID,
invokeA2ARuntime,
Expand Down
116 changes: 116 additions & 0 deletions src/cli/aws/policy-generation.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { getCredentialProvider } from './account';
import {
BedrockAgentCoreControlClient,
GetPolicyGenerationCommand,
ListPolicyGenerationAssetsCommand,
StartPolicyGenerationCommand,
waitUntilPolicyGenerationCompleted,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { WaiterState } from '@smithy/util-waiter';

export interface StartPolicyGenerationOptions {
policyEngineId: string;
description: string;
region: string;
resourceArn: string;
}

export interface StartPolicyGenerationResult {
generationId: string;
}

export interface GetPolicyGenerationOptions {
generationId: string;
policyEngineId: string;
region: string;
}

export interface GetPolicyGenerationResult {
status: string;
statement: string;
}

export async function startPolicyGeneration(
options: StartPolicyGenerationOptions
): Promise<StartPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

const command = new StartPolicyGenerationCommand({
policyEngineId: options.policyEngineId,
resource: { arn: options.resourceArn },
content: {
rawText: options.description,
},
name: `cli_generation_${Date.now()}`,
});

const response = await client.send(command);

if (!response.policyGenerationId) {
throw new Error('No generation ID returned from StartPolicyGeneration');
}

return { generationId: response.policyGenerationId };
}

export async function getPolicyGeneration(options: GetPolicyGenerationOptions): Promise<GetPolicyGenerationResult> {
const client = new BedrockAgentCoreControlClient({
region: options.region,
credentials: getCredentialProvider(),
});

// Use the SDK waiter to poll until generation completes
const waiterResult = await waitUntilPolicyGenerationCompleted(
{ client, maxWaitTime: 120, minDelay: 2, maxDelay: 5 },
{ policyGenerationId: options.generationId, policyEngineId: options.policyEngineId }
);

Comment thread
jesseturner21 marked this conversation as resolved.
if (waiterResult.state !== WaiterState.SUCCESS) {
throw new Error(
`Policy generation did not complete within the timeout period (state: ${waiterResult.state}). ` +
`Generation ID: ${options.generationId}`
);
}

// Check the final status
const getCommand = new GetPolicyGenerationCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const statusResponse = await client.send(getCommand);

if (statusResponse.status === 'GENERATE_FAILED') {
const reasons = statusResponse.statusReasons?.join(', ') ?? 'Unknown reason';
throw new Error(`Policy generation failed: ${reasons}`);
}

// Fetch the generated assets
const assetsCommand = new ListPolicyGenerationAssetsCommand({
policyGenerationId: options.generationId,
policyEngineId: options.policyEngineId,
});

const assetsResponse = await client.send(assetsCommand);
const assets = assetsResponse.policyGenerationAssets ?? [];

if (assets.length === 0) {
throw new Error('Policy generation completed but no assets were returned');
}

// Get the Cedar statement from the first asset
const firstAsset = assets[0]!;
Comment thread
jesseturner21 marked this conversation as resolved.
const cedarStatement = firstAsset.definition?.cedar?.statement;

if (!cedarStatement) {
throw new Error('Policy generation completed but no Cedar policy statement was found in the assets');
}

return {
status: statusResponse.status ?? 'GENERATED',
statement: cedarStatement,
};
}
186 changes: 185 additions & 1 deletion src/cli/cloudformation/__tests__/outputs.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
import { buildDeployedState, parseGatewayOutputs, parseMemoryOutputs } from '../outputs';
import {
buildDeployedState,
parseGatewayOutputs,
parseMemoryOutputs,
parsePolicyEngineOutputs,
parsePolicyOutputs,
} from '../outputs';
import { describe, expect, it } from 'vitest';

describe('buildDeployedState', () => {
Expand DownExpand Up@@ -285,3 +291,181 @@ describe('parseMemoryOutputs', () => {
expect(result).toEqual({});
});
});

describe('parsePolicyEngineOutputs', () => {
it('extracts policy engine outputs matching pattern', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
ApplicationPolicyEngineMyEngineArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
});
});

it('handles multiple policy engines', () => {
const outputs = {
ApplicationPolicyEngineFirstEngineIdOutput123: 'pe-1',
ApplicationPolicyEngineFirstEngineArnOutput123: 'arn:pe-1',
ApplicationPolicyEngineSecondEngineIdOutput456: 'pe-2',
ApplicationPolicyEngineSecondEngineArnOutput456: 'arn:pe-2',
};

const result = parsePolicyEngineOutputs(outputs, ['FirstEngine', 'SecondEngine']);

expect(Object.keys(result)).toHaveLength(2);
expect(result.FirstEngine?.policyEngineId).toBe('pe-1');
expect(result.SecondEngine?.policyEngineId).toBe('pe-2');
});

it('returns empty record when no policy engine outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});

it('skips incomplete policy engine outputs (missing ARN)', () => {
const outputs = {
ApplicationPolicyEngineMyEngineIdOutputABC123: 'pe-123',
};

const result = parsePolicyEngineOutputs(outputs, ['MyEngine']);

expect(result).toEqual({});
});
});

describe('parsePolicyOutputs', () => {
it('extracts policy outputs matching pattern', () => {
const outputs = {
ApplicationPolicyMyEngineDenyAllIdOutputABC123: 'pol-123',
ApplicationPolicyMyEngineDenyAllArnOutputDEF456: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
});
});

it('handles multiple policies across engines', () => {
const outputs = {
ApplicationPolicyEngine1Policy1IdOutput123: 'pol-1',
ApplicationPolicyEngine1Policy1ArnOutput123: 'arn:pol-1',
ApplicationPolicyEngine1Policy2IdOutput456: 'pol-2',
ApplicationPolicyEngine1Policy2ArnOutput456: 'arn:pol-2',
};

const result = parsePolicyOutputs(outputs, [
{ engineName: 'Engine1', policyName: 'Policy1' },
{ engineName: 'Engine1', policyName: 'Policy2' },
]);

expect(Object.keys(result)).toHaveLength(2);
expect(result['Engine1/Policy1']?.policyId).toBe('pol-1');
expect(result['Engine1/Policy2']?.policyId).toBe('pol-2');
});

it('returns empty record when no policy outputs found', () => {
const outputs = {
UnrelatedOutput: 'some-value',
};

const result = parsePolicyOutputs(outputs, [{ engineName: 'MyEngine', policyName: 'DenyAll' }]);

expect(result).toEqual({});
});
});

describe('buildDeployedState with policy data', () => {
it('includes policyEngines in deployed state when provided', () => {
const policyEngines = {
MyEngine: {
policyEngineId: 'pe-123',
policyEngineArn: 'arn:aws:bedrock:us-east-1:123456789012:policy-engine/pe-123',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines,
});

expect(result.targets.default!.resources?.policyEngines).toEqual(policyEngines);
});

it('includes policies in deployed state when provided', () => {
const policies = {
'MyEngine/DenyAll': {
policyId: 'pol-123',
policyArn: 'arn:aws:bedrock:us-east-1:123456789012:policy/pol-123',
engineName: 'MyEngine',
},
};

const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies,
});

expect(result.targets.default!.resources?.policies).toEqual(policies);
});

it('omits policyEngines field when policyEngines is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policyEngines: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});

it('omits policies field when policies is empty object', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
policies: {},
});

expect(result.targets.default!.resources?.policies).toBeUndefined();
});

it('omits policyEngines field when not provided', () => {
const result = buildDeployedState({
targetName: 'default',
stackName: 'TestStack',
agents: {},
gateways: {},
});

expect(result.targets.default!.resources?.policyEngines).toBeUndefined();
});
});
Loading
Loading