From 5264d47ba759b3b9c8dca3555decd81b707a86ca Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Sun, 30 Aug 2026 23:01:28 -0400 Subject: [PATCH 01/12] feat(deploy): auto-provision the default deployment target from STS `project deploy` now synthesizes the `default` target when aws-targets.json is missing, empty, or lacks a `default` entry: the account comes from STS GetCallerIdentity and the region from the CLI's already-resolved effective region, validated against AgentCoreRegionSchema before anything is written. Existing entries are preserved byte-for-byte, the synthesized entry is reported on stderr, and the deploy proceeds in the same invocation. Named targets still require explicit configuration, and unsupported regions or unresolvable credentials fail with actionable errors before the file is touched. Also restores the vended CDK app (bin/cdk.ts, lib/cdk-stack.ts, test/cdk.test.ts, package.json) to its last publishable state: it referenced an AgentCorePayments L3 that no released @aws/agentcore-cdk exports, so every fresh scaffold failed `cdk synth` at tsc before reaching AWS. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- src/assets/cdk/bin/cdk.ts | 46 ++++- src/assets/cdk/lib/cdk-stack.ts | 169 +++++++++++++++++- src/assets/cdk/package.json | 2 +- src/assets/cdk/test/cdk.test.ts | 169 +----------------- src/core/project/backends/cdk/environment.ts | 6 +- src/core/project/manager.test.ts | 171 +++++++++++++++++-- src/core/project/manager.tsx | 115 +++++++++++-- src/handlers/project/deploy/index.test.ts | 62 ++++++- src/handlers/project/deploy/index.ts | 11 +- src/handlers/project/project.test.ts | 9 +- src/handlers/project/types.ts | 6 + src/projectSchemas/aws-targets.ts | 7 + src/testing/TestCoreClient.tsx | 4 + 13 files changed, 564 insertions(+), 213 deletions(-) diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index 9e308d1de..701339bce 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -146,12 +146,53 @@ async function main() { // Extract credentials from deployed state for this target const targetState = (deployedState as Record)?.targets as - Record> | undefined; + | Record> + | undefined; const targetResources = target ? (targetState?.[target.name]?.resources as Record | undefined) : undefined; const credentials = targetResources?.credentials as - Record | undefined; + | Record + | undefined; + + // Payment credential provider ARNs live in the same credentials map as identity credentials + const paymentCredentials = credentials; + + const paymentSpec = specAny.payments?.length + ? specAny.payments.map( + (p: { + name: string; + description?: string; + authorizerType: 'AWS_IAM' | 'CUSTOM_JWT'; + authorizerConfiguration?: unknown; + autoPayment?: boolean; + paymentToolAllowlist?: string[]; + networkPreferences?: string[]; + connectors: { name: string; provider?: string; credentialName: string }[]; + }) => ({ + name: p.name, + description: p.description, + authorizerType: p.authorizerType, + authorizerConfiguration: p.authorizerConfiguration, + autoPayment: p.autoPayment, + paymentToolAllowlist: p.paymentToolAllowlist, + networkPreferences: p.networkPreferences, + connectors: p.connectors.map(c => { + const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn; + if (!credentialProviderArn) { + // Fail fast with an actionable message rather than passing an empty + // ARN that fails opaquely server-side at CreatePaymentConnector. + throw new Error( + `Payment connector "${c.name}" on manager "${p.name}" references credential ` + + `"${c.credentialName}", but no deployed credential provider was found for it. ` + + `Run \`agentcore deploy\` so the credential provider is created first.` + ); + } + return { name: c.name, provider: c.provider, credentialProviderArn }; + }), + }) + ) + : undefined; new AgentCoreStack(app, stackName, { spec, @@ -159,6 +200,7 @@ async function main() { credentials, connectorParametersByFile, harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, + paymentSpec, env, description: target ? `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})` diff --git a/src/assets/cdk/lib/cdk-stack.ts b/src/assets/cdk/lib/cdk-stack.ts index 9592561d2..3dac0669d 100644 --- a/src/assets/cdk/lib/cdk-stack.ts +++ b/src/assets/cdk/lib/cdk-stack.ts @@ -1,12 +1,15 @@ import { AgentCoreApplication, AgentCoreMcp, - AgentCorePayments, + AgentCorePaymentManager, + AgentCorePaymentConnector, type AgentCoreProjectSpec, type AgentCoreMcpSpec, + type CustomJWTAuthorizerConfig, type HarnessDeploymentConfig, } from '@aws/agentcore-cdk'; import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib'; +import * as iam from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; /** @@ -16,6 +19,23 @@ import { Construct } from 'constructs'; */ export type HarnessConfig = HarnessDeploymentConfig; +export interface PaymentConnectorSpec { + name: string; + provider: 'CoinbaseCDP' | 'StripePrivy'; + credentialProviderArn: string; +} + +export interface PaymentSpec { + name: string; + description?: string; + authorizerType: 'AWS_IAM' | 'CUSTOM_JWT'; + authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig }; + autoPayment?: boolean; + paymentToolAllowlist?: string[]; + networkPreferences?: string[]; + connectors: PaymentConnectorSpec[]; +} + export interface AgentCoreStackProps extends StackProps { /** * The AgentCore project specification containing agents, memories, and credentials. @@ -38,6 +58,30 @@ export interface AgentCoreStackProps extends StackProps { * connectorConfigFile path. Forwarded to AgentCoreApplication. */ connectorParametersByFile?: Record>; + /** + * Payment specifications with resolved credential provider ARNs. + */ + paymentSpec?: PaymentSpec[]; +} + +function toCdkId(name: string): string { + return name.replace(/_/g, ''); +} + +/** + * Decide whether a deployed runtime should receive payment env vars + IAM grants. + * Payments today only ships a runtime shim for Python HTTP runtimes; injecting + * AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes + * would surface env vars they cannot consume and would dilute least-privilege + * IAM grants for runtimes that never call ProcessPayment. + */ +function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean { + if (agent.protocol && agent.protocol !== 'HTTP') { + return false; + } + const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : ''; + const entrypointFile = entrypoint.split(':')[0] ?? ''; + return entrypointFile.endsWith('.py'); } /** @@ -53,7 +97,7 @@ export class AgentCoreStack extends Stack { constructor(scope: Construct, id: string, props: AgentCoreStackProps) { super(scope, id, props); - const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile } = props; + const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile, paymentSpec } = props; // Create AgentCoreApplication with all agents and harness roles // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -68,11 +112,6 @@ export class AgentCoreStack extends Stack { appProps.credentials = credentials; } this.application = new AgentCoreApplication(this, 'Application', appProps as any); - new AgentCorePayments(this, 'Payments', { - spec, - credentials, - agentCoreApplication: this.application, - }); // Create AgentCoreMcp if there are gateways configured if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) { @@ -85,6 +124,122 @@ export class AgentCoreStack extends Stack { }); } + // Create payment infrastructure via CFN constructs + if (paymentSpec && paymentSpec.length > 0) { + for (const payment of paymentSpec) { + const mgrId = toCdkId(payment.name); + const manager = new AgentCorePaymentManager(this, `Payment${mgrId}`, { + projectName: spec.name, + name: payment.name, + authorizerType: payment.authorizerType, + description: payment.description, + authorizerConfiguration: payment.authorizerConfiguration, + tags: spec.tags, + }); + + const prefix = `AGENTCORE_PAYMENT_${payment.name.toUpperCase().replace(/-/g, '_')}`; + + // Wire env vars from construct output tokens into eligible agent environments only. + // See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that + // can consume these env vars, and giving them sts:AssumeRole on the + // ProcessPaymentRole would broaden the privilege surface unnecessarily. + for (const env of this.application.environments.values()) { + if (!isPaymentEligibleAgent(env.agent)) { + continue; + } + env.runtime.addEnvironmentVariable(`${prefix}_MANAGER_ARN`, manager.paymentManagerArn); + env.runtime.addEnvironmentVariable(`${prefix}_PROCESS_PAYMENT_ROLE_ARN`, manager.processPaymentRoleArn); + + // Grant runtime execution role permission to assume the ProcessPaymentRole. + // The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the + // caller still needs sts:AssumeRole on its own role to perform the assumption. + env.runtime.role.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: [manager.processPaymentRoleArn], + }) + ); + + // Grant payment data-plane actions directly to the runtime role. + // + // NOTE: This deviates from the canonical role model in the AgentCore Payments + // beta guide, which assigns Get/List/Create instrument+session actions to a + // separate ManagementRole and limits the agent's role to ProcessPayment only. + // The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header) + // calls GetPaymentInstrument internally during the 402 auto-pay path, so the + // runtime role needs read access. CreatePaymentSession is included so + // `agentcore invoke --auto-session` works without a separate ManagementRole + // call. Tighten this if the SDK is updated to accept pre-fetched instrument + // details and split create-session into a backend-only flow. + env.runtime.role.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: [ + 'bedrock-agentcore:GetPaymentInstrument', + 'bedrock-agentcore:ListPaymentInstruments', + 'bedrock-agentcore:GetPaymentInstrumentBalance', + 'bedrock-agentcore:GetPaymentSession', + 'bedrock-agentcore:ListPaymentSessions', + 'bedrock-agentcore:CreatePaymentSession', + 'bedrock-agentcore:ProcessPayment', + ], + resources: [manager.paymentManagerArn, `${manager.paymentManagerArn}/*`], + }) + ); + + if (payment.autoPayment !== undefined) { + env.runtime.addEnvironmentVariable(`${prefix}_AUTO_PAYMENT`, String(payment.autoPayment)); + } + if (payment.paymentToolAllowlist) { + env.runtime.addEnvironmentVariable(`${prefix}_TOOL_ALLOWLIST`, payment.paymentToolAllowlist.join(',')); + } + if (payment.networkPreferences) { + env.runtime.addEnvironmentVariable(`${prefix}_NETWORK_PREFERENCES`, payment.networkPreferences.join(',')); + } + if (payment.authorizerType === 'CUSTOM_JWT') { + env.runtime.addEnvironmentVariable(`${prefix}_AUTH_MODE`, 'bearer'); + } + } + + // Create connectors for this manager + for (const connector of payment.connectors) { + const connId = toCdkId(connector.name); + const conn = new AgentCorePaymentConnector(this, `Payment${mgrId}${connId}`, { + projectName: spec.name, + paymentManager: manager, + connectorName: connector.name, + connectorType: connector.provider, + credentialProviderArn: connector.credentialProviderArn, + }); + + // Wire first connector's ID as env var (eligible agents only) + if (connector === payment.connectors[0]) { + for (const env of this.application.environments.values()) { + if (!isPaymentEligibleAgent(env.agent)) continue; + env.runtime.addEnvironmentVariable(`${prefix}_CONNECTOR_ID`, conn.paymentConnectorId); + } + } + + new CfnOutput(this, `Payment${mgrId}${connId}ConnectorId`, { + value: conn.paymentConnectorId, + }); + } + + // CFN Outputs for post-deploy state parsing + new CfnOutput(this, `Payment${mgrId}ManagerArn`, { + value: manager.paymentManagerArn, + }); + new CfnOutput(this, `Payment${mgrId}ManagerId`, { + value: manager.paymentManagerId, + }); + new CfnOutput(this, `Payment${mgrId}ProcessPaymentRoleArn`, { + value: manager.processPaymentRoleArn, + }); + new CfnOutput(this, `Payment${mgrId}ResourceRetrievalRoleArn`, { + value: manager.resourceRetrievalRoleArn, + }); + } + } + // Stack-level output new CfnOutput(this, 'StackNameOutput', { description: 'Name of the CloudFormation Stack', diff --git a/src/assets/cdk/package.json b/src/assets/cdk/package.json index 407a29fbd..0ac28f946 100644 --- a/src/assets/cdk/package.json +++ b/src/assets/cdk/package.json @@ -23,7 +23,7 @@ "typescript": "~5.9.3" }, "dependencies": { - "@aws/agentcore-cdk": "0.1.0-alpha.49", + "@aws/agentcore-cdk": "0.1.0-alpha.45", "aws-cdk-lib": "~2.266.0", "constructs": "~10.7.0" } diff --git a/src/assets/cdk/test/cdk.test.ts b/src/assets/cdk/test/cdk.test.ts index 2db16484f..8db318ada 100644 --- a/src/assets/cdk/test/cdk.test.ts +++ b/src/assets/cdk/test/cdk.test.ts @@ -1,29 +1,6 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; import * as cdk from 'aws-cdk-lib'; -import { Match, Template } from 'aws-cdk-lib/assertions'; - -const originalCwd = process.cwd(); -const originalInitCwd = process.env.INIT_CWD; -const testRoot = mkdtempSync(join(tmpdir(), 'agentcore-cdk-test-')); -const testConfigDir = join(testRoot, 'agentcore'); -let AgentCoreStack: typeof import('../lib/cdk-stack').AgentCoreStack; - -beforeAll(async () => { - process.chdir(testRoot); - process.env.INIT_CWD = testRoot; - mkdirSync(testConfigDir, { recursive: true }); - writeFileSync(join(testConfigDir, 'agentcore.json'), '{}'); - ({ AgentCoreStack } = await import('../lib/cdk-stack')); -}); - -afterAll(() => { - process.chdir(originalCwd); - if (originalInitCwd === undefined) delete process.env.INIT_CWD; - else process.env.INIT_CWD = originalInitCwd; - rmSync(testRoot, { recursive: true, force: true }); -}); +import { Template } from 'aws-cdk-lib/assertions'; +import { AgentCoreStack } from '../lib/cdk-stack'; test('AgentCoreStack synthesizes with empty spec', () => { const app = new cdk.App(); @@ -52,145 +29,3 @@ test('AgentCoreStack synthesizes with empty spec', () => { Description: 'Name of the CloudFormation Stack', }); }); - -test('AgentCoreStack synthesizes manual and Quick Create payment connectors', () => { - const app = new cdk.App(); - const stack = new AgentCoreStack(app, 'TestStack', { - spec: { - name: 'testproject', - version: 1, - managedBy: 'CDK' as const, - runtimes: [], - memories: [], - credentials: [ - { - authorizerType: 'PaymentCredentialProvider', - name: 'coinbase', - provider: 'CoinbaseCDP', - }, - ], - evaluators: [], - onlineEvalConfigs: [], - configBundles: [], - policyEngines: [], - payments: [ - { - name: 'Payments', - authorizerType: 'AWS_IAM', - connectors: [ - { - name: 'Manual', - provider: 'CoinbaseCDP', - credentialName: 'coinbase', - }, - { - name: 'Quick', - provider: 'CoinbaseCDP', - provisionMode: 'QUICK_CREATE', - }, - ], - }, - ], - agentCoreGateways: [], - mcpRuntimeTools: [], - unassignedTargets: [], - datasets: [], - knowledgeBases: [], - }, - credentials: { - coinbase: { - credentialProviderArn: - 'arn:aws:bedrock-agentcore:us-east-1:123456789012:token-vault/default/paymentcredentialprovider/coinbase', - }, - }, - }); - const template = Template.fromStack(stack); - - template.resourceCountIs('AWS::BedrockAgentCore::PaymentConnector', 2); - template.hasResourceProperties('AWS::BedrockAgentCore::PaymentConnector', { - ConnectorName: 'Manual', - ProvisionMode: Match.absent(), - }); - template.hasResourceProperties('AWS::BedrockAgentCore::PaymentConnector', { - ConnectorName: 'Quick', - ConnectorType: 'CoinbaseCDP', - ProvisionMode: 'QUICK_CREATE', - CredentialProviderConfigurations: [], - }); - expect(Object.keys(template.findOutputs('*')).some(key => key.includes('AuthorizationUrl'))).toBe(true); -}); - -test('AgentCoreStack preserves complete and type-distinct payment resource identities', () => { - const app = new cdk.App(); - const stack = new AgentCoreStack(app, 'TestStack', { - spec: { - name: 'testproject', - version: 1, - managedBy: 'CDK' as const, - runtimes: [], - memories: [], - credentials: [], - evaluators: [], - onlineEvalConfigs: [], - configBundles: [], - policyEngines: [], - payments: [ - { - name: 'Payments', - authorizerType: 'AWS_IAM', - connectors: [ - { - name: 'foo_bar', - provider: 'CoinbaseCDP', - provisionMode: 'QUICK_CREATE', - }, - { - name: 'foobar', - provider: 'CoinbaseCDP', - provisionMode: 'QUICK_CREATE', - }, - ], - }, - { - name: 'A', - authorizerType: 'AWS_IAM', - connectors: [ - { - name: 'B', - provider: 'CoinbaseCDP', - provisionMode: 'QUICK_CREATE', - }, - { - name: 'BC', - provider: 'CoinbaseCDP', - provisionMode: 'QUICK_CREATE', - }, - ], - }, - { - name: 'AB', - authorizerType: 'AWS_IAM', - connectors: [ - { - name: 'C', - provider: 'CoinbaseCDP', - provisionMode: 'QUICK_CREATE', - }, - ], - }, - { - name: 'M1AC1B', - authorizerType: 'AWS_IAM', - connectors: [], - }, - ], - agentCoreGateways: [], - mcpRuntimeTools: [], - unassignedTargets: [], - datasets: [], - knowledgeBases: [], - }, - }); - - Template.fromStack(stack).resourceCountIs('AWS::BedrockAgentCore::PaymentConnector', 5); -}); diff --git a/src/core/project/backends/cdk/environment.ts b/src/core/project/backends/cdk/environment.ts index 11e778389..6cc6ae79c 100644 --- a/src/core/project/backends/cdk/environment.ts +++ b/src/core/project/backends/cdk/environment.ts @@ -150,7 +150,11 @@ export async function probeStack( } } -export const resolveAwsAccount: AccountResolver = async (region, credentials) => { +/** Omitting `credentials` resolves through the default AWS SDK provider chain. */ +export const resolveAwsAccount = async ( + region: string, + credentials?: CdkCredentialProvider, +): Promise => { const { GetCallerIdentityCommand, STSClient } = await import("@aws-sdk/client-sts"); const client = new STSClient({ credentials, region }); try { diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index 89c435159..c306c7062 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -12,7 +12,6 @@ import { type DeployResult, type Project, type ProjectEvent, - type TeardownConfirmationHandler, } from "../../handlers/project/types"; import { createSilentLogger } from "../../testing"; import type { DeployBackendInput, ProjectBackend } from "./backends/types"; @@ -347,8 +346,11 @@ describe("FsProjectManager.build", () => { describe("FsProjectManager.deploy", () => { type DeployCall = { project: Project; input: DeployBackendInput }; - function deployManager() { + const STS_ACCOUNT = "999900001111"; + + function deployManager(options?: { account?: string | Error }) { const calls: DeployCall[] = []; + const accountCalls: string[] = []; const backend: ProjectBackend = { async *build() {}, async *deploy(project, input) { @@ -359,9 +361,16 @@ describe("FsProjectManager.deploy", () => { }; return { calls, + accountCalls, manager: new FsProjectManager({ logger: createSilentLogger(), backends: { CDK: backend }, + resolveAccount: async (region) => { + accountCalls.push(region); + const outcome = options?.account ?? STS_ACCOUNT; + if (outcome instanceof Error) throw outcome; + return outcome; + }, }), }; } @@ -387,11 +396,12 @@ describe("FsProjectManager.deploy", () => { manager: FsProjectManager, project: Project, target: string, - confirmTeardown: TeardownConfirmationHandler = async () => false, + options: { region?: string } = {}, ): Promise<{ events: ProjectEvent[]; result: DeployResult }> { const generator = manager.deploy(project, { target, - confirmTeardown, + region: options.region ?? "us-east-1", + confirmTeardown: async () => false, }); const events: ProjectEvent[] = []; while (true) { @@ -444,16 +454,20 @@ describe("FsProjectManager.deploy", () => { test.each([ ["a missing file", undefined], ["an empty list", []], - ])("rejects %s before invoking the backend", async (_label, configured) => { - const root = await inTempDirectory(); - const subject = deployManager(); - const project = await projectWithTargets(root, configured); - - await expect(deploy(subject.manager, project, "default")).rejects.toThrow( - /No deployment targets are configured/, - ); - expect(subject.calls).toEqual([]); - }); + ])( + "rejects %s before invoking the backend when a named target is requested", + async (_label, configured) => { + const root = await inTempDirectory(); + const subject = deployManager(); + const project = await projectWithTargets(root, configured); + + await expect(deploy(subject.manager, project, "staging")).rejects.toThrow( + /No deployment targets are configured/, + ); + expect(subject.calls).toEqual([]); + expect(subject.accountCalls).toEqual([]); + }, + ); test.each([ ["malformed JSON", "{ not-json"], @@ -483,6 +497,135 @@ describe("FsProjectManager.deploy", () => { ); expect(subject.calls).toEqual([]); }); + + const targetsFile = (root: string) => join(root, "agentcore", "aws-targets.json"); + const SYNTHESIZED: AwsDeploymentTarget = { + name: "default", + account: STS_ACCOUNT, + region: "us-east-2", + }; + const CREATED_MESSAGE = + `Created default deployment target: account ${STS_ACCOUNT}, ` + + `region us-east-2 (${join("agentcore", "aws-targets.json")})`; + + test.each([ + ["a missing file", undefined], + ["an empty list", []], + ])("synthesizes the default target from %s", async (_label, configured) => { + const root = await inTempDirectory(); + const subject = deployManager(); + const project = await projectWithTargets(root, configured); + + const deployed = await deploy(subject.manager, project, "default", { region: "us-east-2" }); + + expect(subject.accountCalls).toEqual(["us-east-2"]); + expect(subject.calls).toHaveLength(1); + expect(subject.calls[0]?.input.target).toEqual(SYNTHESIZED); + expect(deployed.events).toEqual([ + { message: CREATED_MESSAGE }, + { message: "Backend deployment started" }, + ]); + expect(await Bun.file(targetsFile(root)).json()).toEqual([SYNTHESIZED]); + }); + + test("appends the default target and preserves other entries byte for byte", async () => { + const root = await inTempDirectory(); + const subject = deployManager(); + // Non-canonical key order plus a key the schema does not know about, so a + // rewrite through the schema (which would reorder and strip) is caught. + const existing = + `[\n` + + ` {\n` + + ` "region": "eu-west-1",\n` + + ` "name": "prod",\n` + + ` "account": "444455556666",\n` + + ` "note": "hand-tuned"\n` + + ` }\n` + + `]`; + const project = await projectWithTargets(root, existing); + + await deploy(subject.manager, project, "default", { region: "us-east-2" }); + + expect(subject.calls[0]?.input.target).toEqual(SYNTHESIZED); + expect(await Bun.file(targetsFile(root)).text()).toBe( + `[\n` + + ` {\n` + + ` "region": "eu-west-1",\n` + + ` "name": "prod",\n` + + ` "account": "444455556666",\n` + + ` "note": "hand-tuned"\n` + + ` },\n` + + ` {\n` + + ` "name": "default",\n` + + ` "account": "${STS_ACCOUNT}",\n` + + ` "region": "us-east-2"\n` + + ` }\n` + + `]`, + ); + }); + + test("never synthesizes a named target", async () => { + const root = await inTempDirectory(); + const subject = deployManager(); + const project = await projectWithTargets(root, targets); + + await expect(deploy(subject.manager, project, "gamma")).rejects.toThrow( + /no deployment target named 'gamma'.*staging, prod/s, + ); + expect(subject.calls).toEqual([]); + expect(subject.accountCalls).toEqual([]); + expect(await Bun.file(targetsFile(root)).json()).toEqual(targets); + }); + + test("rejects an unsupported region without calling STS or writing the file", async () => { + const root = await inTempDirectory(); + const subject = deployManager(); + const project = await projectWithTargets(root, undefined); + + const attempt = deploy(subject.manager, project, "default", { region: "us-west-1" }); + + await expect(attempt).rejects.toThrow(/'us-west-1' is not an AgentCore-supported region/); + await expect( + deploy(subject.manager, project, "default", { region: "us-west-1" }), + ).rejects.toThrow(/Supported regions: .*us-east-1.*Re-run with --region/s); + expect(subject.calls).toEqual([]); + expect(subject.accountCalls).toEqual([]); + expect(await Bun.file(targetsFile(root)).exists()).toBe(false); + }); + + test("reports an actionable error when the account cannot be resolved", async () => { + const root = await inTempDirectory(); + const subject = deployManager({ + account: new Error("The security token included in the request is expired"), + }); + const project = await projectWithTargets(root, undefined); + + await expect( + deploy(subject.manager, project, "default", { region: "us-east-2" }), + ).rejects.toThrow( + /the AWS account could not be resolved: The security token included in the request is expired[\s\S]*aws configure/, + ); + expect(subject.calls).toEqual([]); + expect(await Bun.file(targetsFile(root)).exists()).toBe(false); + }); + + test("leaves an existing default target alone", async () => { + const root = await inTempDirectory(); + const subject = deployManager(); + const configured: AwsDeploymentTarget[] = [ + { name: "default", account: "111122223333", region: "us-west-2" }, + ]; + const contents = JSON.stringify(configured, null, 2); + const project = await projectWithTargets(root, contents); + + // The requested region differs from the entry's; the entry must win. + const deployed = await deploy(subject.manager, project, "default", { region: "us-east-2" }); + + expect(subject.accountCalls).toEqual([]); + expect(subject.calls[0]?.input.target).toEqual(configured[0]!); + expect(deployed.events).toEqual([{ message: "Backend deployment started" }]); + expect(await Bun.file(targetsFile(root)).text()).toBe(contents); + }); }); describe("FsProjectManager.resolve", () => { diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index fae997d59..8a6696f0e 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -38,13 +38,22 @@ import { enclosingProjectRoot, projectSpecPath } from "./fsUtils"; import { AgentCoreCLIError, InputValidationError, + InvalidEnvironmentError, + MalformedServiceResponseError, NotImplementedError, ProjectStateError, } from "../../errors/errors"; import z from "zod"; import { CdkBackend } from "./backends/cdk"; +import { resolveAwsAccount } from "./backends/cdk/environment"; import type { ProjectBackend } from "./backends/types"; -import { AwsDeploymentTargetsSchema } from "../../projectSchemas/aws-targets"; +import { + AgentCoreRegionSchema, + AwsDeploymentTargetSchema, + AwsDeploymentTargetsSchema, + DEFAULT_TARGET_NAME, + type AwsDeploymentTarget, +} from "../../projectSchemas/aws-targets"; import type { RuntimeResourceConfig } from "../../handlers/project/add/runtime/types"; import type { TemplateRenderer } from "./templates/types"; import { HandlebarsTemplateRenderer } from "./templates/renderer"; @@ -61,6 +70,12 @@ type ProjectManagerConfig = { json?: ReadWriteJson; backends?: Partial>; templateRenderer?: TemplateRenderer; + /** + * Resolves the AWS account behind the active credentials (STS + * GetCallerIdentity), used to synthesize the default deployment target. + * Injectable so unit tests never call AWS. + */ + resolveAccount?: (region: string) => Promise; }; /** @@ -74,6 +89,7 @@ export class FsProjectManager implements ProjectManager { private readonly checkTool: typeof requireTool; private readonly json: ReadWriteJson; private readonly backends: Partial>; + private readonly resolveAccount: (region: string) => Promise; constructor(config: ProjectManagerConfig) { this.logger = config.logger; @@ -91,6 +107,7 @@ export class FsProjectManager implements ProjectManager { }), }; this.templateRenderer = config.templateRenderer ?? new HandlebarsTemplateRenderer(); + this.resolveAccount = config.resolveAccount ?? resolveAwsAccount; } public async resolve(input: ResolveProjectInput): Promise { @@ -497,24 +514,37 @@ export class FsProjectManager implements ProjectManager { input: DeployProjectInput, ): AsyncGenerator { const targetsPath = join(project.rootPath, "agentcore", "aws-targets.json"); - if (!existsSync(targetsPath)) { - throw new ProjectStateError( - `No deployment targets are configured for project '${project.name}'. ` + - `Add ${targetsPath}, for example:\n\n${TARGETS_EXAMPLE}`, - ); - } + const fileExists = existsSync(targetsPath); + const targets = fileExists ? await this.json.read(targetsPath, AwsDeploymentTargetsSchema) : []; - const targets = await this.json.read(targetsPath, AwsDeploymentTargetsSchema); + let target = targets.find((candidate) => candidate.name === input.target); - if (targets.length === 0) { - throw new ProjectStateError( - `No deployment targets are configured for project '${project.name}'. ` + - `Add at least one to ${targetsPath}, for example:\n\n${TARGETS_EXAMPLE}`, - ); + // A freshly created project defines no targets, so the default one is + // synthesized from the environment rather than demanded up front. Only + // `default` gets this treatment: inventing a *named* target would turn a + // typo'd --target into a deployment somewhere unintended. + if (!target && input.target === DEFAULT_TARGET_NAME) { + target = await this.provisionDefaultTarget(project, targetsPath, input.region); + yield { + message: + `Created default deployment target: account ${target.account}, ` + + `region ${target.region} (${join("agentcore", "aws-targets.json")})`, + }; } - const target = targets.find((candidate) => candidate.name === input.target); if (!target) { + if (!fileExists) { + throw new ProjectStateError( + `No deployment targets are configured for project '${project.name}'. ` + + `Add ${targetsPath}, for example:\n\n${TARGETS_EXAMPLE}`, + ); + } + if (targets.length === 0) { + throw new ProjectStateError( + `No deployment targets are configured for project '${project.name}'. ` + + `Add at least one to ${targetsPath}, for example:\n\n${TARGETS_EXAMPLE}`, + ); + } throw new ProjectStateError( `Project '${project.name}' has no deployment target named '${input.target}'. ` + `${targetsPath} defines: ${targets.map(({ name }) => name).join(", ")}.`, @@ -527,6 +557,63 @@ export class FsProjectManager implements ProjectManager { }); } + /** + * Builds the default deployment target from the environment — the active + * credentials' account and the CLI's effective region — and persists it to + * aws-targets.json alongside any targets already defined there. + */ + private async provisionDefaultTarget( + project: Project, + targetsPath: string, + region: string, + ): Promise { + const supportedRegion = AgentCoreRegionSchema.safeParse(region); + if (!supportedRegion.success) { + throw new InputValidationError( + `Cannot create the default deployment target for project '${project.name}': ` + + `'${region}' is not an AgentCore-supported region.\n` + + `Supported regions: ${AgentCoreRegionSchema.options.join(", ")}.\n` + + `Re-run with --region or set AWS_REGION to one of them.`, + ); + } + + let account: string; + try { + account = await this.resolveAccount(supportedRegion.data); + } catch (error) { + const cause = AgentCoreCLIError.fromError(error); + throw new InvalidEnvironmentError( + `Cannot create the default deployment target for project '${project.name}' because ` + + `the AWS account could not be resolved: ${cause.message}\n` + + `Check that valid AWS credentials are configured (for example via 'aws configure', ` + + `AWS_PROFILE, or environment variables) and re-run 'agentcore project deploy'.`, + { cause: error }, + ); + } + + const entry = AwsDeploymentTargetSchema.safeParse({ + name: DEFAULT_TARGET_NAME, + account, + region: supportedRegion.data, + }); + if (!entry.success) { + throw new MalformedServiceResponseError( + `STS returned an AWS account ID that is not usable as a deployment target:\n` + + z.prettifyError(entry.error), + { cause: entry.error }, + ); + } + + // Merged into the raw file contents rather than the schema-parsed targets, + // so existing entries keep their exact key order and any fields the schema + // does not know about. + const existing = existsSync(targetsPath) + ? await this.json.read(targetsPath, z.array(z.record(z.string(), z.unknown()))) + : []; + await this.json.write(targetsPath, [...existing, entry.data]); + return entry.data; + } + private backendFor(project: Project): ProjectBackend { const backend = this.backends[project.spec.managedBy]; if (!backend) { diff --git a/src/handlers/project/deploy/index.test.ts b/src/handlers/project/deploy/index.test.ts index 1c00dafe3..8b6bc0038 100644 --- a/src/handlers/project/deploy/index.test.ts +++ b/src/handlers/project/deploy/index.test.ts @@ -68,6 +68,7 @@ type TestDeployOptions = { isTTY?: boolean; stdin?: string; teardown?: TeardownConfirmationRequest; + resolveAccount?: (region: string) => Promise; }; function testDeployCommand( @@ -77,7 +78,10 @@ function testDeployCommand( ) { const io = testIO({ isTTY: options.isTTY, stdin: options.stdin }); const fake = fakeBackend(result, events, options.teardown); - const core = new TestCoreClient({ backends: { CDK: fake.backend } }); + const core = new TestCoreClient({ + backends: { CDK: fake.backend }, + resolveAccount: options.resolveAccount, + }); const root = createRootHandler(core, { io: io.io, globalConfigAccessor: new TestGlobalConfigAccessor(), @@ -280,11 +284,63 @@ describe("project deploy handler", () => { expect(subject.calls).toEqual([]); }); - test("requires deployment targets to be configured", async () => { + test("requires deployment targets to be configured for a named target", async () => { const subject = testDeployCommand({ outputs: {} }); await inProjectWithTargets(subject, JSON.stringify([])); - await expect(subject.run()).rejects.toThrow(/No deployment targets are configured/); + await expect(subject.run(["--target", "staging"])).rejects.toThrow( + /No deployment targets are configured/, + ); + expect(subject.calls).toEqual([]); + }); + + // The zero-configuration path: a fresh project's aws-targets.json is [], so + // the first deploy must invent the default target rather than demand edits. + test("creates the default target from the environment on first deploy", async () => { + const subject = testDeployCommand({ outputs: { RuntimeArn: "arn:runtime" } }); + const projectRoot = await inProjectWithTargets(subject, JSON.stringify([])); + + await subject.run(["--region", "us-west-2"]); + + expect(subject.calls).toHaveLength(1); + expect(subject.calls[0]?.input.target).toEqual({ + name: "default", + account: "111122223333", + region: "us-west-2", + }); + expect(subject.io.stderr()).toContain( + "Created default deployment target: account 111122223333, region us-west-2", + ); + expect(subject.io.stderr()).toContain("Deployed project 'orders' to target 'default'"); + expect(await Bun.file(join(projectRoot, "agentcore", "aws-targets.json")).json()).toEqual([ + { name: "default", account: "111122223333", region: "us-west-2" }, + ]); + }); + + test("rejects an unsupported region instead of writing an invalid target", async () => { + const subject = testDeployCommand({ outputs: {} }); + const projectRoot = await inProjectWithTargets(subject, JSON.stringify([])); + + const message = await messageFrom(subject.run(["--region", "us-west-1"])); + + expect(message).toContain("'us-west-1' is not an AgentCore-supported region"); + expect(message).toContain("us-east-1"); + expect(subject.calls).toEqual([]); + expect(await Bun.file(join(projectRoot, "agentcore", "aws-targets.json")).text()).toBe("[]"); + }); + + test("explains how to fix unresolvable credentials", async () => { + const subject = testDeployCommand({ outputs: {} }, [], { + resolveAccount: async () => { + throw new Error("Could not load credentials from any providers"); + }, + }); + await inProjectWithTargets(subject, JSON.stringify([])); + + const message = await messageFrom(subject.run(["--region", "us-east-1"])); + + expect(message).toContain("Could not load credentials from any providers"); + expect(message).toContain("aws configure"); expect(subject.calls).toEqual([]); }); }); diff --git a/src/handlers/project/deploy/index.ts b/src/handlers/project/deploy/index.ts index d904d2b0c..6c74a2da1 100644 --- a/src/handlers/project/deploy/index.ts +++ b/src/handlers/project/deploy/index.ts @@ -2,9 +2,10 @@ import { createInterface } from "node:readline/promises"; import z from "zod"; import { UserCancellationError } from "../../../errors/errors"; import type { AppIO } from "../../../io"; +import { DEFAULT_TARGET_NAME } from "../../../projectSchemas/aws-targets"; import { createHandler, flag, ProjectKey } from "../../../router"; import { JsonRendererKey } from "../../../tui"; -import { JsonKey } from "../../keys"; +import { JsonKey, RegionKey } from "../../keys"; import type { ProjectManager, TeardownConfirmationRequest, @@ -21,7 +22,12 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = name: "deploy", description: "deploy the project to AWS", flags: [ - flag("target", "name of the aws-targets.json entry to deploy", z.string().default("default")), + flag( + "target", + "name of the aws-targets.json entry to deploy; the default target is created " + + "automatically from your AWS account and region on first deploy", + z.string().default(DEFAULT_TARGET_NAME), + ), flag( "yes", "confirm removing the target's stack when the project declares nothing to deploy", @@ -44,6 +50,7 @@ export const createDeployProjectHandler = (config: DeployProjectHandlerConfig) = // generator's return value, which `for await` discards. const deployment = config.projectManager.deploy(project, { target: flags.target, + region: ctx.require(RegionKey), confirmTeardown: createTeardownConfirmationHandler(config.io, flags.yes, canPrompt), }); let next = await deployment.next(); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 9c642432d..cb42155df 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -918,8 +918,13 @@ describe("project deploy", () => { await expect(run(["deploy"])).rejects.toThrow(/No AgentCore project found/); }); - test("rejects a project with no deployment targets", async () => { + // A bare `deploy` on a fresh project synthesizes the default target instead + // of rejecting (covered with a stubbed backend in deploy/index.test.ts); only + // a named target still demands configuration. + test("rejects a project with no deployment targets for a named target", async () => { await inProject(); - await expect(run(["deploy"])).rejects.toThrow(/No deployment targets are configured/); + await expect(run(["deploy", "--target", "staging"])).rejects.toThrow( + /No deployment targets are configured/, + ); }); }); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index e40db8e71..4b67f04ef 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -85,6 +85,12 @@ export type TeardownConfirmationHandler = ( export type DeployProjectInput = { /** Name of the aws-targets.json entry to deploy. */ target: string; + /** + * The effective AWS region the CLI already resolved (--region flag, env, + * shared config file). Used to synthesize the default target when + * aws-targets.json does not define one — never to override a defined target. + */ + region: string; /** Requests approval after the backend discovers that this deploy is a teardown. */ confirmTeardown: TeardownConfirmationHandler; }; diff --git a/src/projectSchemas/aws-targets.ts b/src/projectSchemas/aws-targets.ts index 6d847db2d..be699f3f1 100644 --- a/src/projectSchemas/aws-targets.ts +++ b/src/projectSchemas/aws-targets.ts @@ -25,6 +25,13 @@ export const AgentCoreRegionSchema = z.enum([ "us-gov-west-1", ]); +/** + * The target `project deploy` uses when --target is omitted. Only this target + * is ever synthesized from the environment when aws-targets.json lacks it; + * named targets must be defined explicitly so a typo cannot invent one. + */ +export const DEFAULT_TARGET_NAME = "default"; + export const DeploymentTargetNameSchema = z .string() .min(1) diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 8fcce7a46..eba250e6d 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -1209,6 +1209,7 @@ type TestCoreClientOptions = { json?: ReadWriteJson; backends?: Partial>; createCloudFormationClient?: CreateCloudFormationClient; + resolveAccount?: (region: string) => Promise; }; export class TestIdentityClient implements CoreIdentityClient { @@ -2246,6 +2247,9 @@ export class TestCoreClient implements Core { this.projectCommands.push({ command, cwd }); }, checkTool: async () => {}, // CI hosts don't have uv installed + // Deploy synthesizes the default target through STS; stub the lookup so + // tests stay hermetic. + resolveAccount: options?.resolveAccount ?? (async () => "111122223333"), }); } } From 9006630224a8581e8175eaee17479cc122d63e53 Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Sun, 30 Aug 2026 23:29:20 -0400 Subject: [PATCH 02/12] chore: keep generated out/ and dist/ trees out of the test and typecheck gate Leftover e2e scaffolds under out/ (and stale dist/ build output) were being swept up by bun test and tsc, failing the gate on files that are not part of the source tree. Scope both to the repo source, and fix the two react(set-state-in-effect) findings oxlint 1.80 raises in usePagedList and DataTable by deriving/adjusting state during render instead of in effects. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- bunfig.toml | 2 +- src/components/ui/data-table/DataTable.tsx | 11 ++++++++--- src/components/usePagedList.tsx | 13 ++++++------- tsconfig.json | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/bunfig.toml b/bunfig.toml index 1b46fe0f7..63adf5a65 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -3,5 +3,5 @@ # frames are plain text (no ANSI color codes) regardless of whether stdout is a # TTY, keeping frame assertions deterministic across `bun test` and piped runs. preload = ["./src/testing/setup.ts"] -pathIgnorePatterns = ["src/assets/**"] +pathIgnorePatterns = ["src/assets/**", "out/**", "dist/**"] coveragePathIgnorePatterns = ["src/testing/**", "src/assets/**"] diff --git a/src/components/ui/data-table/DataTable.tsx b/src/components/ui/data-table/DataTable.tsx index 2d9745fbe..56b1a95d9 100644 --- a/src/components/ui/data-table/DataTable.tsx +++ b/src/components/ui/data-table/DataTable.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import cliTruncate from "cli-truncate"; import { Box, Text, useInput, useWindowSize } from "ink"; import stringWidth from "string-width"; @@ -79,10 +79,15 @@ export function DataTable>({ const [searchQuery, setSearchQuery] = useState(""); const [searchMode, setSearchMode] = useState(false); - useEffect(() => { + // Reset the cursor when the caller swaps datasets. Adjusting state during + // render (rather than in an effect) applies the reset in the same frame the + // new data first paints. + const [prevResetKey, setPrevResetKey] = useState(selectionResetKey); + if (prevResetKey !== selectionResetKey) { + setPrevResetKey(selectionResetKey); setSelectedRow(0); setCurrentPage(0); - }, [selectionResetKey]); + } // Filter const filtered = data.filter((row) => { diff --git a/src/components/usePagedList.tsx b/src/components/usePagedList.tsx index 76450a309..c50ee32ef 100644 --- a/src/components/usePagedList.tsx +++ b/src/components/usePagedList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useWindowSize } from "ink"; // CHROME_ROWS is everything a picker screen renders around the table rows: @@ -45,17 +45,16 @@ export function usePagedList(maxPageSize?: number): PagedList { const fitsTerminal = Math.max(3, rows - CHROME_ROWS); const pageSize = maxPageSize ? Math.min(fitsTerminal, maxPageSize) : fitsTerminal; + // A resize changes maxResults, which invalidates the token trail (tokens + // encode positions relative to the old page size) — so every read derives + // page-1 values while state.pageSize is stale, and every write rebases onto + // initialPagination(pageSize) first. No effect needed: state catches up on + // the next interaction. const [state, setState] = useState(() => initialPagination(pageSize)); const pageSizeChanged = state.pageSize !== pageSize; const pageIndex = pageSizeChanged ? 0 : state.pageIndex; const token = pageSizeChanged ? undefined : state.tokens[state.pageIndex]; - // A resize changes maxResults, which invalidates the token trail (tokens - // encode positions relative to the old page size) — restart from page 1. - useEffect(() => { - setState((current) => (current.pageSize === pageSize ? current : initialPagination(pageSize))); - }, [pageSize]); - return { pageSize, pageIndex, diff --git a/tsconfig.json b/tsconfig.json index bffe35ab3..bbc90b2b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,5 +27,5 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false }, - "exclude": ["src/assets"] + "exclude": ["src/assets", "out", "dist"] } From b9ba43422164834c577dfee422e8265faf6f48f3 Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Sun, 30 Aug 2026 23:45:46 -0400 Subject: [PATCH 03/12] feat(create): harness-first project creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bare `project create --name X` (and --defaults) now creates a harness project — a managed agent configured by spec — matching the original CLI's default quick start. The harness-only flags (--model-id, --api-key-arn, --api-base, --additional-params, --no-harness-memory, --max-iterations, --max-tokens, --timeout, --truncation-strategy, --container) flow into the harness spec, which is validated up front against the same schema `project add harness` uses and scaffolded through the same addResource path, so the two entry points cannot drift. Runtime scaffolding is selected by --template or the runtime flags exactly as before; mixing the two flag families is a validation error, mirroring the original's dispatch. Tests that relied on the previous hello-world default now scaffold it explicitly via --template. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- README.md | 21 ++ src/core/project/manager.tsx | 24 +- .../project/add/gateway-test-support.ts | 10 +- src/handlers/project/add/harness/index.ts | 8 +- .../project/add/online-eval/index.test.ts | 10 +- .../project/add/online-insight/index.test.ts | 10 +- .../project/add/payment-test-support.ts | 10 +- src/handlers/project/create/index.ts | 268 +++++++++++++++--- src/handlers/project/project.test.ts | 145 +++++++++- src/handlers/project/remove/index.test.ts | 10 +- src/handlers/project/types.ts | 20 +- 11 files changed, 468 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index f09c95470..59e813d9f 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,14 @@ agentcore # interactive TUI │ ├── get # get an evaluator by id (type-agnostic) │ ├── list # list evaluators (server-side paginated) │ └── delete # delete an evaluator by id +├── project # manage an AgentCore project (scaffold → deploy) +│ ├── create # create a project: a managed harness by default, +│ │ # or scaffolded runtime code via --template/--framework +│ ├── add # add a resource to the project (runtime, harness, memory, …) +│ ├── remove # remove a resource from the project +│ ├── build # synthesize the project's CloudFormation templates +│ ├── deploy # deploy to AWS (auto-provisions the default target) +│ └── dev # run the project's agents locally └── config # read/write global config values ``` @@ -118,6 +126,19 @@ Global flags (declared at the root, available on every command): ### Examples +```bash +# Create a project. The default is a harness project: a managed agent +# configured by spec, no model-loop code to maintain. --defaults says so +# explicitly; harness flags (--model-id, --max-iterations, --timeout, …) +# tune it. +agentcore project create --name MyAssistant +cd MyAssistant && agentcore project deploy +agentcore harness invoke --id --prompt "hello" + +# Scaffold runtime code instead (pass a template or framework flags). +agentcore project create --name MyAgent --template strands-python +``` + ```bash # Create a harness; a default execution role is created for you. agentcore harness create \ diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 8a6696f0e..e05d25884 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -143,6 +143,22 @@ export class FsProjectManager implements ProjectManager { ); await projectTree.write(destination); + // A harness project scaffolds through the same addResource flow that + // `project add harness` uses, so a create-time harness and an added one can + // never drift apart. + if (input.scaffoldHarnessInput) { + const scaffolded = await this.resolve({ filePath: destination }); + if (!scaffolded) { + throw new ProjectStateError( + `the project scaffolded at ${destination} could not be read back`, + ); + } + yield* this.addResource(scaffolded, { + resourceType: "harness", + resourceConfig: input.scaffoldHarnessInput, + }); + } + // A failed step leaves the scaffolded files in place; the error tells the // user how to rerun the step by hand. if (!input.skipInstall) { @@ -150,9 +166,11 @@ export class FsProjectManager implements ProjectManager { yield { message: "Installing CDK dependencies with npm" }; await this.run(["npm", "install"], join(destination, "agentcore", "cdk")); - const appDir = join(destination, "app", scaffoldRuntimeInput.runtimeName); - yield* this.installRuntimeDependencies(appDir); - } else if (scaffoldRuntimeInput.build === "Container") { + if (scaffoldRuntimeInput) { + const appDir = join(destination, "app", scaffoldRuntimeInput.runtimeName); + yield* this.installRuntimeDependencies(appDir); + } + } else if (scaffoldRuntimeInput?.build === "Container") { // containers require uv.lock to build, so even with no-install we must generate the lock. const appDir = join(destination, "app", scaffoldRuntimeInput.runtimeName); yield* this.ensureLockFileExists(appDir); diff --git a/src/handlers/project/add/gateway-test-support.ts b/src/handlers/project/add/gateway-test-support.ts index ec7e6c578..9a422baff 100644 --- a/src/handlers/project/add/gateway-test-support.ts +++ b/src/handlers/project/add/gateway-test-support.ts @@ -40,7 +40,15 @@ export function createGatewayProjectTestHarness(directoryPrefix: string) { const directory = await mkdtemp(join(tmpdir(), `agentcore-${directoryPrefix}-`)); tempDirectories.push(directory); process.chdir(directory); - await run(["create", "--name", name, "--skip-install", "--skip-git"]); + await run([ + "create", + "--name", + name, + "--template", + "hello-world-python", + "--skip-install", + "--skip-git", + ]); const projectRoot = join(directory, name); process.chdir(projectRoot); return projectRoot; diff --git a/src/handlers/project/add/harness/index.ts b/src/handlers/project/add/harness/index.ts index cf82e7761..be40dc5a4 100644 --- a/src/handlers/project/add/harness/index.ts +++ b/src/handlers/project/add/harness/index.ts @@ -5,10 +5,12 @@ import { parseJsonFlag, parseTags } from "../../../utils"; import { InputValidationError } from "../../../../errors"; import { HarnessSpecSchema } from "../../../../projectSchemas/harness"; -const DEFAULT_MODEL = { +/** The model a harness runs on when none is configured; `project create`'s + * harness path shares it so the two entry points cannot drift. */ +export const DEFAULT_HARNESS_MODEL = { provider: "bedrock", modelId: "global.anthropic.claude-sonnet-4-6", -}; +} as const; export const createAddHarnessHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -71,7 +73,7 @@ export const createAddHarnessHandler = (config: AddProjectResourceConfig) => handle: async (ctx, flags) => { const harnessInput = { name: flags.name, - model: parseJsonFlag("model", flags["model"]) ?? DEFAULT_MODEL, + model: parseJsonFlag("model", flags["model"]) ?? DEFAULT_HARNESS_MODEL, systemPrompt: flags["system-prompt"], executionRoleArn: flags["execution-role-arn"], tools: parseJsonFlag("tools", flags["tools"]), diff --git a/src/handlers/project/add/online-eval/index.test.ts b/src/handlers/project/add/online-eval/index.test.ts index c27453901..10ca3dd73 100644 --- a/src/handlers/project/add/online-eval/index.test.ts +++ b/src/handlers/project/add/online-eval/index.test.ts @@ -42,7 +42,15 @@ async function run(args: string[], opts?: { core?: TestCoreClient }) { async function inProject(name = "TestProject"): Promise { const directory = await inTempDirectory(); - await run(["create", "--name", name, "--skip-install", "--skip-git"]); + await run([ + "create", + "--name", + name, + "--template", + "hello-world-python", + "--skip-install", + "--skip-git", + ]); const projectRoot = join(directory, name); process.chdir(projectRoot); return projectRoot; diff --git a/src/handlers/project/add/online-insight/index.test.ts b/src/handlers/project/add/online-insight/index.test.ts index bdc449267..69f943cc5 100644 --- a/src/handlers/project/add/online-insight/index.test.ts +++ b/src/handlers/project/add/online-insight/index.test.ts @@ -42,7 +42,15 @@ async function run(args: string[], opts?: { core?: TestCoreClient }) { async function inProject(name = "TestProject"): Promise { const directory = await inTempDirectory(); - await run(["create", "--name", name, "--skip-install", "--skip-git"]); + await run([ + "create", + "--name", + name, + "--template", + "hello-world-python", + "--skip-install", + "--skip-git", + ]); const projectRoot = join(directory, name); process.chdir(projectRoot); return projectRoot; diff --git a/src/handlers/project/add/payment-test-support.ts b/src/handlers/project/add/payment-test-support.ts index 6ef95e707..66665c647 100644 --- a/src/handlers/project/add/payment-test-support.ts +++ b/src/handlers/project/add/payment-test-support.ts @@ -28,7 +28,15 @@ export function createPaymentProjectTestHarness(directoryPrefix: string) { const directory = await mkdtemp(join(tmpdir(), `agentcore-${directoryPrefix}-`)); tempDirectories.push(directory); process.chdir(directory); - await run(["create", "--name", name, "--skip-install", "--skip-git"]); + await run([ + "create", + "--name", + name, + "--template", + "hello-world-python", + "--skip-install", + "--skip-git", + ]); const projectRoot = join(directory, name); process.chdir(projectRoot); return projectRoot; diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 69441eff5..f25a72bd5 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -11,22 +11,57 @@ import { ScaffoldRuntimeInputSchema, type CreateProjectInput, type ProjectManager, + type ScaffoldHarnessInput, type ScaffoldRuntimeInput, } from "../types"; import { ProjectNameSchema } from "../../../projectSchemas/project"; +import { CONTAINER_URI_PATTERN, HarnessSpecSchema } from "../../../projectSchemas/harness"; import { InputValidationError } from "../../../errors"; +import { parseJsonFlag } from "../../utils"; +import { DEFAULT_HARNESS_MODEL } from "../add/harness"; type CreateProjectHandlerConfig = { projectManager: ProjectManager; io: AppIO; }; +// Flags that select the runtime-scaffolding path. Any of these (or --template) +// present routes create away from the default harness path, mirroring the +// original CLI's agent-path dispatch. +const RUNTIME_PATH_FLAGS = [ + "build", + "language", + "framework", + "model-provider", + "api-key", + "runtime-name", + "memory", +] as const; + +// Flags that only make sense for the harness path. +const HARNESS_ONLY_FLAGS = [ + "model-id", + "api-key-arn", + "api-base", + "additional-params", + "max-iterations", + "max-tokens", + "timeout", + "truncation-strategy", + "container", +] as const; + export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) => createHandler({ name: "create", description: "create a new AgentCore project", flags: [ flag("name", "name of the project to create", ProjectNameSchema), + flag( + "defaults", + "create a harness project with default settings (this is the default)", + z.boolean().default(false), + ), flag( "template", "a preset of flags for scaffolding the runtime; compatible flags override preset values", @@ -64,6 +99,44 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = z.enum(MEMORY_SHORTCUT_NAMES).optional(), ), flag("runtime-name", "name of the scaffolded runtime", z.string().max(42).optional()), + flag("model-id", "model ID for the created harness", z.string().optional()), + flag( + "api-key-arn", + "API key credential ARN for the created harness's model provider", + z.string().optional(), + ), + flag( + "api-base", + "base URL for the harness model provider API endpoint (lite_llm)", + z.string().optional(), + ), + flag( + "additional-params", + "provider-specific harness model params as a JSON object (lite_llm)", + z.string().optional(), + ), + flag( + "no-harness-memory", + "disable memory for the created harness (this is the default)", + z.boolean().default(false), + ), + flag( + "max-iterations", + "max agent loop iterations per invocation (harness)", + z.number().optional(), + ), + flag("max-tokens", "max total output tokens per invocation (harness)", z.number().optional()), + flag("timeout", "max duration in seconds per invocation (harness)", z.number().optional()), + flag( + "truncation-strategy", + "context truncation strategy for the harness", + z.enum(["sliding_window", "summarization"]).optional(), + ), + flag( + "container", + "container image URI or Dockerfile path for the harness", + z.string().optional(), + ), flag( "skip-install", "skip installing dependencies (npm install, uv sync)", @@ -72,18 +145,27 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = flag("skip-git", "skip initializing a git repository", z.boolean().default(false)), ], handle: async (_ctx, flags) => { - const scaffoldingFlags = [ - "build", - "language", - "framework", - "model-provider", - "api-key", - "runtime-name", - "memory", - ] as const; - - const presentScaffoldingFlags = scaffoldingFlags.filter((f) => flags[f] !== undefined); + const presentRuntimeFlags: string[] = RUNTIME_PATH_FLAGS.filter( + (f) => flags[f] !== undefined, + ); const isTemplate = flags["template"] !== undefined; + if (isTemplate) presentRuntimeFlags.unshift("template"); + + const presentHarnessFlags: string[] = HARNESS_ONLY_FLAGS.filter( + (f) => flags[f] !== undefined, + ); + if (flags["no-harness-memory"]) presentHarnessFlags.push("no-harness-memory"); + + // Mirrors the original CLI's dispatch: mixing the two paths is an error, + // while --defaults on the runtime path is simply ignored. + if (presentRuntimeFlags.length > 0 && presentHarnessFlags.length > 0) { + throw new InputValidationError( + `Cannot mix runtime scaffolding flags (${formatFlagList(presentRuntimeFlags)}) ` + + `with harness-only flags (${formatFlagList(presentHarnessFlags)}). ` + + `A project is created around either a harness (the default) or scaffolded runtime code.`, + ); + } + const lockedFlag = (["language", "framework"] as const).find( (flagName) => flags[flagName] !== undefined, ); @@ -91,51 +173,143 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = throw new InputValidationError(`--${lockedFlag} cannot override a template`); } - const isCustom = presentScaffoldingFlags.length > 0; - - const source = new SourceResolver({ stdin: config.io.stdin }); - const apiKey = await source.resolveSecret("api-key", flags["api-key"]); - - const runtimeName = flags["runtime-name"] ?? flags["name"]; - const defaultMemory = flags["framework"] === "strands" ? "longAndShortTerm" : "none"; - - const scaffoldRuntimeInput: ScaffoldRuntimeInput = isTemplate - ? resolveRuntimeTemplateShortcut(flags["template"]!, { - runtimeName: flags["runtime-name"], - build: flags["build"], - modelProvider: flags["model-provider"], - apiKey, - memory: flags["memory"], - }) - : isCustom - ? parseScaffoldRuntimeInput({ - runtimeName, - build: flags["build"], - language: flags["language"], - framework: flags["framework"], - modelProvider: flags["model-provider"], - apiKey, - memory: MEMORY_SHORTCUTS[flags["memory"] ?? defaultMemory](runtimeName), - entrypoint: "main.py", - runtimeVersion: flags["build"] === "CodeZip" ? "PYTHON_3_14" : undefined, - }) - : resolveRuntimeTemplateShortcut("hello-world-python"); - - const createInput: CreateProjectInput = { - name: flags["name"], - skipInstall: flags["skip-install"], - skipGit: flags["skip-git"], - scaffoldRuntimeInput, - }; + const isRuntimePath = presentRuntimeFlags.length > 0; + + const createInput: CreateProjectInput = isRuntimePath + ? { + name: flags["name"], + skipInstall: flags["skip-install"], + skipGit: flags["skip-git"], + scaffoldRuntimeInput: await resolveScaffoldRuntimeInput(config, flags), + } + : { + name: flags["name"], + skipInstall: flags["skip-install"], + skipGit: flags["skip-git"], + scaffoldHarnessInput: resolveScaffoldHarnessInput(flags), + }; + + if (!isRuntimePath && !flags["defaults"] && presentHarnessFlags.length === 0) { + config.io.stderr.write( + "Creating a harness project (pass --framework or --template to scaffold agent code instead).\n", + ); + } for await (const event of config.projectManager.create(createInput)) { config.io.stderr.write(`${event.message}\n`); } config.io.stderr.write(`Created project '${flags["name"]}' in ./${flags["name"]}\n`); + config.io.stderr.write(`To deploy it: cd ${flags["name"]} && agentcore project deploy\n`); }, }); +type RuntimePathFlagValues = { + name: string; + template?: (typeof RUNTIME_TEMPLATE_SHORTCUT_NAMES)[number]; + build?: "CodeZip" | "Container"; + language?: "Python"; + framework?: "strands" | "none"; + "model-provider"?: "Bedrock"; + "api-key"?: string; + memory?: (typeof MEMORY_SHORTCUT_NAMES)[number]; + "runtime-name"?: string; +}; + +type HarnessPathFlagValues = { + name: string; + "model-id"?: string; + "api-key-arn"?: string; + "api-base"?: string; + "additional-params"?: string; + "max-iterations"?: number; + "max-tokens"?: number; + timeout?: number; + "truncation-strategy"?: "sliding_window" | "summarization"; + container?: string; +}; + +async function resolveScaffoldRuntimeInput( + config: CreateProjectHandlerConfig, + flags: RuntimePathFlagValues, +): Promise { + const source = new SourceResolver({ stdin: config.io.stdin }); + const apiKey = await source.resolveSecret("api-key", flags["api-key"]); + + const runtimeName = flags["runtime-name"] ?? flags["name"]; + const defaultMemory = flags["framework"] === "strands" ? "longAndShortTerm" : "none"; + + return flags["template"] !== undefined + ? resolveRuntimeTemplateShortcut(flags["template"], { + runtimeName: flags["runtime-name"], + build: flags["build"], + modelProvider: flags["model-provider"], + apiKey, + memory: flags["memory"], + }) + : parseScaffoldRuntimeInput({ + runtimeName, + build: flags["build"], + language: flags["language"], + framework: flags["framework"], + modelProvider: flags["model-provider"], + apiKey, + memory: MEMORY_SHORTCUTS[flags["memory"] ?? defaultMemory](runtimeName), + entrypoint: "main.py", + runtimeVersion: flags["build"] === "CodeZip" ? "PYTHON_3_14" : undefined, + }); +} + +// The harness input validates against the same schema `project add harness` +// uses, before any file is written; the manager then scaffolds it through the +// same addResource path. +function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): ScaffoldHarnessInput { + const additionalParams = parseJsonFlag>( + "additional-params", + flags["additional-params"], + ); + + const input: ScaffoldHarnessInput = { + // A project name always satisfies the harness name grammar (letters and + // digits only), so the harness is named after the project like the + // original CLI does. + name: flags["name"], + model: { + provider: DEFAULT_HARNESS_MODEL.provider, + modelId: flags["model-id"] ?? DEFAULT_HARNESS_MODEL.modelId, + apiKeyArn: flags["api-key-arn"], + apiBase: flags["api-base"], + additionalParams, + }, + maxIterations: flags["max-iterations"], + maxTokens: flags["max-tokens"], + timeoutSeconds: flags["timeout"], + truncation: flags["truncation-strategy"] + ? { strategy: flags["truncation-strategy"] } + : undefined, + // Harness memory is opt-in and disabled by default; --no-harness-memory + // documents the default explicitly. + ...parseContainerFlag(flags["container"]), + }; + + const result = HarnessSpecSchema.safeParse(input); + if (!result.success) + throw new InputValidationError(z.prettifyError(result.error), { cause: result.error }); + return input; +} + +/** A --container value is either an ECR image URI or a local Dockerfile path. */ +function parseContainerFlag( + value: string | undefined, +): Pick { + if (value === undefined) return {}; + return CONTAINER_URI_PATTERN.test(value) ? { containerUri: value } : { dockerfile: value }; +} + +function formatFlagList(flagNames: string[]): string { + return flagNames.map((name) => `--${name}`).join(", "); +} + function parseScaffoldRuntimeInput(input: Partial) { const result = ScaffoldRuntimeInputSchema.safeParse(input); if (!result.success) throw new InputValidationError(z.prettifyError(result.error)); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index cb42155df..064ffd659 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -64,14 +64,146 @@ async function inProject(name = "TestProject"): Promise { } describe("project create", () => { - test("scaffolds the project into a fresh directory named for the project", async () => { + test("scaffolds a harness project by default, named for the project", async () => { const directory = await inTempDirectory(); - await run(["create", "--name", "MyAgent"]); + const { io } = await run(["create", "--name", "MyAgent"]); - // One existence check proves the handler→manager pipe; the full manifest - // is covered by the FsProjectManager snapshot test. const projectRoot = join(directory, "MyAgent"); - expect(await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).exists()).toBe(true); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.harnesses).toEqual([{ name: "MyAgent", path: "app/MyAgent" }]); + expect(spec.runtimes).toEqual([]); + + const harness = await Bun.file(join(projectRoot, "app", "MyAgent", "harness.json")).json(); + expect(harness.model).toEqual({ + provider: "bedrock", + modelId: "global.anthropic.claude-sonnet-4-6", + }); + expect(harness.memory).toBeUndefined(); + expect(await Bun.file(join(projectRoot, "app", "MyAgent", "system-prompt.md")).exists()).toBe( + true, + ); + expect(io.stderr()).toContain("Creating a harness project"); + }); + + test("--defaults selects the harness path explicitly, without the implicit-default notice", async () => { + const directory = await inTempDirectory(); + const { io } = await run(["create", "--name", "MyAgent", "--defaults"]); + + const spec = await Bun.file(join(directory, "MyAgent", "agentcore", "agentcore.json")).json(); + expect(spec.harnesses).toHaveLength(1); + expect(io.stderr()).not.toContain("Creating a harness project"); + }); + + test("harness-only flags flow into the harness spec", async () => { + const directory = await inTempDirectory(); + await run([ + "create", + "--name", + "MyAgent", + "--model-id", + "us.amazon.nova-lite-v1:0", + "--api-key-arn", + "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/k", + "--max-iterations", + "5", + "--max-tokens", + "2048", + "--timeout", + "60", + "--truncation-strategy", + "sliding_window", + "--no-harness-memory", + ]); + + const harness = await Bun.file( + join(directory, "MyAgent", "app", "MyAgent", "harness.json"), + ).json(); + expect(harness).toMatchObject({ + model: { + provider: "bedrock", + modelId: "us.amazon.nova-lite-v1:0", + apiKeyArn: + "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/k", + }, + maxIterations: 5, + maxTokens: 2048, + timeoutSeconds: 60, + truncation: { strategy: "sliding_window" }, + }); + expect(harness.memory).toBeUndefined(); + }); + + test("--container with an image URI records containerUri on the harness", async () => { + const directory = await inTempDirectory(); + await run([ + "create", + "--name", + "MyAgent", + "--container", + "111122223333.dkr.ecr.us-east-1.amazonaws.com/agents:latest", + ]); + + const harness = await Bun.file( + join(directory, "MyAgent", "app", "MyAgent", "harness.json"), + ).json(); + expect(harness.containerUri).toBe("111122223333.dkr.ecr.us-east-1.amazonaws.com/agents:latest"); + expect(harness.dockerfile).toBeUndefined(); + }); + + test("--container with a Dockerfile path vendors the Dockerfile into the harness", async () => { + const directory = await inTempDirectory(); + await Bun.write(join(directory, "MyDockerfile"), "FROM public.ecr.aws/docker/library/python"); + await run(["create", "--name", "MyAgent", "--container", "MyDockerfile"]); + + const harnessRoot = join(directory, "MyAgent", "app", "MyAgent"); + const harness = await Bun.file(join(harnessRoot, "harness.json")).json(); + expect(harness.dockerfile).toBe("Dockerfile"); + expect(await Bun.file(join(harnessRoot, "Dockerfile")).text()).toContain("FROM "); + }); + + test("rejects mixing runtime scaffolding flags with harness-only flags", async () => { + await inTempDirectory(); + await expect( + run(["create", "--name", "MyAgent", "--framework", "strands", "--model-id", "x"]), + ).rejects.toThrow(/Cannot mix runtime scaffolding flags \(--framework\)/); + await expect( + run(["create", "--name", "MyAgent", "--template", "hello-world-python", "--timeout", "9"]), + ).rejects.toThrow(/harness-only flags \(--timeout\)/); + }); + + test("--defaults is ignored when a runtime path flag routes to scaffolding", async () => { + const directory = await inTempDirectory(); + await run(["create", "--name", "MyAgent", "--defaults", "--template", "hello-world-python"]); + + const spec = await Bun.file(join(directory, "MyAgent", "agentcore", "agentcore.json")).json(); + expect(spec.runtimes[0]).toMatchObject({ name: "hello_world" }); + expect(spec.harnesses).toBeUndefined(); + }); + + test("a harness create installs CDK dependencies and git only (no uv sync)", async () => { + const directory = await inTempDirectory(); + const { core } = await run(["create", "--name", "MyAgent"]); + + const projectRoot = join(directory, "MyAgent"); + expect(core.projectCommands).toEqual([ + { command: ["npm", "install"], cwd: join(projectRoot, "agentcore", "cdk") }, + { command: ["git", "init"], cwd: projectRoot }, + ]); + }); + + test("rejects invalid harness flag combinations before scaffolding anything", async () => { + const directory = await inTempDirectory(); + // apiBase is a lite_llm-only model setting; the bedrock harness path + // surfaces the schema's guidance without writing a partial project. + await expect( + run(["create", "--name", "MyAgent", "--api-base", "https://example.com"]), + ).rejects.toThrow(/lite_llm/); + expect(await Bun.file(join(directory, "MyAgent")).exists()).toBe(false); + + await expect( + run(["create", "--name", "MyAgent", "--additional-params", "{not-json"]), + ).rejects.toThrow(/JSON/i); + expect(await Bun.file(join(directory, "MyAgent")).exists()).toBe(false); }); test("rejects an invalid --project-name", async () => { @@ -890,7 +1022,8 @@ describe("project build", () => { test("resolves the project from a nested directory", async () => { const projectRoot = await inBuildableProject(); - process.chdir(join(projectRoot, "app", "hello_world")); + // The default create scaffolds a harness directory named for the project. + process.chdir(join(projectRoot, "app", "MyAgent")); const { core } = await run(["build"]); diff --git a/src/handlers/project/remove/index.test.ts b/src/handlers/project/remove/index.test.ts index e791aa481..d06871d18 100644 --- a/src/handlers/project/remove/index.test.ts +++ b/src/handlers/project/remove/index.test.ts @@ -43,7 +43,15 @@ async function run(args: string[]) { async function inProject(name = "TestProject"): Promise { const directory = await inTempDirectory(); - await run(["create", "--name", name, "--skip-install", "--skip-git"]); + await run([ + "create", + "--name", + name, + "--template", + "hello-world-python", + "--skip-install", + "--skip-git", + ]); const projectRoot = join(directory, name); process.chdir(projectRoot); return projectRoot; diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 4b67f04ef..aa9a6b84c 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -58,10 +58,22 @@ export const ScaffoldRuntimeInputSchema = z export type ScaffoldRuntimeInput = z.infer; -export type CreateProjectInput = CreateProjectInputBase & { - /** The resolved template parameters. The handler maps --template to these before calling the manager. */ - scaffoldRuntimeInput: ScaffoldRuntimeInput; -}; +/** Set of arguments needed to create a project around a harness. */ +export type ScaffoldHarnessInput = z.input; + +export type CreateProjectInput = CreateProjectInputBase & + ( + | { + /** The resolved template parameters. The handler maps --template to these before calling the manager. */ + scaffoldRuntimeInput: ScaffoldRuntimeInput; + scaffoldHarnessInput?: undefined; + } + | { + /** The harness the created project declares (the default create path). */ + scaffoldHarnessInput: ScaffoldHarnessInput; + scaffoldRuntimeInput?: undefined; + } + ); /** A progress step reported while a long-running project operation runs. */ export type ProjectEvent = { From 75bdf6c8ef074f3036751f5a01a439bd0db6908c Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Mon, 31 Aug 2026 00:02:59 -0400 Subject: [PATCH 04/12] feat(create): import a Bedrock Agent as a runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `project create --type import --agent-id --agent-alias-id ` (and the same on `project add runtime`) wraps an existing Amazon Bedrock Agent as an AgentCore runtime: the agent and alias are described through @aws-sdk/client-bedrock-agent to validate they exist (with the global --region naming the agent's region, validated against the supported list), and a proxy runtime is scaffolded from a new bedrock-agent-proxy-python template that forwards prompts via InvokeAgent and streams the reply. The vended bedrock-agent-policy.json grants the execution role bedrock:InvokeAgent on the alias through the runtime's additionalPolicies, so the project deploys and invokes like any other. The original CLI translated the Bedrock Agent's definition into native framework code; this ships the proxy shape instead — the agent stays the brain, the runtime is the AgentCore front door. bun.lock: adds the @aws-sdk/client-bedrock-agent entry; the surrounding churn is bun regenerating the lock format on install. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- README.md | 7 + bun.lock | 988 +++--------------- package.json | 1 + .../bedrock-agent-proxy-python/README.md | 16 + .../bedrock-agent-policy.json | 11 + .../bedrock-agent-proxy-python/main.py | 45 + .../bedrock-agent-proxy-python/pyproject.toml | 19 + src/core/index.tsx | 4 + src/core/project/bedrockAgent.ts | 103 ++ src/core/project/manager.tsx | 2 +- src/core/project/templates/project.ts | 8 +- src/core/project/templates/runtime.ts | 49 + .../project/add/runtime/index.test.ts | 119 ++- src/handlers/project/add/runtime/index.ts | 85 +- src/handlers/project/add/runtime/types.ts | 18 + src/handlers/project/add/types.ts | 3 + src/handlers/project/create/index.ts | 61 +- src/handlers/project/importBedrockAgent.ts | 80 ++ src/handlers/project/index.ts | 10 +- src/handlers/project/project.test.ts | 73 ++ src/handlers/project/types.ts | 5 +- src/handlers/types.tsx | 3 + src/testing/TestCoreClient.tsx | 21 + 23 files changed, 836 insertions(+), 895 deletions(-) create mode 100644 src/assets/templates/bedrock-agent-proxy-python/README.md create mode 100644 src/assets/templates/bedrock-agent-proxy-python/bedrock-agent-policy.json create mode 100644 src/assets/templates/bedrock-agent-proxy-python/main.py create mode 100644 src/assets/templates/bedrock-agent-proxy-python/pyproject.toml create mode 100644 src/core/project/bedrockAgent.ts create mode 100644 src/handlers/project/importBedrockAgent.ts diff --git a/README.md b/README.md index 59e813d9f..ed38e20ea 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,13 @@ agentcore harness invoke --id --prompt "hello" # Scaffold runtime code instead (pass a template or framework flags). agentcore project create --name MyAgent --template strands-python + +# Wrap an existing Amazon Bedrock Agent as a runtime: a generated proxy +# forwards prompts to the agent, so it deploys and invokes like any other +# runtime. --region names the Bedrock Agent's region. Also available as +# `project add runtime --type import` inside a project. +agentcore project create --name MyProxy --type import \ + --agent-id A1B2C3D4E5 --agent-alias-id TSTALIASID --region us-east-1 ``` ```bash diff --git a/bun.lock b/bun.lock index 183450cdf..a4388eef6 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "agentcore", "dependencies": { "@aws-cdk/toolkit-lib": "1.38.2", + "@aws-sdk/client-bedrock-agent": "^3.1092.0", "@aws-sdk/client-bedrock-agentcore": "^3.1092.0", "@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0", "@aws-sdk/client-cloudformation": "^3.1092.0", @@ -51,9 +52,6 @@ }, }, "overrides": { - "@aws-cdk/toolkit-lib": { - "yaml": "^1", - }, "@opentelemetry/core": "^2.10.0", }, "packages": { @@ -61,69 +59,71 @@ "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="], - "@aws-cdk/aws-service-spec": ["@aws-cdk/aws-service-spec@0.1.205", "", { "dependencies": { "@aws-cdk/service-spec-types": "^0.0.271", "@cdklabs/tskb": "^0.0.4" } }, "sha512-uRsezDO9MuyHbKGFPbdD0NnaGxUMk/S4YopCQZWvgTtHRhUkmMdBnaGCLSk/FgusjyK5fmKJsCdGSplx6oE/cg=="], + "@aws-cdk/aws-service-spec": ["@aws-cdk/aws-service-spec@0.1.207", "", { "dependencies": { "@aws-cdk/service-spec-types": "^0.0.273", "@cdklabs/tskb": "^0.0.4" } }, "sha512-6/qaHGVjMLCEWak4Lm2QOjysUkQImS6kuEnOnCX5tfZ+zYyvsqQ3ABSsa+gkbBkYedc8EKZJNGsuQr9cOSPA8g=="], - "@aws-cdk/cdk-assets-lib": ["@aws-cdk/cdk-assets-lib@1.4.16", "", { "dependencies": { "@aws-cdk/cloud-assembly-api": "2.3.0", "@aws-cdk/cloud-assembly-schema": ">=54.20.0", "@aws-sdk/client-ecr": "^3", "@aws-sdk/client-s3": "^3", "@aws-sdk/client-secrets-manager": "^3", "@aws-sdk/client-sts": "^3", "@aws-sdk/credential-providers": "^3", "@aws-sdk/lib-storage": "^3", "@smithy/config-resolver": "^4", "@smithy/node-config-provider": "^4", "fast-glob": "^3.3.3", "mime": "^2", "picomatch": "^4.0.5", "yazl": "^3.3.1" } }, "sha512-stMaN9jZNztIPqvRkMs5nmDq9ejUoBtKD8VT1Ypi4OrKCxbwEikzAX0px2xR5emF6afkDfWs6qXrPiApxweT2A=="], + "@aws-cdk/cdk-assets-lib": ["@aws-cdk/cdk-assets-lib@1.4.17", "", { "dependencies": { "@aws-cdk/cloud-assembly-api": "2.4.0", "@aws-cdk/cloud-assembly-schema": ">=54.21.0", "@aws-sdk/client-ecr": "^3", "@aws-sdk/client-s3": "^3", "@aws-sdk/client-secrets-manager": "^3", "@aws-sdk/client-sts": "^3", "@aws-sdk/credential-providers": "^3", "@aws-sdk/lib-storage": "^3", "@smithy/config-resolver": "^4", "@smithy/node-config-provider": "^4", "cross-spawn": "^7.0.6", "fast-glob": "^3.3.3", "mime": "^2", "picomatch": "^4.0.5", "yazl": "^3.3.1" } }, "sha512-YsLg5aDuZlvvb8t4snz2DzFZwcz5siuyPUTf3BFImoF2YsigvYc07sqsn7zYkdK7tRPB9DtB0ouLlDaaWIRxnA=="], "@aws-cdk/cli-plugin-contract": ["@aws-cdk/cli-plugin-contract@2.182.2", "", {}, "sha512-M1G52uA2JZPErOIKAkr0cjty7Y+/g99KHH7p9ZQv5jpZG7PNhtIraufbgBAo3iCEAH38SDtiPJL3+Ac8EttrIQ=="], "@aws-cdk/cloud-assembly-api": ["@aws-cdk/cloud-assembly-api@2.3.0", "", { "dependencies": { "json-source-map": "^0.6.1", "jsonschema": "^1.5.0", "semver": "^7.8.5" }, "peerDependencies": { "@aws-cdk/cloud-assembly-schema": ">=54.12.0" } }, "sha512-Z+TYWH9YJGDQSwjORTWvfRFHGK5Gzdp73ElVO8mmZrgndlZgNV2LJOcX1HYRlU/OuR7I+Qo5GLpfc/PM1s7JlQ=="], - "@aws-cdk/cloud-assembly-schema": ["@aws-cdk/cloud-assembly-schema@54.20.0", "", { "dependencies": { "jsonschema": "^1.5.0", "semver": "^7.8.5" } }, "sha512-ts2dVBi0VUXOLKcqyLYsHUWYpwGPFd+xm4q+6Y4jjP8orwodjRi2mJrmpy9b0gHnbv8e/hFnmEbqrD0bBJOnPA=="], + "@aws-cdk/cloud-assembly-schema": ["@aws-cdk/cloud-assembly-schema@54.21.0", "", { "dependencies": { "jsonschema": "^1.5.0", "semver": "^7.8.5" } }, "sha512-URh+k3/xG+e48lF41qBn/J8TzxwBaHt7Wsg5ZF+W4l76yiPhFmW7atV0BMVrWy7jh4SGO+yCuuqJe+CNjSk31w=="], - "@aws-cdk/cloudformation-diff": ["@aws-cdk/cloudformation-diff@2.187.3", "", { "dependencies": { "@aws-cdk/aws-service-spec": "^0.1.203", "@aws-cdk/service-spec-types": "^0.0.269", "chalk": "^4", "diff": "^9.0.0", "fast-deep-equal": "^3.1.3", "string-width": "^4", "table": "^6" }, "peerDependencies": { "@aws-sdk/client-cloudformation": "^3" } }, "sha512-ueJyvpzntiybo9MtXGStdY3KIMgtQvvbEuti/uMcjRCL/yOjihleYSyZxU61b2Zg1G9ipvPdY0lSHcgKTVPa/w=="], + "@aws-cdk/cloudformation-diff": ["@aws-cdk/cloudformation-diff@2.187.4", "", { "dependencies": { "@aws-cdk/aws-service-spec": "^0.1.204", "@aws-cdk/service-spec-types": "^0.0.270", "chalk": "^4", "diff": "^9.0.0", "fast-deep-equal": "^3.1.3", "string-width": "^4", "table": "^6" }, "peerDependencies": { "@aws-sdk/client-cloudformation": "^3" } }, "sha512-z3lVdow/u3OVQLq2/4j/2c/7AWDnWhlaoiDhOgjfNC5+JEWPn+6ow4dd0uFCqEccVBVv7guLeMjXQFMVBW7ibw=="], - "@aws-cdk/cx-api": ["@aws-cdk/cx-api@2.266.0", "", { "dependencies": { "@aws-cdk/cloud-assembly-api": "^2.2.6", "semver": "^7.8.5" }, "peerDependencies": { "@aws-cdk/cloud-assembly-schema": ">=53.25.0" } }, "sha512-a4G4TbwetC07QQoOI5PW0TeySMhfaLC8Quwgyq4zrTyzYVxPIA9J54yCwT7UIh3vSVd08EX1e+0YyivzPVZweg=="], + "@aws-cdk/cx-api": ["@aws-cdk/cx-api@2.267.0", "", { "dependencies": { "@aws-cdk/cloud-assembly-api": "^2.2.6", "semver": "^7.8.5" }, "peerDependencies": { "@aws-cdk/cloud-assembly-schema": ">=53.25.0" } }, "sha512-5+19IRJOp6k6KHh8oQdRJB4MMU/GkxthfZDrmjUgyepfLA1Frcn/Wf0Cz0UnRy0icU5eeZEmtd9gfTJjuchT1g=="], - "@aws-cdk/service-spec-types": ["@aws-cdk/service-spec-types@0.0.269", "", { "dependencies": { "@cdklabs/tskb": "^0.0.4" } }, "sha512-jlzH07p08SQvGQVMSTIfzl5a7buF8Lyfml2yU+3tSvipr1vcP96+6/eXPNWD9q31bSbOXZQX0PfTdnxTIYqoHw=="], + "@aws-cdk/service-spec-types": ["@aws-cdk/service-spec-types@0.0.270", "", { "dependencies": { "@cdklabs/tskb": "^0.0.4" } }, "sha512-v1VNwun5i2SsNkSK4R+tMw7RJzTQma9raEVQQfFiqwI0c+7PYBenDZxieX6MOTz2/VocQiEQqgtAn7oIk/8PXg=="], "@aws-cdk/toolkit-lib": ["@aws-cdk/toolkit-lib@1.38.2", "", { "dependencies": { "@aws-cdk/cdk-assets-lib": "^1", "@aws-cdk/cloud-assembly-api": "2.3.0", "@aws-cdk/cloud-assembly-schema": ">=54.18.0", "@aws-cdk/cloudformation-diff": "^2", "@aws-cdk/cx-api": "^2", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-bedrock-agentcore-control": "^3", "@aws-sdk/client-cloudcontrol": "^3", "@aws-sdk/client-cloudformation": "^3", "@aws-sdk/client-cloudtrail": "^3", "@aws-sdk/client-cloudwatch-logs": "^3", "@aws-sdk/client-codebuild": "^3", "@aws-sdk/client-ec2": "^3", "@aws-sdk/client-ecr": "^3", "@aws-sdk/client-ecs": "^3", "@aws-sdk/client-elastic-load-balancing-v2": "^3", "@aws-sdk/client-iam": "^3", "@aws-sdk/client-kms": "^3", "@aws-sdk/client-lambda": "^3", "@aws-sdk/client-route-53": "^3", "@aws-sdk/client-s3": "^3", "@aws-sdk/client-secrets-manager": "^3", "@aws-sdk/client-sfn": "^3", "@aws-sdk/client-ssm": "^3", "@aws-sdk/client-sts": "^3", "@aws-sdk/credential-providers": "^3", "@aws-sdk/ec2-metadata-service": "^3", "@aws-sdk/lib-storage": "^3", "@smithy/middleware-endpoint": "^4", "@smithy/property-provider": "^4", "@smithy/shared-ini-file-loader": "^4", "@smithy/util-retry": "^4", "@smithy/util-waiter": "^4", "cdk-from-cfn": "^0.321.0", "chalk": "^4", "chokidar": "^4", "fast-deep-equal": "^3.1.3", "fast-glob": "^3.3.3", "fs-extra": "^11", "p-limit": "^3", "picomatch": "^4", "semver": "^7.8.5", "split2": "^4.2.0", "wrap-ansi": "^7", "yaml": "^1", "yazl": "^3.3.1" }, "peerDependencies": { "@aws-cdk/cli-plugin-contract": "^2" } }, "sha512-WsyPjZnLr4zk16sp1tuPGXUKZ9elHJLqcpogxfQ+92cKolw9srbRJe57Z5whMZ0HNXarOYf0pOm5hWKqr8UAEw=="], "@aws-sdk/checksums": ["@aws-sdk/checksums@3.1000.29", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A=="], - "@aws-sdk/client-appsync": ["@aws-sdk/client-appsync@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-h+Hj0VeVKc8tcRz8rNQCPJx42xuaHXOtvHfXQZq25LkahMEBpC3RQtoM8CmBi/F7ENvwpOY6jXqrJJrEJeNIYg=="], + "@aws-sdk/client-appsync": ["@aws-sdk/client-appsync@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-plJfd6SEdhUgKLQrwPwZ6547DY+jyN6wUCaIh2/XX4nzSwCqfrNjP4uOUL+h79ELX3oHY1D8GuGMgVx3J9C+hw=="], + + "@aws-sdk/client-bedrock-agent": ["@aws-sdk/client-bedrock-agent@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-RAjn1g6X+u4WwnuwxYU8sKna5q1waTBCM+kzXWAvxSaNbgRsXFL6ZylPjOl0qCDZ7x6aVTj7Jb7Q0z6DZifP3w=="], - "@aws-sdk/client-bedrock-agentcore": ["@aws-sdk/client-bedrock-agentcore@3.1094.0", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/credential-provider-node": "^3.972.71", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-K8+YaYFyaVJvidwVSnmXlHjpOel/qz4ii2TZY2shXbkMoQxftjHuiXlplKeb5pWhzdS092q6YqPR5CZrMH90Ug=="], + "@aws-sdk/client-bedrock-agentcore": ["@aws-sdk/client-bedrock-agentcore@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-VxagAO73X0efH+5oQfOqWc2/q1wTkiZ1IkBg7L0GnPBo4NFYw37H8klNbbrTqoluMAzRiBwsHoGX2jfnjZlDOQ=="], - "@aws-sdk/client-bedrock-agentcore-control": ["@aws-sdk/client-bedrock-agentcore-control@3.1102.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/credential-provider-node": "^3.972.77", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6HjZkg14iiJr8VinzAKy5XRJe5JLrDg9USBia2UZZAg8leGff2/+cyH0y4ZYQqRe1f1TaMZfecNSQLCy3XInjQ=="], + "@aws-sdk/client-bedrock-agentcore-control": ["@aws-sdk/client-bedrock-agentcore-control@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-zta1MHik3+WbRUVwRFyFWSGTEfhaMPDDG+qdzmLAAyK3m7gqYHJr6+p+6dvBeiO6U15FQ1PWSjj2C3O5ikiyAw=="], - "@aws-sdk/client-cloudcontrol": ["@aws-sdk/client-cloudcontrol@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-rfSjvv+uWUMbvGtd3Y1LDGVnwCPqgcHpIefhRafqSC2VfUKXOuP6saxV3qvYBOdIgip4VqdVO3Y3HYah3FMK0Q=="], + "@aws-sdk/client-cloudcontrol": ["@aws-sdk/client-cloudcontrol@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-kpR+c528XFWFT04APYPKrhSCyzdEJPgxryR0wWs2dfIj7Ovqwak+1zLwFOS8zpNqDPEx8xn7j3dqA/HTCo+qLw=="], - "@aws-sdk/client-cloudformation": ["@aws-sdk/client-cloudformation@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-BruB9cm9LKik8IimZ1R+caunLzF+XR9Jkfg6t0TjfPwCSHZPiToaW6q90oEw3/Ye8gbqpbfWGsskNV+zV1C3VQ=="], + "@aws-sdk/client-cloudformation": ["@aws-sdk/client-cloudformation@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Bgq3DGWqogGuz30RYf1x4Ibty5SbPI2d+E7D6A+hJtlQ+x/H98tP8/RWuV7AWfo49vSDJh20cVoABPsIxPqJoQ=="], - "@aws-sdk/client-cloudtrail": ["@aws-sdk/client-cloudtrail@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-thYz+wcIlATdMV2WOk7bWUs2JT4tgh/tm4gggzbDtDX9gee33bUgdrZidjctfXkkCRIVG21ZykxPE21qqp3evw=="], + "@aws-sdk/client-cloudtrail": ["@aws-sdk/client-cloudtrail@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-yY3tLS6cYHpwADftHIzywo3a5Q295LCSfr6eSvxmOjXsGSPLTH+h0nQd19/FpaaC4QOA+kRjmYKMyX4jqVw5kA=="], - "@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.1104.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-node": "^3.972.78", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-cetLaPXxPgAVjVyC+dvLFaoasgt1/SX2RhjhEBTepTX8tX+WMD1pWoyRn9Uk2XYzz3RODdavekmue66r52S7nw=="], + "@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2xaDI2U9RjeFZIP6uPelWDN5HPJMtPx3jMTjvFb0HTL27KjuZcaAsiFtOBy9CuVz6vM++UbN6NGB/AoepYtMWQ=="], - "@aws-sdk/client-codebuild": ["@aws-sdk/client-codebuild@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-3ugGR2uTOqHRm5+ywGD7YhlT4aZ1X7pnyVm7UszOEYolqj9vK0MlBuNGvAKI6pETDmv1xxi44ozuOZKPp6tsHw=="], + "@aws-sdk/client-codebuild": ["@aws-sdk/client-codebuild@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Tt8+JtoZHefmOpxwHeK3/egLzKIpCDU+P9urtDdy/B2jQfy65rX6lFehDMN+LtmHruz/qjl/pCK7etcU3b70IA=="], - "@aws-sdk/client-ec2": ["@aws-sdk/client-ec2@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/middleware-sdk-ec2": "^3.972.58", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-zuMpXyo4Y9F7hZvdzpyGvArRtM648Qsfu0ziqm97KD6xjVHvFVoQbEIeSdKu8BDG1Q0Tbs88LE2aDhT/cYtVXQ=="], + "@aws-sdk/client-ec2": ["@aws-sdk/client-ec2@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/middleware-sdk-ec2": "^3.972.58", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-PsQU07vGnc8OQFJHFSpjc3PnXxinlYPGAkt0bxuPIDpfCrHdd8dpVO+JKQD90cRb/SdLiGw7ctkV3Sd9SN5LQw=="], - "@aws-sdk/client-ecr": ["@aws-sdk/client-ecr@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-pM/taVXOa13YMfhSNuEU6wTyWhXkoleAlRCgXKC2KnZ1D8Gne8P/JoVQ3+H5YjajTt9WPiViy0s83HEXUtcLsw=="], + "@aws-sdk/client-ecr": ["@aws-sdk/client-ecr@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-XuncSMLHCqE0NUgXcLJroY1zjrwc1bbwl2Q9iaFAEGd3GMYQg2WRX0s9Jna55D1sf0dCTKslfeIksxQo9P55Mw=="], - "@aws-sdk/client-ecs": ["@aws-sdk/client-ecs@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wAetkox/xSXlEPbVstdPgIHJoOp8sdBP6GZljxPJ+1t+salRIVana6XVv2QDDWESXooQevVvH+pYI682N7QkBA=="], + "@aws-sdk/client-ecs": ["@aws-sdk/client-ecs@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-E96+8fYusZUrj/2lLvvZ1Gv3f1wjTLLHR3V6RNG4I2m3MbWYLM2M9Rsr6QSUZn3nlTeDWJCnuJ5SGGOWkjCaMg=="], - "@aws-sdk/client-elastic-load-balancing-v2": ["@aws-sdk/client-elastic-load-balancing-v2@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-BxrwXIIDBOg8HBw1LoFFUxJCfGsn1YMuzjx/danjfVLOWs6Q5IgFbBTPDFNbXae7YzlY2YZHgA6rgntFhhOcvg=="], + "@aws-sdk/client-elastic-load-balancing-v2": ["@aws-sdk/client-elastic-load-balancing-v2@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-HBIjw1SfEwEAplkJRPFthxtj2g8dD0fJGFdqTjoLgUckfcNRK75ZNVnpsjGOFeiH+BicXnQqonNhuzBz5tWncQ=="], - "@aws-sdk/client-iam": ["@aws-sdk/client-iam@3.1087.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/credential-provider-node": "^3.972.68", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-S2UBBQnPMTREF52WPz7yabvags3rPYLOPwq8LOPNJIekyAIqUwtGN46z2tqGNr+NyGz9cpEI+3brJgkWKQGNIQ=="], + "@aws-sdk/client-iam": ["@aws-sdk/client-iam@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-FeIx8+zXeGMlrm6TcAoc/GMf7KwAAo2ntdyiOUgR3Xmc4JndKBDpWLJ5jdtpcxLC5chD7xMExtyoJSj1RfUSqg=="], - "@aws-sdk/client-kms": ["@aws-sdk/client-kms@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-rd44h2hIESETtyw2huXsTi1NDKFTM5pFB0ySr8sgvUBoImGyLMr+dIUSyNxvdgsnl2awB0ZS/xoA+ka896kgoA=="], + "@aws-sdk/client-kms": ["@aws-sdk/client-kms@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-zE7Z+jeKxCULk+m7XCZguZtBeWbreEZYtJcDrSkPJoAW9esux3Fx66+wp5WQVY8T4X5dYdsYymGvPIhEfTQpew=="], - "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-YX3Zbz6dm77Ugf5z40aS5fipyXxNAyejA60QELbrL/EinWZTuJLxgkwO6PUGW13IV1/YCJ9pr+CMzorJVqp16w=="], + "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-usBd5zr+q3UuLEY+eHi4pCZbuhgb0xsSvRF9sFW9e5l9cFi6UIk9+cYVrJgM2EMRHqe/tbdC/QXBQ/nQDYHmnQ=="], - "@aws-sdk/client-route-53": ["@aws-sdk/client-route-53@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/middleware-sdk-route53": "^3.972.26", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-xlmhJkcOXEvV+Ygox08FzcPpxfpamKPNeNXC/UjG8B+M8RQ2loCB2droxUeZbdPyG8WI6TJTtYsxLPsO2wDPwQ=="], + "@aws-sdk/client-route-53": ["@aws-sdk/client-route-53@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/middleware-sdk-route53": "^3.972.26", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-W8bDyghNaIOuvfUPwUuTg3dUCzxD5fqtxCf7G2yEOxXICIbM0e7jal2pULHdE8hsj5YnrZxdeuFArlJdHzGksw=="], - "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1116.0", "", { "dependencies": { "@aws-sdk/checksums": "^3.1000.29", "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/middleware-sdk-s3": "^3.972.75", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-UKRl9qSVW0rZpvSOauQNpYAy8+ONBAVYnpfKVtCyOF+FZVT1tl6MunYuHvuarCrroD2/YJs+tHTALYNgAlec3Q=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1121.0", "", { "dependencies": { "@aws-sdk/checksums": "^3.1000.29", "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/middleware-sdk-s3": "^3.972.75", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-hBnoqaVBeWdkgXcJElMXA2yUZWkBCBntu2qmN+tfqmzC+j4LzJC3ox8qIgS2WdMS1cb8UwyBogUVrkRXybNm0A=="], - "@aws-sdk/client-secrets-manager": ["@aws-sdk/client-secrets-manager@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-M2rtmVrfe0TuxCBXtGkFGgZ4WgGZGxYyVj/kAJBzMobDyvWho6SIH6KbTTOLswIzTl+2BVrGdwxxYcEFvVNTAQ=="], + "@aws-sdk/client-secrets-manager": ["@aws-sdk/client-secrets-manager@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-kx66Imv5DomVdd5uwai10Dhf0zZg9Z3EODxJCYRGjmaD0GR08GkvuIPPIqE+ZmpHANCWbur9UKpgyXGJzk60pg=="], - "@aws-sdk/client-sfn": ["@aws-sdk/client-sfn@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-iVcZGWS/5wcgCn2MmPCFiY/lMUSQE3WLNce8W/bZWSJa5DGA+nX6wHPxYK/eRVybZSxUyWh7KB1voVjMEXs8Rg=="], + "@aws-sdk/client-sfn": ["@aws-sdk/client-sfn@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-BdTkxkQgB3vCwHdWci+Bfk7Juvb50GCOVzrhn8uDXQS6XXoncSI+5Arf7naI808xPHqgnxmGv7UNbdtjSfoMQg=="], - "@aws-sdk/client-ssm": ["@aws-sdk/client-ssm@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-AWYIQBmwNL+2rZW/eIirBSvoo9kKMv3pgWhcGiySHnD4xu/fbNM/ArbhkcHWwDLk1Emk5/sMqU4SEDsBDXmgFA=="], + "@aws-sdk/client-ssm": ["@aws-sdk/client-ssm@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-NZejFZ6NIdHqz1v27W9WLUY1vSgJq7u3FfJeEfK0Us1J2QHQIBn79jenEUGu1RcITKs8RH716vEhoi3IGTaT2Q=="], - "@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-xZmNx/gTjMutVcU2sJjfuYj+n+/gGKtdDdQ0PE+RY39pf2tz1Amzcpn02owyGxNb7t8wN7x9f6eR1hlKOw3uiA=="], + "@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wpsgstPgWUgCKk4WFP5qjfg/zWgpmbKEjRBPz5Ydw5jawdd7pDBtH7VrhiJyMSS48PN2c7n8KfkwTRx/20Cs7Q=="], - "@aws-sdk/core": ["@aws-sdk/core@3.976.0", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.36", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.4", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-0cjRaEdlVoOrsNb9pP5q1Syyc8pXw5xSj2Np2ryReRTr9FppIIRVSdZK4lbnfmc2Hvgux/xBOUU6baB7z8//uA=="], + "@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.69", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-vpsh9VWmQVC/nsdzf72F2yUMBOFT1aq+hoLseYV03zjVXVHkjqSNJskFRKPFxc3MRFrHNbSSmOwsbYE15u9ecw=="], @@ -135,7 +135,7 @@ "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.77", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.71", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.60", "@aws-sdk/credential-provider-http": "^3.972.62", "@aws-sdk/credential-provider-ini": "^3.973.5", "@aws-sdk/credential-provider-process": "^3.972.60", "@aws-sdk/credential-provider-sso": "^3.973.4", "@aws-sdk/credential-provider-web-identity": "^3.972.66", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/credential-provider-imds": "^4.4.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-HIg7Q2osBzajQwL+1Vkyh2E7Gim3eTNb9RHIsOxDGjW0eZg4oEKtRs5sioCnc73ilhaOm4gX2lHVF8J7+nt2rg=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q=="], @@ -143,11 +143,11 @@ "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA=="], - "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-cognito-identity": "^3.972.69", "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-login": "^3.972.77", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-y41rRJ1AWtcJka2YdFQ1BfTf0CZDXizh0VOZLwfUzxI2xhG7n88XXn0N0yvLwI73/tjtXalVy94/N5QCO1lgbg=="], + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-cognito-identity": "^3.972.69", "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-login": "^3.972.77", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-lWdkLauoXNDH/lwYWFpYnFhtYqt0lLrQAyuyxak31PiKD5UQ4YT2z3rQckzEfIGyP1VBGvC+n6Ziqm3y3HKmmQ=="], - "@aws-sdk/ec2-metadata-service": ["@aws-sdk/ec2-metadata-service@3.1116.0", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-oGRBmlQ5+Zvkxy1+Xsc0JVKy9pbWk1xyCYPsRJZapesIKFxHAZMVBgMy9cRSFWiAtTPZs28+B+1OfFJPgz8B5w=="], + "@aws-sdk/ec2-metadata-service": ["@aws-sdk/ec2-metadata-service@3.1121.0", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-WQxHpsu2qn3C+RVlZYcCWe8oR3e75wfCn2BP5zNbLabnNfYI2lSwFECwSQCw0ghJx9BIycQzPfUJxm4Q89YU/g=="], - "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.1116.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.1116.0" } }, "sha512-40z1NyzCbRX9bD6g3dJ6E4WDbYPDod6XHTIbNZPnGDYMYH2TPIj6LyRZ94fWPKGYnJpO0Q0Uhi2lSnn2E7PIdg=="], + "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.1121.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.1121.0" } }, "sha512-sx86QqM0/HjKNCS5eQGcaP9Q8GKg3FtCExO/6FfGyw00JNUsFJPmEKNCJIkWY7OmKS9jIOuJe/DtAgfr/+G/bQ=="], "@aws-sdk/middleware-sdk-ec2": ["@aws-sdk/middleware-sdk-ec2@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-k1yW3bEUcy/e9oxIH0zT81sbPCqgERdNf0bOsBZg/tXP75tyEKp8kp8BG9xag64+u9fF8064fLDUdIItWgQxWw=="], @@ -161,9 +161,9 @@ "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q=="], - "@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + "@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], "@aws/agent-inspector": ["@aws/agent-inspector@0.6.1", "", { "dependencies": { "@ag-ui/core": "^0.0.52", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-testing-library": "^7.16.0", "lucide-react": "^0.575.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1" } }, "sha512-+0KJRZe/mK1qenDOGdnCYZdSAt8jZK4tLb8Bc6vbpQqKc3MsRP2ICh0X9mtSAA6iT2KyG7crprsSXZJ3laEijQ=="], @@ -245,7 +245,7 @@ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -279,43 +279,43 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.74.0", "", { "os": "android", "cpu": "arm" }, "sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.80.0", "", { "os": "android", "cpu": "arm" }, "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.74.0", "", { "os": "android", "cpu": "arm64" }, "sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.80.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.74.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.80.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.74.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q=="], + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.80.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.74.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g=="], + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.80.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.74.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g=="], + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.74.0", "", { "os": "linux", "cpu": "arm" }, "sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ=="], + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.74.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w=="], + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.74.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.74.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.80.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.74.0", "", { "os": "linux", "cpu": "none" }, "sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg=="], + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.80.0", "", { "os": "linux", "cpu": "none" }, "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.74.0", "", { "os": "linux", "cpu": "none" }, "sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw=="], + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.80.0", "", { "os": "linux", "cpu": "none" }, "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.74.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.80.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.74.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw=="], + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.74.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg=="], + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.74.0", "", { "os": "none", "cpu": "arm64" }, "sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg=="], + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.80.0", "", { "os": "none", "cpu": "arm64" }, "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.74.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA=="], + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.80.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.74.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.80.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.74.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA=="], + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.80.0", "", { "os": "win32", "cpu": "x64" }, "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q=="], "@pkgr/core": ["@pkgr/core@0.3.6", "", {}, "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA=="], @@ -363,25 +363,25 @@ "@smithy/config-resolver": ["@smithy/config-resolver@4.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-Y1XfSefHIOub9762qm3ShafdlEE/Va8h3kLUeMq765fNeWeNLcOP2YUPr86H1SlyGwZTOqQ67RlBZPZ3k9Djgg=="], - "@smithy/core": ["@smithy/core@3.29.8", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-rpCbCV+TimOBi3VLNBMmtTvgfOWcFIEAru3+TFlG87SL2F+te4jOnnNR+cf3uR4eJ5Qf4LnT80fqnBKgPRS6zA=="], + "@smithy/core": ["@smithy/core@3.29.3", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.10", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-5/Yj9mS2JjTsB3B8ZX7euh77mrY9aXW23ag1yAmFykSRmA6vldqBrgqmSeQ50EjY+5SB8+aE4w14B6LKbBVEhQ=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-7HgK3/pQQHcD5w9lOPtK53/eDMKDp324Nd4KZ8XvxlcKHiWytuM9VwVOB2iy3rutsz/N1WNEWBkaRBayrGnuog=="], "@smithy/node-config-provider": ["@smithy/node-config-provider@4.6.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-zMrXu/O5tPa7GLtra8L4wFG6DACcXT9QV4Ay+WEAjUhXm1dVq7c/q9Qv9gkJZNLY8hmQKg08778kDcxpKNMqOA=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.10", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-ETQz9v/Z+nTQc6fRWTXxUpxJqwpmzB3Tn3WKAdHwWkeT+m+HE5czs6GNG8vW+4vyxXSls65RVcvOZwk7Q/PS/Q=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], "@smithy/property-provider": ["@smithy/property-provider@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-SPJCSCCGpHf5g5b8244ig3WVKIdv2DS+X6cfmy4bpKEYo4VlLYM+bGHpHbfbmp76vegMKQBvs7DEpwQ5YuhKLA=="], "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-XsIDj5gVG4YRxGS4n4TiBDAogPWRHXKZyor6JW/sEuaa/7BvKADe9j45jI2dPYCnYbKkLBmbZ4qN9ns0sH+kMQ=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.6.9", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g5rnEii/mkT0mjVJmlsaOfyNBtHNTecD9Lo4NP8D5HzMUEnZNpz7/FbvBCjNcV4vteHFAxOGiLUYNxPkDZZAPw=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="], - "@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + "@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], "@smithy/util-retry": ["@smithy/util-retry@4.6.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-q6MXFNu+W4ZCNdNKutzDLP/Hzumd1FU9CQX++P/7ylanYIYiGhgZwSzwZWAw+G1RNcfY+RBYv61XcGlSYhj+BA=="], @@ -389,9 +389,9 @@ "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], - "@tanstack/query-core": ["@tanstack/query-core@5.101.2", "", {}, "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.102.8", "", {}, "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg=="], - "@tanstack/react-query": ["@tanstack/react-query@5.101.2", "", { "dependencies": { "@tanstack/query-core": "5.101.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg=="], + "@tanstack/react-query": ["@tanstack/react-query@5.102.8", "", { "dependencies": { "@tanstack/query-core": "5.102.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A=="], "@textlint/ast-node-types": ["@textlint/ast-node-types@15.8.0", "", {}, "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw=="], @@ -403,7 +403,7 @@ "@textlint/types": ["@textlint/types@15.8.0", "", { "dependencies": { "@textlint/ast-node-types": "15.8.0" } }, "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], @@ -421,11 +421,11 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], @@ -445,7 +445,7 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.68.0", "", { "dependencies": { "@typescript-eslint/types": "8.68.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="], "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], @@ -455,7 +455,7 @@ "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -493,7 +493,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], @@ -511,7 +511,7 @@ "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], @@ -599,7 +599,7 @@ "editions": ["editions@6.22.0", "", { "dependencies": { "version-range": "^4.15.0" } }, "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.414", "", {}, "sha512-aYlviXiaXBbzvKgyALpcMmqa3Np3sDr0XnZbEG62n2UpZFbEcjQ4EEMOLGzVPhwVnwTz0lvKY+GcARbunuHekw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.416", "", {}, "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -625,7 +625,7 @@ "es-to-primitive": ["es-to-primitive@1.3.4", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw=="], - "es-toolkit": ["es-toolkit@1.49.0", "", {}, "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g=="], + "es-toolkit": ["es-toolkit@1.52.0", "", {}, "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -663,8 +663,6 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -681,7 +679,7 @@ "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -767,7 +765,7 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], + "ignore": ["ignore@7.0.7", "", {}, "sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -779,7 +777,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ink": ["ink@7.1.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.3", "auto-bind": "^5.0.1", "chalk": "^5.6.2", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.45.1", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", "string-width": "^8.2.0", "terminal-size": "^4.0.1", "type-fest": "^5.5.0", "widest-line": "^6.0.0", "wrap-ansi": "^10.0.0", "ws": "^8.20.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.2.0", "react": ">=19.2.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-VWE6/yeLtFCJBNLflyI2OSylyXK1Rc24LuXup8Qt+icwkmmycFNdbn8IkSp6Frc0h1iA0NOvvi1ajW44U/w3Qg=="], + "ink": ["ink@7.1.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.3", "auto-bind": "^5.0.1", "chalk": "^5.6.2", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.45.1", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", "string-width": "^8.2.0", "terminal-size": "^4.0.1", "type-fest": "^5.5.0", "widest-line": "^6.0.0", "wrap-ansi": "^10.0.0", "ws": "^8.20.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.2.0", "react": ">=19.2.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-Y43xxa1ZSPvpmfLHcN5o+OdP8Rf8ykkNJEuKYOUNZKT8wXVNLFTtEm1nSDMQkfBH+YANF4Xuu0hhZ4ejqAtN2w=="], "ink-scroll-view": ["ink-scroll-view@0.3.7", "", { "peerDependencies": { "ink": "^5 || ^6 || ^7", "react": "^18 || ^19" } }, "sha512-lUBLxSbVry/+UtJhuvRu6wumP43+ScVp69J7e+hmYwz1kTkahWfkVwWOu7Mn1DMPb8AU8bnsmEsr6kvSJvnaRw=="], @@ -869,7 +867,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -895,9 +893,7 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "lint-staged": ["lint-staged@17.0.8", "", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA=="], - - "listr2": ["listr2@10.2.2", "", { "dependencies": { "cli-truncate": "^5.2.0", "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^10.0.0" } }, "sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw=="], + "lint-staged": ["lint-staged@17.4.1", "", { "dependencies": { "picomatch": "^4.0.7", "string-argv": "^0.3.2", "tinyexec": "^1.3.0" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-FmJeudcalbSfg1du+JCfvi5vS6Qt08KgbfLWiHinbef+2JJwUZwAWVoaO1AcJVUTWPfk0t30PMQNwPAeCzYQ+Q=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -907,8 +903,6 @@ "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], - "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -1019,8 +1013,6 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1035,7 +1027,7 @@ "node-exports-info": ["node-exports-info@1.6.2", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag=="], - "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + "node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="], "normalize-package-data": ["normalize-package-data@8.0.0", "", { "dependencies": { "hosted-git-info": "^9.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ=="], @@ -1065,13 +1057,13 @@ "own-keys": ["own-keys@1.0.2", "", { "dependencies": { "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg=="], - "oxlint": ["oxlint@1.74.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.74.0", "@oxlint/binding-android-arm64": "1.74.0", "@oxlint/binding-darwin-arm64": "1.74.0", "@oxlint/binding-darwin-x64": "1.74.0", "@oxlint/binding-freebsd-x64": "1.74.0", "@oxlint/binding-linux-arm-gnueabihf": "1.74.0", "@oxlint/binding-linux-arm-musleabihf": "1.74.0", "@oxlint/binding-linux-arm64-gnu": "1.74.0", "@oxlint/binding-linux-arm64-musl": "1.74.0", "@oxlint/binding-linux-ppc64-gnu": "1.74.0", "@oxlint/binding-linux-riscv64-gnu": "1.74.0", "@oxlint/binding-linux-riscv64-musl": "1.74.0", "@oxlint/binding-linux-s390x-gnu": "1.74.0", "@oxlint/binding-linux-x64-gnu": "1.74.0", "@oxlint/binding-linux-x64-musl": "1.74.0", "@oxlint/binding-openharmony-arm64": "1.74.0", "@oxlint/binding-win32-arm64-msvc": "1.74.0", "@oxlint/binding-win32-ia32-msvc": "1.74.0", "@oxlint/binding-win32-x64-msvc": "1.74.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.24.0", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA=="], + "oxlint": ["oxlint@1.80.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.80.0", "@oxlint/binding-android-arm64": "1.80.0", "@oxlint/binding-darwin-arm64": "1.80.0", "@oxlint/binding-darwin-x64": "1.80.0", "@oxlint/binding-freebsd-x64": "1.80.0", "@oxlint/binding-linux-arm-gnueabihf": "1.80.0", "@oxlint/binding-linux-arm-musleabihf": "1.80.0", "@oxlint/binding-linux-arm64-gnu": "1.80.0", "@oxlint/binding-linux-arm64-musl": "1.80.0", "@oxlint/binding-linux-ppc64-gnu": "1.80.0", "@oxlint/binding-linux-riscv64-gnu": "1.80.0", "@oxlint/binding-linux-riscv64-musl": "1.80.0", "@oxlint/binding-linux-s390x-gnu": "1.80.0", "@oxlint/binding-linux-x64-gnu": "1.80.0", "@oxlint/binding-linux-x64-musl": "1.80.0", "@oxlint/binding-openharmony-arm64": "1.80.0", "@oxlint/binding-win32-arm64-msvc": "1.80.0", "@oxlint/binding-win32-ia32-msvc": "1.80.0", "@oxlint/binding-win32-x64-msvc": "1.80.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-map": ["p-map@7.0.6", "", {}, "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg=="], + "p-map": ["p-map@7.0.7", "", {}, "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -1089,7 +1081,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], @@ -1097,7 +1089,7 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="], + "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], @@ -1105,7 +1097,7 @@ "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], - "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], + "protobufjs": ["protobufjs@7.6.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -1113,7 +1105,7 @@ "rc-config-loader": ["rc-config-loader@4.1.4", "", { "dependencies": { "debug": "^4.4.3", "js-yaml": "^4.1.1", "json5": "^2.2.3", "require-from-string": "^2.0.2" } }, "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ=="], - "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], @@ -1125,7 +1117,7 @@ "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], - "react-router": ["react-router@8.3.0", "", { "dependencies": { "cookie-es": "^3.1.1" }, "peerDependencies": { "react": ">=19.2.7", "react-dom": ">=19.2.7" }, "optionalPeers": ["react-dom"] }, "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ=="], + "react-router": ["react-router@8.3.1", "", { "dependencies": { "cookie-es": "^3.1.1" }, "peerDependencies": { "react": ">=19.2.7", "react-dom": ">=19.2.7" }, "optionalPeers": ["react-dom"] }, "sha512-TEOpiO2g0TJHEOJeRVv4amUFun9v1npCKszvcquNvzETUtJ8udV86ah5eFoHT7g26bsBvT6EiIhqulR8eDF++A=="], "read-pkg": ["read-pkg@10.1.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.4", "normalize-package-data": "^8.0.0", "parse-json": "^8.3.0", "type-fest": "^5.4.4", "unicorn-magic": "^0.4.0" } }, "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg=="], @@ -1155,8 +1147,6 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], @@ -1227,7 +1217,7 @@ "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], - "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + "string.prototype.matchall": ["string.prototype.matchall@4.1.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "get-intrinsic": "^1.3.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.4", "set-function-name": "^2.0.2", "side-channel": "^1.1.1" } }, "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ=="], "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], @@ -1275,7 +1265,7 @@ "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], - "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], @@ -1295,7 +1285,7 @@ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], + "type-fest": ["type-fest@5.9.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -1329,7 +1319,7 @@ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], + "update-browserslist-db": ["update-browserslist-db@1.3.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -1367,7 +1357,7 @@ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -1379,7 +1369,7 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], @@ -1387,7 +1377,9 @@ "@ag-ui/core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@aws-cdk/aws-service-spec/@aws-cdk/service-spec-types": ["@aws-cdk/service-spec-types@0.0.271", "", { "dependencies": { "@cdklabs/tskb": "^0.0.4" } }, "sha512-ool/vxTZyg5KEMRNTnLK4fvKo8UPhIrIOc5a0FE+xuX/RfobAldoyOcvhittacnrXuSo8FAzLI3n4qS/YP0nTQ=="], + "@aws-cdk/aws-service-spec/@aws-cdk/service-spec-types": ["@aws-cdk/service-spec-types@0.0.273", "", { "dependencies": { "@cdklabs/tskb": "^0.0.4" } }, "sha512-Pe1j265hAAl10aiUj+gk5pKbOf3A34uz2mSFgJDPpN6t3A/g6mBjMbrVwZKHIqGHvoksaJ5mo2kKczjDoKq7kw=="], + + "@aws-cdk/cdk-assets-lib/@aws-cdk/cloud-assembly-api": ["@aws-cdk/cloud-assembly-api@2.4.0", "", { "dependencies": { "json-source-map": "^0.6.1", "jsonschema": "^1.5.0", "semver": "^7.8.5" }, "peerDependencies": { "@aws-cdk/cloud-assembly-schema": ">=54.21.0" } }, "sha512-ooStESI1uXNcFAx1IgG6XcN7SsMuenoaxSb1FHqcLqwy7Lq5HUMWM52mtIzrS/uJmoFNvoxQ7fGbk8MfCGIjlw=="], "@aws-cdk/cloud-assembly-schema/jsonschema": ["jsonschema@1.5.0", "", { "bundled": true }, "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw=="], @@ -1395,436 +1387,98 @@ "@aws-cdk/cloudformation-diff/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@aws-cdk/toolkit-lib/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], - - "@aws-sdk/checksums/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], + "@aws-cdk/cx-api/@aws-cdk/cloud-assembly-api": ["@aws-cdk/cloud-assembly-api@2.4.0", "", { "dependencies": { "json-source-map": "^0.6.1", "jsonschema": "^1.5.0", "semver": "^7.8.5" }, "peerDependencies": { "@aws-cdk/cloud-assembly-schema": ">=54.21.0" }, "bundled": true }, "sha512-ooStESI1uXNcFAx1IgG6XcN7SsMuenoaxSb1FHqcLqwy7Lq5HUMWM52mtIzrS/uJmoFNvoxQ7fGbk8MfCGIjlw=="], - "@aws-sdk/checksums/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-cdk/cx-api/@aws-cdk/cloud-assembly-schema": ["@aws-cdk/cloud-assembly-schema@54.21.0", "", { "dependencies": { "jsonschema": "^1.5.0", "semver": "^7.8.5" } }, "sha512-URh+k3/xG+e48lF41qBn/J8TzxwBaHt7Wsg5ZF+W4l76yiPhFmW7atV0BMVrWy7jh4SGO+yCuuqJe+CNjSk31w=="], - "@aws-sdk/checksums/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + "@aws-cdk/cx-api/json-source-map": ["json-source-map@0.6.1", "", {}, "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg=="], - "@aws-sdk/checksums/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], + "@aws-cdk/cx-api/jsonschema": ["jsonschema@1.5.0", "", {}, "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw=="], - "@aws-sdk/client-appsync/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], + "@aws-cdk/cx-api/semver": ["semver@7.8.5", "", { "bundled": true, "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@aws-sdk/client-appsync/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], + "@aws-cdk/toolkit-lib/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], - "@aws-sdk/client-appsync/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-sdk/checksums/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@aws-sdk/client-appsync/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-appsync/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], + "@aws-sdk/client-bedrock-agent/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-appsync/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], + "@aws-sdk/client-bedrock-agentcore/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-appsync/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core": ["@aws-sdk/core@3.977.5", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-O5otOc1c6UZh5HsHAaPdYBcUUR9HL6mtnKqvc8nxN/CKDGUBUpsdh0q8K04Uz/dd1i0TaGyIQuQNqoO7+ad2TQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.77", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.66", "@aws-sdk/credential-provider-http": "^3.972.68", "@aws-sdk/credential-provider-ini": "^3.973.11", "@aws-sdk/credential-provider-process": "^3.972.66", "@aws-sdk/credential-provider-sso": "^3.973.10", "@aws-sdk/credential-provider-web-identity": "^3.972.72", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-l4nitYCN/Ls57vtUfdextCjTjW41JD7lQiAnuR0RTbdByFc/6OmEAzwGd+lrp6CUtiXGQL1FCaYiamfHASrwBw=="], - - "@aws-sdk/client-bedrock-agentcore-control/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-bedrock-agentcore-control/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - - "@aws-sdk/client-bedrock-agentcore-control/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - - "@aws-sdk/client-cloudcontrol/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-cloudcontrol/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-cloudcontrol/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@aws-sdk/client-cloudcontrol/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-cloudcontrol/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-cloudcontrol/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-cloudcontrol/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-cloudformation/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-cloudformation/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-cloudformation/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-cloudformation/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-cloudformation/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-cloudformation/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-cloudformation/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-cloudtrail/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-cloudtrail/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-cloudtrail/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-cloudtrail/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-cloudtrail/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-cloudtrail/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-cloudtrail/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.78", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-ini": "^3.973.12", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng=="], - - "@aws-sdk/client-cloudwatch-logs/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-cloudwatch-logs/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - - "@aws-sdk/client-cloudwatch-logs/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - - "@aws-sdk/client-codebuild/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-codebuild/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-codebuild/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-sdk/client-cloudwatch-logs/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@aws-sdk/client-codebuild/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-codebuild/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-codebuild/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-codebuild/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-ec2/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-ec2/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-ec2/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-ec2/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-ec2/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-ec2/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-ec2/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-ecr/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-ecr/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-ecr/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-ecr/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-ecr/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-ecr/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-ecr/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-ecs/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-ecs/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-ecs/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-ecs/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-ecs/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-ecs/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-ecs/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-elastic-load-balancing-v2/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-elastic-load-balancing-v2/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-iam/@aws-sdk/core": ["@aws-sdk/core@3.975.2", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@aws-sdk/xml-builder": "^3.972.35", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.3", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-iyeXwziyjJpixq5OmhsIyrSWx8vwcI7gDo4yRUC3EP7NQtOo9iAJiIEc3G+/HkhtNXqOhofiCK7Lc34Sq+fJWg=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.68", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.58", "@aws-sdk/credential-provider-http": "^3.972.60", "@aws-sdk/credential-provider-ini": "^3.973.2", "@aws-sdk/credential-provider-process": "^3.972.58", "@aws-sdk/credential-provider-sso": "^3.973.2", "@aws-sdk/credential-provider-web-identity": "^3.972.64", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4akjzW9CjorByYfqXBXmYUh/h7Io3U4DtVgGGh9TQraZ7ZlyJqNyHwDRGiUFnHD+BTOeTbCesCa4sJaK7BGZ7A=="], - - "@aws-sdk/client-iam/@aws-sdk/types": ["@aws-sdk/types@3.974.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-W0IQZR0eaBqlBFIIofMapaWkw1W0U+Xi4dvW+BqwmCEMd8Ng2U6IhkxuPSjMVnR8klLjfuS9PeZWUl1N6UaZdg=="], - - "@aws-sdk/client-iam/@smithy/core": ["@smithy/core@3.29.3", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A=="], - - "@aws-sdk/client-iam/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g=="], - - "@aws-sdk/client-iam/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw=="], - - "@aws-sdk/client-kms/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-kms/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-kms/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-sdk/client-iam/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@aws-sdk/client-kms/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-kms/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-kms/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-kms/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-lambda/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-lambda/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-lambda/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-lambda/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-route-53/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-route-53/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-route-53/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-route-53/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-route-53/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-route-53/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-route-53/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-s3/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-s3/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-s3/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-s3/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-secrets-manager/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-secrets-manager/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-secrets-manager/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-secrets-manager/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-sfn/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-sfn/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-sfn/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-sfn/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-sfn/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-sfn/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-sfn/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-ssm/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-ssm/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-ssm/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-ssm/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-ssm/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-ssm/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-ssm/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/client-sts/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/client-sts/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/client-sts/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/client-sts/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/client-sts/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-sdk/core/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@aws-sdk/credential-provider-cognito-identity/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-provider-cognito-identity/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/credential-provider-env/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-provider-env/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/credential-provider-http/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-provider-http/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/credential-provider-http/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/credential-provider-ini/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/credential-provider-login/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-provider-login/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-BAkxdoe7tpDDqCghGpuOeHQRbm/2znVvOQm0AvpQbA2tbfMN46doN4zx65fv85ImP3KADwc2zQPmbrlI9MPfMg=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.62", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g/0fGqKTb9xpKdd9AtpmV5Eo3DFKbnkpA2+w0peISSlu7NfAoWOuYBFxsu+yWBtxU89ka55ezoZBCbFaS8pjYQ=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.5", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/credential-provider-env": "^3.972.60", "@aws-sdk/credential-provider-http": "^3.972.62", "@aws-sdk/credential-provider-login": "^3.972.67", "@aws-sdk/credential-provider-process": "^3.972.60", "@aws-sdk/credential-provider-sso": "^3.973.4", "@aws-sdk/credential-provider-web-identity": "^3.972.66", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/credential-provider-imds": "^4.4.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-ylubazcRfq2TVus/qXucSXeC42Qdjp5HQxTu68K/BsdMiZlcSLD1zkpoCgApXZX1Y6YJhtGGs7ZHhO/GuIgBlw=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YIo3f99hM43QdYG8hDzwGemnR/pU95b0kramqSJUTleCqaB7+HwKf7YZFHqvOgTqZTPx/mRmNIqoDRr3U0Z3Tw=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.4", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/token-providers": "3.1092.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-BPdmL8sSBOCv4ngZ+3LHxyc3CNqDCEK37CHioCk7zGrTMY5sUtkH8q+o6qA80nn6w3/fyBPGNE7OIRlmoOxRQA=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-kSAziJboOmZmsR9/MTbiNjowl2BPes1bQuJpne4qAZ62ubi8fjfr/aupJSQje6udBoYxXTQbsL0e0kby2la3ng=="], - - "@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.13", "", { "dependencies": { "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-X+2HNZhWi5i3rJsCas0LPf6fTQUaKyJ40zd8aTO/bwpRfpU3biYaqLr7C1WMibL7PVKJalpi1PyybjGPNoHC8Q=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-sdk/credential-provider-node/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@aws-sdk/credential-provider-process/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-provider-process/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/credential-provider-sso/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-provider-sso/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/credential-provider-web-identity/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-provider-web-identity/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/credential-providers/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], - - "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/credential-providers/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/credential-providers/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/ec2-metadata-service/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/ec2-metadata-service/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/ec2-metadata-service/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/ec2-metadata-service/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - "@aws-sdk/lib-storage/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/lib-storage/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/middleware-sdk-ec2/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/middleware-sdk-ec2/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/middleware-sdk-ec2/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/middleware-sdk-ec2/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/middleware-sdk-ec2/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/middleware-sdk-route53/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - - "@aws-sdk/middleware-sdk-route53/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/middleware-sdk-s3/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/middleware-sdk-s3/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/middleware-sdk-s3/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/middleware-sdk-s3/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/nested-clients/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], - - "@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], - - "@aws-sdk/nested-clients/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/signature-v4-multi-region/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - - "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/signature-v4-multi-region/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], - - "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], - "@aws-sdk/token-providers/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@aws-sdk/token-providers/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -1855,16 +1509,22 @@ "@smithy/config-resolver/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], - "@smithy/credential-provider-imds/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], + "@smithy/credential-provider-imds/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@smithy/fetch-http-handler/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@smithy/middleware-endpoint/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@smithy/node-config-provider/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + "@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + "@smithy/property-provider/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@smithy/shared-ini-file-loader/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + "@smithy/signature-v4/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + "@smithy/util-retry/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], "@smithy/util-waiter/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], @@ -1905,17 +1565,7 @@ "ink/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "ink/wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], - - "listr2/cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], - - "listr2/wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], - - "log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - - "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "ink/wrap-ansi": ["wrap-ansi@10.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -1929,7 +1579,7 @@ "rc-config-loader/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "react-devtools-core/ws": ["ws@7.5.12", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg=="], + "react-devtools-core/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], "supports-hyperlinks/has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], @@ -1951,191 +1601,9 @@ "@aws-cdk/cloudformation-diff/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@aws-sdk/checksums/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/checksums/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-appsync/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-appsync/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + "@aws-cdk/cx-api/@aws-cdk/cloud-assembly-schema/jsonschema": ["jsonschema@1.5.0", "", { "bundled": true }, "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bOzP2+zdJ0XrghywB4FaJXtGZCx9yS0AGps+VJ5yEgg30wVyHNmVDBwVDXcRypzQY5iLGCS3NSn0nsuISqjFCQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.68", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lkunS8X+H6V76WE+t/uGQm/U8v0JXK5mLfNFTUAMlE1kqaCjwlmqKJrgCVtqjK/vqnlrSWsLK4Lr4NANBWlfTQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.11", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/credential-provider-env": "^3.972.66", "@aws-sdk/credential-provider-http": "^3.972.68", "@aws-sdk/credential-provider-login": "^3.972.73", "@aws-sdk/credential-provider-process": "^3.972.66", "@aws-sdk/credential-provider-sso": "^3.973.10", "@aws-sdk/credential-provider-web-identity": "^3.972.72", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-KoDEolYtLHG/8C+IiZpXbJWyBOMkrHV+j66Kb9PBXmLv5euGb7aELvuCmLenoGAV6gBW2wM7TsG/1e5iulH4kA=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YOnX6bIhdjx0QfaENu2PB0eFm5MEc9ft8XNGQ+NxMfeLSq9aE+XjWCwDupEnV4UWv5ZFpBLJbTREIx7KNOoqpQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.10", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/token-providers": "3.1102.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-IsXnQ35j5VE+3ZK6aIhT5ypB+Jim3zRwVz0nYuVwyBKZyu/SYx+O2/LQpng8c2EiuwyqceabsDlYrICHDlJPsA=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-nj9Zlsy7ya+fy+jhWTJwgfr7YdtDM4xHyZvgKuftuny0UgROVx9lxwvsWJSLvpKk4lig0m0tHng3k1fEnt0LeA=="], - - "@aws-sdk/client-cloudcontrol/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-cloudcontrol/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-cloudformation/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-cloudformation/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-cloudtrail/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-cloudtrail/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-rcIpk5kxUqDaaNa6Xk23pQ6ViY7jlqzmfFWCahQcBT97ddXaXYYwzCen9Tz1Jvo6aJft6wDl5bN44/Jw5B4oLA=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-nggwJtZ4eeNsUw5IeWBMXsi1ryct5idi0K+/SCRF3kybLubOMaNTb3XCihXpWMiVpyzyPeIrl0zTkzhBH9porA=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.12", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-login": "^3.972.74", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-IlUEejorGTWKb4/Dm7K5Yw4QxUmXLThLhrvBmzVBqZFTbW72cv9LTcITmo1dsnYriALE4h68mOq4LB99x6sQ7Q=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.11", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/token-providers": "3.1103.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-gAQBkBZxUB84d71+pPcI9L+jh2ujhuAVxc/4FgGiWFDjkPBlMKxzd5XDtkSXTFX8Ro7ansnT88+XadasxMeCRw=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SnlEmQa6SjOgs6iOPLUQl1Eyq4AKiAdPQlkOhFhqNfDtDCwibMGvL6QlkSmf3o6vAUSImzdPCxowT5dfQUZP1A=="], - - "@aws-sdk/client-codebuild/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-codebuild/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-ec2/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-ec2/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-ecr/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-ecr/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-ecs/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-ecs/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-iam/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.35", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pXzaWe3evZhjxDXAlMnqISe/XefTCGwBJG4nFTXaWSgAnMkqPEhxEPqJNhhpGesEvKFhvNpnozJJ4GTL11bRYw=="], - - "@aws-sdk/client-iam/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-vyGtvK1rY940eq7JT0yIGKuZ+2kpPSJcHibSvGlit5oiMFDamzC7cxBGLl4FLnd6suihMXDI2FSF2dL6TmBqPA=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g9b9YzDrD5pcKiPBJfCSXRfFMrA39eR0guUhZ5SRm+7vMAVc43+effxbcamxBjSd5bUhrdKo5te/yQuWurLXLA=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.2", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/credential-provider-env": "^3.972.58", "@aws-sdk/credential-provider-http": "^3.972.60", "@aws-sdk/credential-provider-login": "^3.972.64", "@aws-sdk/credential-provider-process": "^3.972.58", "@aws-sdk/credential-provider-sso": "^3.973.2", "@aws-sdk/credential-provider-web-identity": "^3.972.64", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Yr7yxNyQ8aHt9Ww0RPFUZx+xiem+vl7vuwhP0tniTijoesJNV5jou9HCgVpI0GEPAF+89TkOvilE5uRrZJnjaw=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-1nYitRCaDmXWUrpBJt6WlcGjLx1JVsMY8rlYuHHsTYTSaYikbixYdQSyINN2VYq1F798uTO9qHAzytL25M8g3A=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.2", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/token-providers": "3.1087.0", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pjMLaLU/JZi5lVfmR14V1OZqRBTuMHf6AwGNZA0K9hK+JKtO3jcLBarfD8iq5oc8cSowvc/9R32sqMVXZPo6xQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7Buc7p0OvDHW7iBsu4b+YdS0WnaFBDGKDfbVQqaac9dkWiSiUtIoarBDsA1RmOVXZijaZJDoHJFIQiicQvWRlQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA=="], - - "@aws-sdk/client-kms/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-kms/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-lambda/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-route-53/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-route-53/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-sfn/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-sfn/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-ssm/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-ssm/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-sts/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/client-sts/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CCygIKJ9YbI3n84OClSaSppkgKKHVj2TGT33c6FRORZrYNZQ1POmD+ip0FLYokiJAK7sSdc3YVkOsBm90oxWMQ=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.34", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/signature-v4-multi-region": "^3.996.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.34", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/signature-v4-multi-region": "^3.996.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1092.0", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/nested-clients": "^3.997.34", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hBYUAr6iBLNFcsiWTgtBb0stdSw39VOUq4Sp4A5caCNf66BAZplWN4FleKrVpJx5li2YgdnK2DqoFSMWC642FQ=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.34", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/signature-v4-multi-region": "^3.996.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/middleware-sdk-ec2/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/middleware-sdk-ec2/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/middleware-sdk-ec2/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], + "@aws-cdk/cx-api/@aws-cdk/cloud-assembly-schema/semver": ["semver@7.8.5", "", { "bundled": true, "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -2147,20 +1615,6 @@ "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.221.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg=="], - "@smithy/config-resolver/@smithy/core/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@smithy/middleware-endpoint/@smithy/core/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@smithy/node-config-provider/@smithy/core/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@smithy/property-provider/@smithy/core/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@smithy/shared-ini-file-loader/@smithy/core/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@smithy/util-retry/@smithy/core/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - - "@smithy/util-waiter/@smithy/core/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], - "@textlint/linter-formatter/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -2171,12 +1625,6 @@ "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "listr2/cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], - - "log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - - "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "table/slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "table/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -2191,200 +1639,6 @@ "@aws-cdk/cloudformation-diff/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@aws-sdk/checksums/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/checksums/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-appsync/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-appsync/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-tjsxMkTAFkmiV9ycmymapb9nLECWVOwFs0bZMQ9gB9bnbY8/HwfukHZlWbXZZp7qkPU6EXAfOcMm3DioFFEywA=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1102.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Ua700vVvM1q105yABSUQWkCK6FeTrNfU6ORGetJe5BzkZWY7QhkF7SVTOlmDGWRDNd6jbyY0Dv5e+E4bMBEmLg=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], - - "@aws-sdk/client-cloudcontrol/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-cloudcontrol/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-cloudformation/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-cloudformation/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-cloudtrail/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-cloudtrail/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-0AQfDcf99TNmqVKv0owHrw/TQs6i4ZE5t9qmz6NvO53bE/sA/tpXhXL9AAcEP1qHc6Zzjd1UMb69+/9zdhvY3g=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1103.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-N4wy26MNn31ItGVHYHPrEuCIFY4MBBjC+C5v1lJKqIUSA7OZBdhleCY53zCCrXn27hsk7YNOaTuhQu807S4AfQ=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], - - "@aws-sdk/client-codebuild/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-codebuild/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-ec2/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-ec2/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-ecr/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-ecr/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-ecs/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-ecs/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YQoSI4d6kXvoenoG/0Jv/PqaAuukHzGmGXGyHBQYeEUNsYovlNAn/Sw1wp/WQbhcQ3HsEMGgjEahvD3igz6ecQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1087.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-umM+qNq16f2fH+VLM5MqXW4ORNQAjk+TOSto73xbUHcKaU41L48j786r3UWQYlejeJk37NlvRYgxBT+MBkfaYQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], - - "@aws-sdk/client-kms/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-kms/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-lambda/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-lambda/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-route-53/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-route-53/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-sfn/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-sfn/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-ssm/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-ssm/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/client-sts/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/client-sts/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.41", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.41", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.41", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - - "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.40", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.40", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.40", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-cloudwatch-logs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], - - "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], } } diff --git a/package.json b/package.json index d13e82d0e..1232e5277 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ }, "dependencies": { "@aws-cdk/toolkit-lib": "1.38.2", + "@aws-sdk/client-bedrock-agent": "^3.1092.0", "@aws-sdk/client-bedrock-agentcore": "^3.1092.0", "@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0", "@aws-sdk/client-cloudformation": "^3.1092.0", diff --git a/src/assets/templates/bedrock-agent-proxy-python/README.md b/src/assets/templates/bedrock-agent-proxy-python/README.md new file mode 100644 index 000000000..3afa6e14e --- /dev/null +++ b/src/assets/templates/bedrock-agent-proxy-python/README.md @@ -0,0 +1,16 @@ +# {{name}} + +An AgentCore Runtime that proxies the existing Amazon Bedrock Agent +**{{agentName}}** (`{{agentId}}`, alias `{{agentAliasId}}`, region +`{{agentRegion}}`). Invocations of this runtime forward the payload's `prompt` +to the Bedrock Agent and stream its reply back, so the agent can be deployed +and invoked through AgentCore without changing it. + +- `main.py` — the proxy entrypoint. The agent id, alias id, and region are + baked in at import time and can be overridden with the `BEDROCK_AGENT_ID`, + `BEDROCK_AGENT_ALIAS_ID`, and `BEDROCK_AGENT_REGION` environment variables. +- `bedrock-agent-policy.json` — grants the runtime's execution role + `bedrock:InvokeAgent` on the imported agent's alias. It is wired in through + the runtime's `additionalPolicies` entry in `agentcore/agentcore.json`. + +Invoke it with a JSON payload like `{"prompt": "hello"}`. diff --git a/src/assets/templates/bedrock-agent-proxy-python/bedrock-agent-policy.json b/src/assets/templates/bedrock-agent-proxy-python/bedrock-agent-policy.json new file mode 100644 index 000000000..2834af958 --- /dev/null +++ b/src/assets/templates/bedrock-agent-proxy-python/bedrock-agent-policy.json @@ -0,0 +1,11 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "InvokeImportedBedrockAgent", + "Effect": "Allow", + "Action": "bedrock:InvokeAgent", + "Resource": "{{agentAliasArn}}" + } + ] +} diff --git a/src/assets/templates/bedrock-agent-proxy-python/main.py b/src/assets/templates/bedrock-agent-proxy-python/main.py new file mode 100644 index 000000000..e61b4958f --- /dev/null +++ b/src/assets/templates/bedrock-agent-proxy-python/main.py @@ -0,0 +1,45 @@ +# Proxy runtime for the imported Amazon Bedrock Agent "{{agentName}}". +# Generated by `agentcore project create/add runtime --type import`. Requests to +# this runtime are forwarded to the Bedrock Agent, and its reply is streamed +# back — edit or replace this file to take ownership of the behavior. + +import os +import uuid + +import boto3 +from bedrock_agentcore.runtime import BedrockAgentCoreApp + +AGENT_ID = os.environ.get("BEDROCK_AGENT_ID", "{{agentId}}") +AGENT_ALIAS_ID = os.environ.get("BEDROCK_AGENT_ALIAS_ID", "{{agentAliasId}}") +AGENT_REGION = os.environ.get("BEDROCK_AGENT_REGION", "{{agentRegion}}") + +app = BedrockAgentCoreApp() +client = boto3.client("bedrock-agent-runtime", region_name=AGENT_REGION) + + +@app.entrypoint +async def invoke(payload, context): + """Forward the prompt to the Bedrock Agent and stream its completion.""" + prompt = payload.get("prompt", "") + if not isinstance(prompt, str) or not prompt: + yield "No query provided; include a 'prompt' field in the payload." + return + + # Bedrock Agent sessions require ids of 2+ chars; reuse the runtime session + # so multi-turn conversations keep the agent's own memory of the exchange. + session_id = context.session_id or payload.get("sessionId") or uuid.uuid4().hex + + response = client.invoke_agent( + agentId=AGENT_ID, + agentAliasId=AGENT_ALIAS_ID, + sessionId=session_id, + inputText=prompt, + ) + for event in response["completion"]: + chunk = event.get("chunk") + if chunk and "bytes" in chunk: + yield chunk["bytes"].decode("utf-8") + + +if __name__ == "__main__": + app.run() diff --git a/src/assets/templates/bedrock-agent-proxy-python/pyproject.toml b/src/assets/templates/bedrock-agent-proxy-python/pyproject.toml new file mode 100644 index 000000000..a972a4009 --- /dev/null +++ b/src/assets/templates/bedrock-agent-proxy-python/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "{{name}}" +version = "0.1.0" +description = "AgentCore Runtime proxy for the Amazon Bedrock Agent {{agentName}}" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "aws-opentelemetry-distro", + "bedrock-agentcore >= 1.9.1", + "boto3 >= 1.35.0", + "botocore[crt] >= 1.35.0", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] diff --git a/src/core/index.tsx b/src/core/index.tsx index 87948c358..34a080e77 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -21,6 +21,7 @@ import type { import type { Logger } from "../logging"; import type { ProjectManager } from "../handlers/project/types"; import { FsProjectManager } from "./project"; +import { describeBedrockAgent, type DescribeBedrockAgent } from "./project/bedrockAgent"; export type { AwsClients, @@ -42,6 +43,7 @@ type CoreClientConfig = { logger: Logger; fetch?: CoreFetch; newSessionId?: () => string; + describeBedrockAgent?: DescribeBedrockAgent; }; // CoreClient is the single entry point to the Bedrock AgentCore APIs. It owns the @@ -69,6 +71,7 @@ export class CoreClient implements AwsClients { readonly eval: EvalClient; readonly projectManager: ProjectManager; + readonly describeBedrockAgent: DescribeBedrockAgent; constructor(config: CoreClientConfig) { this.createControlClient = config.createControlClient; @@ -93,6 +96,7 @@ export class CoreClient implements AwsClients { logger: this.logger.child({ module: "projectManager" }), createCloudFormationClient: config.createCloudFormationClient, }); + this.describeBedrockAgent = config.describeBedrockAgent ?? describeBedrockAgent; } // control returns the control-plane client for `config`, creating and caching it diff --git a/src/core/project/bedrockAgent.ts b/src/core/project/bedrockAgent.ts new file mode 100644 index 000000000..b4a003b52 --- /dev/null +++ b/src/core/project/bedrockAgent.ts @@ -0,0 +1,103 @@ +import { InputValidationError, MalformedServiceResponseError } from "../../errors"; + +/** + * Regions where an Amazon Bedrock Agent can live for `--type import`, + * mirroring the original CLI's supported-region list. + */ +export const BEDROCK_AGENT_IMPORT_REGIONS = [ + "us-east-1", + "us-west-2", + "eu-west-1", + "eu-central-1", + "ap-southeast-1", + "ap-northeast-1", + "ap-south-1", + "ca-central-1", + "sa-east-1", + "us-gov-west-1", +] as const; + +export type BedrockAgentImportRegion = (typeof BEDROCK_AGENT_IMPORT_REGIONS)[number]; + +export type DescribeBedrockAgentInput = { + region: string; + agentId: string; + agentAliasId: string; +}; + +/** What the proxy scaffold needs to know about the imported agent. */ +export type BedrockAgentMetadata = { + agentName: string; + agentStatus: string; + agentAliasArn: string; + agentAliasName: string; + agentAliasStatus: string; + foundationModel?: string; + description?: string; +}; + +export type DescribeBedrockAgent = ( + input: DescribeBedrockAgentInput, +) => Promise; + +function isNamedError(error: unknown, name: string): boolean { + return error instanceof Error && error.name === name; +} + +/** + * Describes the agent and its alias through the Bedrock Agent control plane, + * both to fail fast on a nonexistent agent/alias and to capture the metadata + * the scaffolded proxy embeds. + */ +export const describeBedrockAgent: DescribeBedrockAgent = async (input) => { + const { BedrockAgentClient, GetAgentCommand, GetAgentAliasCommand } = + await import("@aws-sdk/client-bedrock-agent"); + const client = new BedrockAgentClient({ region: input.region }); + + let agent; + try { + ({ agent } = await client.send(new GetAgentCommand({ agentId: input.agentId }))); + } catch (error) { + if (isNamedError(error, "ResourceNotFoundException")) { + throw new InputValidationError( + `no Bedrock Agent with id '${input.agentId}' exists in ${input.region}; ` + + `check --agent-id and --region`, + { cause: error }, + ); + } + throw error; + } + + let agentAlias; + try { + ({ agentAlias } = await client.send( + new GetAgentAliasCommand({ agentId: input.agentId, agentAliasId: input.agentAliasId }), + )); + } catch (error) { + if (isNamedError(error, "ResourceNotFoundException")) { + throw new InputValidationError( + `Bedrock Agent '${input.agentId}' has no alias with id '${input.agentAliasId}' in ` + + `${input.region}; check --agent-alias-id`, + { cause: error }, + ); + } + throw error; + } + + if (!agent?.agentName || !agentAlias?.agentAliasArn || !agentAlias.agentAliasName) { + throw new MalformedServiceResponseError( + `the Bedrock Agent service returned an incomplete description for agent ` + + `'${input.agentId}' / alias '${input.agentAliasId}'`, + ); + } + + return { + agentName: agent.agentName, + agentStatus: agent.agentStatus ?? "UNKNOWN", + agentAliasArn: agentAlias.agentAliasArn, + agentAliasName: agentAlias.agentAliasName, + agentAliasStatus: agentAlias.agentAliasStatus ?? "UNKNOWN", + foundationModel: agent.foundationModel, + description: agent.description, + }; +}; diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index e05d25884..9a2853dbf 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -139,7 +139,7 @@ export class FsProjectManager implements ProjectManager { const projectTree = await createProjectTree( { templateRenderer: this.templateRenderer, assetSource: this.assetSource }, { projectName: input.name }, - { runtime: scaffoldRuntimeInput }, + { runtime: scaffoldRuntimeInput, importBedrockAgent: input.importBedrockAgent }, ); await projectTree.write(destination); diff --git a/src/core/project/templates/project.ts b/src/core/project/templates/project.ts index 373fcd43b..c2eeab8e4 100644 --- a/src/core/project/templates/project.ts +++ b/src/core/project/templates/project.ts @@ -1,7 +1,10 @@ import { FsTreeNode } from "./fsTree"; import type { AssetSource } from "../source"; import type { ScaffoldRuntimeInput } from "../../../handlers/project/types"; -import type { RuntimeResourceConfig } from "../../../handlers/project/add/runtime/types"; +import type { + ImportBedrockAgentInput, + RuntimeResourceConfig, +} from "../../../handlers/project/add/runtime/types"; import { InputValidationError } from "../../../errors/errors"; import { getRuntimeTemplateResolver } from "./runtime"; import type { SpecEntries, Template, TemplateRenderer } from "./types"; @@ -14,13 +17,14 @@ type CreateProjectConfig = { export async function createProjectTree( config: CreateProjectConfig, input: { projectName: string }, - options?: { runtime?: ScaffoldRuntimeInput }, + options?: { runtime?: ScaffoldRuntimeInput; importBedrockAgent?: ImportBedrockAgentInput }, ): Promise { const templates: Template[] = []; if (options?.runtime) { const runtimeConfig: RuntimeResourceConfig = { name: options.runtime.runtimeName, scaffoldRuntimeInput: options.runtime, + importBedrockAgent: options.importBedrockAgent, }; const resolver = getRuntimeTemplateResolver(config, runtimeConfig); diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 99985d814..0ff45e1b6 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -56,6 +56,49 @@ function buildResolverKey( return `${framework}/${language}`; } +// The IAM policy file the proxy template vends; wired into the runtime's +// additionalPolicies so the execution role may call bedrock:InvokeAgent. +const BEDROCK_AGENT_POLICY_FILE = "bedrock-agent-policy.json"; + +const importBedrockAgentResolver = + (assetSource: AssetSource, templateRenderer: TemplateRenderer) => + async (input: RuntimeResourceConfig) => { + const imported = input.importBedrockAgent!; + if (input.protocol !== undefined && input.protocol !== "HTTP") + throw new InputValidationError("an imported Bedrock Agent proxy only supports HTTP"); + + const context = { + name: toPythonPackageName(input.name), + agentId: imported.agentId, + agentAliasId: imported.agentAliasId, + agentRegion: imported.region, + agentName: imported.agentName, + agentAliasArn: imported.agentAliasArn, + }; + const tree = await FsTreeNode.fromAssetSource( + { assetSource }, + { assetDir: "templates/bedrock-agent-proxy-python" }, + { + rootDirName: input.name, + transformContent: (raw) => templateRenderer.render(raw, context), + }, + ); + + const base = buildRuntimeSpec(input); + return { + tree, + spec: { + runtimes: [ + { + ...base, + protocol: "HTTP" as const, + additionalPolicies: [...(base.additionalPolicies ?? []), BEDROCK_AGENT_POLICY_FILE], + }, + ], + }, + }; + }; + const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: TemplateRenderer) => ({ [buildResolverKey("none", "Python")]: async (input: RuntimeResourceConfig) => { if (input.protocol !== undefined && input.protocol !== "HTTP") @@ -150,6 +193,12 @@ export function getRuntimeTemplateResolver( config: GetRuntimeTemplateResolverConfig, input: RuntimeResourceConfig, ): TemplateResolver | undefined { + // An imported Bedrock Agent always scaffolds the proxy template, regardless + // of the framework/language key. + if (input.importBedrockAgent) { + return { resolve: importBedrockAgentResolver(config.assetSource, config.templateRenderer) }; + } + const { framework, language } = input.scaffoldRuntimeInput; const key = buildResolverKey(framework, language); diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index 59d52a83a..cd3c5275c 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -28,9 +28,9 @@ afterEach(async () => { ); }); -async function run(args: string[]) { +async function run(args: string[], opts?: { core?: TestCoreClient }) { const io = testIO(); - const core = new TestCoreClient(); + const core = opts?.core ?? new TestCoreClient(); const root = createRootHandler(core, { io: io.io, globalConfigAccessor: new TestGlobalConfigAccessor(), @@ -484,3 +484,118 @@ describe("project add runtime", () => { ).rejects.toThrow(/API keys are not compatible with Bedrock model providers/); }); }); + +describe("project add runtime --type import", () => { + const metadata = { + agentName: "SupportAgent", + agentStatus: "PREPARED", + agentAliasArn: "arn:aws:bedrock:us-east-1:111122223333:agent-alias/A1B2C3D4E5/TSTALIASID", + agentAliasName: "live", + agentAliasStatus: "PREPARED", + foundationModel: "us.amazon.nova-lite-v1:0", + }; + + const importArgs = [ + "add", + "runtime", + "--name", + "support_proxy", + "--type", + "import", + "--agent-id", + "A1B2C3D4E5", + "--agent-alias-id", + "TSTALIASID", + "--region", + "us-east-1", + ]; + + test("scaffolds a proxy runtime wrapping the described Bedrock Agent", async () => { + const projectRoot = await inProject(); + const core = new TestCoreClient(); + core.bedrockAgentDescriptions["A1B2C3D4E5/TSTALIASID"] = metadata; + + await run(importArgs, { core }); + + expect(core.describedBedrockAgents).toEqual([ + { region: "us-east-1", agentId: "A1B2C3D4E5", agentAliasId: "TSTALIASID" }, + ]); + + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes[0]).toMatchObject({ + name: "support_proxy", + build: "CodeZip", + entrypoint: "main.py", + codeLocation: "app/support_proxy", + runtimeVersion: "PYTHON_3_14", + protocol: "HTTP", + additionalPolicies: ["bedrock-agent-policy.json"], + }); + + const appDir = join(projectRoot, "app", "support_proxy"); + const main = await Bun.file(join(appDir, "main.py")).text(); + expect(main).toContain('"A1B2C3D4E5"'); + expect(main).toContain('"TSTALIASID"'); + expect(main).toContain('"us-east-1"'); + expect(main).toContain("invoke_agent"); + + const policy = await Bun.file(join(appDir, "bedrock-agent-policy.json")).json(); + expect(policy.Statement[0]).toMatchObject({ + Action: "bedrock:InvokeAgent", + Resource: metadata.agentAliasArn, + }); + + const pyproject = await Bun.file(join(appDir, "pyproject.toml")).text(); + expect(pyproject).toContain('name = "support_proxy"'); + expect(pyproject).toContain("boto3"); + }); + + test("warns when the agent is not PREPARED", async () => { + await inProject(); + const core = new TestCoreClient(); + core.bedrockAgentDescriptions["A1B2C3D4E5/TSTALIASID"] = { + ...metadata, + agentStatus: "NOT_PREPARED", + }; + + const { io } = await run(importArgs, { core }); + expect(io.stderr()).toContain("not PREPARED"); + }); + + test("rejects a nonexistent agent with the describe error", async () => { + await inProject(); + await expect(run(importArgs)).rejects.toThrow(/no Bedrock Agent with id 'A1B2C3D4E5'/); + }); + + test("rejects an unsupported --region before any service call", async () => { + await inProject(); + const core = new TestCoreClient(); + const args = [...importArgs.slice(0, -2), "--region", "eu-north-1"]; + await expect(run(args, { core })).rejects.toThrow(/not a supported Bedrock Agent region/); + expect(core.describedBedrockAgents).toEqual([]); + }); + + test("requires --agent-id and --agent-alias-id with --type import", async () => { + await inProject(); + await expect( + run(["add", "runtime", "--name", "p", "--type", "import", "--region", "us-east-1"]), + ).rejects.toThrow(/requires both --agent-id and --agent-alias-id/); + }); + + test("rejects --agent-id without --type import", async () => { + await inProject(); + await expect(run(["add", "runtime", "--name", "p", "--agent-id", "A1"])).rejects.toThrow( + /--agent-id and --agent-alias-id require --type import/, + ); + }); + + test("rejects scaffolding flags combined with --type import", async () => { + await inProject(); + await expect(run([...importArgs, "--framework", "strands"])).rejects.toThrow( + /--framework is a scaffolding flag/, + ); + await expect(run([...importArgs, "--template", "hello-world-python"])).rejects.toThrow( + /--template is a scaffolding flag/, + ); + }); +}); diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 4a753a59a..b2b7c301e 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -14,7 +14,13 @@ import { resolveRuntimeTemplateShortcut, } from "../../shortcuts"; import { ScaffoldRuntimeInputSchema, type ScaffoldRuntimeInput } from "../../types"; -import { RuntimeResourceConfigSchema } from "./types"; +import { RuntimeResourceConfigSchema, type ImportBedrockAgentInput } from "./types"; +import { describeBedrockAgent } from "../../../../core/project/bedrockAgent"; +import { + importScaffoldRuntimeInput, + resolveImportBedrockAgentInput, +} from "../../importBedrockAgent"; +import { RegionKey } from "../../../keys"; export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -23,6 +29,21 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => flags: [ flag("name", "the name of the runtime", z.string().max(42).optional()), flag("description", "an optional description of the runtime", z.string().optional()), + flag( + "type", + "create scaffolds new agent code (the default); import wraps an existing Bedrock Agent", + z.enum(["create", "import"]).optional(), + ), + flag( + "agent-id", + "Bedrock Agent ID to import (requires --type import)", + z.string().optional(), + ), + flag( + "agent-alias-id", + "Bedrock Agent Alias ID to import (requires --type import)", + z.string().optional(), + ), flag( "template", "a preset of flags for scaffolding the runtime; compatible flags override preset values", @@ -121,34 +142,61 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => throw new InputValidationError(`--${lockedFlag} cannot override a template`); } + const isImport = flags["type"] === "import"; + if (isImport && (isTemplate || presentScaffoldingFlags.length > 0)) { + const offending = isTemplate ? "template" : presentScaffoldingFlags[0]; + throw new InputValidationError( + `--type import wraps an existing Bedrock Agent; --${offending} is a scaffolding ` + + `flag and cannot be combined with it`, + ); + } + if (!isImport && (flags["agent-id"] !== undefined || flags["agent-alias-id"] !== undefined)) { + throw new InputValidationError("--agent-id and --agent-alias-id require --type import"); + } + const isCustom = presentScaffoldingFlags.length > 0; const source = new SourceResolver({ stdin: config.io.stdin }); const apiKey = await source.resolveSecret("api-key", flags["api-key"]); const runtimeName = flags.name; + + let importBedrockAgent: ImportBedrockAgentInput | undefined; + if (isImport) { + const { imported, warnings } = await resolveImportBedrockAgentInput({ + describeBedrockAgent: config.describeBedrockAgent ?? describeBedrockAgent, + region: ctx.require(RegionKey), + agentId: flags["agent-id"], + agentAliasId: flags["agent-alias-id"], + }); + importBedrockAgent = imported; + for (const warning of warnings) config.io.stderr.write(`${warning}\n`); + } + const defaultMemory = flags.framework === "strands" ? "longAndShortTerm" : "none"; - const scaffoldRuntimeInput: ScaffoldRuntimeInput = isTemplate - ? resolveRuntimeTemplateShortcut(flags.template!, { - runtimeName: flags.name, - build: flags.build, - modelProvider: flags["model-provider"], - apiKey, - memory: flags.memory, - }) - : isCustom - ? parseScaffoldRuntimeInput({ - runtimeName, + const scaffoldRuntimeInput: ScaffoldRuntimeInput = isImport + ? importScaffoldRuntimeInput(runtimeName) + : isTemplate + ? resolveRuntimeTemplateShortcut(flags.template!, { + runtimeName: flags.name, build: flags.build, - language: flags.language, - framework: flags.framework, modelProvider: flags["model-provider"], apiKey, - memory: MEMORY_SHORTCUTS[flags.memory ?? defaultMemory](runtimeName), - entrypoint: "main.py", - runtimeVersion: flags.build === "CodeZip" ? "PYTHON_3_14" : undefined, + memory: flags.memory, }) - : resolveRuntimeTemplateShortcut("hello-world-python", { runtimeName: flags.name }); + : isCustom + ? parseScaffoldRuntimeInput({ + runtimeName, + build: flags.build, + language: flags.language, + framework: flags.framework, + modelProvider: flags["model-provider"], + apiKey, + memory: MEMORY_SHORTCUTS[flags.memory ?? defaultMemory](runtimeName), + entrypoint: "main.py", + runtimeVersion: flags.build === "CodeZip" ? "PYTHON_3_14" : undefined, + }) + : resolveRuntimeTemplateShortcut("hello-world-python", { runtimeName: flags.name }); const inputEnvironmentVariables = parseJsonFlag>( "environment-variables", @@ -180,6 +228,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => ), tags: parseTags(flags["tags"]), scaffoldRuntimeInput, + importBedrockAgent, }; const result = RuntimeResourceConfigSchema.safeParse(runtimeInput); diff --git a/src/handlers/project/add/runtime/types.ts b/src/handlers/project/add/runtime/types.ts index 9331e5051..286be2999 100644 --- a/src/handlers/project/add/runtime/types.ts +++ b/src/handlers/project/add/runtime/types.ts @@ -1,6 +1,22 @@ import z from "zod"; import { ProjectRuntimeSchema } from "../../../../projectSchemas/runtime"; import { ScaffoldRuntimeInputSchema } from "../../types"; +import { BEDROCK_AGENT_IMPORT_REGIONS } from "../../../../core/project/bedrockAgent"; + +/** + * The imported Bedrock Agent a proxy runtime wraps: the caller-provided + * addressing plus the metadata captured from the describe calls. + */ +export const ImportBedrockAgentInputSchema = z.object({ + agentId: z.string().min(1), + agentAliasId: z.string().min(1), + region: z.enum(BEDROCK_AGENT_IMPORT_REGIONS), + agentName: z.string().min(1), + agentAliasArn: z.string().min(1), + foundationModel: z.string().optional(), + description: z.string().optional(), +}); +export type ImportBedrockAgentInput = z.infer; const RuntimeInfraConfigSchema = z.object({ name: ProjectRuntimeSchema.shape.name, @@ -21,5 +37,7 @@ const RuntimeInfraConfigSchema = z.object({ export const RuntimeResourceConfigSchema = RuntimeInfraConfigSchema.extend({ scaffoldRuntimeInput: ScaffoldRuntimeInputSchema, + /** Present when the runtime is a proxy for an imported Bedrock Agent. */ + importBedrockAgent: ImportBedrockAgentInputSchema.optional(), }); export type RuntimeResourceConfig = z.infer; diff --git a/src/handlers/project/add/types.ts b/src/handlers/project/add/types.ts index 26943b932..9937d58d0 100644 --- a/src/handlers/project/add/types.ts +++ b/src/handlers/project/add/types.ts @@ -1,7 +1,10 @@ import type { AppIO } from "../../../io"; +import type { DescribeBedrockAgent } from "../../../core/project/bedrockAgent"; import type { ProjectManager } from "../types"; export type AddProjectResourceConfig = { projectManager: ProjectManager; io: AppIO; + /** Describes a Bedrock Agent for --type import; injectable for tests. */ + describeBedrockAgent?: DescribeBedrockAgent; }; diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index f25a72bd5..405006ba7 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -19,10 +19,19 @@ import { CONTAINER_URI_PATTERN, HarnessSpecSchema } from "../../../projectSchema import { InputValidationError } from "../../../errors"; import { parseJsonFlag } from "../../utils"; import { DEFAULT_HARNESS_MODEL } from "../add/harness"; +import { + describeBedrockAgent, + type DescribeBedrockAgent, +} from "../../../core/project/bedrockAgent"; +import { importScaffoldRuntimeInput, resolveImportBedrockAgentInput } from "../importBedrockAgent"; +import type { ImportBedrockAgentInput } from "../add/runtime/types"; +import { RegionKey } from "../../keys"; type CreateProjectHandlerConfig = { projectManager: ProjectManager; io: AppIO; + /** Describes a Bedrock Agent for --type import; injectable for tests. */ + describeBedrockAgent?: DescribeBedrockAgent; }; // Flags that select the runtime-scaffolding path. Any of these (or --template) @@ -36,6 +45,9 @@ const RUNTIME_PATH_FLAGS = [ "api-key", "runtime-name", "memory", + "type", + "agent-id", + "agent-alias-id", ] as const; // Flags that only make sense for the harness path. @@ -99,6 +111,21 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = z.enum(MEMORY_SHORTCUT_NAMES).optional(), ), flag("runtime-name", "name of the scaffolded runtime", z.string().max(42).optional()), + flag( + "type", + "create scaffolds new agent code (the default); import wraps an existing Bedrock Agent", + z.enum(["create", "import"]).optional(), + ), + flag( + "agent-id", + "Bedrock Agent ID to import (requires --type import)", + z.string().optional(), + ), + flag( + "agent-alias-id", + "Bedrock Agent Alias ID to import (requires --type import)", + z.string().optional(), + ), flag("model-id", "model ID for the created harness", z.string().optional()), flag( "api-key-arn", @@ -144,7 +171,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = ), flag("skip-git", "skip initializing a git repository", z.boolean().default(false)), ], - handle: async (_ctx, flags) => { + handle: async (ctx, flags) => { const presentRuntimeFlags: string[] = RUNTIME_PATH_FLAGS.filter( (f) => flags[f] !== undefined, ); @@ -173,14 +200,44 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = throw new InputValidationError(`--${lockedFlag} cannot override a template`); } + const isImport = flags["type"] === "import"; + const scaffoldingChoiceFlags = ( + ["build", "language", "framework", "model-provider", "api-key", "memory"] as const + ).filter((f) => flags[f] !== undefined); + if (isImport && (isTemplate || scaffoldingChoiceFlags.length > 0)) { + const offending = isTemplate ? "template" : scaffoldingChoiceFlags[0]; + throw new InputValidationError( + `--type import wraps an existing Bedrock Agent; --${offending} is a scaffolding ` + + `flag and cannot be combined with it`, + ); + } + if (!isImport && (flags["agent-id"] !== undefined || flags["agent-alias-id"] !== undefined)) { + throw new InputValidationError("--agent-id and --agent-alias-id require --type import"); + } + const isRuntimePath = presentRuntimeFlags.length > 0; + let importBedrockAgent: ImportBedrockAgentInput | undefined; + if (isImport) { + const { imported, warnings } = await resolveImportBedrockAgentInput({ + describeBedrockAgent: config.describeBedrockAgent ?? describeBedrockAgent, + region: ctx.require(RegionKey), + agentId: flags["agent-id"], + agentAliasId: flags["agent-alias-id"], + }); + importBedrockAgent = imported; + for (const warning of warnings) config.io.stderr.write(`${warning}\n`); + } + const createInput: CreateProjectInput = isRuntimePath ? { name: flags["name"], skipInstall: flags["skip-install"], skipGit: flags["skip-git"], - scaffoldRuntimeInput: await resolveScaffoldRuntimeInput(config, flags), + scaffoldRuntimeInput: isImport + ? importScaffoldRuntimeInput(flags["runtime-name"] ?? flags["name"]) + : await resolveScaffoldRuntimeInput(config, flags), + importBedrockAgent, } : { name: flags["name"], diff --git a/src/handlers/project/importBedrockAgent.ts b/src/handlers/project/importBedrockAgent.ts new file mode 100644 index 000000000..7fbc1b0ea --- /dev/null +++ b/src/handlers/project/importBedrockAgent.ts @@ -0,0 +1,80 @@ +import { InputValidationError } from "../../errors"; +import { + BEDROCK_AGENT_IMPORT_REGIONS, + type DescribeBedrockAgent, +} from "../../core/project/bedrockAgent"; +import type { ImportBedrockAgentInput } from "./add/runtime/types"; +import type { ScaffoldRuntimeInput } from "./types"; + +/** + * The fixed scaffold shape of a Bedrock Agent proxy runtime: plain Python, + * CodeZip. The proxy template supplies the code; these values only shape the + * runtime spec entry. + */ +export function importScaffoldRuntimeInput(runtimeName: string): ScaffoldRuntimeInput { + return { + runtimeName, + build: "CodeZip", + language: "Python", + framework: "none", + modelProvider: "Bedrock", + entrypoint: "main.py", + runtimeVersion: "PYTHON_3_14", + }; +} + +export type ResolveImportInput = { + describeBedrockAgent: DescribeBedrockAgent; + /** The CLI's effective region (--region flag, env, shared config). */ + region: string; + agentId?: string; + agentAliasId?: string; +}; + +/** + * Validates the import addressing, describes the agent and alias through the + * service, and returns the proxy scaffold's input plus any advisory warnings. + */ +export async function resolveImportBedrockAgentInput( + input: ResolveImportInput, +): Promise<{ imported: ImportBedrockAgentInput; warnings: string[] }> { + if (!input.agentId || !input.agentAliasId) { + throw new InputValidationError("--type import requires both --agent-id and --agent-alias-id"); + } + + const region = BEDROCK_AGENT_IMPORT_REGIONS.find((candidate) => candidate === input.region); + if (!region) { + throw new InputValidationError( + `'${input.region}' is not a supported Bedrock Agent region for import. ` + + `Supported regions: ${BEDROCK_AGENT_IMPORT_REGIONS.join(", ")}. ` + + `Pass --region to select the agent's region.`, + ); + } + + const metadata = await input.describeBedrockAgent({ + region, + agentId: input.agentId, + agentAliasId: input.agentAliasId, + }); + + const warnings: string[] = []; + if (metadata.agentStatus !== "PREPARED") { + warnings.push( + `Warning: Bedrock Agent '${metadata.agentName}' is in status ${metadata.agentStatus} ` + + `(not PREPARED); invocations may fail until it is prepared.`, + ); + } + + return { + imported: { + agentId: input.agentId, + agentAliasId: input.agentAliasId, + region, + agentName: metadata.agentName, + agentAliasArn: metadata.agentAliasArn, + ...(metadata.foundationModel && { foundationModel: metadata.foundationModel }), + ...(metadata.description && { description: metadata.description }), + }, + warnings, + }; +} diff --git a/src/handlers/project/index.ts b/src/handlers/project/index.ts index 5ae74e1be..7106e4993 100644 --- a/src/handlers/project/index.ts +++ b/src/handlers/project/index.ts @@ -24,14 +24,20 @@ type ProjectHandlerConfig = { export function createProjectHandler({ core, io }: ProjectHandlerConfig): Router { const projectManager: ProjectManager = core.projectManager; - const config = { projectManager, io }; + const config = { projectManager, io, describeBedrockAgent: core.describeBedrockAgent }; const project = new Router("project", "manage an AgentCore project"); // Without a default, a bare `agentcore project` falls back to Commander's help // and a usage exit code instead of the menu every sibling router opens. project.default(renderTui(core, io)); - project.handler(createCreateProjectHandler({ projectManager, io })); + project.handler( + createCreateProjectHandler({ + projectManager, + io, + describeBedrockAgent: core.describeBedrockAgent, + }), + ); project.handler(createAddProjectResourceHandler(config)); project.handler( withProject({ projectManager: config.projectManager })( diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 064ffd659..55081b20d 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -191,6 +191,79 @@ describe("project create", () => { ]); }); + test("--type import scaffolds a Bedrock Agent proxy project", async () => { + const directory = await inTempDirectory(); + const core = new TestCoreClient(); + core.bedrockAgentDescriptions["A1B2C3D4E5/TSTALIASID"] = { + agentName: "SupportAgent", + agentStatus: "PREPARED", + agentAliasArn: "arn:aws:bedrock:us-east-1:111122223333:agent-alias/A1B2C3D4E5/TSTALIASID", + agentAliasName: "live", + agentAliasStatus: "PREPARED", + }; + + await run( + [ + "create", + "--name", + "MyImport", + "--type", + "import", + "--agent-id", + "A1B2C3D4E5", + "--agent-alias-id", + "TSTALIASID", + "--region", + "us-east-1", + ], + { core }, + ); + + const projectRoot = join(directory, "MyImport"); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.harnesses).toBeUndefined(); + expect(spec.runtimes[0]).toMatchObject({ + name: "MyImport", + build: "CodeZip", + runtimeVersion: "PYTHON_3_14", + additionalPolicies: ["bedrock-agent-policy.json"], + }); + + const main = await Bun.file(join(projectRoot, "app", "MyImport", "main.py")).text(); + expect(main).toContain('"A1B2C3D4E5"'); + const policy = await Bun.file( + join(projectRoot, "app", "MyImport", "bedrock-agent-policy.json"), + ).json(); + expect(policy.Statement[0].Resource).toBe( + "arn:aws:bedrock:us-east-1:111122223333:agent-alias/A1B2C3D4E5/TSTALIASID", + ); + }); + + test("--type import conflicts with harness-only and scaffolding flags", async () => { + await inTempDirectory(); + await expect( + run(["create", "--name", "MyImport", "--type", "import", "--model-id", "x"]), + ).rejects.toThrow(/Cannot mix runtime scaffolding flags \(--type\)/); + await expect( + run([ + "create", + "--name", + "MyImport", + "--type", + "import", + "--agent-id", + "A", + "--agent-alias-id", + "B", + "--framework", + "strands", + ]), + ).rejects.toThrow(/--framework is a scaffolding flag/); + await expect(run(["create", "--name", "MyImport", "--agent-id", "A"])).rejects.toThrow( + /--agent-id and --agent-alias-id require --type import/, + ); + }); + test("rejects invalid harness flag combinations before scaffolding anything", async () => { const directory = await inTempDirectory(); // apiBase is a lite_llm-only model setting; the bedrock harness path diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index aa9a6b84c..7720da87e 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -6,7 +6,7 @@ import { MemorySchema } from "../../projectSchemas/memory"; import type { EvaluatorSchema } from "../../projectSchemas/evaluator"; import type { ProjectSpecSchema } from "../../projectSchemas/project"; import z from "zod"; -import type { RuntimeResourceConfig } from "./add/runtime/types"; +import type { ImportBedrockAgentInput, RuntimeResourceConfig } from "./add/runtime/types"; import type { OnlineEvalConfigSchema } from "../../projectSchemas/online-eval-config"; import { AgentNameSchema, BuildTypeSchema, EntrypointSchema } from "../../projectSchemas/runtime"; import { RuntimeVersionSchema } from "../../projectSchemas/constants"; @@ -66,12 +66,15 @@ export type CreateProjectInput = CreateProjectInputBase & | { /** The resolved template parameters. The handler maps --template to these before calling the manager. */ scaffoldRuntimeInput: ScaffoldRuntimeInput; + /** Present when the runtime proxies an imported Bedrock Agent. */ + importBedrockAgent?: ImportBedrockAgentInput; scaffoldHarnessInput?: undefined; } | { /** The harness the created project declares (the default create path). */ scaffoldHarnessInput: ScaffoldHarnessInput; scaffoldRuntimeInput?: undefined; + importBedrockAgent?: undefined; } ); diff --git a/src/handlers/types.tsx b/src/handlers/types.tsx index f129805a8..6194eaf45 100644 --- a/src/handlers/types.tsx +++ b/src/handlers/types.tsx @@ -6,6 +6,7 @@ import type { CoreMemoryClient } from "./memory/types.tsx"; import type { CoreRuntimeClient } from "./runtime/types.tsx"; import type { Context } from "../router"; import type { ProjectManager } from "./project/types.ts"; +import type { DescribeBedrockAgent } from "../core/project/bedrockAgent"; export interface Core { harness: CoreHarnessClient; @@ -15,6 +16,8 @@ export interface Core { gateway: CoreGatewayClient; eval: CoreEvalClient; projectManager: ProjectManager; + /** Describes a Bedrock Agent + alias for `--type import`. */ + describeBedrockAgent: DescribeBedrockAgent; } // ScreenProps is the common prop set every TUI screen receives. `ctx` carries the diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index eba250e6d..d2ce65301 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -165,6 +165,11 @@ import type { Logger } from "../logging"; import type { ReadWriteJson } from "../io"; import { createSilentLogger } from "./logging"; import { FsProjectManager, type ProjectBackend } from "../core/project"; +import type { + BedrockAgentMetadata, + DescribeBedrockAgent, + DescribeBedrockAgentInput, +} from "../core/project/bedrockAgent"; import type { ManagedBy } from "../projectSchemas/project"; import { InputValidationError } from "../errors"; @@ -2237,6 +2242,22 @@ export class TestCoreClient implements Core { // recorded instead of spawned so tests stay fast and hermetic. readonly projectCommands: { command: string[]; cwd: string }[] = []; + // Seed with `agentId/agentAliasId` keys to make Bedrock Agents resolvable + // through describeBedrockAgent; unseeded ids reject like the service would. + readonly bedrockAgentDescriptions: Record = {}; + readonly describedBedrockAgents: DescribeBedrockAgentInput[] = []; + readonly describeBedrockAgent: DescribeBedrockAgent = async (input) => { + this.describedBedrockAgents.push(input); + const metadata = this.bedrockAgentDescriptions[`${input.agentId}/${input.agentAliasId}`]; + if (!metadata) { + throw new InputValidationError( + `no Bedrock Agent with id '${input.agentId}' exists in ${input.region}; ` + + `check --agent-id and --region`, + ); + } + return metadata; + }; + constructor(options?: TestCoreClientOptions) { this.projectManager = new FsProjectManager({ logger: options?.logger ?? createSilentLogger(), From 18a8b82128efb163c3fba24daa8ce095df8ec3c4 Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Sun, 30 Aug 2026 23:52:53 -0400 Subject: [PATCH 05/12] feat(remove): cover all addable resources and add remove all project remove now accepts credential, config-bundle, online-eval, online-insight, and memory; removing a credential also deletes the .env.local keys it reserved (EnvLocalFile gains removeKeys with snapshot/rollback). Removing a resource that does not exist now throws ResourceNotFoundError instead of warn-and-rewrite. New `project remove all` empties every resource collection in agentcore.json (spec-level; app/ code and aws-targets.json are kept) behind a y/N prompt with --yes for non-interactive use. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- README.md | 16 +- src/core/project/envLocal.test.ts | 68 ++++++ src/core/project/envLocal.ts | 39 +++ src/core/project/manager.test.ts | 153 +++++++++++- src/core/project/manager.tsx | 143 +++++++++-- src/handlers/project/remove/index.test.ts | 281 +++++++++++++++++++++- src/handlers/project/remove/index.ts | 93 ++++++- src/handlers/project/types.ts | 22 +- src/testing/index.tsx | 2 +- 9 files changed, 771 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index ed38e20ea..16626581a 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,14 @@ agentcore # interactive TUI │ ├── create # create a project: a managed harness by default, │ │ # or scaffolded runtime code via --template/--framework │ ├── add # add a resource to the project (runtime, harness, memory, …) -│ ├── remove # remove a resource from the project +│ ├── remove # remove a resource from the project spec (spec-level; +│ │ # code under app/ is kept). Resource types: harness, +│ │ # runtime, credential, config-bundle, online-eval, +│ │ # online-insight, memory, gateway, gateway-target, +│ │ # gateway-connector, policy-engine, policy, +│ │ # payment-manager, payment-connector — or `all`, which +│ │ # empties every resource collection (y/N prompt; --yes +│ │ # skips it for non-interactive use) │ ├── build # synthesize the project's CloudFormation templates │ ├── deploy # deploy to AWS (auto-provisions the default target) │ └── dev # run the project's agents locally @@ -236,6 +243,13 @@ agentcore eval evaluator code-based create \ agentcore eval evaluator get --id --json agentcore eval evaluator list --max-results 20 --json agentcore eval evaluator delete --id --json + +# Remove resources from a project's spec (run inside the project) +agentcore project remove memory --name recall +agentcore project remove credential --name svc-key # also deletes its .env.local entries +agentcore project remove gateway-target --gateway tools --name search +agentcore project remove all # y/N prompt; empties every collection +agentcore project remove all --yes # non-interactive ``` Source-aware values: any field flag documented as such accepts the value inline, diff --git a/src/core/project/envLocal.test.ts b/src/core/project/envLocal.test.ts index 37d8cb4ca..cb808c5d8 100644 --- a/src/core/project/envLocal.test.ts +++ b/src/core/project/envLocal.test.ts @@ -95,3 +95,71 @@ test("rejects a value that contains a single quote", async () => { /single quote/, ); }); + +test("removeKeys deletes an entry and its comment while leaving neighbors", async () => { + const root = await tempRoot(); + const file = new EnvLocalFile(root); + await file.insertIfNew([ + { key: "KEEP", value: "1", comment: "kept entry" }, + { key: "DROP", value: "2", comment: "dropped entry" }, + ]); + + const result = await file.removeKeys(["DROP"]); + + expect(result).toEqual({ removed: ["DROP"], missing: [] }); + expect(await Bun.file(file.path).text()).toBe("# kept entry\nKEEP='1'\n"); +}); + +test("removeKeys reports keys that are not present without rewriting the file", async () => { + const root = await tempRoot(); + const file = new EnvLocalFile(root); + await Bun.write(file.path, "USER_MANAGED=1\n"); + + const result = await file.removeKeys(["ABSENT"]); + + expect(result).toEqual({ removed: [], missing: ["ABSENT"] }); + expect(await Bun.file(file.path).text()).toBe("USER_MANAGED=1\n"); + await file.rollback(); // nothing was written, so nothing to restore + expect(await Bun.file(file.path).text()).toBe("USER_MANAGED=1\n"); +}); + +test("removeKeys on a missing file reports every key as missing", async () => { + const root = await tempRoot(); + const file = new EnvLocalFile(root); + + expect(await file.removeKeys(["A", "B"])).toEqual({ removed: [], missing: ["A", "B"] }); + expect(existsSync(file.path)).toBe(false); +}); + +test("removeKeys never deletes a non-comment line above the entry", async () => { + const root = await tempRoot(); + const file = new EnvLocalFile(root); + await Bun.write(file.path, "USER_MANAGED=1\nDROP=2\n"); + + await file.removeKeys(["DROP"]); + + expect(await Bun.file(file.path).text()).toBe("USER_MANAGED=1\n"); +}); + +test("rollback restores the content removeKeys deleted", async () => { + const root = await tempRoot(); + const file = new EnvLocalFile(root); + await Bun.write(file.path, "# api key\nSECRET='v'\nOTHER=1\n"); + + await file.removeKeys(["SECRET"]); + expect(await Bun.file(file.path).text()).toBe("OTHER=1\n"); + + await file.rollback(); + expect(await Bun.file(file.path).text()).toBe("# api key\nSECRET='v'\nOTHER=1\n"); +}); + +testPosix("removeKeys keeps owner-only permissions on the rewritten file", async () => { + const root = await tempRoot(); + const file = new EnvLocalFile(root); + await Bun.write(file.path, "# c\nDROP='v'\nKEEP=1\n"); + await chmod(file.path, 0o644); + + await file.removeKeys(["DROP"]); + + expect((await stat(file.path)).mode & 0o777).toBe(0o600); +}); diff --git a/src/core/project/envLocal.ts b/src/core/project/envLocal.ts index 32143f3db..720f3a5af 100644 --- a/src/core/project/envLocal.ts +++ b/src/core/project/envLocal.ts @@ -8,6 +8,7 @@ import type { EnvLocalEntry } from "../../handlers/project/types"; export const ENV_LOCAL_RELATIVE_PATH = join("agentcore", ".env.local"); const KEY_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/; +const COMMENT_LINE = /^\s*#/; const SECRET_FILE_MODE = 0o600; /** @@ -65,6 +66,44 @@ export class EnvLocalFile { return { written, skipped }; } + /** + * Deletes entries by key, along with the comment line `insertIfNew` wrote + * directly above each one. Keys that are absent (or a file that does not + * exist) are reported rather than treated as errors, so callers can say + * exactly what changed. Snapshots the prior state for `rollback`, like + * `insertIfNew`. + */ + async removeKeys(keys: string[]): Promise<{ removed: string[]; missing: string[] }> { + const targets = new Set(keys); + const existing = await this.readOrNull(); + if (existing === null) return { removed: [], missing: [...targets] }; + await this.enforcePermissions(); + + const kept: string[] = []; + const removedKeys = new Set(); + for (const line of existing.split("\n")) { + const key = KEY_LINE.exec(line)?.[1]; + if (key !== undefined && targets.has(key)) { + // Drop the entry's own comment line, but never an unrelated line above. + const previous = kept[kept.length - 1]; + if (previous !== undefined && COMMENT_LINE.test(previous)) kept.pop(); + removedKeys.add(key); + continue; + } + kept.push(line); + } + + if (removedKeys.size > 0) { + // Preserve the earliest snapshot so rollback undoes this whole operation. + if (this.snapshot === undefined) this.snapshot = existing; + await atomicWrite(this.path, kept.join("\n"), { mode: SECRET_FILE_MODE }); + } + return { + removed: [...targets].filter((key) => removedKeys.has(key)), + missing: [...targets].filter((key) => !removedKeys.has(key)), + }; + } + /** Restores the file to its pre-write state; a no-op when nothing was written. */ async rollback(): Promise { if (this.snapshot === undefined) return; diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index c306c7062..90f3fe88d 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -2,12 +2,20 @@ import { afterEach, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import { join, relative } from "node:path"; import { tmpdir } from "node:os"; -import { DeserializationError, ProjectStateError } from "../../errors/errors"; +import { + DeserializationError, + InputValidationError, + ProjectStateError, + ResourceNotFoundError, +} from "../../errors/errors"; import type { AwsDeploymentTarget } from "../../projectSchemas/aws-targets"; +import { credentialEnvVarName } from "../../projectSchemas/credential"; import { ProjectSpecSchema } from "../../projectSchemas/project"; +import { ENV_LOCAL_RELATIVE_PATH } from "./envLocal"; import { FsProjectManager } from "./manager"; import { resolveRuntimeTemplateShortcut } from "../../handlers/project/shortcuts"; import { + type AddResourceInput, type CreateProjectInput, type DeployResult, type Project, @@ -686,3 +694,146 @@ describe("FsProjectManager.resolve", () => { ); }); }); + +describe("FsProjectManager removal", () => { + async function runAdd( + subject: FsProjectManager, + project: Project, + input: AddResourceInput, + ): Promise { + const iterator = subject.addResource(project, input); + while (true) { + const next = await iterator.next(); + if (next.done) return next.value; + } + } + + async function createdProject(): Promise<{ subject: FsProjectManager; project: Project }> { + await inTempDirectory(); + const subject = manager().manager; + const { project } = await runCreate(subject, { + name: "example", + scaffoldRuntimeInput: HELLO_WORLD_PYTHON, + }); + return { subject, project }; + } + + test.each(["harness", "memory", "credential", "config-bundle", "online-eval"] as const)( + "removeResource throws ResourceNotFoundError for an unknown %s", + async (resourceType) => { + const { subject, project } = await createdProject(); + + const removal = subject.removeResource(project, { resourceType, name: "ghost" }); + + await expect(removal).rejects.toBeInstanceOf(ResourceNotFoundError); + await expect(removal).rejects.toThrow( + `no ${resourceType} named 'ghost' exists in this project`, + ); + }, + ); + + test("removing a credential deletes the .env.local keys it reserved", async () => { + const { subject, project } = await createdProject(); + const envKey = credentialEnvVarName("svc-key"); + const updated = await runAdd(subject, project, { + resourceType: "credential", + resourceConfig: { authorizerType: "ApiKeyCredentialProvider", name: "svc-key" }, + envEntries: [{ key: envKey, value: "sekret", comment: "API key for 'svc-key'" }], + }); + const envPath = join(project.rootPath, ENV_LOCAL_RELATIVE_PATH); + expect(await Bun.file(envPath).text()).toContain(envKey); + + const result = await subject.removeResource(updated, { + resourceType: "credential", + name: "svc-key", + }); + + expect(result.removedEnvKeys).toEqual([envKey]); + expect(result.project.spec.credentials).toEqual([]); + expect(await Bun.file(envPath).text()).not.toContain(envKey); + }); + + test("a removal that fails spec validation rolls back the .env.local edit", async () => { + const { subject, project } = await createdProject(); + // A payment connector references the credential, so removing the + // credential must be rejected — and the staged env deletion undone. + let current = await runAdd(subject, project, { + resourceType: "credential", + resourceConfig: { + authorizerType: "PaymentCredentialProvider", + name: "pay-cred", + provider: "CoinbaseCDP", + }, + envEntries: [ + { key: credentialEnvVarName("pay-cred", "_API_KEY_ID"), value: "id", comment: "c" }, + { key: credentialEnvVarName("pay-cred", "_API_KEY_SECRET"), value: "s", comment: "c" }, + { key: credentialEnvVarName("pay-cred", "_WALLET_SECRET"), value: "w", comment: "c" }, + ], + }); + current = await runAdd(subject, current, { + resourceType: "payment-manager", + resourceConfig: { name: "payments" }, + }); + current = await runAdd(subject, current, { + resourceType: "payment-connector", + managerName: "payments", + resourceConfig: { name: "conn", credentialName: "pay-cred" }, + }); + const envPath = join(project.rootPath, ENV_LOCAL_RELATIVE_PATH); + const before = await Bun.file(envPath).text(); + const specBefore = await Bun.file(join(project.rootPath, "agentcore", "agentcore.json")).text(); + + await expect( + subject.removeResource(current, { resourceType: "credential", name: "pay-cred" }), + ).rejects.toBeInstanceOf(InputValidationError); + + expect(await Bun.file(envPath).text()).toBe(before); + expect(await Bun.file(join(project.rootPath, "agentcore", "agentcore.json")).text()).toBe( + specBefore, + ); + }); + + test("removeAllResources empties every collection and cleans .env.local", async () => { + const { subject, project } = await createdProject(); + const envKey = credentialEnvVarName("svc-key"); + let current = await runAdd(subject, project, { + resourceType: "credential", + resourceConfig: { authorizerType: "ApiKeyCredentialProvider", name: "svc-key" }, + envEntries: [{ key: envKey, value: "sekret", comment: "c" }], + }); + current = await runAdd(subject, current, { + resourceType: "memory", + resourceConfig: { name: "recall", eventExpiryDuration: 30, strategies: [] }, + }); + current = await runAdd(subject, current, { + resourceType: "payment-manager", + resourceConfig: { name: "payments" }, + }); + const envPath = join(project.rootPath, ENV_LOCAL_RELATIVE_PATH); + + const result = await subject.removeAllResources(current); + + expect(result.removedEnvKeys).toEqual([envKey]); + expect(result.project.spec.runtimes).toEqual([]); + expect(result.project.spec.memories).toEqual([]); + expect(result.project.spec.credentials).toEqual([]); + expect(result.project.spec.payments).toBeUndefined(); + expect(result.project.spec.name).toBe("example"); + expect(result.project.spec.managedBy).toBe("CDK"); + expect(await Bun.file(envPath).text()).not.toContain(envKey); + + // The spec on disk matches what was returned. + const onDisk = await Bun.file(join(project.rootPath, "agentcore", "agentcore.json")).json(); + expect(onDisk.runtimes).toEqual([]); + expect(onDisk.payments).toBeUndefined(); + }); + + test("removeAllResources is idempotent on an already-empty project", async () => { + const { subject, project } = await createdProject(); + const once = await subject.removeAllResources(project); + const twice = await subject.removeAllResources(once.project); + + expect(twice.removedEnvKeys).toEqual([]); + expect(twice.project.spec.runtimes).toEqual([]); + }); +}); diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 9a2853dbf..8d4d8983e 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -12,6 +12,7 @@ import type { ProjectEvent, ProjectResource, RemoveResourceInput, + RemoveResourceResult, } from "../../handlers/project/types"; import type { Logger } from "../../logging"; import { @@ -28,7 +29,10 @@ import { createProjectTree } from "./templates/project"; import { getRuntimeTemplateResolver } from "./templates/runtime"; import { ProjectSpecSchema, type ManagedBy } from "../../projectSchemas/project"; import { ConfigBundleSchema } from "../../projectSchemas/config-bundle"; -import { CredentialSchema } from "../../projectSchemas/credential"; +import { + CredentialSchema, + credentialEnvironmentVariableNames, +} from "../../projectSchemas/credential"; import { MemorySchema } from "../../projectSchemas/memory"; import { EvaluatorSchema } from "../../projectSchemas/evaluator"; import { OnlineEvalConfigSchema } from "../../projectSchemas/online-eval-config"; @@ -42,6 +46,7 @@ import { MalformedServiceResponseError, NotImplementedError, ProjectStateError, + ResourceNotFoundError, } from "../../errors/errors"; import z from "zod"; import { CdkBackend } from "./backends/cdk"; @@ -421,7 +426,10 @@ export class FsProjectManager implements ProjectManager { return projectSpecPath(project.rootPath); } - public async removeResource(project: Project, input: RemoveResourceInput): Promise { + public async removeResource( + project: Project, + input: RemoveResourceInput, + ): Promise { const agentCoreSpecPath = this.getProjectSpecPath(project); const existingProjectSpec = await this.json.read(agentCoreSpecPath, ProjectSpecSchema); @@ -462,22 +470,28 @@ export class FsProjectManager implements ProjectManager { } else if (input.resourceType === "gateway-target") { const gateways = [...existingProjectSpec.agentCoreGateways]; const gatewayIndex = gateways.findIndex((gateway) => gateway.name === input.gatewayName); - if (gatewayIndex >= 0) { - const gateway = gateways[gatewayIndex]!; - const targets = gateway.targets.filter((target) => target.name !== input.name); - removed = targets.length !== gateway.targets.length; - gateways[gatewayIndex] = { ...gateway, targets }; + if (gatewayIndex < 0) { + throw new ResourceNotFoundError( + `no gateway named '${input.gatewayName}' exists in this project`, + ); } + const gateway = gateways[gatewayIndex]!; + const targets = gateway.targets.filter((target) => target.name !== input.name); + removed = targets.length !== gateway.targets.length; + gateways[gatewayIndex] = { ...gateway, targets }; newSpec = { ...existingProjectSpec, agentCoreGateways: gateways }; } else if (input.resourceType === "payment-connector") { const payments = [...(existingProjectSpec.payments ?? [])]; const managerIndex = payments.findIndex((manager) => manager.name === input.managerName); - if (managerIndex >= 0) { - const manager = payments[managerIndex]!; - const connectors = manager.connectors.filter((connector) => connector.name !== input.name); - removed = connectors.length !== manager.connectors.length; - payments[managerIndex] = { ...manager, connectors }; + if (managerIndex < 0) { + throw new ResourceNotFoundError( + `no payment-manager named '${input.managerName}' exists in this project`, + ); } + const manager = payments[managerIndex]!; + const connectors = manager.connectors.filter((connector) => connector.name !== input.name); + removed = connectors.length !== manager.connectors.length; + payments[managerIndex] = { ...manager, connectors }; newSpec = { ...existingProjectSpec, payments }; } else { const projectSpecKey = toProjectSpecKey(input.resourceType); @@ -487,26 +501,107 @@ export class FsProjectManager implements ProjectManager { newSpec = { ...existingProjectSpec, [projectSpecKey]: newResources }; } - if (!removed) - this.logger - .child({ input }) - .warn(`unable to remove resource from project that does not exist.`); + if (!removed) { + throw new ResourceNotFoundError( + `no ${input.resourceType} named '${input.name}' exists in this project`, + ); + } + + // A credential's secret material lives in .env.local, so removing the + // credential also deletes the keys it reserved (none when an external + // secretRef holds the material). + let envFile: EnvLocalFile | undefined; + let removedEnvKeys: string[] = []; + if (input.resourceType === "credential") { + const credential = existingProjectSpec.credentials.find( + (candidate) => candidate.name === input.name, + )!; + const envKeys = credentialEnvironmentVariableNames(credential); + if (envKeys.length > 0) { + envFile = new EnvLocalFile(project.rootPath); + removedEnvKeys = (await envFile.removeKeys(envKeys)).removed; + } + } - const newSpecParseResult = ProjectSpecSchema.safeParse(newSpec); + const newProjectSpec = await this.commitSpec(agentCoreSpecPath, newSpec, envFile); - if (!newSpecParseResult.success) - throw new InputValidationError(z.prettifyError(newSpecParseResult.error), { - cause: newSpecParseResult.error, - }); + return { + project: { ...project, spec: newProjectSpec }, + removedEnvKeys, + }; + } - const newProjectSpec = await this.json.write(agentCoreSpecPath, newSpecParseResult.data); + public async removeAllResources(project: Project): Promise { + const agentCoreSpecPath = this.getProjectSpecPath(project); + const existingProjectSpec = await this.json.read(agentCoreSpecPath, ProjectSpecSchema); + + let envFile: EnvLocalFile | undefined; + let removedEnvKeys: string[] = []; + const envKeys = existingProjectSpec.credentials.flatMap((credential) => + credentialEnvironmentVariableNames(credential), + ); + if (envKeys.length > 0) { + envFile = new EnvLocalFile(project.rootPath); + removedEnvKeys = (await envFile.removeKeys(envKeys)).removed; + } + + // A spec-level reset, mirroring the original CLI's `remove all`: every + // resource collection is emptied while name, version, managedBy, tags, and + // $schema survive. Code under app/ and aws-targets.json are left in place + // so a following deploy can tear down the target's stack. + const newSpec = { + ...existingProjectSpec, + runtimes: [], + memories: [], + knowledgeBases: [], + credentials: [], + evaluators: [], + onlineEvalConfigs: [], + agentCoreGateways: [], + policyEngines: [], + configBundles: [], + abTests: [], + harnesses: [], + mcpRuntimeTools: undefined, + unassignedTargets: undefined, + datasets: undefined, + httpGateways: undefined, + payments: undefined, + }; + + const newProjectSpec = await this.commitSpec(agentCoreSpecPath, newSpec, envFile); return { - ...project, - spec: newProjectSpec, + project: { ...project, spec: newProjectSpec }, + removedEnvKeys, }; } + // Validates and writes an updated spec; a failure rolls back any .env.local + // edit staged for the same removal so the two files stay consistent. + private async commitSpec( + agentCoreSpecPath: string, + newSpec: unknown, + envFile: EnvLocalFile | undefined, + ): Promise> { + try { + const newSpecParseResult = ProjectSpecSchema.safeParse(newSpec); + if (!newSpecParseResult.success) + throw new InputValidationError(z.prettifyError(newSpecParseResult.error), { + cause: newSpecParseResult.error, + }); + return await this.json.write(agentCoreSpecPath, newSpecParseResult.data); + } catch (err) { + await envFile?.rollback().catch((e) => { + const error = AgentCoreCLIError.fromError(e); + this.logger + .child({ errorName: error.name, errorMessage: error.message }) + .warn(`failed to roll back ${ENV_LOCAL_RELATIVE_PATH}`); + }); + throw err; + } + } + private async scaffoldRuntimeResources(outputPath: string, input: RuntimeResourceConfig) { const resolver = getRuntimeTemplateResolver( { assetSource: this.assetSource, templateRenderer: this.templateRenderer }, diff --git a/src/handlers/project/remove/index.test.ts b/src/handlers/project/remove/index.test.ts index d06871d18..137b88a20 100644 --- a/src/handlers/project/remove/index.test.ts +++ b/src/handlers/project/remove/index.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { existsSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -8,9 +9,16 @@ import { TestCoreClient, TestGlobalConfigAccessor, testIO, + type TestIOOptions, } from "../../../testing"; -import { InputValidationError } from "../../../errors"; +import { + InputValidationError, + ResourceNotFoundError, + UserCancellationError, +} from "../../../errors"; import { projectSpec, writeProjectSpec } from "../add/gateway-test-support"; +import { credentialEnvVarName } from "../../../projectSchemas/credential"; +import { ENV_LOCAL_RELATIVE_PATH } from "../../../core/project/envLocal"; const originalCwd = process.cwd(); const tempDirectories: string[] = []; @@ -29,8 +37,8 @@ afterEach(async () => { ); }); -async function run(args: string[]) { - const io = testIO(); +async function run(args: string[], ioOptions?: TestIOOptions) { + const io = testIO(ioOptions); const core = new TestCoreClient(); const root = createRootHandler(core, { io: io.io, @@ -93,12 +101,6 @@ describe("project remove", () => { specKey: "harnesses", expectedRemaining: ["keep_me"], }, - { - label: "removing a non-existent resource succeeds (no-op)", - commands: [["remove", "harness", "--name", "ghost"]], - specKey: "harnesses", - expectedRemaining: [], - }, { label: "gateway", commands: [ @@ -109,6 +111,71 @@ describe("project remove", () => { specKey: "agentCoreGateways", expectedRemaining: ["keep"], }, + { + label: "config-bundle", + commands: [ + [ + "add", + "config-bundle", + "--name", + "OrdersConfig", + "--components", + '{"arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/orders-agent":{"configuration":{"temperature":0.2}}}', + ], + ["remove", "config-bundle", "--name", "OrdersConfig"], + ], + specKey: "configBundles", + expectedRemaining: [], + }, + { + label: "online-eval", + commands: [ + [ + "add", + "online-eval", + "--name", + "quality", + "--agent", + "hello_world", + "--evaluator", + "Builtin.Correctness", + "--sampling-rate", + "5", + ], + ["remove", "online-eval", "--name", "quality"], + ], + specKey: "onlineEvalConfigs", + expectedRemaining: [], + }, + { + label: "online-insight", + commands: [ + [ + "add", + "online-insight", + "--name", + "failures", + "--agent", + "hello_world", + "--insight", + "Builtin.Insight.FailureAnalysis", + "--sampling-rate", + "5", + ], + ["remove", "online-insight", "--name", "failures"], + ], + specKey: "onlineEvalConfigs", + expectedRemaining: [], + }, + { + label: "memory", + commands: [ + ["add", "memory", "--name", "recall"], + ["remove", "memory", "--name", "recall"], + ], + specKey: "memories", + expectedRemaining: [], + }, ])("$label", async ({ commands, specKey, expectedRemaining }) => { const projectRoot = await inProject(); @@ -121,6 +188,63 @@ describe("project remove", () => { expect(remaining.map((r) => r.name)).toEqual(expectedRemaining); }); + test("removing a non-existent resource fails with a not-found error", async () => { + const projectRoot = await inProject(); + const before = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).text(); + + const removal = run(["remove", "harness", "--name", "ghost"]); + await expect(removal).rejects.toBeInstanceOf(ResourceNotFoundError); + await expect(removal).rejects.toThrow(`no harness named 'ghost' exists in this project`); + + // The spec file is untouched, unlike the old warn-and-rewrite behavior. + expect(await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).text()).toBe(before); + }); + + test("removing a target of a non-existent gateway names the missing gateway", async () => { + await inProject(); + await expect( + run(["remove", "gateway-target", "--gateway", "ghost", "--name", "t"]), + ).rejects.toThrow(`no gateway named 'ghost' exists in this project`); + }); + + test("removing a credential deletes its .env.local entry and reports it", async () => { + const projectRoot = await inProject(); + await run(["add", "credentials", "api-key", "--name", "svc-key", "--api-key", "-"], { + stdin: "sekret", + }); + const envPath = join(projectRoot, ENV_LOCAL_RELATIVE_PATH); + const envKey = credentialEnvVarName("svc-key"); + expect(await Bun.file(envPath).text()).toContain(`${envKey}='sekret'`); + + const { io } = await run(["remove", "credential", "--name", "svc-key"]); + + expect((await projectSpec(projectRoot)).credentials).toEqual([]); + expect(await Bun.file(envPath).text()).not.toContain(envKey); + expect(io.stderr()).toContain(`removed '${envKey}' from ${ENV_LOCAL_RELATIVE_PATH}`); + expect(io.stdout()).toContain("removed credential with name 'svc-key' from project"); + }); + + test("removing a secret-reference credential leaves .env.local alone", async () => { + const projectRoot = await inProject(); + await run([ + "add", + "credentials", + "api-key", + "--name", + "ext-key", + "--api-key-secret-reference", + '{"secretId":"arn:aws:secretsmanager:us-east-1:123456789012:secret:x","jsonKey":"k"}', + ]); + const envPath = join(projectRoot, ENV_LOCAL_RELATIVE_PATH); + await Bun.write(envPath, "USER_MANAGED=1\n"); + + const { io } = await run(["remove", "credential", "--name", "ext-key"]); + + expect((await projectSpec(projectRoot)).credentials).toEqual([]); + expect(await Bun.file(envPath).text()).toBe("USER_MANAGED=1\n"); + expect(io.stderr()).not.toContain("removed '"); + }); + test.each([ { resource: "gateway-target", @@ -322,3 +446,142 @@ describe("project remove", () => { expect(spec.agentCoreGateways[0].policyEngineConfiguration).toBeUndefined(); }); }); + +describe("project remove all", () => { + // Fills a project with one of everything the CLI can add, plus an + // unassignedTargets entry only reachable by editing the spec. + async function populatedProject(): Promise { + const projectRoot = await inProject(); + await run(["add", "harness", "--name", "my_harness"]); + await run(["add", "gateway", "--name", "tools"]); + await run([ + "add", + "gateway-target", + "--gateway", + "tools", + "--name", + "search", + "--endpoint", + "https://search.example.com", + ]); + await run(["add", "credentials", "api-key", "--name", "svc-key", "--api-key", "-"], { + stdin: "sekret", + }); + await run(["add", "memory", "--name", "recall"]); + await run(["add", "policy-engine", "--name", "Guardrails"]); + await run([ + "add", + "policy", + "--engine", + "Guardrails", + "--name", + "DenyAll", + "--statement", + "forbid (principal, action, resource);", + ]); + await run(["add", "payment-manager", "--name", "payments"]); + await run([ + "add", + "config-bundle", + "--name", + "OrdersConfig", + "--components", + '{"arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/orders-agent":{"configuration":{"temperature":0.2}}}', + ]); + const spec = await projectSpec(projectRoot); + spec.unassignedTargets = [structuredClone(spec.agentCoreGateways[0].targets[0])]; + spec.unassignedTargets[0].name = "orphan"; + await writeProjectSpec(projectRoot, spec); + return projectRoot; + } + + test("--yes empties every resource collection while keeping non-resource fields", async () => { + const projectRoot = await populatedProject(); + const before = await projectSpec(projectRoot); + const envPath = join(projectRoot, ENV_LOCAL_RELATIVE_PATH); + const envKey = credentialEnvVarName("svc-key"); + + const { io } = await run(["remove", "all", "--yes"]); + + const spec = await projectSpec(projectRoot); + for (const collection of [ + "runtimes", + "memories", + "knowledgeBases", + "credentials", + "evaluators", + "onlineEvalConfigs", + "agentCoreGateways", + "policyEngines", + "configBundles", + "abTests", + "harnesses", + ]) { + expect(spec[collection]).toEqual([]); + } + for (const collection of [ + "mcpRuntimeTools", + "unassignedTargets", + "datasets", + "httpGateways", + "payments", + ]) { + expect(spec[collection]).toBeUndefined(); + } + expect(spec.name).toBe(before.name); + expect(spec.version).toBe(before.version); + expect(spec.managedBy).toBe(before.managedBy); + + // Removal stays spec-level: scaffolded code and the credential's env entry. + expect(existsSync(join(projectRoot, "app", "hello_world"))).toBe(true); + expect(await Bun.file(envPath).text()).not.toContain(envKey); + expect(io.stderr()).toContain(`removed '${envKey}' from ${ENV_LOCAL_RELATIVE_PATH}`); + expect(io.stdout()).toContain("removed all resources from project"); + }); + + test("prompts on a TTY and proceeds on 'y'", async () => { + const projectRoot = await inProject(); + + const { io } = await run(["remove", "all"], { isTTY: true, stdin: "y\n" }); + + expect(io.stderr()).toContain("Remove every resource from project 'TestProject'?"); + expect((await projectSpec(projectRoot)).runtimes).toEqual([]); + }); + + test("declining the prompt cancels without touching the spec", async () => { + const projectRoot = await inProject(); + const before = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).text(); + + await expect(run(["remove", "all"], { isTTY: true, stdin: "n\n" })).rejects.toBeInstanceOf( + UserCancellationError, + ); + + expect(await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).text()).toBe(before); + }); + + test("without --yes and without a TTY it fails rather than proceeding", async () => { + const projectRoot = await inProject(); + const before = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).text(); + + const removal = run(["remove", "all"]); + await expect(removal).rejects.toBeInstanceOf(InputValidationError); + await expect(removal).rejects.toThrow(/--yes/); + + expect(await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).text()).toBe(before); + }); + + test("rejects --name alongside all", async () => { + await inProject(); + await expect(run(["remove", "all", "--name", "x", "--yes"])).rejects.toThrow( + "--name is not valid when removing all resources", + ); + }); + + test("is idempotent on an already-empty project", async () => { + const projectRoot = await inProject(); + await run(["remove", "all", "--yes"]); + await run(["remove", "all", "--yes"]); + + expect((await projectSpec(projectRoot)).runtimes).toEqual([]); + }); +}); diff --git a/src/handlers/project/remove/index.ts b/src/handlers/project/remove/index.ts index 13f66f669..40bfc132a 100644 --- a/src/handlers/project/remove/index.ts +++ b/src/handlers/project/remove/index.ts @@ -1,7 +1,10 @@ +import { createInterface } from "node:readline/promises"; import { argument, createHandler, flag, ProjectKey } from "../../../router"; -import { InputValidationError } from "../../../errors"; +import { InputValidationError, UserCancellationError } from "../../../errors"; import z from "zod"; import type { AppIO } from "../../../io"; +import { ENV_LOCAL_RELATIVE_PATH } from "../../../core/project/envLocal"; +import { JsonKey } from "../../keys"; import type { ProjectManager } from "../types"; type RemoveProjectResourceConfig = { @@ -22,15 +25,25 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) "name of the parent payment manager for a connector", z.string().min(1).optional(), ), + flag( + "yes", + "skip the confirmation prompt when removing all resources", + z.boolean().default(false), + ), ], arguments: [ argument( "resource", - "type of resource to remove", + "type of resource to remove ('all' empties every resource collection)", z .enum([ "harness", "runtime", + "credential", + "config-bundle", + "online-eval", + "online-insight", + "memory", "gateway", "gateway-target", "gateway-connector", @@ -38,15 +51,14 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) "policy", "payment-manager", "payment-connector", + "all", ]) .optional(), ), ], handle: async (ctx, flags, args) => { const resource = args["resource"]; - const name = flags["name"]; if (!resource) throw new InputValidationError(`resource argument is required to remove`); - if (!name) throw new InputValidationError(`--name is required option`); if (flags.gateway && resource !== "gateway-target" && resource !== "gateway-connector") { throw new InputValidationError( @@ -61,17 +73,33 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) } const project = ctx.require(ProjectKey); + + if (resource === "all") { + if (flags.name) { + throw new InputValidationError(`--name is not valid when removing all resources`); + } + await confirmRemoveAll(config.io, ctx.require(JsonKey), flags.yes, project.name); + const result = await config.projectManager.removeAllResources(project); + reportEnvCleanup(config.io, result.removedEnvKeys); + config.io.stdout.write(`removed all resources from project`); + return; + } + + const name = flags["name"]; + if (!name) throw new InputValidationError(`--name is required option`); + + let result; if (resource === "gateway-target" || resource === "gateway-connector") { if (!flags.gateway) { throw new InputValidationError(`--gateway is required option`); } - await config.projectManager.removeResource(project, { + result = await config.projectManager.removeResource(project, { resourceType: "gateway-target", gatewayName: flags.gateway, name, }); } else if (resource === "policy") { - await config.projectManager.removeResource(project, { + result = await config.projectManager.removeResource(project, { resourceType: "policy", engineName: flags.engine, name, @@ -80,18 +108,67 @@ export const createRemoveProjectHandler = (config: RemoveProjectResourceConfig) if (!flags.manager) { throw new InputValidationError(`--manager is required option`); } - await config.projectManager.removeResource(project, { + result = await config.projectManager.removeResource(project, { resourceType: "payment-connector", managerName: flags.manager, name, }); } else { - await config.projectManager.removeResource(project, { + result = await config.projectManager.removeResource(project, { resourceType: resource, name, }); } + reportEnvCleanup(config.io, result.removedEnvKeys); config.io.stdout.write(`removed ${resource} with name '${name}' from project`); }, }); + +function reportEnvCleanup(io: AppIO, removedEnvKeys: string[]): void { + for (const key of removedEnvKeys) { + io.stderr.write(`removed '${key}' from ${ENV_LOCAL_RELATIVE_PATH}\n`); + } +} + +// Mirrors the deploy handler's teardown confirmation: --yes bypasses the +// prompt, a non-interactive session fails rather than proceeding, and a +// decline (or SIGINT) raises UserCancellationError. +async function confirmRemoveAll( + io: AppIO, + jsonOutput: boolean, + confirmed: boolean, + projectName: string, +): Promise { + if (confirmed) return; + const canPrompt = !jsonOutput && io.stdin.isTTY && io.stdout.isTTY && io.stderr.isTTY; + if (!canPrompt) { + throw new InputValidationError( + `removing all resources is destructive; re-run with --yes to confirm non-interactively`, + ); + } + if (!(await promptForRemoveAll(io, projectName))) { + throw new UserCancellationError(); + } +} + +async function promptForRemoveAll(io: AppIO, projectName: string): Promise { + const readline = createInterface({ input: io.stdin, output: io.stderr }); + try { + const cancelled = new Promise((_resolve, reject) => { + const cancel = () => reject(new UserCancellationError()); + readline.once("SIGINT", cancel); + readline.once("close", cancel); + }); + const answer = await Promise.race([ + readline.question( + `Remove every resource from project '${projectName}'?\n` + + `This empties each resource collection in agentcore.json; code under app/ is kept. (y/N) `, + ), + cancelled, + ]); + return /^(?:y|yes)$/i.test(answer.trim()); + } finally { + readline.close(); + } +} diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 7720da87e..93f032393 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -245,6 +245,13 @@ export type RemoveResourceInput = name: string; }; +/** The outcome of a spec-level removal. */ +export type RemoveResourceResult = { + project: Project; + /** .env.local keys deleted because the removed credential(s) reserved them. */ + removedEnvKeys: string[]; +}; + /** * The primary interface for interacting with projects */ @@ -264,6 +271,17 @@ export interface ProjectManager { /** Add a resource to an existing AgentCore project. */ addResource(project: Project, input: AddResourceInput): AsyncGenerator; - /** Remove a resource from an existing AgentCore project. */ - removeResource(project: Project, input: RemoveResourceInput): Promise; + /** + * Remove a resource from an existing AgentCore project. Throws + * ResourceNotFoundError when nothing with the given name exists. + */ + removeResource(project: Project, input: RemoveResourceInput): Promise; + + /** + * Empty every resource collection in the project spec, leaving name, + * version, managedBy, and other non-resource fields intact. Spec-level only: + * code directories under app/ and aws-targets.json survive, so a following + * deploy can tear down the target's stack. + */ + removeAllResources(project: Project): Promise; } diff --git a/src/testing/index.tsx b/src/testing/index.tsx index 44de6961e..7597dd900 100644 --- a/src/testing/index.tsx +++ b/src/testing/index.tsx @@ -1,6 +1,6 @@ export { parse, stringify } from "./serialization"; export { fixtureFactories, fixtureFetch, isRecording, matchGolden, settle } from "./fixtures"; -export { testIO, ttyTestIO, type TestIO, type TtyInput } from "./testIO"; +export { testIO, ttyTestIO, type TestIO, type TestIOOptions, type TtyInput } from "./testIO"; export { tick, waitFor, WaitForTimeoutError } from "./timing"; export { TestCoreClient, From 7a5c5a7ef3b958d1467f7ceefdf9baef6afec470 Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Mon, 31 Aug 2026 00:07:39 -0400 Subject: [PATCH 06/12] feat(cli): add --version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bare `agentcore --version` (and -V) prints the package version embedded at build time and exits 0, in dev, bundled, and compiled forms. Handled as a pre-parse intercept on the root router rather than a Commander version option: a root-level --version option would shadow subcommands that declare their own `--version ` flag (harness version get, runtime version get, dataset --version, …). Root-only, matching the original CLI. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- src/handlers/index.tsx | 4 ++++ src/router/router.test.ts | 41 ++++++++++++++++++++++++++++++++++++++- src/router/router.tsx | 31 +++++++++++++++++++++++++++-- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx index 188f61224..a2ff49e03 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -14,6 +14,7 @@ import type { AppIO } from "../io"; import type { Core } from "./types.tsx"; import type { Logger } from "../logging"; import type { GlobalConfigAccessor } from "../globalConfig"; +import { PACKAGE_VERSION } from "../constants"; export interface RootHandlerConfig { io: AppIO; @@ -25,6 +26,9 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router const { io, logger } = config; const root = new Router("agentcore", "the platform for production AI agents"); + // `agentcore --version` prints the build-time package version. + root.version(PACKAGE_VERSION); + // Add global flags root.groupFlags(RegionKey, DebugKey, JsonKey, EndpointKey); diff --git a/src/router/router.test.ts b/src/router/router.test.ts index dba80053b..6a8eb899f 100644 --- a/src/router/router.test.ts +++ b/src/router/router.test.ts @@ -17,7 +17,7 @@ import { type Handler, type Middleware, } from "./index"; -import { InputValidationError } from "../errors"; +import { AgentCoreCLIError, InputValidationError } from "../errors"; import { DefaultTelemetryClient } from "../telemetry"; import { createSilentLogger, TestGlobalConfigAccessor } from "../testing"; @@ -788,3 +788,42 @@ test.each([ exit_reason: shouldThrow ? "failure" : "success", }); }); + +test("--version on a versioned router prints the version and maps to exit 0", async () => { + const root = new Router("agentcore").version("9.9.9"); + root.handler( + createHandler({ + name: "noop", + description: "", + flags: [], + handle: async () => {}, + }), + ); + + // Commander surfaces the version exit through exitOverride as a + // CommanderError; the error layer maps its exitCode 0 through unchanged. + const error = await root.route(["node", "agentcore", "--version"]).then( + () => { + throw new Error("expected --version to exit via CommanderError"); + }, + (e: unknown) => e, + ); + expect(error).toMatchObject({ code: "commander.version", message: "9.9.9" }); + expect(AgentCoreCLIError.fromError(error).exitCode).toBe(0); +}); + +test("--version is an unknown option on a router without a version", async () => { + const root = new Router("agentcore"); + root.handler( + createHandler({ + name: "noop", + description: "", + flags: [], + handle: async () => {}, + }), + ); + + await expect(root.route(["node", "agentcore", "--version"])).rejects.toMatchObject({ + code: "commander.unknownOption", + }); +}); diff --git a/src/router/router.tsx b/src/router/router.tsx index d45835246..e33133ad2 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -4,7 +4,7 @@ import { type Context, type ContextKey, ValueContext, contextKey } from "./conte import { applyGlobalFlags, formatParameterDetails, parseFlags, toOption } from "./flags"; import { parseArguments, toCommanderArgument } from "./args"; -import { Command } from "commander"; +import { Command, CommanderError } from "commander"; import type { Logger } from "../logging"; import type { GlobalConfigAccessor } from "../globalConfig"; import type { Project } from "../handlers/project/types"; @@ -226,6 +226,7 @@ export class Router implements Handler, MiddlewareProvider, DefaultHandlerProvid private globalFlags: GlobalFlag[] = []; private defaultHandle?: DefaultHandle; private tuiCommandNames?: ReadonlySet; + private cliVersion?: string; constructor( private readonly cmdName: string, @@ -269,6 +270,16 @@ export class Router implements Handler, MiddlewareProvider, DefaultHandlerProvid return this; } + // version makes a bare `--version`/`-V` on this router print the given + // string and exit 0. It is handled before Commander parses (root command + // only, like the original CLI) rather than registered as a Commander + // option: a root-level --version option would shadow subcommands that + // declare their own `--version ` flag (e.g. `harness version get`). + version(version: string): this { + this.cliVersion = version; + return this; + } + // --- Handler API: a router is itself a mountable branch node --- name(): string { @@ -327,6 +338,22 @@ export class Router implements Handler, MiddlewareProvider, DefaultHandlerProvid // --- Router execution --- async route(argv: string[], ctx: Context = ValueContext.EmptyContext()): Promise { - await compile(this, ctx).parseAsync(argv); + const commandArgs = argv.slice(2); + if ( + this.cliVersion && + commandArgs.length === 1 && + (commandArgs[0] === "--version" || commandArgs[0] === "-V") + ) { + process.stdout.write(`${this.cliVersion}\n`); + // The same exit shape Commander's own version option produces, so the + // error layer maps it to a silent exit 0. + throw new CommanderError(0, "commander.version", this.cliVersion); + } + + const command = compile(this, ctx); + if (this.cliVersion) { + command.addHelpText("after", `\nRun '${this.cmdName} --version' to print the CLI version.`); + } + await command.parseAsync(argv); } } From b8367f3b6ebc8c7ed408542f1dd14fcdcb72b2de Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Mon, 31 Aug 2026 00:11:12 -0400 Subject: [PATCH 07/12] feat(project): export a harness to a Strands runtime agent `agentcore project export harness` converts a harness into an editable Python Strands runtime agent: it renders the strands-http-python template under app// with a context mapped from the harness spec (model incl. Bedrock Mantle/OpenAI/Gemini/LiteLLM, system prompt, remote MCP + inline-function tools, path/s3/git skills, in-project memory, truncation, and execution limits via a vendored hooks/execution_limits.py), registers the runtime in agentcore.json (the harness entry stays), installs its deps with uv sync, and writes an EXPORT_NOTES.md in the agent directory listing precise manual follow-ups for everything that cannot be mapped mechanically (gateway/browser/code-interpreter tools, external/managed memory, aws skills, custom Dockerfiles, ...). The harness comes from `--name` (in-project files) or `--arn` (fetched from the service via GetHarness, using the region embedded in the ARN); exactly one is required, and the project is resolved and validated by withProject before any service fetch. `--target-agent-name` defaults to Agent, `--build` overrides the CodeZip default (containerUri / dockerfile harnesses auto-select Container), and `--json` emits a machine-readable summary on stdout while progress and notes stay on stderr. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- README.md | 12 + .../hooks/execution_limits.py | 54 + .../strands-http-python/model/load.py | 2 +- src/components/Root.tsx | 11 +- src/core/project/manager.export.test.ts | 288 +++++ src/core/project/manager.tsx | 216 +++- src/core/project/templates/export.test.ts | 650 ++++++++++ src/core/project/templates/export.ts | 1060 +++++++++++++++++ src/core/project/templates/runtime.ts | 6 +- src/handlers/project/export/harness.test.ts | 268 +++++ src/handlers/project/export/harness.ts | 115 ++ src/handlers/project/export/index.ts | 17 + .../project/export/serviceHarness.test.ts | 279 +++++ src/handlers/project/export/serviceHarness.ts | 246 ++++ src/handlers/project/export/types.ts | 11 + src/handlers/project/index.ts | 2 + src/handlers/project/types.ts | 40 + 17 files changed, 3273 insertions(+), 4 deletions(-) create mode 100644 src/assets/templates/strands-http-python/hooks/execution_limits.py create mode 100644 src/core/project/manager.export.test.ts create mode 100644 src/core/project/templates/export.test.ts create mode 100644 src/core/project/templates/export.ts create mode 100644 src/handlers/project/export/harness.test.ts create mode 100644 src/handlers/project/export/harness.ts create mode 100644 src/handlers/project/export/index.ts create mode 100644 src/handlers/project/export/serviceHarness.test.ts create mode 100644 src/handlers/project/export/serviceHarness.ts create mode 100644 src/handlers/project/export/types.ts diff --git a/README.md b/README.md index 16626581a..c87c27e25 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ agentcore # interactive TUI │ ├── create # create a project: a managed harness by default, │ │ # or scaffolded runtime code via --template/--framework │ ├── add # add a resource to the project (runtime, harness, memory, …) +│ ├── export +│ │ └── harness # convert a harness into an editable Strands runtime agent │ ├── remove # remove a resource from the project spec (spec-level; │ │ # code under app/ is kept). Resource types: harness, │ │ # runtime, credential, config-bundle, online-eval, @@ -122,6 +124,16 @@ agentcore # interactive TUI └── config # read/write global config values ``` +`project export harness` "ejects" a harness to code you own: it renders a +Python Strands agent under `app//` mapping the harness spec +(model, system prompt, tools, skills, memory, execution limits), registers the +new runtime in `agentcore.json` (the harness entry stays), and writes an +`EXPORT_NOTES.md` in the agent directory listing anything that could not be +mapped mechanically. Pass `--name ` for an in-project harness or +`--arn ` to fetch a deployed one (the fetch uses the region +embedded in the ARN); `--target-agent-name` overrides the default +`Agent`, and `--build CodeZip|Container` overrides the build type. + Global flags (declared at the root, available on every command): | Flag | Purpose | diff --git a/src/assets/templates/strands-http-python/hooks/execution_limits.py b/src/assets/templates/strands-http-python/hooks/execution_limits.py new file mode 100644 index 000000000..057f348d8 --- /dev/null +++ b/src/assets/templates/strands-http-python/hooks/execution_limits.py @@ -0,0 +1,54 @@ +import time +from typing import Optional + +from strands.hooks import BeforeModelCallEvent +from strands.hooks.registry import HookProvider, HookRegistry +from strands.types.exceptions import EventLoopException + + +class ExecutionLimitExceeded(Exception): + def __init__(self, message: str) -> None: + super().__init__(message) + + +class ExecutionLimitsHook(HookProvider): + def __init__( + self, + max_iterations: Optional[int] = None, + max_tokens: Optional[int] = None, + timeout_seconds: Optional[float] = None, + ) -> None: + self._max_iterations = max_iterations + self._max_tokens = max_tokens + self._timeout_seconds = timeout_seconds + self._iteration_count = 0 + self._start_time = time.monotonic() + + def register_hooks(self, registry: HookRegistry, **kwargs) -> None: + registry.add_callback(BeforeModelCallEvent, self._check_limits) + + def _check_limits(self, event: BeforeModelCallEvent) -> None: + self._iteration_count += 1 + + if self._max_iterations is not None and self._iteration_count > self._max_iterations: + raise EventLoopException( + ExecutionLimitExceeded(f"Max iterations exceeded: {self._max_iterations}") + ) + + if self._timeout_seconds is not None: + elapsed = time.monotonic() - self._start_time + if elapsed > self._timeout_seconds: + raise EventLoopException( + ExecutionLimitExceeded( + f"Timeout exceeded: {self._timeout_seconds}s (elapsed {elapsed:.1f}s)" + ) + ) + + if self._max_tokens is not None: + used = event.agent.event_loop_metrics.accumulated_usage.get("outputTokens", 0) + if used >= self._max_tokens: + raise EventLoopException( + ExecutionLimitExceeded( + f"Max output tokens exceeded: {used}/{self._max_tokens}" + ) + ) diff --git a/src/assets/templates/strands-http-python/model/load.py b/src/assets/templates/strands-http-python/model/load.py index 0b3b23eac..05da58b20 100644 --- a/src/assets/templates/strands-http-python/model/load.py +++ b/src/assets/templates/strands-http-python/model/load.py @@ -65,7 +65,7 @@ def load_model(): def load_model() -> BedrockModel: """Get Bedrock model client using IAM credentials.""" - return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}"{{#if modelMaxTokens}}, max_tokens={{modelMaxTokens}}{{/if}}) + return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}"{{#if modelMaxTokens}}, max_tokens={{modelMaxTokens}}{{/if}}{{#if modelTemperature}}, temperature={{modelTemperature}}{{/if}}{{#if modelTopP}}, top_p={{modelTopP}}{{/if}}) {{/if}} {{/if}} {{#if (eq modelProvider "Anthropic")}} diff --git a/src/components/Root.tsx b/src/components/Root.tsx index b6ddc465c..76285c548 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -114,7 +114,16 @@ import type { Context } from "../router"; // PROJECT_COMMANDS are the `agentcore project` subcommands that are listed in // the menu but have no screen of their own yet. Each is routed explicitly so // selecting it reports "not implemented" error -const PROJECT_COMMANDS = ["create", "add", "remove", "dev", "deploy", "status", "build"] as const; +const PROJECT_COMMANDS = [ + "create", + "add", + "export", + "remove", + "dev", + "deploy", + "status", + "build", +] as const; export interface RootProps { // path is the command path to the executing node (e.g. "/agentcore"). diff --git a/src/core/project/manager.export.test.ts b/src/core/project/manager.export.test.ts new file mode 100644 index 000000000..02b0de805 --- /dev/null +++ b/src/core/project/manager.export.test.ts @@ -0,0 +1,288 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import z from "zod"; +import { FsProjectManager } from "./manager"; +import { FsReadWriteJson, type ReadWriteJson } from "../../io"; +import { createSilentLogger } from "../../testing"; +import { resolveRuntimeTemplateShortcut } from "../../handlers/project/shortcuts"; +import type { ExportHarnessInput, Project, ProjectEvent } from "../../handlers/project/types"; +import { HarnessSpecSchema } from "../../projectSchemas/harness"; + +const originalCwd = process.cwd(); +const tempDirectories: string[] = []; + +async function inTempDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), "agentcore-export-manager-")); + tempDirectories.push(directory); + process.chdir(directory); + return process.cwd(); +} + +afterEach(async () => { + process.chdir(originalCwd); + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +function manager(options: { json?: ReadWriteJson } = {}) { + const commands: { command: string[]; cwd: string }[] = []; + return { + manager: new FsProjectManager({ + logger: createSilentLogger(), + json: options.json, + runner: async (command, { cwd }) => { + commands.push({ command, cwd }); + }, + checkTool: async () => {}, + }), + commands, + }; +} + +async function drain(generator: AsyncGenerator): Promise { + let next = await generator.next(); + while (!next.done) next = await generator.next(); + return next.value; +} + +/** Creates a project with a harness built from `harness` overrides; returns the refreshed project. */ +async function projectWithHarness( + subject: FsProjectManager, + harness: Record = {}, +): Promise { + await inTempDirectory(); + let project = await drain( + subject.create({ + name: "orders", + skipInstall: true, + skipGit: true, + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("hello-world-python"), + }), + ); + project = await drain( + subject.addResource(project, { + resourceType: "harness", + resourceConfig: { + name: "assistant", + model: { provider: "bedrock", modelId: "us.amazon.nova-lite-v1:0" }, + systemPrompt: "You are a terse assistant.", + ...harness, + } as z.input, + }), + ); + return project; +} + +function exportInput(overrides: Partial = {}): ExportHarnessInput { + return { harnessName: "assistant", targetAgentName: "assistantAgent", ...overrides }; +} + +describe("FsProjectManager.exportHarness rendered tree", () => { + test("includes hooks/ only when the harness sets execution limits", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject, { maxIterations: 3 }); + + const result = await drain(subject.exportHarness(project, exportInput())); + + expect(existsSync(join(result.agentPath, "hooks", "execution_limits.py"))).toBe(true); + const main = await Bun.file(join(result.agentPath, "main.py")).text(); + expect(main).toContain( + "from hooks.execution_limits import ExecutionLimitExceeded, ExecutionLimitsHook", + ); + expect(main).toContain("max_iterations=3,"); + }); + + test("leaves hooks/ and memory/ out of a plain export", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject); + + const result = await drain(subject.exportHarness(project, exportInput())); + + expect(existsSync(join(result.agentPath, "hooks"))).toBe(false); + expect(existsSync(join(result.agentPath, "memory"))).toBe(false); + expect(existsSync(join(result.agentPath, "Dockerfile"))).toBe(false); + }); + + test("wires an in-project memory through memory/session.py", async () => { + const { manager: subject } = manager(); + let project = await projectWithHarness(subject, { + memory: { mode: "existing", name: "chat_history" }, + }); + project = await drain( + subject.addResource(project, { + resourceType: "memory", + resourceConfig: { + name: "chat_history", + eventExpiryDuration: 30, + strategies: [{ type: "SEMANTIC" }], + }, + }), + ); + + const result = await drain(subject.exportHarness(project, exportInput())); + + const session = await Bun.file(join(result.agentPath, "memory", "session.py")).text(); + expect(session).toContain('MEMORY_ID = os.getenv("MEMORY_CHAT_HISTORY_ID")'); + expect(await Bun.file(join(result.agentPath, "main.py")).text()).toContain( + "from memory.session import get_memory_session_manager", + ); + expect(result.notes).toEqual([]); + }); + + test("renders the template Dockerfile for a plain Container export", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject); + + const result = await drain(subject.exportHarness(project, exportInput({ build: "Container" }))); + + expect(await Bun.file(join(result.agentPath, "Dockerfile")).text()).toContain("uv sync"); + expect(existsSync(join(result.agentPath, ".dockerignore"))).toBe(true); + const spec = await Bun.file(join(project.rootPath, "agentcore", "agentcore.json")).json(); + const runtime = spec.runtimes.find((r: { name: string }) => r.name === "assistantAgent"); + expect(runtime.build).toBe("Container"); + expect(runtime.dockerfile).toBe("Dockerfile"); + }); + + test("writes a FROM stub for a containerUri harness", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject, { + containerUri: "111122223333.dkr.ecr.us-east-1.amazonaws.com/base-image:latest", + }); + + const result = await drain(subject.exportHarness(project, exportInput())); + + expect(await Bun.file(join(result.agentPath, "Dockerfile")).text()).toContain( + "FROM 111122223333.dkr.ecr.us-east-1.amazonaws.com/base-image:latest", + ); + expect(result.notes.map((note) => note.category)).toEqual([ + "containerUri: verify Python in base image", + ]); + }); + + test("writes generated IAM policy files next to the code", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject, { + skills: [{ s3Uri: "s3://skills-bucket/team" }], + }); + + const result = await drain(subject.exportHarness(project, exportInput())); + + const policy = await Bun.file(join(result.agentPath, "s3-skills-policy.json")).json(); + expect(policy.Statement[0].Resource).toEqual(["arn:aws:s3:::skills-bucket/team/*"]); + const spec = await Bun.file(join(project.rootPath, "agentcore", "agentcore.json")).json(); + const runtime = spec.runtimes.find((r: { name: string }) => r.name === "assistantAgent"); + expect(runtime.additionalPolicies).toEqual(["s3-skills-policy.json"]); + }); +}); + +describe("FsProjectManager.exportHarness side effects", () => { + test("writes MCP header secrets to .env.local and registers their credentials", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject, { + tools: [ + { + type: "remote_mcp", + name: "internal", + config: { + remoteMcp: { url: "https://mcp.internal.example", headers: { "X-Api-Key": "s3cret" } }, + }, + }, + ], + }); + + await drain(subject.exportHarness(project, exportInput())); + + const envLocal = await Bun.file(join(project.rootPath, "agentcore", ".env.local")).text(); + expect(envLocal).toContain("AGENTCORE_CREDENTIAL_ORDERSMCPINTERNALXAPIKEY='s3cret'"); + const spec = await Bun.file(join(project.rootPath, "agentcore", "agentcore.json")).json(); + expect(spec.credentials).toEqual([ + { authorizerType: "ApiKeyCredentialProvider", name: "ordersMcpinternalXApiKey" }, + ]); + }); + + test("exports a prefetched (service) harness without touching harness files", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject); + + const result = await drain( + subject.exportHarness(project, { + prefetched: { + spec: HarnessSpecSchema.parse({ + name: "remote_harness", + model: { provider: "bedrock", modelId: "us.amazon.nova-lite-v1:0" }, + }), + systemPrompt: "Fetched prompt.", + }, + targetAgentName: "exported_arn", + }), + ); + + expect(result.harnessName).toBe("remote_harness"); + expect(await Bun.file(join(result.agentPath, "main.py")).text()).toContain("Fetched prompt."); + }); + + test("cleans up the agent dir and .env.local when the spec write fails", async () => { + const failing = failingWriteJson(); + const { manager: subject } = manager({ json: failing.json }); + const project = await projectWithHarness(subject, { + tools: [ + { + type: "remote_mcp", + name: "internal", + config: { + remoteMcp: { url: "https://mcp.internal.example", headers: { "X-Api-Key": "s3cret" } }, + }, + }, + ], + }); + + failing.failNextWrite(); + await expect(drain(subject.exportHarness(project, exportInput()))).rejects.toThrow("disk full"); + + expect(existsSync(join(project.rootPath, "app", "assistantAgent"))).toBe(false); + // The scaffolded .env.local survives, but the staged secret is rolled back. + expect(await Bun.file(join(project.rootPath, "agentcore", ".env.local")).text()).not.toContain( + "AGENTCORE_CREDENTIAL_ORDERSMCPINTERNALXAPIKEY", + ); + const spec = await Bun.file(join(project.rootPath, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes.map((r: { name: string }) => r.name)).not.toContain("assistantAgent"); + }); + + test("reads the harness from its registry path", async () => { + const { manager: subject } = manager(); + const project = await projectWithHarness(subject, { + model: { provider: "bedrock", modelId: "us.amazon.nova-lite-v1:0", maxTokens: 128 }, + }); + + const result = await drain(subject.exportHarness(project, exportInput())); + + expect(await Bun.file(join(result.agentPath, "model", "load.py")).text()).toContain( + "max_tokens=128", + ); + // The system prompt comes from system-prompt.md, the file add-harness wrote. + expect(await Bun.file(join(result.agentPath, "main.py")).text()).toContain( + 'DEFAULT_SYSTEM_PROMPT = """You are a terse assistant."""', + ); + }); +}); + +/** A ReadWriteJson that can be told to fail its next write, delegating otherwise. */ +function failingWriteJson() { + const real = new FsReadWriteJson({ logger: createSilentLogger() }); + let shouldFail = false; + const json: ReadWriteJson = { + read: (path, schema) => real.read(path, schema), + write: (path, data) => { + if (shouldFail) { + shouldFail = false; + throw new Error("disk full"); + } + return real.write(path, data); + }, + }; + return { json, failNextWrite: () => (shouldFail = true) }; +} diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index 8d4d8983e..82dc14ab1 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -1,11 +1,13 @@ import { existsSync } from "node:fs"; -import { rm } from "node:fs/promises"; +import { copyFile, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import type { AddResourceInput, CreateProjectInput, DeployProjectInput, DeployResult, + ExportHarnessInput, + ExportHarnessResult, ResolveProjectInput, Project, ProjectManager, @@ -27,6 +29,15 @@ import { ENV_LOCAL_RELATIVE_PATH, EnvLocalFile } from "./envLocal"; import { getHarnessTemplateResolver } from "./templates/harness"; import { createProjectTree } from "./templates/project"; import { getRuntimeTemplateResolver } from "./templates/runtime"; +import { + DEFAULT_EXPORT_SYSTEM_PROMPT, + EXPORT_NOTES_FILENAME, + buildDockerfileStub, + buildExportNotesMarkdown, + mapHarnessToExportPlan, +} from "./templates/export"; +import { HarnessSpecSchema } from "../../projectSchemas/harness"; +import { FsTreeNode } from "./templates/fsTree"; import { ProjectSpecSchema, type ManagedBy } from "../../projectSchemas/project"; import { ConfigBundleSchema } from "../../projectSchemas/config-bundle"; import { @@ -602,6 +613,198 @@ export class FsProjectManager implements ProjectManager { } } + public async *exportHarness( + project: Project, + input: ExportHarnessInput, + ): AsyncGenerator { + const agentCoreSpecPath = this.getProjectSpecPath(project); + const { targetAgentName } = input; + + yield { message: `Reading project spec file at '${agentCoreSpecPath}'` }; + const projectSpec = await this.json.read(agentCoreSpecPath, ProjectSpecSchema); + + // Resolve the harness spec + system prompt: from the prefetched service + // payload (--arn) or from the in-project harness files (--name). + let harnessName: string; + let spec: z.output; + let systemPrompt: string; + let harnessDir: string | undefined; + if (input.prefetched) { + spec = input.prefetched.spec; + harnessName = spec.name; + const prompt = input.prefetched.systemPrompt?.trim(); + systemPrompt = + prompt && prompt.length > 0 ? prompt : (spec.systemPrompt ?? DEFAULT_EXPORT_SYSTEM_PROMPT); + } else { + harnessName = input.harnessName!; + const entry = projectSpec.harnesses.find((candidate) => candidate.name === harnessName); + if (!entry) { + const available = projectSpec.harnesses.map((candidate) => candidate.name).join(", "); + throw new ResourceNotFoundError( + `Harness '${harnessName}' not found in agentcore.json. ` + + `Available harnesses: ${available || "none"}`, + ); + } + harnessDir = join(project.rootPath, entry.path); + yield { message: `Reading harness configuration from '${join(entry.path, "harness.json")}'` }; + spec = await this.json.read(join(harnessDir, "harness.json"), HarnessSpecSchema); + const promptPath = join(harnessDir, "system-prompt.md"); + const filePrompt = existsSync(promptPath) + ? (await readFile(promptPath, "utf-8")).trim() + : undefined; + systemPrompt = + filePrompt && filePrompt.length > 0 + ? filePrompt + : (spec.systemPrompt ?? DEFAULT_EXPORT_SYSTEM_PROMPT); + } + + // Refuse to overwrite anything: the target name must be free in the spec + // (runtimes AND harnesses share the app/ namespace) and on disk. A leftover + // directory with no spec entry would otherwise be silently overwritten. + if (projectSpec.runtimes.some((runtime) => runtime.name === targetAgentName)) { + throw new InputValidationError( + `a runtime with name '${targetAgentName}' already exists; choose a different --target-agent-name`, + ); + } + if (projectSpec.harnesses.some((harness) => harness.name === targetAgentName)) { + throw new InputValidationError( + `a harness with name '${targetAgentName}' already exists; choose a different --target-agent-name`, + ); + } + const agentDir = join(project.rootPath, "app", targetAgentName); + if (existsSync(agentDir)) { + throw new InputValidationError( + `the directory 'app/${targetAgentName}/' already exists; remove it or choose a different --target-agent-name`, + ); + } + + yield { message: `Mapping harness '${harnessName}' to the Strands runtime template` }; + const plan = mapHarnessToExportPlan({ + harnessName, + targetAgentName, + spec, + systemPrompt, + projectSpec, + build: input.build, + harnessDockerfileExists: + spec.dockerfile !== undefined && + harnessDir !== undefined && + existsSync(join(harnessDir, spec.dockerfile)), + }); + + const isContainer = plan.buildType === "Container"; + yield { message: `Rendering agent code at 'app/${targetAgentName}'` }; + const tree = await FsTreeNode.fromAssetSource( + { assetSource: this.assetSource }, + { assetDir: "templates/strands-http-python" }, + { + rootDirName: targetAgentName, + transformContent: (raw) => this.templateRenderer.render(raw, plan.context), + filter: (name, isDir) => { + if (isDir && name === "memory") return plan.hasMemory; + if (isDir && name === "hooks") return plan.hasExecutionLimits; + // The template's own Dockerfile is used only for a plain Container + // export; containerUri/custom-Dockerfile harnesses replace it below. + if (name === "Dockerfile") + return isContainer && plan.dockerfilePlan.source === "template"; + if (name === ".dockerignore") return isContainer; + return true; + }, + }, + ); + + // Everything under agentDir is created by this export; remove it when a + // later step fails so no orphan directory outlives its spec entry. + const cleanupAgentDir = () => + rm(agentDir, { recursive: true, force: true }).catch((e) => { + const error = AgentCoreCLIError.fromError(e); + this.logger + .child({ errorName: error.name, errorMessage: error.message }) + .warn(`failed to clean up ${agentDir}`); + }); + + let envFile: EnvLocalFile | undefined; + try { + await tree.write(join(project.rootPath, "app")); + + // Post-render files the template cannot express. + if (plan.dockerfilePlan.source === "stub") { + await writeFile( + join(agentDir, "Dockerfile"), + buildDockerfileStub(plan.dockerfilePlan.containerUri), + ); + } else if (plan.dockerfilePlan.source === "harnessCopy") { + await copyFile(join(harnessDir!, spec.dockerfile!), join(agentDir, "Dockerfile")); + } + for (const [fileName, policyDoc] of Object.entries(plan.policyFiles)) { + await writeFile(join(agentDir, fileName), `${JSON.stringify(policyDoc, null, 2)}\n`); + } + + yield { message: `Writing ${EXPORT_NOTES_FILENAME}` }; + const notesPath = join(agentDir, EXPORT_NOTES_FILENAME); + await writeFile( + notesPath, + buildExportNotesMarkdown( + plan.notes, + harnessName, + targetAgentName, + await readStrandsVersion(agentDir), + ), + ); + + if (plan.envEntries.length > 0) { + envFile = new EnvLocalFile(project.rootPath); + yield { message: `Updating secrets file at '${envFile.path}'` }; + const { skipped } = await envFile.insertIfNew(plan.envEntries); + for (const key of skipped) { + yield { + message: `'${key}' already exists in ${ENV_LOCAL_RELATIVE_PATH}; left unchanged`, + }; + } + } + + yield { message: `Updating project spec file at '${agentCoreSpecPath}'` }; + projectSpec.runtimes.push(plan.runtime); + for (const credential of plan.credentials) { + if (!projectSpec.credentials.some((candidate) => candidate.name === credential.name)) { + projectSpec.credentials.push(credential); + } + } + const parsed = ProjectSpecSchema.safeParse(projectSpec); + if (!parsed.success) { + throw new ProjectStateError(z.prettifyError(parsed.error), { cause: parsed.error }); + } + await this.json.write(agentCoreSpecPath, parsed.data); + } catch (err) { + this.logger.warn( + `harness export failed; attempting best-effort cleanup of staged changes under ${agentDir}`, + ); + await Promise.all([ + cleanupAgentDir(), + envFile?.rollback().catch((e) => { + const error = AgentCoreCLIError.fromError(e); + this.logger + .child({ errorName: error.name, errorMessage: error.message }) + .warn(`failed to roll back ${ENV_LOCAL_RELATIVE_PATH}`); + }), + ]); + throw err; + } + + // Deps go in only after the spec commit: a sync failure past this point + // leaves a consistent project the user can finish with a manual `uv sync`, + // so it must NOT trigger the cleanup above. + yield* this.installRuntimeDependencies(agentDir); + + return { + harnessName, + agentName: targetAgentName, + agentPath: agentDir, + notesPath: join(agentDir, EXPORT_NOTES_FILENAME), + notes: plan.notes, + }; + } + private async scaffoldRuntimeResources(outputPath: string, input: RuntimeResourceConfig) { const resolver = getRuntimeTemplateResolver( { assetSource: this.assetSource, templateRenderer: this.templateRenderer }, @@ -810,6 +1013,17 @@ function toProjectSpecKey(resourceType: ProjectResource) { } } +/** The strands-agents requirement from the rendered pyproject.toml, for EXPORT_NOTES.md. */ +async function readStrandsVersion(agentDir: string): Promise { + try { + const pyproject = await readFile(join(agentDir, "pyproject.toml"), "utf-8"); + const match = /strands-agents\s*([~><=]+\s*[\d.]+)/.exec(pyproject); + return match ? `strands-agents ${match[1]}` : "strands-agents (version unknown)"; + } catch { + return "strands-agents (version unknown)"; + } +} + function parseResource( schema: TSchema, input: z.input, diff --git a/src/core/project/templates/export.test.ts b/src/core/project/templates/export.test.ts new file mode 100644 index 000000000..543a7f141 --- /dev/null +++ b/src/core/project/templates/export.test.ts @@ -0,0 +1,650 @@ +import { describe, expect, test } from "bun:test"; +import z from "zod"; +import { InputValidationError } from "../../../errors/errors"; +import { HarnessSpecSchema, type HarnessSpec } from "../../../projectSchemas/harness"; +import { ProjectSpecSchema } from "../../../projectSchemas/project"; +import { + ALLOWED_TOOLS_NOTE_CATEGORY, + AWS_SKILLS_NOTE_CATEGORY, + BROWSER_TOOL_NOTE_CATEGORY, + CODE_INTERPRETER_TOOL_NOTE_CATEGORY, + CONTAINER_URI_NOTE_CATEGORY, + CUSTOM_DOCKERFILE_NOTE_CATEGORY, + GATEWAY_TOOL_NOTE_CATEGORY, + GIT_SKILLS_AUTH_NOTE_CATEGORY, + LITELLM_NO_API_KEY_NOTE_CATEGORY, + MALFORMED_S3_SKILL_NOTE_CATEGORY, + MCP_HEADER_CREDS_NOTE_CATEGORY, + MEMORY_ARN_NOTE_CATEGORY, + MEMORY_MANAGED_NOTE_CATEGORY, + MEMORY_NAME_NOT_FOUND_NOTE_CATEGORY, + MISSING_DOCKERFILE_NOTE_CATEGORY, + MODEL_API_KEY_NOTE_CATEGORY, + PATH_SKILLS_NOTE_CATEGORY, + buildExportNotesMarkdown, + formatExportNotes, + mapHarnessToExportPlan, + matchesAllowedTools, + type HarnessExportInput, +} from "./export"; + +function harness(spec: Record): HarnessSpec { + return HarnessSpecSchema.parse({ + name: "assistant", + model: { provider: "bedrock", modelId: "us.amazon.nova-lite-v1:0" }, + ...spec, + }); +} + +function projectSpec(overrides: Record = {}) { + return ProjectSpecSchema.parse({ + name: "orders", + version: 1, + managedBy: "CDK", + ...overrides, + }); +} + +function plan(overrides: Partial & { spec?: HarnessSpec } = {}) { + return mapHarnessToExportPlan({ + harnessName: "assistant", + targetAgentName: "assistantAgent", + spec: harness({}), + systemPrompt: "You are a terse assistant.", + projectSpec: projectSpec(), + ...overrides, + }); +} + +function categories(result: ReturnType): string[] { + return result.notes.map((note) => note.category); +} + +describe("mapHarnessToExportPlan model mapping", () => { + test("maps a bedrock model with sampling params and limits into the render context", () => { + const result = plan({ + spec: harness({ + model: { + provider: "bedrock", + modelId: "us.amazon.nova-lite-v1:0", + temperature: 0.2, + topP: 0.9, + maxTokens: 512, + }, + maxIterations: 5, + maxTokens: 2048, + timeoutSeconds: 60, + }), + }); + + expect(result.context.modelProvider).toBe("Bedrock"); + expect(result.context.modelId).toBe("us.amazon.nova-lite-v1:0"); + expect(result.context.modelTemperature).toBe("0.2"); + expect(result.context.modelTopP).toBe("0.9"); + expect(result.context.modelMaxTokens).toBe("512"); + expect(result.context.bedrockMantle).toBeUndefined(); + expect(result.hasExecutionLimits).toBe(true); + expect(result.context.maxIterations).toBe(5); + expect(result.context.maxTokens).toBe(2048); + expect(result.context.timeoutSeconds).toBe(60); + expect(result.context.systemPromptText).toBe("You are a terse assistant."); + expect(result.notes).toEqual([]); + }); + + test("keeps a legal temperature of 0 truthy for the template", () => { + const result = plan({ + spec: harness({ + model: { provider: "bedrock", modelId: "us.amazon.nova-lite-v1:0", temperature: 0 }, + }), + }); + expect(result.context.modelTemperature).toBe("0"); + }); + + test("routes an OpenAI-compatible bedrock model through the Mantle branch with its IAM policy", () => { + const result = plan({ + spec: harness({ + model: { provider: "bedrock", modelId: "openai.gpt-oss-120b", apiFormat: "responses" }, + }), + }); + + expect(result.context.bedrockMantle).toBe(true); + expect(result.context.mantleApiFormat).toBe("responses"); + expect(result.context.mantleProprietary).toBe(false); + expect(Object.keys(result.policyFiles)).toEqual(["bedrock-mantle-policy.json"]); + expect(result.runtime.additionalPolicies).toEqual(["bedrock-mantle-policy.json"]); + }); + + test("wires an open_ai model through an AgentCore Identity credential", () => { + const result = plan({ + spec: harness({ + model: { + provider: "open_ai", + modelId: "gpt-4.1", + apiKeyArn: + "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/MyOpenAiKey", + }, + }), + }); + + expect(result.context.modelProvider).toBe("OpenAI"); + expect(result.context.hasIdentity).toBe(true); + expect(result.context.identityProviders).toEqual([ + { name: "MyOpenAiKey", envVarName: "AGENTCORE_CREDENTIAL_MYOPENAIKEY" }, + ]); + expect(result.credentials).toEqual([ + { authorizerType: "ApiKeyCredentialProvider", name: "MyOpenAiKey" }, + ]); + expect(categories(result)).toEqual([MODEL_API_KEY_NOTE_CATEGORY]); + }); + + test("does not duplicate a credential the project already declares", () => { + const result = plan({ + spec: harness({ + model: { + provider: "gemini", + modelId: "gemini-2.5-flash", + apiKeyArn: + "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/GemKey", + }, + }), + projectSpec: projectSpec({ + credentials: [{ authorizerType: "ApiKeyCredentialProvider", name: "GemKey" }], + }), + }); + + expect(result.context.modelProvider).toBe("Gemini"); + expect(result.credentials).toEqual([]); + }); + + test("threads LiteLLM apiBase and additionalParams and trusts bedrock/ models without a key", () => { + const result = plan({ + spec: harness({ + model: { + provider: "lite_llm", + modelId: "bedrock/us.amazon.nova-lite-v1:0", + apiBase: "https://litellm.example", + additionalParams: { max_retries: 2 }, + }, + }), + }); + + expect(result.context.modelProvider).toBe("LiteLLM"); + expect(result.context.litellmApiBase).toBe("https://litellm.example"); + expect(result.context.litellmAdditionalParams).toEqual({ max_retries: 2 }); + expect(result.notes).toEqual([]); + }); + + test("warns when a keyless LiteLLM model is not Bedrock-backed", () => { + const result = plan({ + spec: harness({ model: { provider: "lite_llm", modelId: "openai/gpt-4.1" } }), + }); + expect(categories(result)).toEqual([LITELLM_NO_API_KEY_NOTE_CATEGORY]); + }); +}); + +describe("mapHarnessToExportPlan tools", () => { + test("maps remote MCP and inline function tools into the render context", () => { + const result = plan({ + spec: harness({ + tools: [ + { + type: "remote_mcp", + name: "exa", + config: { remoteMcp: { url: "https://mcp.exa.ai/mcp" } }, + }, + { + type: "inline_function", + name: "get_weather", + config: { + inlineFunction: { + description: "Get the weather", + inputSchema: { type: "object", properties: { city: { type: "string" } } }, + }, + }, + }, + ], + }), + }); + + expect(result.context.remoteMcpTools).toEqual([ + { name: "exa", url: "https://mcp.exa.ai/mcp", headerCredentials: undefined }, + ]); + expect(result.context.inlineFunctionTools).toEqual([ + { + name: "get_weather", + description: "Get the weather", + inputSchema: { type: "object", properties: { city: { type: "string" } } }, + }, + ]); + expect(result.notes).toEqual([]); + }); + + test("turns remote MCP headers into identity credentials plus .env.local material", () => { + const result = plan({ + spec: harness({ + tools: [ + { + type: "remote_mcp", + name: "internal", + config: { + remoteMcp: { + url: "https://mcp.internal.example", + headers: { "X-Api-Key": "s3cret" }, + }, + }, + }, + ], + }), + }); + + const tools = result.context.remoteMcpTools as { + headerCredentials?: { headerKey: string; credentialName: string; envVarName: string }[]; + }[]; + expect(tools[0]!.headerCredentials).toEqual([ + { + headerKey: "X-Api-Key", + credentialName: "ordersMcpinternalXApiKey", + envVarName: "AGENTCORE_CREDENTIAL_ORDERSMCPINTERNALXAPIKEY", + }, + ]); + expect(result.credentials).toEqual([ + { authorizerType: "ApiKeyCredentialProvider", name: "ordersMcpinternalXApiKey" }, + ]); + expect(result.envEntries).toEqual([ + { + key: "AGENTCORE_CREDENTIAL_ORDERSMCPINTERNALXAPIKEY", + value: "s3cret", + comment: '"X-Api-Key" header for MCP tool "internal" (exported from harness "assistant")', + }, + ]); + expect(categories(result)).toEqual([MCP_HEADER_CREDS_NOTE_CATEGORY]); + }); + + test("emits a follow-up note for each unmappable tool type instead of code", () => { + const result = plan({ + spec: harness({ + tools: [ + { + type: "agentcore_gateway", + name: "gw", + config: { + agentCoreGateway: { + gatewayArn: "arn:aws:bedrock-agentcore:us-east-1:111122223333:gateway/g-1", + }, + }, + }, + { type: "agentcore_browser", name: "browser" }, + { type: "agentcore_code_interpreter", name: "ci" }, + ], + }), + }); + + expect(result.context.hasBrowser).toBe(false); + expect(result.context.hasCodeInterpreter).toBe(false); + expect(result.context.hasGateway).toBe(false); + expect(result.context.remoteMcpTools).toBeUndefined(); + expect(categories(result)).toEqual([ + GATEWAY_TOOL_NOTE_CATEGORY, + BROWSER_TOOL_NOTE_CATEGORY, + CODE_INTERPRETER_TOOL_NOTE_CATEGORY, + ]); + expect(result.notes[0]!.message).toContain("gateway/g-1"); + }); + + test("includes the harness builtins unless allowedTools filters them out", () => { + const unrestricted = plan({}); + expect(unrestricted.context.hasShell).toBe(true); + expect(unrestricted.context.hasFileOperations).toBe(true); + + const restricted = plan({ + spec: harness({ + allowedTools: ["@builtin/shell", "exa"], + tools: [ + { + type: "remote_mcp", + name: "exa", + config: { remoteMcp: { url: "https://mcp.exa.ai/mcp" } }, + }, + { + type: "remote_mcp", + name: "other", + config: { remoteMcp: { url: "https://other.example" } }, + }, + ], + }), + }); + expect(restricted.context.hasShell).toBe(true); + expect(restricted.context.hasFileOperations).toBe(false); + expect(restricted.context.remoteMcpTools).toEqual([ + { name: "exa", url: "https://mcp.exa.ai/mcp", headerCredentials: undefined }, + ]); + expect(categories(restricted)).toEqual([ALLOWED_TOOLS_NOTE_CATEGORY]); + }); +}); + +describe("matchesAllowedTools", () => { + test.each([ + ["*", "anything", true], + ["exa", "exa", true], + ["e*", "exa", true], + ["@builtin/shell", "builtin/shell", true], + ["@builtin", "builtin/shell", true], + ["@server/tool", "server_tool", true], + ["exa", "other", false], + ["@builtin/shell", "builtin/file_operations", false], + ])("pattern %s vs %s -> %p", (pattern, name, expected) => { + expect(matchesAllowedTools(name, [pattern])).toBe(expected); + }); +}); + +describe("mapHarnessToExportPlan memory", () => { + test("wires an in-project memory by name with its strategies", () => { + const result = plan({ + spec: harness({ memory: { mode: "existing", name: "chat_history", actorId: "actor-1" } }), + projectSpec: projectSpec({ + memories: [ + { name: "chat_history", eventExpiryDuration: 30, strategies: [{ type: "SEMANTIC" }] }, + ], + }), + }); + + expect(result.hasMemory).toBe(true); + expect(result.context.memoryEnvVarName).toBe("MEMORY_CHAT_HISTORY_ID"); + expect(result.context.memoryStrategies).toEqual(["SEMANTIC"]); + expect(result.context.actorId).toBe("actor-1"); + expect(result.notes).toEqual([]); + }); + + test("notes a by-name memory that is not in the project", () => { + const result = plan({ + spec: harness({ memory: { mode: "existing", name: "missing" } }), + }); + expect(result.hasMemory).toBe(false); + expect(categories(result)).toEqual([MEMORY_NAME_NOT_FOUND_NOTE_CATEGORY]); + }); + + test("notes an external memory referenced by ARN", () => { + const result = plan({ + spec: harness({ + memory: { + mode: "existing", + arn: "arn:aws:bedrock-agentcore:us-east-1:111122223333:memory/m-1", + }, + }), + }); + expect(result.hasMemory).toBe(false); + expect(categories(result)).toEqual([MEMORY_ARN_NOTE_CATEGORY]); + expect(result.notes[0]!.message).toContain("memory/m-1"); + }); + + test("notes managed harness memory and disables none", () => { + expect(categories(plan({ spec: harness({ memory: { mode: "managed" } }) }))).toEqual([ + MEMORY_MANAGED_NOTE_CATEGORY, + ]); + const disabled = plan({ spec: harness({ memory: { mode: "disabled" } }) }); + expect(disabled.hasMemory).toBe(false); + expect(disabled.notes).toEqual([]); + }); +}); + +describe("mapHarnessToExportPlan skills", () => { + test("maps path, s3, and git skills and generates the S3 read policy", () => { + const result = plan({ + spec: harness({ + build: undefined, + skills: [ + { path: "local_skill" }, + { s3Uri: "s3://skills-bucket/team/" }, + { gitUrl: "https://github.com/example/skills.git", path: "subdir" }, + ], + }), + }); + + expect(result.context.hasSkillsFetcher).toBe(true); + expect(result.context.hasFetchedSkills).toBe(true); + expect(result.context.pathSkills).toEqual(["local_skill"]); + expect(result.context.s3Skills).toEqual(["s3://skills-bucket/team/"]); + expect(result.context.gitSkills).toEqual([ + { url: "https://github.com/example/skills.git", path: "subdir" }, + ]); + expect(result.policyFiles["s3-skills-policy.json"]).toEqual({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Action: "s3:GetObject", + Resource: ["arn:aws:s3:::skills-bucket/team/*"], + }, + { Effect: "Allow", Action: "s3:ListBucket", Resource: ["arn:aws:s3:::skills-bucket"] }, + ], + }); + expect(result.runtime.additionalPolicies).toEqual(["s3-skills-policy.json"]); + // CodeZip path skills need the container filesystem — flagged for follow-up. + expect(categories(result)).toEqual([PATH_SKILLS_NOTE_CATEGORY]); + }); + + test("notes a malformed s3 URI instead of generating IAM for it", () => { + const result = plan({ spec: harness({ skills: [{ s3Uri: "s3://" }] }) }); + expect(categories(result)).toEqual([MALFORMED_S3_SKILL_NOTE_CATEGORY]); + expect(result.policyFiles).toEqual({}); + }); + + test("references the git skill credential provider and notes aws skills", () => { + const result = plan({ + spec: harness({ + skills: [ + { + gitUrl: "https://github.com/example/private.git", + auth: { + credentialArn: + "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/GitPat", + }, + }, + { awsSkills: { paths: ["aws/foo"] } }, + ], + }), + }); + + expect(result.credentials).toEqual([ + { authorizerType: "ApiKeyCredentialProvider", name: "GitPat" }, + ]); + expect((result.context.gitSkills as unknown[])[0]).toMatchObject({ + url: "https://github.com/example/private.git", + credentialArn: + "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/GitPat", + }); + expect(categories(result)).toEqual([GIT_SKILLS_AUTH_NOTE_CATEGORY, AWS_SKILLS_NOTE_CATEGORY]); + }); +}); + +describe("mapHarnessToExportPlan truncation", () => { + test("maps sliding window config to the Strands conversation manager kwargs", () => { + const result = plan({ + spec: harness({ + truncation: { + strategy: "sliding_window", + config: { slidingWindow: { messagesCount: 20 } }, + }, + }), + }); + expect(result.context.truncationStrategy).toBe("sliding_window"); + expect(result.context.truncationConfig).toEqual({ window_size: 20 }); + }); + + test("maps summarization config keys to snake_case", () => { + const result = plan({ + spec: harness({ + truncation: { + strategy: "summarization", + config: { summarization: { summaryRatio: 0.4, preserveRecentMessages: 6 } }, + }, + }), + }); + expect(result.context.truncationConfig).toEqual({ + summary_ratio: 0.4, + preserve_recent_messages: 6, + }); + }); + + test('treats strategy "none" as no conversation manager override', () => { + const result = plan({ spec: harness({ truncation: { strategy: "none" } }) }); + expect(result.context.truncationStrategy).toBeUndefined(); + }); +}); + +describe("mapHarnessToExportPlan build types and Dockerfiles", () => { + test("defaults to CodeZip with the PYTHON_3_14 runtime", () => { + const result = plan({}); + expect(result.buildType).toBe("CodeZip"); + expect(result.dockerfilePlan).toEqual({ source: "none" }); + expect(result.runtime.runtimeVersion).toBe("PYTHON_3_14"); + expect(result.runtime.dockerfile).toBeUndefined(); + }); + + test("a plain --build Container uses the template Dockerfile", () => { + const result = plan({ build: "Container" }); + expect(result.buildType).toBe("Container"); + expect(result.dockerfilePlan).toEqual({ source: "template" }); + expect(result.runtime.dockerfile).toBe("Dockerfile"); + expect(result.runtime.runtimeVersion).toBeUndefined(); + }); + + test("a containerUri harness gets a FROM-stub Dockerfile and a verify note", () => { + const result = plan({ + spec: harness({ + containerUri: "111122223333.dkr.ecr.us-east-1.amazonaws.com/base-image:latest", + }), + }); + expect(result.buildType).toBe("Container"); + expect(result.dockerfilePlan).toEqual({ + source: "stub", + containerUri: "111122223333.dkr.ecr.us-east-1.amazonaws.com/base-image:latest", + }); + expect(categories(result)).toEqual([CONTAINER_URI_NOTE_CATEGORY]); + }); + + test("rejects forcing CodeZip onto a containerUri harness", () => { + expect(() => + plan({ + build: "CodeZip", + spec: harness({ + containerUri: "111122223333.dkr.ecr.us-east-1.amazonaws.com/base-image:latest", + }), + }), + ).toThrow(InputValidationError); + }); + + test("copies a custom harness Dockerfile with a build-layer note when it exists", () => { + const result = plan({ + spec: harness({ dockerfile: "Dockerfile" }), + harnessDockerfileExists: true, + }); + expect(result.dockerfilePlan).toEqual({ source: "harnessCopy" }); + expect(categories(result)).toEqual([CUSTOM_DOCKERFILE_NOTE_CATEGORY]); + }); + + test("notes a declared-but-missing harness Dockerfile", () => { + const result = plan({ + spec: harness({ dockerfile: "Dockerfile" }), + harnessDockerfileExists: false, + }); + expect(result.dockerfilePlan).toEqual({ source: "none" }); + expect(categories(result)).toEqual([MISSING_DOCKERFILE_NOTE_CATEGORY]); + // The runtime entry still expects the Dockerfile the user will create. + expect(result.runtime.dockerfile).toBe("Dockerfile"); + }); +}); + +describe("mapHarnessToExportPlan runtime spec entry", () => { + test("produces a deployable runtimes[] entry and keeps infra fields", () => { + const result = plan({ + spec: harness({ + environmentVariables: { LOG_LEVEL: "debug" }, + lifecycleConfig: { idleRuntimeSessionTimeout: 900 }, + networkMode: "VPC", + networkConfig: { subnets: ["subnet-12345678"], securityGroups: ["sg-12345678"] }, + sessionStoragePath: "/mnt/session", + efsAccessPoints: [ + { + accessPointArn: + "arn:aws:elasticfilesystem:us-east-1:111122223333:access-point/fsap-0123456789abcdef0", + mountPath: "/mnt/tools", + }, + ], + tags: { team: "search" }, + executionRoleArn: "arn:aws:iam::111122223333:role/HarnessRole", + }), + }); + + expect(result.runtime).toEqual({ + name: "assistantAgent", + build: "CodeZip", + entrypoint: "main.py", + codeLocation: "app/assistantAgent" as (typeof result.runtime)["codeLocation"], + protocol: "HTTP", + runtimeVersion: "PYTHON_3_14", + envVars: [{ name: "LOG_LEVEL", value: "debug" }], + networkMode: "VPC", + networkConfig: { subnets: ["subnet-12345678"], securityGroups: ["sg-12345678"] }, + lifecycleConfiguration: { idleRuntimeSessionTimeout: 900 }, + filesystemConfigurations: [ + { sessionStorage: { mountPath: "/mnt/session" } }, + { + efsAccessPoint: { + accessPointArn: + "arn:aws:elasticfilesystem:us-east-1:111122223333:access-point/fsap-0123456789abcdef0", + mountPath: "/mnt/tools", + }, + }, + ], + tags: { team: "search" }, + }); + // The harness role must never leak onto the new runtime. + expect(result.runtime.executionRoleArn).toBeUndefined(); + }); + + test("the produced entry validates inside a project spec", () => { + const result = plan({}); + const spec = projectSpec(); + spec.runtimes.push(result.runtime); + expect(() => ProjectSpecSchema.parse(spec)).not.toThrow(z.ZodError); + }); +}); + +describe("export notes rendering", () => { + test("buildExportNotesMarkdown lists each note under its category", () => { + const markdown = buildExportNotesMarkdown( + [{ category: "A category", message: "Do the thing." }], + "assistant", + "assistantAgent", + "strands-agents ~= 1.15.0", + ); + expect(markdown).toContain("# Export Notes — assistant → assistantAgent"); + expect(markdown).toContain("Strands version: strands-agents ~= 1.15.0"); + expect(markdown).toContain("## Items requiring manual follow-up"); + expect(markdown).toContain("### A category"); + expect(markdown).toContain("Do the thing."); + }); + + test("buildExportNotesMarkdown says when nothing is left to do", () => { + const markdown = buildExportNotesMarkdown([], "assistant", "assistantAgent", "v"); + expect(markdown).toContain("No manual steps required."); + }); + + test("formatExportNotes renders a warning block or a quiet confirmation", () => { + expect(formatExportNotes([], "notes.md")).toEqual([ + { text: "No manual follow-up required. (Details: notes.md)", tone: "dim" }, + ]); + const lines = formatExportNotes( + [{ category: "Cat", message: "line one\nline two" }], + "notes.md", + ); + expect(lines.map((line) => line.text)).toEqual([ + "1 export note requiring manual follow-up:", + " - Cat", + " line one", + " line two", + "These notes are also saved to notes.md", + ]); + }); +}); diff --git a/src/core/project/templates/export.ts b/src/core/project/templates/export.ts new file mode 100644 index 000000000..5cc289010 --- /dev/null +++ b/src/core/project/templates/export.ts @@ -0,0 +1,1060 @@ +import type { z } from "zod"; +import type { BuildType, ProjectRuntime } from "../../../projectSchemas/runtime"; +import type { + HarnessMemoryRef, + HarnessSkill, + HarnessSkillGitSource, + HarnessSkillPathSource, + HarnessSkillS3Source, + HarnessSkillAwsSkillsSource, + HarnessSpec, + HarnessTool, + HarnessTruncationConfig, +} from "../../../projectSchemas/harness"; +import type { ProjectSpecSchema } from "../../../projectSchemas/project"; +import { credentialEnvVarName, type Credential } from "../../../projectSchemas/credential"; +import type { Memory } from "../../../projectSchemas/memory"; +import type { EnvLocalEntry } from "../../../handlers/project/types"; +import { InputValidationError } from "../../../errors/errors"; +import { toPythonPackageName } from "./runtime"; + +type ProjectSpec = z.infer; + +export const EXPORT_NOTES_FILENAME = "EXPORT_NOTES.md"; +export const DEFAULT_EXPORT_SYSTEM_PROMPT = "You are a helpful assistant."; + +/** A manual follow-up item recorded while mapping, written to EXPORT_NOTES.md. */ +export interface ExportNote { + category: string; + message: string; +} + +/** A single rendered output line + a tone the caller maps to its own styling. */ +export interface ExportNoteLine { + text: string; + tone: "warn" | "dim"; +} + +/** Everything the mapper needs; all reads are done by the caller. */ +export interface HarnessExportInput { + harnessName: string; + targetAgentName: string; + /** The parsed harness spec (from app//harness.json or the service). */ + spec: HarnessSpec; + /** The resolved system prompt text (system-prompt.md > spec.systemPrompt > default). */ + systemPrompt: string; + /** The current project spec, for memory lookups and credential dedup. */ + projectSpec: ProjectSpec; + /** Build override from --build; when absent the harness spec decides. */ + build?: BuildType; + /** + * Whether the harness directory holds the Dockerfile that `spec.dockerfile` + * names (local harnesses only; the caller checks the filesystem). + */ + harnessDockerfileExists?: boolean; +} + +/** How the exported agent's Dockerfile is produced (Container builds only). */ +export type DockerfilePlan = + /** Render the stock template Dockerfile (plain --build Container). */ + | { source: "template" } + /** Write a FROM- stub extending the harness's prebuilt image. */ + | { source: "stub"; containerUri: string } + /** Copy the harness's own Dockerfile from the harness directory. */ + | { source: "harnessCopy" } + /** CodeZip — no Dockerfile at all. */ + | { source: "none" }; + +/** The pure mapping result; the project manager executes it against the filesystem. */ +export interface HarnessExportPlan { + /** Handlebars context for rendering the strands-http-python template. */ + context: Record; + /** The runtimes[] entry to append to agentcore.json. */ + runtime: ProjectRuntime; + /** New credential entries to append (already-present names are pre-filtered). */ + credentials: Credential[]; + /** Secret material for agentcore/.env.local (e.g. remote MCP header values). */ + envEntries: EnvLocalEntry[]; + /** Generated IAM policy documents written into the agent dir, keyed by filename. */ + policyFiles: Record; + /** Whether the render includes the memory/ module. */ + hasMemory: boolean; + /** Whether the render includes hooks/execution_limits.py. */ + hasExecutionLimits: boolean; + buildType: BuildType; + dockerfilePlan: DockerfilePlan; + notes: ExportNote[]; +} + +// ============================================================================ +// Note categories +// ============================================================================ + +export const ALLOWED_TOOLS_NOTE_CATEGORY = "allowedTools: per-invocation overrides dropped"; +export const GATEWAY_TOOL_NOTE_CATEGORY = "Gateway tool not exported — wire up manually"; +export const BROWSER_TOOL_NOTE_CATEGORY = "Browser tool not exported — wire up manually"; +export const CODE_INTERPRETER_TOOL_NOTE_CATEGORY = + "Code-interpreter tool not exported — wire up manually"; +export const MEMORY_ARN_NOTE_CATEGORY = "External memory reference not exported"; +export const MEMORY_MANAGED_NOTE_CATEGORY = "Managed harness memory not exported"; +export const MEMORY_NAME_NOT_FOUND_NOTE_CATEGORY = "Memory reference could not be resolved"; +export const PATH_SKILLS_NOTE_CATEGORY = "path skills require container filesystem"; +export const GIT_SKILLS_CONTAINER_NOTE_CATEGORY = "git skills require git in container image"; +export const GIT_SKILLS_AUTH_NOTE_CATEGORY = "git skill credential provider referenced"; +export const AWS_SKILLS_NOTE_CATEGORY = + "AWS skills omitted — not available outside managed harness"; +export const MALFORMED_S3_SKILL_NOTE_CATEGORY = + "S3 skill URI is malformed — no S3 read permission generated"; +export const MCP_HEADER_CREDS_NOTE_CATEGORY = "MCP tool header credentials"; +export const LITELLM_NO_API_KEY_NOTE_CATEGORY = "LiteLLM model may require an API key"; +export const MODEL_API_KEY_NOTE_CATEGORY = "Model API key credential referenced"; +export const CONTAINER_URI_NOTE_CATEGORY = "containerUri: verify Python in base image"; +export const CUSTOM_DOCKERFILE_NOTE_CATEGORY = + "Custom harness Dockerfile needs the agent build layer"; +export const MISSING_DOCKERFILE_NOTE_CATEGORY = "Dockerfile not found — create it before deploying"; + +// ============================================================================ +// Public entry point +// ============================================================================ + +export function mapHarnessToExportPlan(input: HarnessExportInput): HarnessExportPlan { + const { spec, targetAgentName, projectSpec } = input; + const notes: ExportNote[] = []; + const credentials: Credential[] = []; + const envEntries: EnvLocalEntry[] = []; + const policyFiles: Record = {}; + const additionalPolicies: string[] = []; + + const buildType = resolveBuildType(spec, input.build); + if (buildType === "CodeZip" && (spec.containerUri || spec.dockerfile)) { + const what = spec.containerUri + ? `containerUri (${spec.containerUri})` + : `dockerfile (${spec.dockerfile})`; + throw new InputValidationError( + `Harness "${spec.name}" uses ${what}, which requires a Container build. ` + + `Re-export with --build Container.`, + ); + } + + const allowedToolPatterns = spec.allowedTools ?? ["*"]; + if (!(allowedToolPatterns.length === 1 && allowedToolPatterns[0] === "*")) { + notes.push({ + category: ALLOWED_TOOLS_NOTE_CATEGORY, + message: + "The harness allowedTools filter has been applied statically at code-generation time. " + + "Tools excluded at export will not be available at runtime, and callers cannot override " + + "the tool list per invocation (unlike the harness).", + }); + } + + const model = resolveModel(spec, projectSpec, credentials, notes); + const memory = resolveMemory(spec, projectSpec, notes); + const tools = resolveTools( + spec, + allowedToolPatterns, + projectSpec, + credentials, + envEntries, + notes, + ); + const skills = resolveSkills(spec, buildType, targetAgentName, credentials, notes); + for (const [file, doc] of Object.entries(skills.policyFiles)) policyFiles[file] = doc; + if (model.policyFile) policyFiles[model.policyFile.name] = model.policyFile.doc; + additionalPolicies.push(...Object.keys(policyFiles)); + + const hasExecutionLimits = + spec.maxIterations !== undefined || + spec.maxTokens !== undefined || + spec.timeoutSeconds !== undefined; + + const dockerfilePlan = resolveDockerfilePlan( + spec, + buildType, + targetAgentName, + input.harnessDockerfileExists ?? false, + notes, + ); + + const filesystemConfigurations = buildFilesystemConfigurations(spec); + const envVars = Object.entries(spec.environmentVariables ?? {}).map(([name, value]) => ({ + name, + value, + })); + + const context: Record = { + name: toPythonPackageName(targetAgentName), + isExportHarness: true, + entrypoint: "main", + enableOtel: true, + hasConfigBundle: false, + hasPayment: false, + isVpc: spec.networkMode === "VPC", + protocol: "HTTP", + // Model + ...model.context, + // System prompt (written verbatim into main.py) + systemPromptText: input.systemPrompt, + // Memory + hasMemory: memory.provider !== undefined, + memoryEnvVarName: memory.provider?.envVarName, + memoryStrategies: memory.provider?.strategies ?? [], + actorId: memory.actorId, + // Gateways are never exported as code (see resolveTools); the template still + // needs the keys so its conditionals resolve. + hasGateway: false, + gatewayProviders: [], + gatewayAuthTypes: [], + // Tools. Empty collections become undefined: the template's custom `or`/ + // `some` helpers use JS truthiness, where [] is truthy, unlike `{{#if}}`. + inlineFunctionTools: undefinedIfEmpty(tools.inlineFunctionTools), + remoteMcpTools: undefinedIfEmpty(tools.remoteMcpTools), + hasShell: tools.hasShell, + hasFileOperations: tools.hasFileOperations, + hasBrowser: false, + hasCodeInterpreter: false, + // Skills + hasSkillsFetcher: skills.hasSkillsFetcher, + hasFetchedSkills: skills.hasFetchedSkills, + pathSkills: skills.pathSkills, + s3Skills: undefinedIfEmpty(skills.s3Skills), + gitSkills: undefinedIfEmpty(skills.gitSkills), + // Execution limits (numbers are schema-validated >= 1, so plain #if works) + hasExecutionLimits, + maxIterations: spec.maxIterations, + maxTokens: spec.maxTokens, + timeoutSeconds: spec.timeoutSeconds, + // Conversation truncation + truncationStrategy: + spec.truncation?.strategy === "none" ? undefined : spec.truncation?.strategy, + truncationConfig: resolveTruncationConfig(spec.truncation), + // Filesystem mounts (informational for the template; tools are harness builtins) + sessionStorageMountPath: spec.sessionStoragePath, + efsMounts: (spec.efsAccessPoints ?? []).map(({ mountPath }) => ({ mountPath })), + s3Mounts: (spec.s3AccessPoints ?? []).map(({ mountPath }) => ({ mountPath })), + needsOs: + !!spec.sessionStoragePath || + (spec.efsAccessPoints?.length ?? 0) > 0 || + (spec.s3AccessPoints?.length ?? 0) > 0, + }; + + const runtime: ProjectRuntime = { + name: targetAgentName, + build: buildType, + entrypoint: "main.py", + codeLocation: `app/${targetAgentName}` as ProjectRuntime["codeLocation"], + protocol: "HTTP", + ...(buildType === "CodeZip" && { runtimeVersion: "PYTHON_3_14" as const }), + ...(buildType === "Container" && { dockerfile: "Dockerfile" }), + ...(envVars.length > 0 && { envVars }), + ...(spec.networkMode && { networkMode: spec.networkMode }), + ...(spec.networkMode === "VPC" && spec.networkConfig && { networkConfig: spec.networkConfig }), + ...(spec.authorizerType && { authorizerType: spec.authorizerType }), + ...(spec.authorizerConfiguration && { + authorizerConfiguration: spec.authorizerConfiguration, + }), + ...(spec.lifecycleConfig && { lifecycleConfiguration: spec.lifecycleConfig }), + ...(filesystemConfigurations.length > 0 && { filesystemConfigurations }), + ...(additionalPolicies.length > 0 && { additionalPolicies }), + ...(spec.connections?.length && { connections: spec.connections }), + ...(spec.tags && { tags: spec.tags }), + // NOTE: the harness's executionRoleArn is deliberately NOT carried over. The + // exported agent is a new runtime that needs its own CDK-managed role so the + // construct can attach the runtime baseline, additionalPolicies, and grants. + }; + + return { + context, + runtime, + credentials, + envEntries, + policyFiles, + hasMemory: memory.provider !== undefined, + hasExecutionLimits, + buildType, + dockerfilePlan, + notes, + }; +} + +// ============================================================================ +// Model +// ============================================================================ + +interface ModelResolution { + context: Record; + policyFile?: { name: string; doc: unknown }; +} + +/** A Bedrock model whose apiFormat routes it through the OpenAI-compatible Mantle endpoint. */ +function isBedrockMantleModel(spec: HarnessSpec): boolean { + return ( + spec.model.provider === "bedrock" && + (spec.model.apiFormat === "responses" || spec.model.apiFormat === "chat_completions") + ); +} + +/** + * Proprietary OpenAI models (e.g. openai.gpt-5.x) are served on the Bedrock + * Mantle `/openai/v1` path; open-source ones (openai.gpt-oss-*) use `/v1`. + */ +function isProprietaryOpenAiModel(modelId: string): boolean { + return modelId.startsWith("openai.") && !modelId.includes("gpt-oss"); +} + +function resolveModel( + spec: HarnessSpec, + projectSpec: ProjectSpec, + credentials: Credential[], + notes: ExportNote[], +): ModelResolution { + const model = spec.model; + const context: Record = { + modelId: model.modelId, + // Stringified so a legal 0 (temperature/topP) stays truthy for {{#if}}. + modelMaxTokens: model.maxTokens !== undefined ? String(model.maxTokens) : undefined, + modelTemperature: model.temperature !== undefined ? String(model.temperature) : undefined, + modelTopP: model.topP !== undefined ? String(model.topP) : undefined, + hasIdentity: false, + identityProviders: [] as { name: string; envVarName: string }[], + }; + + switch (model.provider) { + case "bedrock": { + context.modelProvider = "Bedrock"; + if (isBedrockMantleModel(spec)) { + context.bedrockMantle = true; + context.mantleApiFormat = model.apiFormat; + context.mantleProprietary = isProprietaryOpenAiModel(model.modelId); + // Mantle is invoked via the bedrock-mantle service, not bedrock:InvokeModel, + // so the runtime role's default Bedrock grant is insufficient. + return { + context, + policyFile: { + name: "bedrock-mantle-policy.json", + doc: { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Action: "bedrock-mantle:CreateInference", + Resource: "arn:aws:bedrock-mantle:*:*:project/default", + }, + { + Effect: "Allow", + Action: "bedrock-mantle:CallWithBearerToken", + Resource: "*", + }, + ], + }, + }, + }; + } + return { context }; + } + case "open_ai": + case "gemini": { + context.modelProvider = model.provider === "open_ai" ? "OpenAI" : "Gemini"; + // The schema guarantees apiKeyArn for these providers. + attachIdentityProvider( + context, + model.apiKeyArn!, + model.provider, + projectSpec, + credentials, + notes, + ); + return { context }; + } + case "lite_llm": { + context.modelProvider = "LiteLLM"; + if (model.apiBase) context.litellmApiBase = model.apiBase; + if (model.additionalParams && Object.keys(model.additionalParams).length > 0) { + context.litellmAdditionalParams = model.additionalParams; + } + if (model.apiKeyArn) { + attachIdentityProvider( + context, + model.apiKeyArn, + model.provider, + projectSpec, + credentials, + notes, + ); + } else if (!model.modelId.startsWith("bedrock/")) { + // A bedrock/... LiteLLM model authenticates via the execution role; any + // other keyless provider prefix typically fails at first invocation. + notes.push({ + category: LITELLM_NO_API_KEY_NOTE_CATEGORY, + message: + `The LiteLLM model "${model.modelId}" is not a Bedrock-backed (bedrock/...) model, but ` + + `the harness has no apiKeyArn. The exported agent constructs LiteLLMModel without an ` + + `API key and will fail at first invocation if the provider requires one. Add an ` + + `API-key credential to the harness (model apiKeyArn), or use a bedrock/ model id ` + + `(which authenticates via the execution role).`, + }); + } + return { context }; + } + } +} + +/** + * Wire a non-Bedrock model's API key through AgentCore Identity: derive the + * credential-provider name from the token-vault ARN, reference it from the + * generated load.py, and register a credential entry so deploy grants access. + */ +function attachIdentityProvider( + context: Record, + apiKeyArn: string, + provider: string, + projectSpec: ProjectSpec, + credentials: Credential[], + notes: ExportNote[], +): void { + // ARN form: arn:aws:bedrock-agentcore:::token-vault//apikeycredentialprovider/ + const arnNameMatch = /\/apikeycredentialprovider\/([^/]+)$/.exec(apiKeyArn); + const credentialName = arnNameMatch ? arnNameMatch[1]! : `${projectSpec.name}${provider}ApiKey`; + const envVarName = credentialEnvVarName(credentialName); + + context.hasIdentity = true; + context.identityProviders = [{ name: credentialName, envVarName }]; + + const exists = projectSpec.credentials.some((c) => c.name === credentialName); + if (!exists) { + credentials.push({ authorizerType: "ApiKeyCredentialProvider", name: credentialName }); + } + notes.push({ + category: MODEL_API_KEY_NOTE_CATEGORY, + message: + `The harness model authenticates with the AgentCore Identity API-key provider ` + + `"${credentialName}" (${apiKeyArn}). A credential entry referencing it was added to ` + + `agentcore.json so the deployed agent can fetch the key. For local development ` + + `(\`agentcore project dev\`), add ${envVarName}= to agentcore/.env.local.`, + }); +} + +// ============================================================================ +// Memory +// ============================================================================ + +interface MemoryResolution { + provider?: { name: string; envVarName: string; strategies: string[] }; + actorId?: string; +} + +function resolveMemory( + spec: HarnessSpec, + projectSpec: ProjectSpec, + notes: ExportNote[], +): MemoryResolution { + const memory: HarnessMemoryRef | undefined = spec.memory; + if (!memory || memory.mode === "disabled") return {}; + + if (memory.mode === "managed") { + notes.push({ + category: MEMORY_MANAGED_NOTE_CATEGORY, + message: + "The harness used managed memory, which the service provisions and owns. The exported " + + "agent has no memory wired. Add a project memory (`agentcore project add memory`) and " + + "re-run the export, or wire memory/session.py to an existing AgentCore Memory by hand.", + }); + return {}; + } + + // mode === "existing" + if (memory.name) { + const entry: Memory | undefined = projectSpec.memories.find((m) => m.name === memory.name); + if (!entry) { + notes.push({ + category: MEMORY_NAME_NOT_FOUND_NOTE_CATEGORY, + message: + `The harness references the project memory "${memory.name}", but no memory with that ` + + `name exists in agentcore.json, so the exported agent has no memory wired. Add the ` + + `memory to the project and re-export, or wire memory/session.py by hand.`, + }); + return { actorId: memory.actorId }; + } + return { + provider: { + name: entry.name, + // Must match the env var the CDK injects for project memories. + envVarName: `MEMORY_${entry.name.toUpperCase()}_ID`, + strategies: entry.strategies.map(({ type }) => type), + }, + actorId: memory.actorId, + }; + } + + if (memory.arn) { + notes.push({ + category: MEMORY_ARN_NOTE_CATEGORY, + message: + `The harness references the external memory ${memory.arn}. The exported agent cannot be ` + + `wired to it automatically: the runtime role needs memory permissions on that ARN and the ` + + `memory id must reach the agent as an environment variable. Either add the memory to this ` + + `project and re-export, or grant access manually and set the env var read by ` + + `memory/session.py.`, + }); + } + return { actorId: memory.actorId }; +} + +// ============================================================================ +// Tools +// ============================================================================ + +interface ToolsResolution { + inlineFunctionTools: { + name: string; + description: string; + inputSchema: Record; + }[]; + remoteMcpTools: { + name: string; + url: string; + headerCredentials?: { headerKey: string; credentialName: string; envVarName: string }[]; + }[]; + hasShell: boolean; + hasFileOperations: boolean; +} + +function resolveTools( + spec: HarnessSpec, + allowedPatterns: string[], + projectSpec: ProjectSpec, + credentials: Credential[], + envEntries: EnvLocalEntry[], + notes: ExportNote[], +): ToolsResolution { + const result: ToolsResolution = { + inlineFunctionTools: [], + remoteMcpTools: [], + // Builtin tools are always available in the harness runtime; include them + // unless the allowedTools filter excludes them. + hasShell: isBuiltinIncluded("shell", allowedPatterns), + hasFileOperations: isBuiltinIncluded("file_operations", allowedPatterns), + }; + + for (const tool of spec.tools) { + if (!matchesAllowedTools(tool.name, allowedPatterns)) continue; + + switch (tool.type) { + case "inline_function": { + const cfg = configOf(tool, "inlineFunction") as + { description: string; inputSchema: Record } | undefined; + if (cfg) { + result.inlineFunctionTools.push({ + name: tool.name, + description: cfg.description, + inputSchema: cfg.inputSchema, + }); + } + break; + } + case "remote_mcp": { + const cfg = configOf(tool, "remoteMcp") as + { url: string; headers?: Record } | undefined; + if (!cfg) break; + const headerKeys = Object.keys(cfg.headers ?? {}); + let headerCredentials: ToolsResolution["remoteMcpTools"][number]["headerCredentials"]; + if (headerKeys.length > 0) { + headerCredentials = []; + const toolPrefix = tool.name.replace(/[^A-Za-z0-9]/g, ""); + for (const headerKey of headerKeys) { + const credentialName = `${projectSpec.name}Mcp${toolPrefix}${headerKey.replace(/[^A-Za-z0-9]/g, "")}`; + const envVarName = credentialEnvVarName(credentialName); + headerCredentials.push({ headerKey, credentialName, envVarName }); + if ( + !projectSpec.credentials.some((c) => c.name === credentialName) && + !credentials.some((c) => c.name === credentialName) + ) { + credentials.push({ + authorizerType: "ApiKeyCredentialProvider", + name: credentialName, + }); + } + envEntries.push({ + key: envVarName, + value: cfg.headers![headerKey] ?? "", + comment: `"${headerKey}" header for MCP tool "${tool.name}" (exported from harness "${spec.name}")`, + }); + } + notes.push({ + category: MCP_HEADER_CREDS_NOTE_CATEGORY, + message: + `MCP tool "${tool.name}" sends request headers whose values are managed via ` + + `AgentCore Identity. Credential entries were added to agentcore.json and the header ` + + `values written to agentcore/.env.local; they are provisioned on ` + + `\`agentcore project deploy\`.\n\n` + + headerCredentials + .map((h) => ` ${h.credentialName} (env var: ${h.envVarName})`) + .join("\n"), + }); + } + result.remoteMcpTools.push({ name: tool.name, url: cfg.url, headerCredentials }); + break; + } + case "agentcore_gateway": { + const cfg = configOf(tool, "agentCoreGateway") as { gatewayArn?: string } | undefined; + notes.push({ + category: GATEWAY_TOOL_NOTE_CATEGORY, + message: + `The gateway tool "${tool.name}"${cfg?.gatewayArn ? ` (${cfg.gatewayArn})` : ""} was ` + + `not exported: gateway URL discovery, outbound auth, and IAM wiring are managed by ` + + `the harness runtime. To keep these tools, connect an MCP client to the gateway in ` + + `mcp_client/client.py and grant the runtime role bedrock-agentcore:InvokeGateway on ` + + `the gateway (or its OAuth token flow) before deploying.`, + }); + break; + } + case "agentcore_browser": { + notes.push({ + category: BROWSER_TOOL_NOTE_CATEGORY, + message: + `The browser tool "${tool.name}" was not exported. Standalone Strands agents drive ` + + `AgentCore Browser via strands-agents-tools (AgentCoreBrowser), which needs a ` + + `Container build, the browser identifier, and bedrock-agentcore browser permissions ` + + `on the runtime role. Add the dependency and tool wiring in main.py manually if you ` + + `need it.`, + }); + break; + } + case "agentcore_code_interpreter": { + notes.push({ + category: CODE_INTERPRETER_TOOL_NOTE_CATEGORY, + message: + `The code-interpreter tool "${tool.name}" was not exported. Standalone Strands agents ` + + `use strands-agents-tools (AgentCoreCodeInterpreter), which needs the interpreter ` + + `identifier and bedrock-agentcore code-interpreter permissions on the runtime role. ` + + `Add the dependency and tool wiring in main.py manually if you need it.`, + }); + break; + } + } + } + + return result; +} + +function undefinedIfEmpty(values: T[]): T[] | undefined { + return values.length > 0 ? values : undefined; +} + +function configOf(tool: HarnessTool, key: string): unknown { + if (!tool.config || !(key in tool.config)) return undefined; + return (tool.config as Record)[key]; +} + +// ============================================================================ +// Skills +// ============================================================================ + +interface SkillsResolution { + hasSkillsFetcher: boolean; + hasFetchedSkills: boolean; + pathSkills: string[]; + s3Skills: string[]; + gitSkills: { url: string; path?: string; credentialArn?: string; username?: string }[]; + policyFiles: Record; +} + +export function isPathSkill(skill: HarnessSkill): skill is HarnessSkillPathSource { + return "path" in skill && !("gitUrl" in skill); +} + +function isS3Skill(skill: HarnessSkill): skill is HarnessSkillS3Source { + return "s3Uri" in skill; +} + +function isGitSkill(skill: HarnessSkill): skill is HarnessSkillGitSource { + return "gitUrl" in skill; +} + +function isAwsSkill(skill: HarnessSkill): skill is HarnessSkillAwsSkillsSource { + return "awsSkills" in skill; +} + +function resolveSkills( + spec: HarnessSpec, + buildType: BuildType, + targetAgentName: string, + credentials: Credential[], + notes: ExportNote[], +): SkillsResolution { + const pathSkills = spec.skills.filter(isPathSkill).map((s) => s.path); + const s3SkillSources = spec.skills.filter(isS3Skill); + const gitSkillSources = spec.skills.filter(isGitSkill); + const awsSkills = spec.skills.filter(isAwsSkill); + const policyFiles: Record = {}; + + if (pathSkills.length > 0 && buildType === "CodeZip") { + notes.push({ + category: PATH_SKILLS_NOTE_CATEGORY, + message: + `The following skill paths must exist on the container filesystem at runtime: ` + + `${pathSkills.join(", ")}. For CodeZip builds, path skills are not supported — switch to ` + + `a Container build and COPY the skill directory into app/${targetAgentName}/, or use ` + + `s3/git skill variants.`, + }); + } + + if (gitSkillSources.length > 0 && buildType === "Container") { + notes.push({ + category: GIT_SKILLS_CONTAINER_NOTE_CATEGORY, + message: + "The agent clones git skill repositories at runtime using `git`. The default Container " + + "base image does not include git. Add it to your Dockerfile before deploying:\n\n" + + " RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*", + }); + } + + // The agent fetches S3 skills with boto3 at runtime, so the runtime execution + // role needs S3 read access the managed harness never granted. + if (s3SkillSources.length > 0) { + const malformedUris: string[] = []; + const objectResources: string[] = []; + const bucketResources: string[] = []; + for (const { s3Uri } of s3SkillSources) { + const parsed = parseS3SkillArns(s3Uri); + if (!parsed) { + malformedUris.push(s3Uri); + continue; + } + if (!objectResources.includes(parsed.objectArn)) objectResources.push(parsed.objectArn); + if (!bucketResources.includes(parsed.bucketArn)) bucketResources.push(parsed.bucketArn); + } + if (malformedUris.length > 0) { + notes.push({ + category: MALFORMED_S3_SKILL_NOTE_CATEGORY, + message: + `These S3 skill URIs could not be parsed into a bucket, so no S3 read permission was ` + + `generated for them: ${malformedUris.map((u) => `"${u}"`).join(", ")}. The exported ` + + `agent still attempts to fetch these skills at runtime and will fail with S3 ` + + `AccessDenied. Fix the s3Uri values (expected \`s3:///\`) on this ` + + `agent in agentcore/agentcore.json and re-deploy.`, + }); + } + if (objectResources.length > 0) { + policyFiles["s3-skills-policy.json"] = { + Version: "2012-10-17", + Statement: [ + { Effect: "Allow", Action: "s3:GetObject", Resource: objectResources }, + { Effect: "Allow", Action: "s3:ListBucket", Resource: bucketResources }, + ], + }; + } + } + + // Private git skills reference an API-key credential provider for clone auth. + // Persist a name-only credential entry per provider so deploy grants access. + const seenGitCredentials = new Set(); + for (const skill of gitSkillSources) { + const reference = skill.auth?.credentialArn ?? skill.auth?.credentialName; + if (!reference) continue; + const name = reference.includes("/") + ? reference.slice(reference.lastIndexOf("/") + 1) + : reference; + if (seenGitCredentials.has(name)) continue; + seenGitCredentials.add(name); + if (!credentials.some((c) => c.name === name)) { + credentials.push({ authorizerType: "ApiKeyCredentialProvider", name }); + } + notes.push({ + category: GIT_SKILLS_AUTH_NOTE_CATEGORY, + message: + `The git skill ${skill.gitUrl} clones with the AgentCore Identity credential provider ` + + `"${name}". A credential entry referencing it was added to agentcore.json so the ` + + `deployed agent can fetch the token. The provider itself must already exist in ` + + `AgentCore Identity.`, + }); + } + + if (awsSkills.length > 0) { + const patterns = awsSkills.map((s) => s.awsSkills.paths?.join(", ") ?? "all").join("; "); + notes.push({ + category: AWS_SKILLS_NOTE_CATEGORY, + message: + `AWS skills are a managed harness feature and are not available in standalone Strands ` + + `agents. The following skill patterns have been omitted: ${patterns}. You can copy the ` + + `equivalent skills from https://github.com/aws/agent-toolkit-for-aws/tree/main/skills ` + + `into your project and load them as path or git skills instead.`, + }); + } + + return { + hasSkillsFetcher: spec.skills.length > 0, + hasFetchedSkills: s3SkillSources.length > 0 || gitSkillSources.length > 0, + pathSkills, + s3Skills: s3SkillSources.map((s) => s.s3Uri), + gitSkills: gitSkillSources.map((s) => ({ + url: s.gitUrl, + ...(s.path && { path: s.path }), + ...((s.auth?.credentialArn ?? s.auth?.credentialName) && { + credentialArn: s.auth!.credentialArn ?? s.auth!.credentialName, + }), + ...(s.auth?.username && { username: s.auth.username }), + })), + policyFiles, + }; +} + +/** + * Parse an s3:// skill URI into its bucket and object ARNs (undefined when the + * URI has no bucket). S3 ARNs are region/account-less. + */ +export function parseS3SkillArns( + s3Uri: string, +): { bucket: string; bucketArn: string; objectArn: string } | undefined { + const withoutScheme = s3Uri.replace(/^s3:\/\//, ""); + const [bucket, ...prefixParts] = withoutScheme.split("/"); + if (!bucket) return undefined; + const bucketArn = `arn:aws:s3:::${bucket}`; + const prefix = prefixParts.join("/").replace(/\/+$/, ""); + const objectArn = prefix ? `${bucketArn}/${prefix}/*` : `${bucketArn}/*`; + return { bucket, bucketArn, objectArn }; +} + +// ============================================================================ +// Build type + Dockerfile +// ============================================================================ + +function resolveBuildType(spec: HarnessSpec, override?: BuildType): BuildType { + if (override) return override; + if (spec.containerUri || spec.dockerfile) return "Container"; + return "CodeZip"; +} + +function resolveDockerfilePlan( + spec: HarnessSpec, + buildType: BuildType, + targetAgentName: string, + harnessDockerfileExists: boolean, + notes: ExportNote[], +): DockerfilePlan { + if (buildType !== "Container") return { source: "none" }; + if (spec.containerUri) { + notes.push({ + category: CONTAINER_URI_NOTE_CATEGORY, + message: + `The harness used a pre-built container image as its execution environment ` + + `(${spec.containerUri}). The generated Dockerfile extends that image directly ` + + `(FROM ) and layers the Strands agent code on top. If your base image does ` + + `not include Python 3.12+ or uv, add an install step before the \`uv sync\` steps. If ` + + `the base image is a private ECR repository, also grant the CodeBuild project that ` + + `builds this agent permission to pull it.`, + }); + return { source: "stub", containerUri: spec.containerUri }; + } + if (spec.dockerfile) { + if (!harnessDockerfileExists) { + notes.push({ + category: MISSING_DOCKERFILE_NOTE_CATEGORY, + message: + `The harness declares a custom Dockerfile, but no Dockerfile was found in its ` + + `directory, so nothing was copied. Create app/${targetAgentName}/Dockerfile ` + + `(including the Strands agent build layer) before \`agentcore project deploy\`.`, + }); + return { source: "none" }; + } + notes.push({ + category: CUSTOM_DOCKERFILE_NOTE_CATEGORY, + message: + `The harness used a custom Dockerfile that describes its execution environment. It has ` + + `been copied to app/${targetAgentName}/Dockerfile unchanged, but the exported agent will ` + + `NOT run as-is: a harness Dockerfile has no dependency install, code copy, or startup ` + + `command (the harness runtime supplied those). Append the Strands agent build layer ` + + `before \`agentcore project deploy\` (adjust if your base image is not Python 3.12+/uv):\n\n` + + ` WORKDIR /app\n` + + ` RUN pip install --no-cache-dir uv\n` + + ` COPY pyproject.toml uv.lock ./\n` + + ` RUN uv sync --frozen --no-dev --no-install-project\n` + + ` COPY . .\n` + + ` RUN uv sync --frozen --no-dev\n` + + ` EXPOSE 8080\n` + + ` CMD ["opentelemetry-instrument", "python", "-m", "main"]`, + }); + return { source: "harnessCopy" }; + } + return { source: "template" }; +} + +/** Dockerfile stub for a containerUri harness: extend the image, layer the agent on top. */ +export function buildDockerfileStub(containerUri: string): string { + return [ + `# Base image from the source harness: ${containerUri}`, + "# The generated Strands agent is layered on top. If the base image does not", + "# include Python 3.12+ or uv, add install steps before the COPY/RUN below.", + `FROM ${containerUri}`, + "", + "RUN pip install --no-cache-dir uv", + "", + "WORKDIR /app", + "", + "ENV UV_SYSTEM_PYTHON=1 \\", + " UV_COMPILE_BYTECODE=1 \\", + " UV_NO_PROGRESS=1 \\", + " PYTHONUNBUFFERED=1 \\", + ' PATH="/app/.venv/bin:$PATH"', + "", + "COPY pyproject.toml uv.lock ./", + "RUN uv sync --frozen --no-dev --no-install-project", + "", + "COPY . .", + "RUN uv sync --frozen --no-dev", + "", + "EXPOSE 8080 8000 9000", + "", + 'CMD ["opentelemetry-instrument", "python", "-m", "main"]', + "", + ].join("\n"); +} + +// ============================================================================ +// Filesystem mounts +// ============================================================================ + +function buildFilesystemConfigurations( + spec: HarnessSpec, +): NonNullable { + return [ + ...(spec.sessionStoragePath + ? [{ sessionStorage: { mountPath: spec.sessionStoragePath } }] + : []), + ...(spec.efsAccessPoints ?? []).map((efsAccessPoint) => ({ efsAccessPoint })), + ...(spec.s3AccessPoints ?? []).map((s3FilesAccessPoint) => ({ s3FilesAccessPoint })), + ]; +} + +// ============================================================================ +// Truncation +// ============================================================================ + +function resolveTruncationConfig( + truncation: HarnessTruncationConfig | undefined, +): Record | undefined { + if (!truncation?.config) return undefined; + const { strategy, config } = truncation; + if (strategy === "sliding_window" && "slidingWindow" in config) { + const sw = config.slidingWindow; + return sw?.messagesCount !== undefined ? { window_size: sw.messagesCount } : undefined; + } + if (strategy === "summarization" && "summarization" in config) { + const s = config.summarization as Record; + const keyMap: Record = { + summaryRatio: "summary_ratio", + preserveRecentMessages: "preserve_recent_messages", + summarizationSystemPrompt: "summarization_system_prompt", + }; + const out = Object.fromEntries( + Object.entries(keyMap) + .filter(([key]) => s[key] !== undefined) + .map(([key, target]) => [target, s[key]]), + ); + return Object.keys(out).length > 0 ? out : undefined; + } + return undefined; +} + +// ============================================================================ +// allowedTools matching (mirrors the harness runtime's _matches() semantics) +// ============================================================================ + +export function matchesAllowedTools(toolName: string, patterns: string[]): boolean { + if (patterns.includes("*")) return true; + for (const pattern of patterns) { + if (pattern === toolName) return true; + if (pattern.startsWith("@")) { + const slashIdx = pattern.indexOf("/", 1); + const pServer = slashIdx === -1 ? pattern.slice(1) : pattern.slice(1, slashIdx); + const pTool = slashIdx === -1 ? "*" : pattern.slice(slashIdx + 1); + const slashInName = toolName.indexOf("/"); + if (slashInName === -1) { + // MCP tools stored as "server_tool" flat names — keep legacy behaviour + if (fnmatch(`${pServer}_${pTool}`, toolName)) return true; + } else { + // Qualified names like "builtin/shell" + const nameServer = toolName.slice(0, slashInName); + const nameTool = toolName.slice(slashInName + 1); + if (fnmatch(pServer, nameServer) && fnmatch(pTool, nameTool)) return true; + } + } else if (fnmatch(pattern, toolName)) { + return true; + } + } + return false; +} + +/** Builtins are keyed as "builtin/": only @builtin or @builtin/ patterns match. */ +function isBuiltinIncluded(builtinName: string, patterns: string[]): boolean { + return matchesAllowedTools(`builtin/${builtinName}`, patterns); +} + +function fnmatch(pattern: string, str: string): boolean { + const re = new RegExp( + "^" + + pattern + .replace(/[.+^${}()|[\]\\]/g, "\\$&") + .replace(/\*/g, ".*") + .replace(/\?/g, ".") + + "$", + ); + return re.test(str); +} + +// ============================================================================ +// EXPORT_NOTES.md + display formatting +// ============================================================================ + +/** Render the EXPORT_NOTES.md content written into the exported agent's directory. */ +export function buildExportNotesMarkdown( + notes: ExportNote[], + harnessName: string, + agentName: string, + strandsVersion: string, +): string { + const today = new Date().toISOString().split("T")[0]; + const lines: string[] = [ + `# Export Notes — ${harnessName} → ${agentName}`, + "", + `Exported on: ${today}`, + `Strands version: ${strandsVersion}`, + `Source harness: ${harnessName}`, + `Generated agent: app/${agentName}/`, + "", + ]; + + if (notes.length === 0) { + lines.push("No manual steps required."); + } else { + lines.push("## Items requiring manual follow-up"); + for (const note of notes) { + lines.push("", `### ${note.category}`, note.message); + } + } + + lines.push(""); + return lines.join("\n"); +} + +/** + * Format export notes into styled lines for the export success path. Pure so the + * CLI (and a future TUI screen) render identical wording. + */ +export function formatExportNotes(notes: ExportNote[], notesFileHint: string): ExportNoteLine[] { + if (notes.length === 0) { + return [{ text: `No manual follow-up required. (Details: ${notesFileHint})`, tone: "dim" }]; + } + + const label = notes.length === 1 ? "note" : "notes"; + const lines: ExportNoteLine[] = [ + { text: `${notes.length} export ${label} requiring manual follow-up:`, tone: "warn" }, + ]; + for (const note of notes) { + lines.push({ text: ` - ${note.category}`, tone: "warn" }); + for (const messageLine of note.message.split("\n")) { + lines.push({ text: ` ${messageLine}`, tone: "dim" }); + } + } + lines.push({ text: `These notes are also saved to ${notesFileHint}`, tone: "dim" }); + return lines; +} diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 0ff45e1b6..b4254c693 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -42,7 +42,7 @@ function buildRuntimeSpec(input: RuntimeResourceConfig): ProjectRuntime { * Valid names consist only of ASCII letters, numbers, period, underscore, and * hyphen, and must start and end with a letter or number. */ -function toPythonPackageName(name: string): string { +export function toPythonPackageName(name: string): string { return name .replace(/[^a-zA-Z0-9._-]/g, "-") .replace(/^[^a-zA-Z0-9]+/, "") @@ -168,6 +168,10 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa transformContent: (raw) => templateRenderer.render(raw, context), filter: (name, isDir) => { if (isDir && name === "memory") return memory !== undefined; + // hooks/ carries the execution-limits capability, which only + // `project export harness` renders (harnesses can cap + // iterations/tokens/time; scaffolded runtimes cannot). + if (isDir && name === "hooks") return false; if (name === "Dockerfile" || name === ".dockerignore") return isContainer; return true; }, diff --git a/src/handlers/project/export/harness.test.ts b/src/handlers/project/export/harness.test.ts new file mode 100644 index 000000000..a34e1e4ee --- /dev/null +++ b/src/handlers/project/export/harness.test.ts @@ -0,0 +1,268 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { createRootHandler } from "../../index"; +import { + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../../../testing"; + +const HARNESS_ARN = "arn:aws:bedrock-agentcore:us-west-2:111122223333:harness/h-abc123"; + +function testExportCommand() { + const core = new TestCoreClient(); + // A fresh root per invocation, so wiring-time state (e.g. the add router's + // pinned cwd) always reflects the directory the test has cd'd into. The core + // client is shared so mock responses and recorded calls span invocations. + const route = (args: string[]) => { + const io = testIO({}); + const root = createRootHandler(core, { + io: io.io, + globalConfigAccessor: new TestGlobalConfigAccessor(), + logger: createSilentLogger(), + }); + subject.io = io; + return root.route(["node", "agentcore", ...args]); + }; + const subject = { + /** IO captured for the most recent invocation. */ + io: undefined as unknown as ReturnType, + core, + project: (args: string[]) => route(["project", ...args]), + run: (args: string[] = []) => route(["project", "export", "harness", ...args]), + }; + return subject; +} + +const originalCwd = process.cwd(); +const tempDirectories: string[] = []; + +async function inTempDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), "agentcore-export-")); + tempDirectories.push(directory); + process.chdir(directory); + return process.cwd(); +} + +afterEach(async () => { + process.chdir(originalCwd); + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +/** Scaffolds a project with one harness named `exportme` and cds into it. */ +async function inProjectWithHarness( + subject: ReturnType, +): Promise { + const directory = await inTempDirectory(); + await subject.project([ + "create", + "--name", + "orders", + "--template", + "hello-world-python", + "--skip-install", + "--skip-git", + ]); + const projectRoot = join(directory, "orders"); + process.chdir(projectRoot); + await subject.project([ + "add", + "harness", + "--name", + "exportme", + "--model", + JSON.stringify({ provider: "bedrock", modelId: "us.amazon.nova-lite-v1:0", maxTokens: 256 }), + "--system-prompt", + "You are a terse assistant.", + ]); + return projectRoot; +} + +describe("project export harness handler", () => { + test("requires exactly one of --name and --arn", async () => { + const subject = testExportCommand(); + await inProjectWithHarness(subject); + + await expect(subject.run([])).rejects.toThrow(/exactly one of --name .* or --arn/); + await expect(subject.run(["--name", "exportme", "--arn", HARNESS_ARN])).rejects.toThrow( + /exactly one of --name .* or --arn/, + ); + }); + + test("exports an in-project harness to a buildable runtime and registers it", async () => { + const subject = testExportCommand(); + const projectRoot = await inProjectWithHarness(subject); + + await subject.run(["--name", "exportme"]); + + // Generated code reflects the harness spec. + const agentDir = join(projectRoot, "app", "exportmeAgent"); + expect(await Bun.file(join(agentDir, "main.py")).text()).toContain( + 'DEFAULT_SYSTEM_PROMPT = """You are a terse assistant."""', + ); + expect(await Bun.file(join(agentDir, "model", "load.py")).text()).toContain( + 'BedrockModel(model_id="us.amazon.nova-lite-v1:0", max_tokens=256)', + ); + expect(await Bun.file(join(agentDir, "EXPORT_NOTES.md")).text()).toContain( + "# Export Notes — exportme → exportmeAgent", + ); + + // agentcore.json gains the runtime; the harness entry stays. + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes).toContainEqual({ + name: "exportmeAgent", + build: "CodeZip", + entrypoint: "main.py", + codeLocation: "app/exportmeAgent", + protocol: "HTTP", + runtimeVersion: "PYTHON_3_14", + }); + expect(spec.harnesses).toEqual([{ name: "exportme", path: "app/exportme" }]); + + // Dependencies are installed in the new agent dir. + expect(subject.core.projectCommands).toContainEqual({ + command: ["uv", "sync"], + cwd: agentDir, + }); + + expect(subject.io.stderr()).toContain( + "Exported harness 'exportme' to runtime agent 'exportmeAgent'", + ); + expect(subject.io.stdout()).toBe(""); + }); + + test("derives the default target name and honors --target-agent-name", async () => { + const subject = testExportCommand(); + const projectRoot = await inProjectWithHarness(subject); + + await subject.run(["--name", "exportme", "--target-agent-name", "my_agent"]); + + expect(existsSync(join(projectRoot, "app", "my_agent", "main.py"))).toBe(true); + await expect( + subject.run(["--name", "exportme", "--target-agent-name", "9bad"]), + ).rejects.toThrow(/invalid --target-agent-name/); + }); + + test("refuses to overwrite an existing runtime, harness, or directory", async () => { + const subject = testExportCommand(); + const projectRoot = await inProjectWithHarness(subject); + + // The scaffolded template runtime already owns its name. + await expect( + subject.run(["--name", "exportme", "--target-agent-name", "hello_world"]), + ).rejects.toThrow(/runtime with name 'hello_world' already exists/); + // A harness name is just as taken. + await expect( + subject.run(["--name", "exportme", "--target-agent-name", "exportme"]), + ).rejects.toThrow(/harness with name 'exportme' already exists/); + + // A second export of the same harness collides with the first. + await subject.run(["--name", "exportme"]); + const specBefore = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).text(); + await expect(subject.run(["--name", "exportme"])).rejects.toThrow( + /runtime with name 'exportmeAgent' already exists/, + ); + expect(await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).text()).toBe( + specBefore, + ); + }); + + test("fails clearly when the harness is not in the project", async () => { + const subject = testExportCommand(); + await inProjectWithHarness(subject); + + await expect(subject.run(["--name", "nope"])).rejects.toThrow( + /Harness 'nope' not found .* Available harnesses: exportme/, + ); + }); + + test("emits a machine-readable summary with --json", async () => { + const subject = testExportCommand(); + const projectRoot = await inProjectWithHarness(subject); + + await subject.run(["--name", "exportme", "--json"]); + + expect(JSON.parse(subject.io.stdout())).toEqual({ + harnessName: "exportme", + agentName: "exportmeAgent", + agentPath: join(projectRoot, "app", "exportmeAgent"), + notesPath: join(projectRoot, "app", "exportmeAgent", "EXPORT_NOTES.md"), + notes: [], + }); + }); + + test("exports a service harness by ARN, fetching from the ARN's region", async () => { + const subject = testExportCommand(); + const projectRoot = await inProjectWithHarness(subject); + subject.core.harness.setGetResponse({ + harness: { + harnessId: "h-abc123", + harnessName: "remote_harness", + arn: HARNESS_ARN, + status: "READY", + executionRoleArn: "arn:aws:iam::111122223333:role/HarnessRole", + createdAt: new Date(0), + updatedAt: new Date(0), + model: { bedrockModelConfig: { modelId: "us.amazon.nova-lite-v1:0" } }, + systemPrompt: [{ text: "Fetched prompt." }], + tools: [], + skills: [], + }, + } as never); + + await subject.run(["--arn", HARNESS_ARN, "--target-agent-name", "exported_arn"]); + + expect(subject.core.harness.calls).toEqual([ + { + method: "getHarness", + args: ["h-abc123", expect.objectContaining({ region: "us-west-2" })], + }, + ]); + expect(await Bun.file(join(projectRoot, "app", "exported_arn", "main.py")).text()).toContain( + 'DEFAULT_SYSTEM_PROMPT = """Fetched prompt."""', + ); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes.map((runtime: { name: string }) => runtime.name)).toContain( + "exported_arn", + ); + }); + + test("defaults the --arn target name from the fetched harness name", async () => { + const subject = testExportCommand(); + const projectRoot = await inProjectWithHarness(subject); + subject.core.harness.setGetResponse({ + harness: { + harnessName: "remote_harness", + model: { bedrockModelConfig: { modelId: "us.amazon.nova-lite-v1:0" } }, + }, + } as never); + + await subject.run(["--arn", HARNESS_ARN]); + + expect(existsSync(join(projectRoot, "app", "remote_harnessAgent", "main.py"))).toBe(true); + }); + + test("validates the project before fetching from the service", async () => { + const subject = testExportCommand(); + await inTempDirectory(); // not a project + + await expect(subject.run(["--arn", HARNESS_ARN])).rejects.toThrow(/No AgentCore project found/); + expect(subject.core.harness.calls).toEqual([]); + }); + + test("rejects a malformed --arn before calling the service", async () => { + const subject = testExportCommand(); + await inProjectWithHarness(subject); + + await expect(subject.run(["--arn", "arn:aws:not-a-harness"])).rejects.toThrow( + /not a valid harness ARN/, + ); + expect(subject.core.harness.calls).toEqual([]); + }); +}); diff --git a/src/handlers/project/export/harness.ts b/src/handlers/project/export/harness.ts new file mode 100644 index 000000000..388053239 --- /dev/null +++ b/src/handlers/project/export/harness.ts @@ -0,0 +1,115 @@ +import z from "zod"; +import { InputValidationError } from "../../../errors"; +import { createHandler, flag, ProjectKey } from "../../../router"; +import { JsonRendererKey } from "../../../tui"; +import { JsonKey } from "../../keys"; +import { AgentNameSchema, BuildTypeSchema } from "../../../projectSchemas/runtime"; +import { formatExportNotes } from "../../../core/project/templates/export"; +import { coreOptsFromCtx } from "../../utils"; +import type { ExportHarnessInput } from "../types"; +import type { ExportProjectResourceConfig } from "./types"; +import { harnessIdFromArn, mapServiceHarnessToSpec, regionFromHarnessArn } from "./serviceHarness"; + +export const createExportHarnessHandler = (config: ExportProjectResourceConfig) => + createHandler({ + name: "harness", + description: "convert a harness into an editable Strands runtime agent", + flags: [ + flag("name", "the name of an in-project harness to export", z.string().optional()), + flag( + "arn", + "the ARN of a deployed harness to fetch from the service and export", + z.string().optional(), + ), + flag( + "target-agent-name", + "the name of the generated runtime agent (default: Agent)", + z.string().optional(), + ), + flag( + "build", + "build type for the exported agent: CodeZip or Container", + BuildTypeSchema.optional(), + ), + ], + handle: async (ctx, flags) => { + if (!!flags.name === !!flags.arn) { + throw new InputValidationError( + "specify exactly one of --name (in-project harness) or --arn (deployed harness)", + ); + } + + // withProject has already resolved and validated the enclosing project — + // before any service fetch, so a broken project fails fast. + const project = ctx.require(ProjectKey); + const jsonOutput = ctx.require(JsonKey); + + let input: ExportHarnessInput; + if (flags.arn) { + config.io.stderr.write(`Fetching harness from the service\n`); + const harnessId = harnessIdFromArn(flags.arn); + // The ARN names the region the harness lives in; fall back to the CLI's + // resolved region only when the ARN carries none. + const coreOpts = coreOptsFromCtx(ctx); + const region = regionFromHarnessArn(flags.arn) ?? coreOpts.region; + const response = await config.core.harness.getHarness(harnessId, { ...coreOpts, region }); + if (!response.harness) { + throw new InputValidationError(`the service returned no harness for "${flags.arn}"`); + } + const { spec, systemPrompt } = mapServiceHarnessToSpec(response.harness); + input = { + prefetched: { spec, systemPrompt }, + targetAgentName: resolveTargetAgentName(flags["target-agent-name"], spec.name), + build: flags.build, + }; + } else { + input = { + harnessName: flags.name!, + targetAgentName: resolveTargetAgentName(flags["target-agent-name"], flags.name!), + build: flags.build, + }; + } + + // Progress goes to stderr, keeping stdout for machine output. Driven by + // hand because the result is the generator's return value. + const exportRun = config.projectManager.exportHarness(project, input); + let next = await exportRun.next(); + while (!next.done) { + config.io.stderr.write(`${next.value.message}\n`); + next = await exportRun.next(); + } + const result = next.value; + + config.io.stderr.write( + `Exported harness '${result.harnessName}' to runtime agent '${result.agentName}' (${result.agentPath})\n`, + ); + for (const line of formatExportNotes(result.notes, result.notesPath)) { + config.io.stderr.write(`${line.text}\n`); + } + config.io.stderr.write( + "Next steps: review the generated code, then `agentcore project build` and `agentcore project deploy`\n", + ); + + if (jsonOutput) { + ctx.require(JsonRendererKey).renderJson({ + harnessName: result.harnessName, + agentName: result.agentName, + agentPath: result.agentPath, + notesPath: result.notesPath, + notes: result.notes, + }); + } + }, + }); + +/** Default the target agent name to `Agent` and validate it. */ +function resolveTargetAgentName(flagValue: string | undefined, harnessName: string): string { + const targetAgentName = flagValue ?? `${harnessName}Agent`; + const parsed = AgentNameSchema.safeParse(targetAgentName); + if (!parsed.success) { + throw new InputValidationError( + `invalid --target-agent-name "${targetAgentName}": ${parsed.error.issues[0]?.message ?? "invalid name"}`, + ); + } + return parsed.data; +} diff --git a/src/handlers/project/export/index.ts b/src/handlers/project/export/index.ts new file mode 100644 index 000000000..a88604a85 --- /dev/null +++ b/src/handlers/project/export/index.ts @@ -0,0 +1,17 @@ +import { withProject } from "../../../middleware/"; +import { Router } from "../../../router"; +import { createExportHarnessHandler } from "./harness"; +import type { ExportProjectResourceConfig } from "./types"; + +export function createExportProjectResourceHandler(config: ExportProjectResourceConfig): Router { + const projectExport = new Router( + "export", + "convert project resources into editable code you own", + ); + // The project is resolved and validated up front, so `--arn` never fetches + // from the service on behalf of a directory that is not a valid project. + // No pinned cwd: the invocation-time working directory is the one searched. + projectExport.use(withProject({ projectManager: config.projectManager })); + projectExport.handler(createExportHarnessHandler(config)); + return projectExport; +} diff --git a/src/handlers/project/export/serviceHarness.test.ts b/src/handlers/project/export/serviceHarness.test.ts new file mode 100644 index 000000000..dedaae749 --- /dev/null +++ b/src/handlers/project/export/serviceHarness.test.ts @@ -0,0 +1,279 @@ +import { describe, expect, test } from "bun:test"; +import type { Harness } from "@aws-sdk/client-bedrock-agentcore-control"; +import { InputValidationError, MalformedServiceResponseError } from "../../../errors"; +import { harnessIdFromArn, mapServiceHarnessToSpec, regionFromHarnessArn } from "./serviceHarness"; + +const ARN = "arn:aws:bedrock-agentcore:us-west-2:111122223333:harness/h-abc123"; + +function serviceHarness(overrides: Partial = {}): Harness { + return { + harnessId: "h-abc123", + harnessName: "assistant", + arn: ARN, + status: "READY", + executionRoleArn: "arn:aws:iam::111122223333:role/HarnessRole", + createdAt: new Date(0), + updatedAt: new Date(0), + model: { + bedrockModelConfig: { + modelId: "us.amazon.nova-lite-v1:0", + temperature: 0.3, + maxTokens: 512, + }, + }, + systemPrompt: [{ text: "Be terse." }], + tools: [], + skills: [], + allowedTools: undefined, + truncation: undefined, + environment: undefined, + ...overrides, + } as Harness; +} + +describe("harness ARN helpers", () => { + test("extracts the harness id and region from an ARN", () => { + expect(harnessIdFromArn(ARN)).toBe("h-abc123"); + expect(regionFromHarnessArn(ARN)).toBe("us-west-2"); + }); + + test("rejects a malformed harness ARN and tolerates a missing region", () => { + expect(() => harnessIdFromArn("arn:aws:foo:bar")).toThrow(InputValidationError); + expect(regionFromHarnessArn("not-an-arn")).toBeUndefined(); + }); +}); + +describe("mapServiceHarnessToSpec", () => { + test("maps a bedrock harness with prompt, limits, env vars, and containerUri", () => { + const { spec, systemPrompt } = mapServiceHarnessToSpec( + serviceHarness({ + systemPrompt: [{ text: "Be terse." }, { text: "Be kind." }], + maxIterations: 4, + maxTokens: 1024, + timeoutSeconds: 30, + environmentVariables: { LOG_LEVEL: "debug" }, + environmentArtifact: { + containerConfiguration: { + containerUri: "111122223333.dkr.ecr.us-west-2.amazonaws.com/base:latest", + }, + }, + truncation: { + strategy: "sliding_window", + config: { slidingWindow: { messagesCount: 12 } }, + }, + } as Partial), + ); + + expect(spec.name).toBe("assistant"); + expect(spec.model).toEqual({ + provider: "bedrock", + modelId: "us.amazon.nova-lite-v1:0", + temperature: 0.3, + maxTokens: 512, + }); + expect(systemPrompt).toBe("Be terse.\nBe kind."); + expect(spec.maxIterations).toBe(4); + expect(spec.maxTokens).toBe(1024); + expect(spec.timeoutSeconds).toBe(30); + expect(spec.environmentVariables).toEqual({ LOG_LEVEL: "debug" }); + expect(spec.containerUri).toBe("111122223333.dkr.ecr.us-west-2.amazonaws.com/base:latest"); + expect(spec.truncation).toEqual({ + strategy: "sliding_window", + config: { slidingWindow: { messagesCount: 12 } }, + }); + // The harness role must not follow the export. + expect(spec.executionRoleArn).toBeUndefined(); + }); + + test("maps every skill source variant and drops unknown members", () => { + const { spec } = mapServiceHarnessToSpec( + serviceHarness({ + skills: [ + { path: "local_skill" }, + { s3: { uri: "s3://bucket/prefix" } }, + { + git: { + url: "https://github.com/example/skills.git", + path: "subdir", + auth: { + credentialArn: + "arn:aws:bedrock-agentcore:us-west-2:111122223333:token-vault/default/apikeycredentialprovider/GitPat", + username: "bot", + }, + }, + }, + { awsSkills: { paths: ["aws/foo"] } }, + { $unknown: ["mystery", {}] }, + ] as Harness["skills"], + }), + ); + + expect(spec.skills).toEqual([ + { path: "local_skill" }, + { s3Uri: "s3://bucket/prefix" }, + { + gitUrl: "https://github.com/example/skills.git", + path: "subdir", + auth: { + credentialArn: + "arn:aws:bedrock-agentcore:us-west-2:111122223333:token-vault/default/apikeycredentialprovider/GitPat", + username: "bot", + }, + }, + { awsSkills: { paths: ["aws/foo"] } }, + ]); + }); + + test("maps tools by passing their config through", () => { + const { spec } = mapServiceHarnessToSpec( + serviceHarness({ + tools: [ + { + type: "remote_mcp", + name: "exa", + config: { remoteMcp: { url: "https://mcp.exa.ai/mcp" } }, + }, + { type: "agentcore_code_interpreter" }, + ] as Harness["tools"], + }), + ); + expect(spec.tools).toEqual([ + { type: "remote_mcp", name: "exa", config: { remoteMcp: { url: "https://mcp.exa.ai/mcp" } } }, + { type: "agentcore_code_interpreter", name: "agentcore_code_interpreter" }, + ]); + }); + + test.each([ + [ + "an existing memory by arn", + { + agentCoreMemoryConfiguration: { + arn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:memory/m-1", + actorId: "actor-1", + }, + }, + { + mode: "existing", + arn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:memory/m-1", + actorId: "actor-1", + }, + ], + [ + "a provisioned managed memory as existing-by-arn", + { + managedMemoryConfiguration: { + arn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:memory/m-2", + }, + }, + { mode: "existing", arn: "arn:aws:bedrock-agentcore:us-west-2:111122223333:memory/m-2" }, + ], + [ + "an unprovisioned managed memory as managed", + { managedMemoryConfiguration: {} }, + { mode: "managed" }, + ], + ["disabled memory as disabled", { disabled: {} }, { mode: "disabled" }], + ])("maps %s", (_label, memory, expected) => { + const { spec } = mapServiceHarnessToSpec(serviceHarness({ memory } as Partial)); + expect(spec.memory).toEqual(expected as never); + }); + + test("maps the runtime environment: VPC, lifecycle, and filesystem mounts", () => { + const { spec } = mapServiceHarnessToSpec( + serviceHarness({ + environment: { + agentCoreRuntimeEnvironment: { + networkConfiguration: { + networkMode: "VPC", + networkModeConfig: { + subnets: ["subnet-12345678"], + securityGroups: ["sg-12345678"], + }, + }, + lifecycleConfiguration: { idleRuntimeSessionTimeout: 900 }, + filesystemConfigurations: [ + { sessionStorage: { mountPath: "/mnt/session" } }, + { + efsAccessPoint: { + accessPointArn: + "arn:aws:elasticfilesystem:us-west-2:111122223333:access-point/fsap-0123456789abcdef0", + mountPath: "/mnt/tools", + }, + }, + ], + }, + }, + } as Partial), + ); + + expect(spec.networkMode).toBe("VPC"); + expect(spec.networkConfig).toEqual({ + subnets: ["subnet-12345678"], + securityGroups: ["sg-12345678"], + }); + expect(spec.lifecycleConfig).toEqual({ idleRuntimeSessionTimeout: 900 }); + expect(spec.sessionStoragePath).toBe("/mnt/session"); + expect(spec.efsAccessPoints).toEqual([ + { + accessPointArn: + "arn:aws:elasticfilesystem:us-west-2:111122223333:access-point/fsap-0123456789abcdef0", + mountPath: "/mnt/tools", + }, + ]); + }); + + test("rejects a VPC harness without explicit subnets/security groups before anything is written", () => { + expect(() => + mapServiceHarnessToSpec( + serviceHarness({ + environment: { + agentCoreRuntimeEnvironment: { + networkConfiguration: { networkMode: "VPC" }, + }, + }, + } as Partial), + ), + ).toThrow(InputValidationError); + }); + + test("maps openai and litellm model configs", () => { + const openAi = mapServiceHarnessToSpec( + serviceHarness({ + model: { + openAiModelConfig: { + modelId: "gpt-4.1", + apiKeyArn: + "arn:aws:bedrock-agentcore:us-west-2:111122223333:token-vault/default/apikeycredentialprovider/K", + apiFormat: "responses", + }, + }, + } as Partial), + ).spec; + expect(openAi.model.provider).toBe("open_ai"); + expect(openAi.model.apiFormat).toBe("responses"); + + const liteLlm = mapServiceHarnessToSpec( + serviceHarness({ + model: { + liteLlmModelConfig: { + modelId: "bedrock/us.amazon.nova-lite-v1:0", + apiBase: "https://litellm.example", + additionalParams: { max_retries: 2 }, + }, + }, + } as Partial), + ).spec; + expect(liteLlm.model.provider).toBe("lite_llm"); + expect(liteLlm.model.apiBase).toBe("https://litellm.example"); + expect(liteLlm.model.additionalParams).toEqual({ max_retries: 2 }); + }); + + test("wraps an inexpressible payload in a MalformedServiceResponseError", () => { + expect(() => mapServiceHarnessToSpec(serviceHarness({ model: undefined }))).toThrow( + MalformedServiceResponseError, + ); + expect(() => + mapServiceHarnessToSpec(serviceHarness({ harnessName: "definitely not a valid name" })), + ).toThrow(MalformedServiceResponseError); + }); +}); diff --git a/src/handlers/project/export/serviceHarness.ts b/src/handlers/project/export/serviceHarness.ts new file mode 100644 index 000000000..1aeabab20 --- /dev/null +++ b/src/handlers/project/export/serviceHarness.ts @@ -0,0 +1,246 @@ +import type { + Harness, + HarnessSkill as ApiHarnessSkill, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import z from "zod"; +import { InputValidationError, MalformedServiceResponseError } from "../../../errors"; +import { HarnessSpecSchema, type HarnessSpec } from "../../../projectSchemas/harness"; + +/** Extract the harness id from a harness ARN (`.../harness/` -> ``). */ +export function harnessIdFromArn(arn: string): string { + const match = /:harness\/([^/]+)$/.exec(arn); + if (!match?.[1]) { + throw new InputValidationError( + `"${arn}" is not a valid harness ARN (expected ...:harness/)`, + ); + } + return match[1]; +} + +/** + * The region embedded in a harness ARN (`arn::bedrock-agentcore::...`), + * or undefined when the ARN carries none. The harness lives in this region, so + * it takes precedence over the CLI's resolved region for the export fetch. + */ +export function regionFromHarnessArn(arn: string): string | undefined { + const match = /^arn:[^:]+:bedrock-agentcore:([a-z0-9-]+):/.exec(arn); + return match?.[1] || undefined; +} + +/** + * Map a control-plane Harness (GetHarness response) onto the local + * {@link HarnessSpecSchema} shape, so the `--arn` path feeds the export mapper + * exactly like an in-project harness. Throws when the payload cannot be + * expressed as a valid local spec. + */ +export function mapServiceHarnessToSpec(harness: Harness): { + spec: HarnessSpec; + systemPrompt?: string; +} { + const joinedPrompt = (harness.systemPrompt ?? []) + .map((block) => ("text" in block ? block.text : undefined)) + .filter((text): text is string => typeof text === "string" && text.length > 0) + .join("\n"); + const systemPrompt = joinedPrompt.length > 0 ? joinedPrompt : undefined; + + const candidate = clean({ + name: harness.harnessName, + model: mapModel(harness.model), + tools: (harness.tools ?? []).map((tool) => + clean({ + type: tool.type, + name: tool.name ?? tool.type, + config: tool.config, + }), + ), + skills: (harness.skills ?? []).map(mapSkill).filter((skill) => skill !== undefined), + allowedTools: harness.allowedTools, + memory: mapMemory(harness.memory), + maxIterations: harness.maxIterations ?? undefined, + maxTokens: harness.maxTokens ?? undefined, + timeoutSeconds: harness.timeoutSeconds ?? undefined, + truncation: harness.truncation, + containerUri: harness.environmentArtifact?.containerConfiguration?.containerUri, + environmentVariables: harness.environmentVariables, + // The harness's executionRoleArn is deliberately NOT carried: the exported + // agent is a new runtime that gets its own CDK-managed execution role. + ...mapRuntimeEnvironment(harness), + }); + + const parsed = HarnessSpecSchema.safeParse(candidate); + if (!parsed.success) { + throw new MalformedServiceResponseError( + `The fetched harness cannot be expressed as a local harness spec:\n${z.prettifyError(parsed.error)}`, + { cause: parsed.error }, + ); + } + return { spec: parsed.data, systemPrompt }; +} + +function mapModel(model: Harness["model"]): Record { + if (model?.bedrockModelConfig) { + const c = model.bedrockModelConfig; + return clean({ + provider: "bedrock", + modelId: c.modelId, + apiFormat: c.apiFormat, + temperature: c.temperature, + topP: c.topP, + maxTokens: c.maxTokens, + }); + } + if (model?.openAiModelConfig) { + const c = model.openAiModelConfig; + return clean({ + provider: "open_ai", + modelId: c.modelId, + apiKeyArn: c.apiKeyArn, + apiFormat: c.apiFormat, + temperature: c.temperature, + topP: c.topP, + maxTokens: c.maxTokens, + }); + } + if (model?.geminiModelConfig) { + const c = model.geminiModelConfig; + return clean({ + provider: "gemini", + modelId: c.modelId, + apiKeyArn: c.apiKeyArn, + temperature: c.temperature, + topP: c.topP, + topK: c.topK, + maxTokens: c.maxTokens, + }); + } + if (model?.liteLlmModelConfig) { + const c = model.liteLlmModelConfig; + return clean({ + provider: "lite_llm", + modelId: c.modelId, + apiKeyArn: c.apiKeyArn, + apiBase: c.apiBase, + temperature: c.temperature, + topP: c.topP, + maxTokens: c.maxTokens, + additionalParams: c.additionalParams, + }); + } + throw new MalformedServiceResponseError( + "The fetched harness has no recognized model configuration.", + ); +} + +/** Service skill union -> the flat local skill shape; unknown members are dropped. */ +function mapSkill(skill: ApiHarnessSkill): Record | undefined { + if ("path" in skill && skill.path) return { path: skill.path }; + if ("s3" in skill && skill.s3?.uri) return { s3Uri: skill.s3.uri }; + if ("git" in skill && skill.git?.url) { + const { url, path, auth } = skill.git; + return clean({ + gitUrl: url, + path, + auth: auth?.credentialArn + ? clean({ credentialArn: auth.credentialArn, username: auth.username }) + : undefined, + }); + } + if ("awsSkills" in skill && skill.awsSkills) { + return { awsSkills: clean({ paths: skill.awsSkills.paths }) }; + } + return undefined; +} + +/** + * Service memory union -> the local memory ref. A provisioned harness memory + * (managed, with a service-populated ARN) is referenced by ARN like any + * bring-your-own memory; managed-without-ARN keeps the `managed` marker so the + * export mapper can emit its follow-up note. + */ +function mapMemory(memory: Harness["memory"]): Record | undefined { + if (!memory) return undefined; + if ("agentCoreMemoryConfiguration" in memory && memory.agentCoreMemoryConfiguration?.arn) { + const { arn, actorId, messagesCount } = memory.agentCoreMemoryConfiguration; + return clean({ mode: "existing", arn, actorId, messagesCount }); + } + if ("managedMemoryConfiguration" in memory && memory.managedMemoryConfiguration) { + const arn = memory.managedMemoryConfiguration.arn; + if (arn) return { mode: "existing", arn }; + return { mode: "managed" }; + } + if ("disabled" in memory && memory.disabled) return { mode: "disabled" }; + return undefined; +} + +/** + * Runtime-environment block -> networkMode/networkConfig, lifecycleConfig, and + * filesystem mounts. A VPC harness without explicit subnets/security groups + * cannot be expressed locally; fail here — before anything is written — with a + * clear message instead of a downstream schema error. + */ +function mapRuntimeEnvironment(harness: Harness): Record { + const env = + harness.environment && "agentCoreRuntimeEnvironment" in harness.environment + ? harness.environment.agentCoreRuntimeEnvironment + : undefined; + if (!env) return {}; + const out: Record = {}; + + const net = env.networkConfiguration; + if (net?.networkMode === "VPC") { + const subnets = net.networkModeConfig?.subnets; + const securityGroups = net.networkModeConfig?.securityGroups; + if (!subnets?.length || !securityGroups?.length) { + throw new InputValidationError( + "This harness runs in a VPC but its network configuration is missing explicit subnets " + + "and/or security groups, which the exported agent requires. Re-create the harness with " + + "explicit VPC subnets and security groups, or export a non-VPC harness.", + ); + } + out.networkMode = "VPC"; + out.networkConfig = { subnets, securityGroups }; + } + + const lifecycle = env.lifecycleConfiguration; + if (lifecycle && (lifecycle.idleRuntimeSessionTimeout != null || lifecycle.maxLifetime != null)) { + out.lifecycleConfig = clean({ + idleRuntimeSessionTimeout: lifecycle.idleRuntimeSessionTimeout ?? undefined, + maxLifetime: lifecycle.maxLifetime ?? undefined, + }); + } + + const efs: { accessPointArn: string; mountPath: string }[] = []; + const s3: { accessPointArn: string; mountPath: string }[] = []; + for (const fs of env.filesystemConfigurations ?? []) { + if ("sessionStorage" in fs && fs.sessionStorage?.mountPath) { + out.sessionStoragePath = fs.sessionStorage.mountPath; + } else if ( + "efsAccessPoint" in fs && + fs.efsAccessPoint?.accessPointArn && + fs.efsAccessPoint.mountPath + ) { + efs.push({ + accessPointArn: fs.efsAccessPoint.accessPointArn, + mountPath: fs.efsAccessPoint.mountPath, + }); + } else if ( + "s3FilesAccessPoint" in fs && + fs.s3FilesAccessPoint?.accessPointArn && + fs.s3FilesAccessPoint.mountPath + ) { + s3.push({ + accessPointArn: fs.s3FilesAccessPoint.accessPointArn, + mountPath: fs.s3FilesAccessPoint.mountPath, + }); + } + } + if (efs.length) out.efsAccessPoints = efs; + if (s3.length) out.s3AccessPoints = s3; + + return out; +} + +/** Drop undefined-valued keys so optional fields stay omitted. */ +function clean>(obj: T): T { + return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as T; +} diff --git a/src/handlers/project/export/types.ts b/src/handlers/project/export/types.ts new file mode 100644 index 000000000..b4d8b7cd1 --- /dev/null +++ b/src/handlers/project/export/types.ts @@ -0,0 +1,11 @@ +import type { AppIO } from "../../../io"; +import type { Core } from "../../types"; +import type { ProjectManager } from "../types"; + +/** Dependencies for `project export` handlers. */ +export type ExportProjectResourceConfig = { + projectManager: ProjectManager; + /** Service clients, for exporting a harness fetched by ARN. */ + core: Core; + io: AppIO; +}; diff --git a/src/handlers/project/index.ts b/src/handlers/project/index.ts index 7106e4993..22c1d2a6b 100644 --- a/src/handlers/project/index.ts +++ b/src/handlers/project/index.ts @@ -16,6 +16,7 @@ import { createStatusProjectHandler } from "./status"; import { createBuildProjectHandler } from "./build"; import type { ProjectManager } from "./types"; import { createAddProjectResourceHandler } from "./add"; +import { createExportProjectResourceHandler } from "./export"; type ProjectHandlerConfig = { core: Core; @@ -39,6 +40,7 @@ export function createProjectHandler({ core, io }: ProjectHandlerConfig): Router }), ); project.handler(createAddProjectResourceHandler(config)); + project.handler(createExportProjectResourceHandler({ projectManager, core, io })); project.handler( withProject({ projectManager: config.projectManager })( createRemoveProjectHandler({ projectManager: config.projectManager, io: config.io }), diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 93f032393..0963dbb35 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -1,4 +1,6 @@ import { HarnessSpecSchema } from "../../projectSchemas/harness"; +import type { BuildType } from "../../projectSchemas/runtime"; +import type { ExportNote } from "../../core/project/templates/export"; import type { CredentialSchema } from "../../projectSchemas/credential"; import type { PaymentConnectorSchema, PaymentManagerSchema } from "../../projectSchemas/payment"; import type { ConfigBundleSchema } from "../../projectSchemas/config-bundle"; @@ -214,6 +216,33 @@ export type AddResourceInput = export type ProjectResource = AddResourceInput["resourceType"]; +/** Input for {@link ProjectManager.exportHarness}. */ +export type ExportHarnessInput = { + /** Name of an in-project harness. Mutually exclusive with `prefetched`. */ + harnessName?: string; + /** A harness spec + system prompt fetched from the service (the `--arn` path). */ + prefetched?: { + spec: z.output; + systemPrompt?: string; + }; + /** Name of the runtime agent to generate. */ + targetAgentName: string; + /** Build override; when absent the harness spec decides (CodeZip unless it demands Container). */ + build?: BuildType; +}; + +/** Result of {@link ProjectManager.exportHarness}. */ +export type ExportHarnessResult = { + harnessName: string; + agentName: string; + /** Absolute path of the generated agent directory. */ + agentPath: string; + /** Absolute path of the EXPORT_NOTES.md file inside it. */ + notesPath: string; + /** Manual follow-up items also written to EXPORT_NOTES.md. */ + notes: ExportNote[]; +}; + export type RemoveResourceInput = | { resourceType: @@ -284,4 +313,15 @@ export interface ProjectManager { * deploy can tear down the target's stack. */ removeAllResources(project: Project): Promise; + + /** + * Convert a harness into an editable Strands runtime agent: render the agent + * code under app//, register the runtime in agentcore.json + * (the source harness entry is kept), and write EXPORT_NOTES.md for anything + * that could not be mapped mechanically. + */ + exportHarness( + project: Project, + input: ExportHarnessInput, + ): AsyncGenerator; } From 1f284d24941eeba8cc4ad602e6b7129ac13cd5ac Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Sun, 30 Aug 2026 23:45:37 -0400 Subject: [PATCH 08/12] refactor(core): extract shared observability helpers Move the runtime log-group derivation, the Insights query-value sanitizer, and the paginating CloudWatch Logs Insights query runner out of the private scope of src/core/eval.tsx into src/core/observability.ts so the upcoming runtime logs/traces commands can share them. The runner's row-ceiling error becomes an injectable policy (InsightsRowLimit); eval passes its existing message, so behavior is unchanged. Also adds parseTimeString, porting the old CLI's time-parser semantics (now / relative 5m-1h-2d / epoch ms / ISO 8601) with a typed InputValidationError on invalid input. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- src/core/eval.tsx | 116 +++++++--------------- src/core/observability.test.ts | 158 ++++++++++++++++++++++++++++++ src/core/observability.ts | 169 +++++++++++++++++++++++++++++++++ 3 files changed, 363 insertions(+), 80 deletions(-) create mode 100644 src/core/observability.test.ts create mode 100644 src/core/observability.ts diff --git a/src/core/eval.tsx b/src/core/eval.tsx index fd51e04ee..1f0d551f1 100644 --- a/src/core/eval.tsx +++ b/src/core/eval.tsx @@ -88,13 +88,7 @@ import { type DataSourceConfig as DataPlaneDataSourceConfig, type CloudWatchFilterConfig, } from "@aws-sdk/client-bedrock-agentcore"; -import { - GetQueryResultsCommand, - ResourceNotFoundException, - StartQueryCommand, - type CloudWatchLogsClient, - type ResultField, -} from "@aws-sdk/client-cloudwatch-logs"; +import { ResourceNotFoundException, type ResultField } from "@aws-sdk/client-cloudwatch-logs"; import type { DocumentType } from "@smithy/types"; import { randomUUID } from "node:crypto"; import { unlink } from "node:fs/promises"; @@ -104,13 +98,20 @@ import { Transform } from "node:stream"; import { setTimeout as sleep } from "node:timers/promises"; import { AgentCoreCLIError, - CloudWatchQueryError, ERROR_SOURCE, FileWriteError, InputValidationError, NetworkingError, ResourceNotFoundError, } from "../errors"; +import { + DEFAULT_ENDPOINT_QUALIFIER, + INSIGHTS_MAX_ROWS, + runInsightsQuery, + runtimeLogGroup, + sanitizeQueryValue, + type InsightsRowLimit, +} from "./observability"; import type { BatchEvaluationDetail, CodeBasedUpdate, @@ -166,7 +167,6 @@ import { scopePolicyName, } from "./onlineEvalExecutionRole"; -const DEFAULT_ENDPOINT_QUALIFIER = "DEFAULT"; const DEFAULT_INGESTION_WAIT_MS = 180_000; const DATASET_EXAMPLES_BATCH_LIMIT = 1000; const DATASET_MUTATION_PAYLOAD_LIMIT_BYTES = 5 * 1024 * 1024; @@ -190,8 +190,17 @@ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; // A hard Evaluate limit: at most 10 trace/span ids per request. const EVALUATE_TARGET_BATCH = 10; -// CloudWatch Logs Insights hard ceiling: a query returns at most 100k rows. -const INSIGHTS_MAX_ROWS = 100_000; +// Eval's row-ceiling policy for the shared Insights runner: overflowing the +// CloudWatch hard ceiling means a partial conversation would be scored, so the +// remedy is eval-specific (narrow the session scope or go through batch). +const EVAL_INSIGHTS_ROW_LIMIT: InsightsRowLimit = { + maxRows: INSIGHTS_MAX_ROWS, + buildError: (maxRows) => + new InputValidationError( + `Too many spans in scope (>= ${maxRows}). Narrow --session-ids or the time ` + + `window, or use 'eval batch-evaluation' for large jobs.`, + ), +}; const DEFAULT_BATCH_INSIGHTS_PAGE_SIZE = 50; @@ -593,7 +602,14 @@ export class EvalClient implements CoreEvalClient { // Runtime group required (missing = agent has no traces); aws/spans optional now // https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-configure.html#observability-configure-unified-traces const [runtimeRows, sharedRows] = await Promise.all([ - runInsightsQuery(logs, [logGroupName], queryString, startSec, endSec).catch((error) => { + runInsightsQuery( + logs, + [logGroupName], + queryString, + startSec, + endSec, + EVAL_INSIGHTS_ROW_LIMIT, + ).catch((error) => { if (error instanceof ResourceNotFoundException) { throw new ResourceNotFoundError( `No telemetry found for agent "${input.agent}": its runtime log group ${logGroupName} ` + @@ -603,7 +619,14 @@ export class EvalClient implements CoreEvalClient { } throw error; }), - runInsightsQuery(logs, [SPANS_LOG_GROUP], queryString, startSec, endSec).catch((error) => { + runInsightsQuery( + logs, + [SPANS_LOG_GROUP], + queryString, + startSec, + endSec, + EVAL_INSIGHTS_ROW_LIMIT, + ).catch((error) => { if (error instanceof ResourceNotFoundException) return []; throw error; }), @@ -1736,13 +1759,6 @@ function endWithNewline(): Transform { }); } -// runtimeLogGroup mirrors the old CLI's derivation (src/cli/aws/cloudwatch.ts): -// AgentCore always writes a runtime endpoint's traces to this fixed path, keyed -// by the runtime *id*. -function runtimeLogGroup(runtimeId: string, endpoint: string): string { - return `/aws/bedrock-agentcore/runtimes/${runtimeId}-${endpoint}`; -} - // runtimeServiceName derives the CloudWatch trace service name that scopes a // CreateOnlineEvaluationConfig data source to one runtime endpoint's sessions: // `{runtimeName}.{endpoint}`, keyed by the runtime *name* (verified against @@ -1805,12 +1821,6 @@ async function agentDataSource( }; } -// sanitizeQueryValue strips single quotes so an id can't break out of the quoted -// Insights filter literal it is interpolated into (matches the old CLI). -function sanitizeQueryValue(value: string): string { - return value.replace(/'/g, ""); -} - function buildSpanQuery(serviceName: string, sessionIds?: string[], traceId?: string): string { let query = `fields @message, attributes.session.id as sessionId, traceId, spanId | filter resource.attributes.service.name in ['${sanitizeQueryValue(serviceName)}']`; @@ -1825,60 +1835,6 @@ function buildSpanQuery(serviceName: string, sessionIds?: string[], traceId?: st return query; } -// runInsightsQuery starts a CloudWatch Logs Insights query, waits for it to finish, -// then drains all result pages. GetQueryResults returns <=10k rows per call, so a -// long session's spans span multiple pages (nextToken); dropping any would score a -// partial conversation. Fails fast if the ceiling is hit — that belongs in batch. -async function runInsightsQuery( - logs: CloudWatchLogsClient, - logGroupNames: string[], - queryString: string, - startSec: number, - endSec: number, -): Promise { - const started = await logs.send( - new StartQueryCommand({ logGroupNames, queryString, startTime: startSec, endTime: endSec }), - ); - const queryId = started.queryId; - - // Phase 1: wait for completion. A large scan can take minutes, so the deadline is - // generous; each poll costs one cheap GetQueryResults call. - let status = "Running"; - for (let i = 0; i < 300 && status !== "Complete"; i++) { - const result = await logs.send(new GetQueryResultsCommand({ queryId })); - status = result.status ?? "Unknown"; - if (status === "Failed" || status === "Cancelled" || status === "Timeout") { - throw new CloudWatchQueryError(`CloudWatch Logs Insights query ${status.toLowerCase()}`, { - meta: { queryId, status }, - }); - } - if (status !== "Complete") await new Promise((resolve) => setTimeout(resolve, 1000)); - } - if (status !== "Complete") { - throw new CloudWatchQueryError("CloudWatch Logs Insights query did not finish in time", { - meta: { queryId, status }, - }); - } - - // Phase 2: drain pages. Terminates on nextToken; total is bounded by the query's - // `| limit INSIGHTS_MAX_ROWS`. - const rows: ResultField[][] = []; - let nextToken: string | undefined; - do { - const result = await logs.send(new GetQueryResultsCommand({ queryId, nextToken })); - rows.push(...(result.results ?? [])); - nextToken = result.nextToken; - } while (nextToken); - - if (rows.length >= INSIGHTS_MAX_ROWS) { - throw new InputValidationError( - `Too many spans in scope (>= ${INSIGHTS_MAX_ROWS}). Narrow --session-ids or the time ` + - `window, or use 'eval batch-evaluation' for large jobs.`, - ); - } - return rows; -} - // Group parsed @message docs by session, keeping only sessions with >=1 span // (Evaluate rejects log-only sessions), and derive each session's trace/tool ids. function groupSpansBySession(rows: ResultField[][], logger: Logger): SessionTrace[] { diff --git a/src/core/observability.test.ts b/src/core/observability.test.ts new file mode 100644 index 000000000..2f9955a5b --- /dev/null +++ b/src/core/observability.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, test } from "bun:test"; +import { + GetQueryResultsCommand, + StartQueryCommand, + type CloudWatchLogsClient, +} from "@aws-sdk/client-cloudwatch-logs"; +import { CloudWatchQueryError, InputValidationError, ResultTruncationError } from "../errors"; +import { + parseTimeString, + runInsightsQuery, + runtimeLogGroup, + sanitizeQueryValue, +} from "./observability"; + +describe("runtimeLogGroup", () => { + test("derives the fixed per-runtime path keyed by runtime id and endpoint", () => { + expect(runtimeLogGroup("my_agent-AbC123XyZ9", "DEFAULT")).toBe( + "/aws/bedrock-agentcore/runtimes/my_agent-AbC123XyZ9-DEFAULT", + ); + }); +}); + +describe("sanitizeQueryValue", () => { + test("strips single quotes so values cannot escape a quoted Insights literal", () => { + expect(sanitizeQueryValue("abc'| drop '123")).toBe("abc| drop 123"); + expect(sanitizeQueryValue("clean-id")).toBe("clean-id"); + }); +}); + +describe("parseTimeString", () => { + const NOW = 1_700_000_000_000; + const now = () => NOW; + + test('parses "now" as the current time', () => { + expect(parseTimeString("now", now)).toBe(NOW); + }); + + test("parses relative durations for every unit as that long ago", () => { + expect(parseTimeString("30s", now)).toBe(NOW - 30_000); + expect(parseTimeString("5m", now)).toBe(NOW - 5 * 60_000); + expect(parseTimeString("1h", now)).toBe(NOW - 3_600_000); + expect(parseTimeString("2d", now)).toBe(NOW - 2 * 86_400_000); + }); + + test("parses epoch milliseconds (13+ digits) literally", () => { + expect(parseTimeString("1709391000000", now)).toBe(1_709_391_000_000); + }); + + test("parses ISO 8601 timestamps", () => { + expect(parseTimeString("2026-03-02T14:30:00Z", now)).toBe(Date.parse("2026-03-02T14:30:00Z")); + }); + + test("trims surrounding whitespace", () => { + expect(parseTimeString(" 15m ", now)).toBe(NOW - 15 * 60_000); + }); + + test("rejects empty input with a typed error", () => { + expect(() => parseTimeString(" ", now)).toThrow(InputValidationError); + expect(() => parseTimeString("", now)).toThrow("Time string cannot be empty"); + }); + + test("rejects garbage with a typed error naming the accepted forms", () => { + expect(() => parseTimeString("yesterday-ish", now)).toThrow(InputValidationError); + expect(() => parseTimeString("5x", now)).toThrow( + 'Invalid time string: "5x". Use relative durations (5m, 1h, 2d), ISO 8601, epoch ms, or "now".', + ); + }); +}); + +type Send = (command: unknown) => Promise; + +function fakeLogs(send: Send): CloudWatchLogsClient { + return { send } as unknown as CloudWatchLogsClient; +} + +function row(field: string, value: string) { + return [{ field, value }]; +} + +describe("runInsightsQuery", () => { + test("starts the query, waits for completion, and drains every result page", async () => { + // Poll phase sees Complete on the first read; the drain phase then re-reads + // page one and follows nextToken to page two. + const logs = fakeLogs(async (command) => { + if (command instanceof StartQueryCommand) { + expect(command.input).toEqual({ + logGroupNames: ["/aws/group-a", "/aws/group-b"], + queryString: "fields @message", + startTime: 100, + endTime: 200, + }); + return { queryId: "q-1" }; + } + expect(command).toBeInstanceOf(GetQueryResultsCommand); + const input = (command as GetQueryResultsCommand).input; + expect(input.queryId).toBe("q-1"); + if (input.nextToken === "page-2") { + return { status: "Complete", results: [row("@message", "second")] }; + } + return { + status: "Complete", + results: [row("@message", "first")], + nextToken: "page-2", + }; + }); + + const rows = await runInsightsQuery( + logs, + ["/aws/group-a", "/aws/group-b"], + "fields @message", + 100, + 200, + ); + expect(rows).toEqual([row("@message", "first"), row("@message", "second")]); + }); + + test("throws a typed error when the query reaches a terminal failure state", async () => { + const logs = fakeLogs(async (command) => { + if (command instanceof StartQueryCommand) return { queryId: "q-2" }; + return { status: "Failed" }; + }); + + await expect(runInsightsQuery(logs, ["/aws/g"], "q", 0, 1)).rejects.toThrow( + CloudWatchQueryError, + ); + await expect(runInsightsQuery(logs, ["/aws/g"], "q", 0, 1)).rejects.toThrow( + "CloudWatch Logs Insights query failed", + ); + }); + + test("fails loudly with the default truncation error when the row ceiling is hit", async () => { + const logs = fakeLogs(async (command) => { + if (command instanceof StartQueryCommand) return { queryId: "q-3" }; + return { status: "Complete", results: [row("@message", "a"), row("@message", "b")] }; + }); + + await expect( + runInsightsQuery(logs, ["/aws/g"], "q", 0, 1, { + maxRows: 2, + buildError: (maxRows) => new ResultTruncationError(`hit ceiling ${maxRows}`), + }), + ).rejects.toThrow("hit ceiling 2"); + }); + + test("lets the caller supply a domain-specific row-ceiling error", async () => { + const logs = fakeLogs(async (command) => { + if (command instanceof StartQueryCommand) return { queryId: "q-4" }; + return { status: "Complete", results: [row("@message", "a")] }; + }); + + await expect( + runInsightsQuery(logs, ["/aws/g"], "q", 0, 1, { + maxRows: 1, + buildError: () => new InputValidationError("narrow the scope"), + }), + ).rejects.toThrow(InputValidationError); + }); +}); diff --git a/src/core/observability.ts b/src/core/observability.ts new file mode 100644 index 000000000..2dc2617d0 --- /dev/null +++ b/src/core/observability.ts @@ -0,0 +1,169 @@ +import { + GetQueryResultsCommand, + StartQueryCommand, + type CloudWatchLogsClient, + type ResultField, +} from "@aws-sdk/client-cloudwatch-logs"; +import { + CloudWatchQueryError, + InputValidationError, + ResultTruncationError, + type AgentCoreCLIError, +} from "../errors"; + +// Shared CloudWatch observability helpers. AgentCore Runtimes write their logs +// and OTel telemetry to per-runtime CloudWatch log groups; both the eval flows +// (session discovery, batch results) and the runtime observability commands +// (`runtime logs` / `runtime traces`) read them, so the derivations and the +// Logs Insights query runner live here rather than privately in one feature. + +/** The default runtime endpoint qualifier used when none is specified. */ +export const DEFAULT_ENDPOINT_QUALIFIER = "DEFAULT"; + +// CloudWatch Logs Insights hard ceiling: a query returns at most 100k rows. +export const INSIGHTS_MAX_ROWS = 100_000; + +/** + * CloudWatch log group path for an AgentCore runtime endpoint. AgentCore always + * writes a runtime endpoint's logs and traces to this fixed path, keyed by the + * runtime *id* (mirrors the old CLI's src/cli/aws/cloudwatch.ts derivation). + */ +export function runtimeLogGroup(runtimeId: string, endpoint: string): string { + return `/aws/bedrock-agentcore/runtimes/${runtimeId}-${endpoint}`; +} + +/** + * Strips single quotes so an interpolated id can't break out of the quoted + * Insights filter literal it is embedded in (matches the old CLI). + */ +export function sanitizeQueryValue(value: string): string { + return value.replace(/'/g, ""); +} + +/** + * Row-ceiling policy for {@link runInsightsQuery}: when a query drains `maxRows` + * or more rows the result may be truncated, so the runner fails loudly with the + * caller's error rather than returning a silently partial result. Callers with + * a domain-specific remedy (e.g. eval's "narrow --session-ids") supply their + * own `buildError`. + */ +export interface InsightsRowLimit { + maxRows: number; + buildError: (maxRows: number) => AgentCoreCLIError; +} + +const DEFAULT_ROW_LIMIT: InsightsRowLimit = { + maxRows: INSIGHTS_MAX_ROWS, + buildError: (maxRows) => + new ResultTruncationError( + `CloudWatch Logs Insights returned too many rows (>= ${maxRows}); narrow the time window`, + ), +}; + +/** + * Starts a CloudWatch Logs Insights query, waits for it to finish, then drains + * all result pages. GetQueryResults returns <=10k rows per call, so a large + * result spans multiple pages (nextToken); dropping any would silently return a + * partial result. Fails fast when the row ceiling is hit — see + * {@link InsightsRowLimit}. + */ +export async function runInsightsQuery( + logs: CloudWatchLogsClient, + logGroupNames: string[], + queryString: string, + startSec: number, + endSec: number, + rowLimit: InsightsRowLimit = DEFAULT_ROW_LIMIT, +): Promise { + const started = await logs.send( + new StartQueryCommand({ logGroupNames, queryString, startTime: startSec, endTime: endSec }), + ); + const queryId = started.queryId; + + // Phase 1: wait for completion. A large scan can take minutes, so the deadline is + // generous; each poll costs one cheap GetQueryResults call. + let status = "Running"; + for (let i = 0; i < 300 && status !== "Complete"; i++) { + const result = await logs.send(new GetQueryResultsCommand({ queryId })); + status = result.status ?? "Unknown"; + if (status === "Failed" || status === "Cancelled" || status === "Timeout") { + throw new CloudWatchQueryError(`CloudWatch Logs Insights query ${status.toLowerCase()}`, { + meta: { queryId, status }, + }); + } + if (status !== "Complete") await new Promise((resolve) => setTimeout(resolve, 1000)); + } + if (status !== "Complete") { + throw new CloudWatchQueryError("CloudWatch Logs Insights query did not finish in time", { + meta: { queryId, status }, + }); + } + + // Phase 2: drain pages. Terminates on nextToken; total is bounded by the + // query's own `| limit`. + const rows: ResultField[][] = []; + let nextToken: string | undefined; + do { + const result = await logs.send(new GetQueryResultsCommand({ queryId, nextToken })); + rows.push(...(result.results ?? [])); + nextToken = result.nextToken; + } while (nextToken); + + if (rows.length >= rowLimit.maxRows) { + throw rowLimit.buildError(rowLimit.maxRows); + } + return rows; +} + +const RELATIVE_DURATION_RE = /^(\d+)([smhd])$/; + +const UNIT_TO_MS: Record = { + s: 1_000, + m: 60_000, + h: 3_600_000, + d: 86_400_000, +}; + +/** + * Parses a user-facing time string into epoch milliseconds. + * + * Supported forms (mirrors the old CLI's src/lib/utils/time-parser.ts): + * - "now" + * - Relative durations, meaning that long *ago*: "30s", "5m", "1h", "2d" + * - Epoch milliseconds: "1709391000000" (13+ digits) + * - Anything Date.parse accepts, e.g. ISO 8601: "2026-03-02T14:30:00Z" + * + * The reference clock is injectable for tests. + */ +export function parseTimeString(input: string, now: () => number = Date.now): number { + const trimmed = input.trim(); + if (trimmed === "") { + throw new InputValidationError("Time string cannot be empty"); + } + + if (trimmed === "now") { + return now(); + } + + const match = RELATIVE_DURATION_RE.exec(trimmed); + if (match) { + const value = parseInt(match[1]!, 10); + const ms = UNIT_TO_MS[match[2]!]!; + return now() - value * ms; + } + + // Epoch milliseconds: all digits, at least 13 of them — shorter all-digit + // strings fall through to Date parsing below, like the old CLI. + if (/^\d{13,}$/.test(trimmed)) { + return parseInt(trimmed, 10); + } + + const date = new Date(trimmed); + if (!isNaN(date.getTime())) { + return date.getTime(); + } + + throw new InputValidationError( + `Invalid time string: "${input}". Use relative durations (5m, 1h, 2d), ISO 8601, epoch ms, or "now".`, + ); +} From ee95933833f5d2f8ad1a887236891ca8e08b6b21 Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Sun, 30 Aug 2026 23:59:08 -0400 Subject: [PATCH 09/12] feat(runtime): logs streaming and search Adds `agentcore runtime logs`: by default it live-tails the runtime's CloudWatch log group via StartLiveTail (reconnecting transparently over the ~3h session cap; Ctrl+C exits 130), and with --since/--until it switches to a bounded FilterLogEvents search, paginated to completion oldest-to-newest. --level/--query compose into a server-side filter pattern (ported from the old CLI's filter-pattern.ts), --limit caps search results, and --json emits JSON Lines. A missing log group maps to "has the runtime been invoked yet?" guidance. Addressing follows `runtime invoke --id`; without --id inside a project the deployed runtime is resolved live from the target stack's CloudFormation outputs (default target, one runtime auto-selects, several list candidates), through a new ObservabilityClient wired into CoreClient and the Core contract. The runtime router's TUI dispatch is limited to its existing children so a bare `runtime logs` streams instead of opening the TUI. The gitignore's blanket `logs` entry is scoped to the repo root so the handler directory is trackable. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- .gitignore | 4 +- README.md | 9 + src/core/index.tsx | 12 + src/core/observability.test.ts | 383 +++++++++++++++++- src/core/observability.ts | 287 +++++++++++++ src/errors/index.tsx | 1 + src/handlers/runtime/index.tsx | 23 +- .../runtime/logs/filterPattern.test.ts | 23 ++ src/handlers/runtime/logs/filterPattern.ts | 31 ++ src/handlers/runtime/logs/index.tsx | 116 ++++++ src/handlers/runtime/logs/logs.test.tsx | 184 +++++++++ .../runtime/resolveRuntimeTarget.test.ts | 97 +++++ src/handlers/runtime/resolveRuntimeTarget.ts | 59 +++ src/handlers/runtime/runtime.test.tsx | 1 + src/handlers/runtime/types.tsx | 51 +++ src/handlers/types.tsx | 3 +- src/testing/TestCoreClient.tsx | 50 ++- 17 files changed, 1321 insertions(+), 13 deletions(-) create mode 100644 src/handlers/runtime/logs/filterPattern.test.ts create mode 100644 src/handlers/runtime/logs/filterPattern.ts create mode 100644 src/handlers/runtime/logs/index.tsx create mode 100644 src/handlers/runtime/logs/logs.test.tsx create mode 100644 src/handlers/runtime/resolveRuntimeTarget.test.ts create mode 100644 src/handlers/runtime/resolveRuntimeTarget.ts diff --git a/.gitignore b/.gitignore index dd4fee5f8..a9bd81ee6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,8 @@ src/assets/agent-inspector/ coverage *.lcov -# logs -logs +# logs (root-level runtime output only; src/**/logs are real modules) +/logs _.log report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json diff --git a/README.md b/README.md index c87c27e25..9221f2bea 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ agentcore # interactive TUI │ ├── get # fetch a Runtime by id │ ├── list # list Runtimes (server-side paginated) │ ├── invoke # invoke a Runtime headlessly or in a persistent console +│ ├── logs # follow a Runtime's logs live, or search a time window │ ├── version │ │ ├── get # get a specific Runtime version │ │ └── list # list a Runtime's versions @@ -195,6 +196,14 @@ agentcore runtime version list --id --max-results 20 agentcore runtime endpoint get --id --qualifier DEFAULT agentcore runtime endpoint list --id --max-results 20 +# Follow a Runtime's logs live (Ctrl+C to stop); inside a project --id is optional +agentcore runtime logs --id +agentcore runtime logs --id --level error --query "database" + +# Search a past window instead (--since/--until switch to search mode) +agentcore runtime logs --id --since 1h --limit 100 +agentcore runtime logs --id --since 2026-08-30T12:00:00Z --until now --json + # Inspect AgentCore Memories without project configuration or deployment agentcore memory get --id agentcore memory get --id --view without_decryption diff --git a/src/core/index.tsx b/src/core/index.tsx index 34a080e77..720e356f2 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -7,7 +7,9 @@ import { GatewayClient } from "./gateway"; import { HarnessClient } from "./harness"; import { IdentityClient } from "./identity"; import { MemoryClient } from "./memory"; +import { ObservabilityClient } from "./observability"; import { RuntimeClient } from "./runtime"; +import { FsReadWriteJson } from "../io"; import type { AwsClients, ClientConfig, @@ -69,6 +71,7 @@ export class CoreClient implements AwsClients { readonly runtime: RuntimeClient; readonly gateway: GatewayClient; readonly eval: EvalClient; + readonly observability: ObservabilityClient; readonly projectManager: ProjectManager; readonly describeBedrockAgent: DescribeBedrockAgent; @@ -92,6 +95,15 @@ export class CoreClient implements AwsClients { config.newSessionId, ); + // Observability resolves a project's deployed runtime from its stack + // outputs, so it reads aws-targets.json through the same JSON layer the + // project manager uses. + this.observability = new ObservabilityClient(this, { + readJson: new FsReadWriteJson({ + logger: this.logger.child({ module: "observability" }), + }), + }); + this.projectManager = new FsProjectManager({ logger: this.logger.child({ module: "projectManager" }), createCloudFormationClient: config.createCloudFormationClient, diff --git a/src/core/observability.test.ts b/src/core/observability.test.ts index 2f9955a5b..235577f82 100644 --- a/src/core/observability.test.ts +++ b/src/core/observability.test.ts @@ -1,15 +1,34 @@ import { describe, expect, test } from "bun:test"; import { + DescribeLogGroupsCommand, + FilterLogEventsCommand, GetQueryResultsCommand, + ResourceNotFoundException, + StartLiveTailCommand, StartQueryCommand, type CloudWatchLogsClient, + type StartLiveTailResponseStream, } from "@aws-sdk/client-cloudwatch-logs"; -import { CloudWatchQueryError, InputValidationError, ResultTruncationError } from "../errors"; +import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { + CloudWatchQueryError, + InputValidationError, + ProjectStateError, + ResourceNotFoundError, + ResultTruncationError, +} from "../errors"; +import type { ReadWriteJson } from "../io"; +import type { Project } from "../handlers/project/types"; +import type { AwsClients } from "./types"; +import { + ObservabilityClient, parseTimeString, runInsightsQuery, runtimeLogGroup, sanitizeQueryValue, + type DescribeStackOutputs, } from "./observability"; describe("runtimeLogGroup", () => { @@ -156,3 +175,365 @@ describe("runInsightsQuery", () => { ).rejects.toThrow(InputValidationError); }); }); + +const OPTIONS = { region: "us-east-1" }; + +function clientWith(logs: CloudWatchLogsClient, describeStackOutputs?: DescribeStackOutputs) { + const clients = { logs: () => logs } as unknown as AwsClients; + const readJson: ReadWriteJson = { + read: async (filePath, schema) => + schema.parse(JSON.parse(await Bun.file(filePath).text())) as never, + write: async () => { + throw new Error("not implemented"); + }, + } as ReadWriteJson; + return new ObservabilityClient(clients, { readJson, describeStackOutputs }); +} + +function fakeProject(rootPath: string, name = "My_Project"): Project { + return { name, rootPath, spec: {} } as unknown as Project; +} + +function projectWithTargets( + targets: { name: string; account: string; region: string }[] | undefined, +): Project { + const root = mkdtempSync(join(tmpdir(), "obs-test-")); + if (targets) { + mkdirSync(join(root, "agentcore"), { recursive: true }); + writeFileSync(join(root, "agentcore", "aws-targets.json"), JSON.stringify(targets)); + } + return fakeProject(root); +} + +const TARGETS = [{ name: "default", account: "111122223333", region: "us-east-2" }]; + +describe("ObservabilityClient.resolveDeployedRuntime", () => { + const noLogs = fakeLogs(async () => { + throw new Error("unexpected CloudWatch call"); + }); + + test("resolves the single deployed runtime from the target stack's outputs", async () => { + const described: { stackName?: string; region?: string } = {}; + const client = clientWith(noLogs, async (stackName, region) => { + described.stackName = stackName; + described.region = region; + return [ + { OutputKey: "StackNameOutput", OutputValue: "AgentCore-My-Project-default" }, + { + OutputKey: "ApplicationAgentHelloWorldRuntimeArnOutput0DF4BB9A", + OutputValue: "arn:aws:bedrock-agentcore:us-east-2:1:runtime/hello_world-AbC", + }, + { + OutputKey: "ApplicationAgentHelloWorldRuntimeIdOutput1CCED486", + OutputValue: "hello_world-AbC123XyZ9", + }, + ]; + }); + + const resolved = await client.resolveDeployedRuntime(projectWithTargets(TARGETS), "default"); + + // The stack name mirrors the vended CDK app: underscores sanitized to hyphens. + expect(described).toEqual({ stackName: "AgentCore-My-Project-default", region: "us-east-2" }); + expect(resolved).toEqual({ + runtimeId: "hello_world-AbC123XyZ9", + region: "us-east-2", + stackName: "AgentCore-My-Project-default", + targetName: "default", + }); + }); + + test("lists the candidates when several runtimes are deployed", async () => { + const client = clientWith(noLogs, async () => [ + { OutputKey: "ApplicationAgentOneRuntimeIdOutputAAAAAAAA", OutputValue: "one-AAAA" }, + { OutputKey: "ApplicationAgentTwoRuntimeIdOutputBBBBBBBB", OutputValue: "two-BBBB" }, + ]); + + await expect( + client.resolveDeployedRuntime(projectWithTargets(TARGETS), "default"), + ).rejects.toThrow("choose one with --id: one-AAAA, two-BBBB"); + }); + + test("fails with deploy guidance when the stack does not exist", async () => { + const client = clientWith(noLogs, async () => undefined); + + await expect( + client.resolveDeployedRuntime(projectWithTargets(TARGETS), "default"), + ).rejects.toThrow( + "Stack 'AgentCore-My-Project-default' is not deployed in us-east-2. " + + "Run 'agentcore project deploy' first, or pass --id .", + ); + }); + + test("fails when the stack exports no runtime ids", async () => { + const client = clientWith(noLogs, async () => [ + { OutputKey: "StackNameOutput", OutputValue: "AgentCore-My-Project-default" }, + ]); + + await expect( + client.resolveDeployedRuntime(projectWithTargets(TARGETS), "default"), + ).rejects.toThrow(ResourceNotFoundError); + }); + + test("fails when the named target is not configured", async () => { + const client = clientWith(noLogs, async () => []); + + await expect( + client.resolveDeployedRuntime(projectWithTargets(TARGETS), "production"), + ).rejects.toThrow("has no deployment target named 'production'"); + }); + + test("fails when the project has no aws-targets.json", async () => { + const client = clientWith(noLogs, async () => []); + + await expect( + client.resolveDeployedRuntime(projectWithTargets(undefined), "default"), + ).rejects.toThrow(ProjectStateError); + }); +}); + +describe("ObservabilityClient.searchRuntimeLogs", () => { + const SEARCH = { + runtimeId: "my_agent-AbC123XyZ9", + startTimeMs: 1_000, + endTimeMs: 2_000, + }; + + async function collect(events: AsyncGenerator<{ timestamp: number; message: string }>) { + const out: { timestamp: number; message: string }[] = []; + for await (const event of events) out.push(event); + return out; + } + + test("paginates FilterLogEvents to completion, oldest to newest", async () => { + const inputs: unknown[] = []; + const logs = fakeLogs(async (command) => { + expect(command).toBeInstanceOf(FilterLogEventsCommand); + const input = (command as FilterLogEventsCommand).input; + inputs.push(input); + if (input.nextToken === "page-2") { + return { events: [{ timestamp: 3, message: "three" }] }; + } + return { + events: [ + { timestamp: 1, message: "one" }, + { timestamp: 2, message: "two" }, + ], + nextToken: "page-2", + }; + }); + + const events = await collect(clientWith(logs).searchRuntimeLogs(SEARCH, OPTIONS)); + + expect(events).toEqual([ + { timestamp: 1, message: "one" }, + { timestamp: 2, message: "two" }, + { timestamp: 3, message: "three" }, + ]); + expect(inputs[0]).toEqual({ + logGroupName: "/aws/bedrock-agentcore/runtimes/my_agent-AbC123XyZ9-DEFAULT", + startTime: 1_000, + endTime: 2_000, + }); + expect(inputs[1]).toMatchObject({ nextToken: "page-2" }); + }); + + test("caps yielded events at limit and requests no more than needed", async () => { + const limits: (number | undefined)[] = []; + const logs = fakeLogs(async (command) => { + const input = (command as FilterLogEventsCommand).input; + limits.push(input.limit); + if (input.nextToken === "page-2") { + return { + events: [ + { timestamp: 3, message: "three" }, + { timestamp: 4, message: "four" }, + ], + }; + } + return { + events: [ + { timestamp: 1, message: "one" }, + { timestamp: 2, message: "two" }, + ], + nextToken: "page-2", + }; + }); + + const events = await collect( + clientWith(logs).searchRuntimeLogs({ ...SEARCH, limit: 3 }, OPTIONS), + ); + + expect(events.map((event) => event.message)).toEqual(["one", "two", "three"]); + expect(limits).toEqual([3, 1]); + }); + + test("passes the filter pattern through to FilterLogEvents", async () => { + const logs = fakeLogs(async (command) => { + expect((command as FilterLogEventsCommand).input.filterPattern).toBe("ERROR database"); + return { events: [] }; + }); + + await collect( + clientWith(logs).searchRuntimeLogs({ ...SEARCH, filterPattern: "ERROR database" }, OPTIONS), + ); + }); + + test("translates a missing log group into invoked-yet guidance", async () => { + const logs = fakeLogs(async () => { + throw new ResourceNotFoundException({ + message: "The specified log group does not exist.", + $metadata: {}, + }); + }); + + await expect(collect(clientWith(logs).searchRuntimeLogs(SEARCH, OPTIONS))).rejects.toThrow( + "No logs found for runtime 'my_agent-AbC123XyZ9': log group " + + "/aws/bedrock-agentcore/runtimes/my_agent-AbC123XyZ9-DEFAULT does not exist. " + + "Has the runtime been invoked yet?", + ); + }); +}); + +describe("ObservabilityClient.streamRuntimeLogs", () => { + const STREAM = { runtimeId: "my_agent-AbC123XyZ9" }; + const LOG_GROUP = "/aws/bedrock-agentcore/runtimes/my_agent-AbC123XyZ9-DEFAULT"; + const GROUP_ARN = `arn:aws:logs:us-east-1:111122223333:log-group:${LOG_GROUP}`; + + type LiveTailEvent = Partial; + + function liveTailLogs( + sessions: (LiveTailEvent[] | Error)[], + groups: { logGroupName?: string; logGroupArn?: string; arn?: string }[] = [ + { logGroupName: LOG_GROUP, logGroupArn: GROUP_ARN }, + ], + ) { + const starts: unknown[] = []; + const logs = fakeLogs(async (command) => { + if (command instanceof DescribeLogGroupsCommand) { + expect(command.input.logGroupNamePrefix).toBe(LOG_GROUP); + return { logGroups: groups }; + } + expect(command).toBeInstanceOf(StartLiveTailCommand); + starts.push((command as StartLiveTailCommand).input); + const session = sessions[starts.length - 1] ?? []; + return { + responseStream: (async function* () { + if (session instanceof Error) throw session; + yield* session as StartLiveTailResponseStream[]; + })(), + }; + }); + return { logs, starts }; + } + + function update(...messages: string[]): LiveTailEvent { + return { + sessionUpdate: { + sessionResults: messages.map((message, i) => ({ timestamp: 1_000 + i, message })), + }, + }; + } + + async function collect(client: ObservabilityClient, signal: AbortSignal) { + const out: string[] = []; + for await (const event of client.streamRuntimeLogs(STREAM, OPTIONS, signal)) { + out.push(event.message); + } + return out; + } + + test("yields live-tail session updates and stops when the stream ends normally", async () => { + const { logs, starts } = liveTailLogs([[update("one", "two"), update("three")]]); + + const messages = await collect(clientWith(logs), new AbortController().signal); + + expect(messages).toEqual(["one", "two", "three"]); + expect(starts).toEqual([{ logGroupIdentifiers: [GROUP_ARN] }]); + }); + + test("reconnects when the session reports a timeout event", async () => { + const { logs, starts } = liveTailLogs([ + [update("one"), { SessionTimeoutException: { name: "SessionTimeoutException" } } as never], + [update("two")], + ]); + + const messages = await collect(clientWith(logs), new AbortController().signal); + + expect(messages).toEqual(["one", "two"]); + expect(starts).toHaveLength(2); + }); + + test("reconnects when the stream throws a session timeout", async () => { + const timeout = Object.assign(new Error("session timed out"), { + name: "SessionTimeoutException", + }); + const { logs, starts } = liveTailLogs([timeout, [update("after-reconnect")]]); + + const messages = await collect(clientWith(logs), new AbortController().signal); + + expect(messages).toEqual(["after-reconnect"]); + expect(starts).toHaveLength(2); + }); + + test("propagates non-timeout stream errors", async () => { + const { logs } = liveTailLogs([new Error("stream exploded")]); + + await expect(collect(clientWith(logs), new AbortController().signal)).rejects.toThrow( + "stream exploded", + ); + }); + + test("returns cleanly when aborted mid-session", async () => { + const controller = new AbortController(); + const { logs, starts } = liveTailLogs([ + [update("one"), { SessionTimeoutException: { name: "SessionTimeoutException" } } as never], + ]); + + const messages: string[] = []; + for await (const event of clientWith(logs).streamRuntimeLogs( + STREAM, + OPTIONS, + controller.signal, + )) { + messages.push(event.message); + controller.abort(); + } + + // The timeout after the abort must not trigger a reconnect. + expect(messages).toEqual(["one"]); + expect(starts).toHaveLength(1); + }); + + test("passes the filter pattern to the live tail", async () => { + const { logs, starts } = liveTailLogs([[]]); + + for await (const _ of clientWith(logs).streamRuntimeLogs( + { ...STREAM, filterPattern: "ERROR" }, + OPTIONS, + new AbortController().signal, + )) { + // drain + } + + expect(starts).toEqual([{ logGroupIdentifiers: [GROUP_ARN], logEventFilterPattern: "ERROR" }]); + }); + + test("strips the legacy ARN's trailing :* when the modern field is absent", async () => { + const { logs, starts } = liveTailLogs( + [[]], + [{ logGroupName: LOG_GROUP, arn: `${GROUP_ARN}:*` }], + ); + + await collect(clientWith(logs), new AbortController().signal); + + expect(starts).toEqual([{ logGroupIdentifiers: [GROUP_ARN] }]); + }); + + test("fails with invoked-yet guidance when the log group does not exist", async () => { + const { logs } = liveTailLogs([[]], []); + + await expect(collect(clientWith(logs), new AbortController().signal)).rejects.toThrow( + "Has the runtime been invoked yet?", + ); + }); +}); diff --git a/src/core/observability.ts b/src/core/observability.ts index 2dc2617d0..d937f0096 100644 --- a/src/core/observability.ts +++ b/src/core/observability.ts @@ -1,15 +1,36 @@ import { + DescribeLogGroupsCommand, + FilterLogEventsCommand, GetQueryResultsCommand, + ResourceNotFoundException, + StartLiveTailCommand, StartQueryCommand, type CloudWatchLogsClient, type ResultField, } from "@aws-sdk/client-cloudwatch-logs"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; import { CloudWatchQueryError, InputValidationError, + ProjectStateError, + ResourceNotFoundError, ResultTruncationError, type AgentCoreCLIError, } from "../errors"; +import type { ReadWriteJson } from "../io"; +import type { Project } from "../handlers/project/types"; +import type { + CoreObservabilityClient, + DeployedRuntime, + RuntimeLogEvent, + SearchRuntimeLogsInput, + StreamRuntimeLogsInput, +} from "../handlers/runtime/types"; +import { AwsDeploymentTargetsSchema } from "../projectSchemas/aws-targets"; +import { isStackNotFound } from "./project/backends/cdk/environment"; +import type { AwsClients, CoreOptions } from "./types"; +import { toClientConfig } from "./utils"; // Shared CloudWatch observability helpers. AgentCore Runtimes write their logs // and OTel telemetry to per-runtime CloudWatch log groups; both the eval flows @@ -167,3 +188,269 @@ export function parseTimeString(input: string, now: () => number = Date.now): nu `Invalid time string: "${input}". Use relative durations (5m, 1h, 2d), ISO 8601, epoch ms, or "now".`, ); } + +/** + * Reads one stack's outputs via CloudFormation DescribeStacks, returning + * undefined when the stack does not exist. Injectable so unit tests never call + * AWS. + */ +export type DescribeStackOutputs = ( + stackName: string, + region: string, +) => Promise<{ OutputKey?: string; OutputValue?: string }[] | undefined>; + +// Real describer: lazily imports the CloudFormation SDK (kept off the CLI +// startup path, like the CDK backend's stackReader) and resolves credentials +// through the SDK's default provider chain, matching every other client +// factory in src/core/factories.tsx. +const describeStackOutputsWithSdk: DescribeStackOutputs = async (stackName, region) => { + const { CloudFormationClient, DescribeStacksCommand } = + await import("@aws-sdk/client-cloudformation"); + const client = new CloudFormationClient({ region }); + try { + const response = await client.send(new DescribeStacksCommand({ StackName: stackName })); + return response.Stacks?.[0]?.Outputs ?? []; + } catch (error) { + // A missing stack surfaces as a thrown ValidationError, not an empty result. + if (isStackNotFound(error)) return undefined; + throw error; + } finally { + client.destroy(); + } +}; + +// The vended CDK app names project stacks `AgentCore--` with +// underscores sanitized to hyphens (see src/assets/cdk/bin/cdk.ts). Deriving it +// here lets deployed state be read live from CloudFormation without a local +// state file. +function targetStackName(projectName: string, targetName: string): string { + const sanitize = (name: string) => name.replace(/_/g, "-"); + return `AgentCore-${sanitize(projectName)}-${sanitize(targetName)}`; +} + +// The L3 constructs export each runtime's id as a stack output whose +// CDK-generated logical id ends in `RuntimeIdOutput` plus an optional 8-char +// uppercase-hex uniquifier (e.g. ApplicationAgentHelloWorldRuntimeIdOutput1CCED486). +const RUNTIME_ID_OUTPUT_RE = /RuntimeIdOutput([0-9A-F]{8})?$/; + +export interface ObservabilityClientDeps { + /** Reads agentcore/aws-targets.json. */ + readJson: ReadWriteJson; + /** Stack-output reader; defaults to a live CloudFormation DescribeStacks. */ + describeStackOutputs?: DescribeStackOutputs; +} + +/** + * ObservabilityClient reads the CloudWatch-backed telemetry of deployed + * AgentCore Runtimes: live-tail and search over the per-runtime log group, and + * resolution of a project's deployed runtime id from its CloudFormation stack + * outputs (runtime ids are not persisted locally, so the stack is the source + * of truth). + */ +export class ObservabilityClient implements CoreObservabilityClient { + private readonly clients: AwsClients; + private readonly readJson: ReadWriteJson; + private readonly describeStackOutputs: DescribeStackOutputs; + + constructor(clients: AwsClients, deps: ObservabilityClientDeps) { + this.clients = clients; + this.readJson = deps.readJson; + this.describeStackOutputs = deps.describeStackOutputs ?? describeStackOutputsWithSdk; + } + + /** + * Resolves the single deployed runtime of `project`'s `targetName` target by + * reading the target's CloudFormation stack outputs. Exactly one deployed + * runtime resolves; none or several fail with guidance (pass --id to choose). + * The returned region is the deployment target's — that is where the stack + * and its log groups live. + */ + async resolveDeployedRuntime(project: Project, targetName: string): Promise { + const targetsPath = join(project.rootPath, "agentcore", "aws-targets.json"); + if (!existsSync(targetsPath)) { + throw new ProjectStateError( + `Project '${project.name}' has no deployment targets (${targetsPath} not found). ` + + `Run 'agentcore project deploy' first, or pass --id .`, + ); + } + const targets = await this.readJson.read(targetsPath, AwsDeploymentTargetsSchema); + const target = targets.find((candidate) => candidate.name === targetName); + if (!target) { + throw new ProjectStateError( + `Project '${project.name}' has no deployment target named '${targetName}'. ` + + `${targetsPath} defines: ${targets.map(({ name }) => name).join(", ") || "none"}.`, + ); + } + + const stackName = targetStackName(project.name, target.name); + const outputs = await this.describeStackOutputs(stackName, target.region); + if (outputs === undefined) { + throw new ProjectStateError( + `Stack '${stackName}' is not deployed in ${target.region}. ` + + `Run 'agentcore project deploy' first, or pass --id .`, + ); + } + + const runtimeIds = outputs + .filter((output) => output.OutputKey && RUNTIME_ID_OUTPUT_RE.test(output.OutputKey)) + .map((output) => output.OutputValue) + .filter((value): value is string => Boolean(value)); + + if (runtimeIds.length === 0) { + throw new ResourceNotFoundError( + `Stack '${stackName}' in ${target.region} exports no runtime ids. ` + + `Deploy a runtime first, or pass --id .`, + ); + } + if (runtimeIds.length > 1) { + throw new InputValidationError( + `Project '${project.name}' has multiple deployed runtimes; choose one with ` + + `--id: ${runtimeIds.join(", ")}`, + ); + } + + return { + runtimeId: runtimeIds[0]!, + region: target.region, + stackName, + targetName: target.name, + }; + } + + /** + * Live-tails a runtime's log group via StartLiveTail, yielding events as they + * arrive. A live-tail session is server-capped (~3h); when it times out a new + * session is started transparently, so the stream runs until `signal` aborts + * (in which case the generator simply returns). + */ + async *streamRuntimeLogs( + input: StreamRuntimeLogsInput, + options: CoreOptions, + signal: AbortSignal, + ): AsyncGenerator { + const logGroupName = runtimeLogGroup(input.runtimeId, DEFAULT_ENDPOINT_QUALIFIER); + const logs = this.clients.logs(toClientConfig(options)); + + // StartLiveTail addresses log groups by ARN. DescribeLogGroups resolves it + // without hand-assembling one (partition/account), and doubles as the + // existence check so a never-invoked runtime fails with guidance instead of + // an opaque service error. + const described = await logs.send( + new DescribeLogGroupsCommand({ logGroupNamePrefix: logGroupName }), + { abortSignal: signal }, + ); + const group = (described.logGroups ?? []).find( + (candidate) => candidate.logGroupName === logGroupName, + ); + // The legacy `arn` field carries a trailing `:*` that StartLiveTail rejects. + const logGroupArn = group?.logGroupArn ?? group?.arn?.replace(/:\*$/, ""); + if (!logGroupArn) { + throw missingLogGroupError(input.runtimeId, logGroupName); + } + + while (!signal.aborted) { + let response; + try { + response = await logs.send( + new StartLiveTailCommand({ + logGroupIdentifiers: [logGroupArn], + ...(input.filterPattern ? { logEventFilterPattern: input.filterPattern } : {}), + }), + { abortSignal: signal }, + ); + } catch (error) { + if (signal.aborted) return; + throw error; + } + if (!response.responseStream) return; + + let sessionTimedOut = false; + try { + for await (const event of response.responseStream) { + if (signal.aborted) return; + if (event.sessionUpdate) { + for (const logEvent of event.sessionUpdate.sessionResults ?? []) { + yield { + timestamp: logEvent.timestamp ?? Date.now(), + message: logEvent.message ?? "", + }; + } + } + if (event.SessionTimeoutException) { + sessionTimedOut = true; + break; + } + } + } catch (error) { + if (signal.aborted) return; + if ((error as { name?: string }).name === "SessionTimeoutException") { + sessionTimedOut = true; + } else { + throw error; + } + } + + // A stream that ended without timing out was closed deliberately + // (server-side or by the caller); only a timeout warrants a reconnect. + if (!sessionTimedOut) return; + } + } + + /** + * Searches a runtime's log group over a closed time window via + * FilterLogEvents, paginating to completion and yielding events oldest to + * newest. `limit` caps the total number of events yielded. + */ + async *searchRuntimeLogs( + input: SearchRuntimeLogsInput, + options: CoreOptions, + signal?: AbortSignal, + ): AsyncGenerator { + const logGroupName = runtimeLogGroup(input.runtimeId, DEFAULT_ENDPOINT_QUALIFIER); + const logs = this.clients.logs(toClientConfig(options)); + + let nextToken: string | undefined; + let yielded = 0; + do { + let response; + try { + response = await logs.send( + new FilterLogEventsCommand({ + logGroupName, + startTime: input.startTimeMs, + endTime: input.endTimeMs, + ...(input.filterPattern ? { filterPattern: input.filterPattern } : {}), + ...(nextToken ? { nextToken } : {}), + // FilterLogEvents accepts at most 10k events per page. + ...(input.limit ? { limit: Math.min(input.limit - yielded, 10_000) } : {}), + }), + { abortSignal: signal }, + ); + } catch (error) { + if (error instanceof ResourceNotFoundException) { + throw missingLogGroupError(input.runtimeId, logGroupName, error); + } + throw error; + } + + for (const event of response.events ?? []) { + if (input.limit !== undefined && yielded >= input.limit) return; + yield { timestamp: event.timestamp ?? Date.now(), message: event.message ?? "" }; + yielded++; + } + nextToken = response.nextToken; + } while (nextToken && (input.limit === undefined || yielded < input.limit)); + } +} + +function missingLogGroupError( + runtimeId: string, + logGroupName: string, + cause?: unknown, +): ResourceNotFoundError { + return new ResourceNotFoundError( + `No logs found for runtime '${runtimeId}': log group ${logGroupName} does not exist. ` + + `Has the runtime been invoked yet?`, + { cause, meta: { runtimeId, logGroupName } }, + ); +} diff --git a/src/errors/index.tsx b/src/errors/index.tsx index f60149243..e97206d86 100644 --- a/src/errors/index.tsx +++ b/src/errors/index.tsx @@ -12,6 +12,7 @@ export { NetworkingError, NotImplementedError, ProjectFileExistsError, + ProjectStateError, ResourceNotFoundError, ResultTruncationError, RuntimeInvokeResponseError, diff --git a/src/handlers/runtime/index.tsx b/src/handlers/runtime/index.tsx index edcc9e4a7..44bfbf339 100644 --- a/src/handlers/runtime/index.tsx +++ b/src/handlers/runtime/index.tsx @@ -7,15 +7,22 @@ import { createRuntimeEndpointHandler } from "./endpoint"; import { createGetRuntimeHandler } from "./get"; import { createInvokeRuntimeHandler } from "./invoke"; import { createListRuntimesHandler } from "./list"; +import { createRuntimeLogsHandler } from "./logs"; import { createRuntimeVersionHandler } from "./version"; export function createRuntimeHandler(core: Core, io: AppIO): Router { - return new Router("runtime", "inspect AgentCore Runtimes") - .use(withTuiOnEmptyFlagsAndArgs(core, io)) - .default(renderTui(core, io)) - .handler(createGetRuntimeHandler(core)) - .handler(createListRuntimesHandler(core)) - .handler(createInvokeRuntimeHandler(core, io)) - .handler(createRuntimeVersionHandler(core, io)) - .handler(createRuntimeEndpointHandler(core, io)); + return ( + new Router("runtime", "inspect AgentCore Runtimes") + .use(withTuiOnEmptyFlagsAndArgs(core, io)) + .default(renderTui(core, io)) + // logs is headless-only: a bare `runtime logs` means "follow the project + // runtime's logs", so it must never fall into the TUI. + .supportedTuiCommands("get", "list", "invoke", "version", "endpoint") + .handler(createGetRuntimeHandler(core)) + .handler(createListRuntimesHandler(core)) + .handler(createInvokeRuntimeHandler(core, io)) + .handler(createRuntimeVersionHandler(core, io)) + .handler(createRuntimeEndpointHandler(core, io)) + .handler(createRuntimeLogsHandler(core, io)) + ); } diff --git a/src/handlers/runtime/logs/filterPattern.test.ts b/src/handlers/runtime/logs/filterPattern.test.ts new file mode 100644 index 000000000..3cc2b6ee2 --- /dev/null +++ b/src/handlers/runtime/logs/filterPattern.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "bun:test"; +import { buildFilterPattern } from "./filterPattern"; + +describe("buildFilterPattern", () => { + test("returns undefined when neither level nor query is set", () => { + expect(buildFilterPattern({})).toBeUndefined(); + }); + + test("maps each level to its uppercase token", () => { + expect(buildFilterPattern({ level: "error" })).toBe("ERROR"); + expect(buildFilterPattern({ level: "warn" })).toBe("WARN"); + expect(buildFilterPattern({ level: "info" })).toBe("INFO"); + expect(buildFilterPattern({ level: "debug" })).toBe("DEBUG"); + }); + + test("passes the query through as-is", () => { + expect(buildFilterPattern({ query: '"timed out"' })).toBe('"timed out"'); + }); + + test("combines level and query with a space (implicit AND)", () => { + expect(buildFilterPattern({ level: "error", query: "database" })).toBe("ERROR database"); + }); +}); diff --git a/src/handlers/runtime/logs/filterPattern.ts b/src/handlers/runtime/logs/filterPattern.ts new file mode 100644 index 000000000..b8eb156e5 --- /dev/null +++ b/src/handlers/runtime/logs/filterPattern.ts @@ -0,0 +1,31 @@ +// CloudWatch Logs filter-pattern assembly for `runtime logs`, ported from the +// old CLI's src/cli/commands/logs/filter-pattern.ts. + +export const LOG_LEVELS = ["error", "warn", "info", "debug"] as const; + +export type LogLevel = (typeof LOG_LEVELS)[number]; + +// Runtime log lines embed their level as uppercase text (ERROR, WARN, ...), so +// a level filter is just that token in the pattern. +const LEVEL_MAP: Record = { + error: "ERROR", + warn: "WARN", + info: "INFO", + debug: "DEBUG", +}; + +/** + * Builds a CloudWatch Logs filter pattern from the --level and --query options. + * The level maps to its uppercase token; the query passes through as-is; both + * combine with a space, which CloudWatch treats as an implicit AND. Returns + * undefined when neither is set (no server-side filtering). + */ +export function buildFilterPattern(options: { + level?: LogLevel; + query?: string; +}): string | undefined { + const parts: string[] = []; + if (options.level) parts.push(LEVEL_MAP[options.level]); + if (options.query) parts.push(options.query); + return parts.length > 0 ? parts.join(" ") : undefined; +} diff --git a/src/handlers/runtime/logs/index.tsx b/src/handlers/runtime/logs/index.tsx new file mode 100644 index 000000000..c35346e67 --- /dev/null +++ b/src/handlers/runtime/logs/index.tsx @@ -0,0 +1,116 @@ +import z from "zod"; +import { parseTimeString } from "../../../core/observability"; +import { InputValidationError } from "../../../errors"; +import type { AppIO } from "../../../io"; +import { createHandler, flag } from "../../../router"; +import { withUserCancellation } from "../../../runnable"; +import { JsonRendererKey } from "../../../tui"; +import { JsonKey } from "../../keys"; +import type { Core } from "../../types"; +import { runtimeIdSchema } from "../invoke/request"; +import { resolveRuntimeTarget } from "../resolveRuntimeTarget"; +import type { RuntimeLogEvent } from "../types"; +import { buildFilterPattern, LOG_LEVELS } from "./filterPattern"; + +// Search mode's default window when only one bound is given: the last hour. +const DEFAULT_SEARCH_WINDOW_MS = 3_600_000; + +const levelSchema = z + .preprocess( + (value) => (typeof value === "string" ? value.toLowerCase() : value), + z.enum(LOG_LEVELS), + ) + .optional(); + +const timeSchema = z.string().min(1).optional(); + +/** + * `runtime logs` follows a deployed runtime's CloudWatch log group live + * (default), or searches a past time window when --since/--until is given. + * Follow mode runs until Ctrl+C, which exits with the conventional SIGINT + * status (130). + */ +export const createRuntimeLogsHandler = (core: Core, io: AppIO) => + createHandler({ + name: "logs", + description: "stream or search a Runtime's logs", + flags: [ + flag( + "id", + "the ID of the Runtime (defaults to the project's deployed runtime)", + runtimeIdSchema.optional(), + ), + flag( + "since", + 'search window start: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default 1h ago; enables search mode)', + timeSchema, + ), + flag( + "until", + 'search window end: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default now; enables search mode)', + timeSchema, + ), + flag("level", `filter by log level (${LOG_LEVELS.join(", ")})`, levelSchema), + flag("query", "server-side text filter", z.string().optional()), + flag( + "limit", + "maximum number of log lines to return (search mode)", + z.number().int().positive().optional(), + ), + ], + handle: async (ctx, flags) => { + const json = ctx.require(JsonKey); + const renderer = ctx.require(JsonRendererKey); + + // --since / --until switch from live tail to a bounded search. + const searchMode = flags.since !== undefined || flags.until !== undefined; + if (!searchMode && flags.limit !== undefined) { + throw new InputValidationError( + "--limit applies to search mode; add --since and/or --until", + ); + } + const filterPattern = buildFilterPattern({ level: flags.level, query: flags.query }); + const startTimeMs = + flags.since !== undefined + ? parseTimeString(flags.since) + : Date.now() - DEFAULT_SEARCH_WINDOW_MS; + const endTimeMs = flags.until !== undefined ? parseTimeString(flags.until) : Date.now(); + + const writeEvent = (event: RuntimeLogEvent) => { + const timestamp = new Date(event.timestamp).toISOString(); + if (json) { + renderer.renderJsonLine({ timestamp, message: event.message }); + } else { + io.stdout.write(`${timestamp} ${event.message.trimEnd()}\n`); + } + }; + + await withUserCancellation(async (signal) => { + const target = await resolveRuntimeTarget(core, ctx, flags.id); + + if (searchMode) { + const events = core.observability.searchRuntimeLogs( + { + runtimeId: target.runtimeId, + startTimeMs, + endTimeMs, + filterPattern, + limit: flags.limit, + }, + target.options, + signal, + ); + for await (const event of events) writeEvent(event); + return; + } + + io.stderr.write(`Streaming logs for runtime ${target.runtimeId}... (Ctrl+C to stop)\n`); + const events = core.observability.streamRuntimeLogs( + { runtimeId: target.runtimeId, filterPattern }, + target.options, + signal, + ); + for await (const event of events) writeEvent(event); + }); + }, + }); diff --git a/src/handlers/runtime/logs/logs.test.tsx b/src/handlers/runtime/logs/logs.test.tsx new file mode 100644 index 000000000..cf3c7d2e4 --- /dev/null +++ b/src/handlers/runtime/logs/logs.test.tsx @@ -0,0 +1,184 @@ +import { describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createSilentLogger, TestCoreClient, testIO } from "../../../testing"; +import { TestGlobalConfigAccessor } from "../../../testing/globalConfig"; +import { createRootHandler } from "../../index"; +import type { SearchRuntimeLogsInput, StreamRuntimeLogsInput } from "../types"; + +const REGION = "us-west-2"; + +// Fixed epoch bounds keep the tests clock-independent. +const SINCE_MS = 1_709_391_000_000; +const UNTIL_MS = 1_709_394_600_000; + +function testLogsCommand() { + const core = new TestCoreClient(); + const io = testIO(); + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + return { + core, + io, + route: (args: string[]) => root.route(["node", "agentcore", ...args, "--region", REGION]), + }; +} + +describe("runtime logs", () => { + test("searches when --since/--until are given and renders human lines", async () => { + const { core, io, route } = testLogsCommand(); + core.observability.logEvents = [ + { timestamp: SINCE_MS, message: "hello world\n" }, + { timestamp: SINCE_MS + 1_000, message: "second line" }, + ]; + + await route([ + "runtime", + "logs", + "--id", + "my_agent-AbC123XyZ9", + "--since", + `${SINCE_MS}`, + "--until", + `${UNTIL_MS}`, + ]); + + expect(core.observability.calls).toHaveLength(1); + const call = core.observability.calls[0]!; + expect(call.method).toBe("searchRuntimeLogs"); + expect(call.args[0] as SearchRuntimeLogsInput).toEqual({ + runtimeId: "my_agent-AbC123XyZ9", + startTimeMs: SINCE_MS, + endTimeMs: UNTIL_MS, + filterPattern: undefined, + limit: undefined, + }); + expect(call.args[1]).toEqual({ region: REGION, endpointUrl: undefined }); + + // Human mode: ` ` with the trailing newline normalized. + expect(io.stdout()).toBe( + "2024-03-02T14:50:00.000Z hello world\n2024-03-02T14:50:01.000Z second line", + ); + }); + + test("--json emits one JSON object per event (JSON Lines)", async () => { + const { core, io, route } = testLogsCommand(); + core.observability.logEvents = [{ timestamp: SINCE_MS, message: "hello" }]; + + await route([ + "runtime", + "logs", + "--id", + "my_agent-AbC123XyZ9", + "--since", + `${SINCE_MS}`, + "--json", + ]); + + expect(io.stdout()).toBe('{"timestamp":"2024-03-02T14:50:00.000Z","message":"hello"}'); + }); + + test("level and query compose into a CloudWatch filter pattern", async () => { + const { core, route } = testLogsCommand(); + + await route([ + "runtime", + "logs", + "--id", + "rt-1", + "--since", + "1709391000000", + "--level", + "ERROR", + "--query", + "database", + "--limit", + "25", + ]); + + const input = core.observability.calls[0]!.args[0] as SearchRuntimeLogsInput; + // --level is case-insensitive, like the old CLI. + expect(input.filterPattern).toBe("ERROR database"); + expect(input.limit).toBe(25); + }); + + test("rejects an invalid --level", async () => { + const { route } = testLogsCommand(); + + await expect(route(["runtime", "logs", "--id", "rt-1", "--level", "loud"])).rejects.toThrow( + "Invalid value for option '--level'", + ); + }); + + test("follows by default, announcing the stream on stderr", async () => { + const { core, io, route } = testLogsCommand(); + core.observability.logEvents = [{ timestamp: SINCE_MS, message: "tailed" }]; + + await route(["runtime", "logs", "--id", "my_agent-AbC123XyZ9"]); + + expect(core.observability.calls).toHaveLength(1); + const call = core.observability.calls[0]!; + expect(call.method).toBe("streamRuntimeLogs"); + expect(call.args[0] as StreamRuntimeLogsInput).toEqual({ + runtimeId: "my_agent-AbC123XyZ9", + filterPattern: undefined, + }); + expect(io.stderr()).toContain( + "Streaming logs for runtime my_agent-AbC123XyZ9... (Ctrl+C to stop)", + ); + expect(io.stdout()).toBe("2024-03-02T14:50:00.000Z tailed"); + }); + + test("rejects --limit outside search mode", async () => { + const { route } = testLogsCommand(); + + await expect(route(["runtime", "logs", "--id", "rt-1", "--limit", "5"])).rejects.toThrow( + "--limit applies to search mode; add --since and/or --until", + ); + }); + + test("rejects an unparseable --since", async () => { + const { route } = testLogsCommand(); + + await expect( + route(["runtime", "logs", "--id", "rt-1", "--since", "yesterday-ish"]), + ).rejects.toThrow('Invalid time string: "yesterday-ish"'); + }); + + test("auto-resolves the project's deployed runtime when --id is omitted", async () => { + const { core, route } = testLogsCommand(); + + // A minimal-but-valid project for the on-disk project resolution. + const root = mkdtempSync(join(tmpdir(), "logs-project-")); + mkdirSync(join(root, "agentcore"), { recursive: true }); + writeFileSync( + join(root, "agentcore", "agentcore.json"), + JSON.stringify({ name: "LogsProj", version: 1 }), + ); + + const previousCwd = process.cwd(); + process.chdir(root); + try { + await route(["runtime", "logs", "--since", `${SINCE_MS}`]); + } finally { + process.chdir(previousCwd); + } + + const [resolveCall, searchCall] = core.observability.calls; + expect(resolveCall!.method).toBe("resolveDeployedRuntime"); + expect(resolveCall!.args[1]).toBe("default"); + expect(searchCall!.method).toBe("searchRuntimeLogs"); + // The stubbed deployed runtime (and its target region) win over --region. + expect((searchCall!.args[0] as SearchRuntimeLogsInput).runtimeId).toBe( + "project_runtime-0000000000", + ); + expect(searchCall!.args[1]).toMatchObject({ + region: core.observability.resolveDeployedRuntimeResponse.region, + }); + }); +}); diff --git a/src/handlers/runtime/resolveRuntimeTarget.test.ts b/src/handlers/runtime/resolveRuntimeTarget.test.ts new file mode 100644 index 000000000..c46184597 --- /dev/null +++ b/src/handlers/runtime/resolveRuntimeTarget.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { InputValidationError } from "../../errors"; +import { RegionKey } from "../keys"; +import { ValueContext } from "../../router"; +import type { Core } from "../types"; +import type { Project } from "../project/types"; +import type { DeployedRuntime } from "./types"; +import { resolveRuntimeTarget } from "./resolveRuntimeTarget"; + +const ctx = ValueContext.EmptyContext().withValue(RegionKey, "us-east-1"); + +const PROJECT = { name: "Proj", rootPath: "/proj", spec: {} } as unknown as Project; + +const DEPLOYED: DeployedRuntime = { + runtimeId: "proj_agent-AbC123XyZ9", + region: "eu-west-1", + stackName: "AgentCore-Proj-default", + targetName: "default", +}; + +function stubCore(config: { + resolve: () => Promise; + deployed?: DeployedRuntime; +}): { core: Core; observabilityCalls: unknown[][] } { + const observabilityCalls: unknown[][] = []; + const core = { + projectManager: { resolve: config.resolve }, + observability: { + resolveDeployedRuntime: async (project: Project, targetName: string) => { + observabilityCalls.push([project, targetName]); + return config.deployed ?? DEPLOYED; + }, + }, + } as unknown as Core; + return { core, observabilityCalls }; +} + +describe("resolveRuntimeTarget", () => { + test("an explicit --id wins and keeps the ambient region", async () => { + const { core, observabilityCalls } = stubCore({ resolve: async () => undefined }); + + const target = await resolveRuntimeTarget(core, ctx, "explicit-id", tmpdir()); + + expect(target.runtimeId).toBe("explicit-id"); + expect(target.options).toEqual({ region: "us-east-1", endpointUrl: undefined }); + expect(target.project).toBeUndefined(); + expect(observabilityCalls).toHaveLength(0); + }); + + test("an explicit --id attaches the enclosing project as context", async () => { + const { core } = stubCore({ resolve: async () => PROJECT }); + + const target = await resolveRuntimeTarget(core, ctx, "explicit-id", "/proj/somewhere"); + + expect(target.project).toBe(PROJECT); + }); + + test("an explicit --id survives a broken project spec", async () => { + const { core } = stubCore({ + resolve: async () => { + throw new Error("agentcore.json is corrupt"); + }, + }); + + const target = await resolveRuntimeTarget(core, ctx, "explicit-id", tmpdir()); + + expect(target.runtimeId).toBe("explicit-id"); + expect(target.project).toBeUndefined(); + }); + + test("without --id the project's default-target runtime resolves, region included", async () => { + const { core, observabilityCalls } = stubCore({ resolve: async () => PROJECT }); + + const target = await resolveRuntimeTarget(core, ctx, undefined, "/proj/app"); + + expect(observabilityCalls).toEqual([[PROJECT, "default"]]); + expect(target.runtimeId).toBe("proj_agent-AbC123XyZ9"); + // The deployment target's region wins: the stack and log groups live there. + expect(target.options.region).toBe("eu-west-1"); + expect(target.project).toBe(PROJECT); + }); + + test("without --id and outside a project, a usage error demands --id", async () => { + const { core } = stubCore({ resolve: async () => undefined }); + const outside = mkdtempSync(join(tmpdir(), "no-project-")); + + await expect(resolveRuntimeTarget(core, ctx, undefined, outside)).rejects.toThrow( + InputValidationError, + ); + await expect(resolveRuntimeTarget(core, ctx, undefined, outside)).rejects.toThrow( + "required option '--id ' not specified", + ); + }); +}); diff --git a/src/handlers/runtime/resolveRuntimeTarget.ts b/src/handlers/runtime/resolveRuntimeTarget.ts new file mode 100644 index 000000000..8f0e7c6d4 --- /dev/null +++ b/src/handlers/runtime/resolveRuntimeTarget.ts @@ -0,0 +1,59 @@ +import { InputValidationError } from "../../errors"; +import { ExitCode } from "../../runnable"; +import type { Context } from "../../router"; +import type { CoreOptions } from "../../core/types"; +import { DEFAULT_TARGET_NAME } from "../../projectSchemas/aws-targets"; +import type { Core } from "../types"; +import { coreOptsFromCtx } from "../utils"; +import type { Project } from "../project/types"; + +export interface RuntimeTarget { + runtimeId: string; + /** CoreOptions to use for this runtime's CloudWatch reads. */ + options: CoreOptions; + /** The enclosing project, when the command ran inside one. */ + project?: Project; +} + +/** + * Resolves which runtime an observability command (`runtime logs` / + * `runtime traces`) addresses. An explicit --id wins and works anywhere; without + * one the enclosing project's deployed runtime is resolved live from its + * CloudFormation stack outputs (default target). Outside a project, --id is + * required. + * + * When resolving automatically, the deployment target's region overrides the + * ambient one: the stack and its log groups live there. + */ +export async function resolveRuntimeTarget( + core: Core, + ctx: Context, + id: string | undefined, + cwd: string = process.cwd(), +): Promise { + const options = coreOptsFromCtx(ctx); + + if (id !== undefined) { + // With an explicit --id the project is only context (e.g. default output + // paths); a broken project spec must not block addressing a runtime + // directly. + const project = await core.projectManager.resolve({ filePath: cwd }).catch(() => undefined); + return { runtimeId: id, options, project }; + } + + const project = await core.projectManager.resolve({ filePath: cwd }); + if (!project) { + throw new InputValidationError( + "required option '--id ' not specified " + + "(run inside an AgentCore project to resolve the deployed runtime automatically)", + { exitCode: ExitCode.USAGE }, + ); + } + + const deployed = await core.observability.resolveDeployedRuntime(project, DEFAULT_TARGET_NAME); + return { + runtimeId: deployed.runtimeId, + options: { ...options, region: deployed.region }, + project, + }; +} diff --git a/src/handlers/runtime/runtime.test.tsx b/src/handlers/runtime/runtime.test.tsx index 4c2c9a8a1..73d5ff90b 100644 --- a/src/handlers/runtime/runtime.test.tsx +++ b/src/handlers/runtime/runtime.test.tsx @@ -77,6 +77,7 @@ describe("runtime command hierarchy", () => { "invoke", "version", "endpoint", + "logs", ]); expect( runtime diff --git a/src/handlers/runtime/types.tsx b/src/handlers/runtime/types.tsx index a7ff1c38a..6c8d74cda 100644 --- a/src/handlers/runtime/types.tsx +++ b/src/handlers/runtime/types.tsx @@ -6,6 +6,7 @@ import type { ListAgentRuntimeVersionsResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; import type { CoreOptions } from "../../core/types"; +import type { Project } from "../project/types"; export type RuntimeInvokeRequest = { runtimeId: string; @@ -80,3 +81,53 @@ export interface CoreRuntimeClient { options: CoreOptions, ): Promise; } + +/** One CloudWatch log event from a runtime's log group. */ +export type RuntimeLogEvent = { + /** Epoch milliseconds. */ + timestamp: number; + message: string; +}; + +/** A project runtime resolved live from its CloudFormation stack outputs. */ +export type DeployedRuntime = { + runtimeId: string; + /** The deployment target's region — where the stack and log groups live. */ + region: string; + stackName: string; + targetName: string; +}; + +export type StreamRuntimeLogsInput = { + runtimeId: string; + /** CloudWatch Logs filter pattern applied server-side. */ + filterPattern?: string; +}; + +export type SearchRuntimeLogsInput = { + runtimeId: string; + /** Window start, epoch milliseconds (inclusive). */ + startTimeMs: number; + /** Window end, epoch milliseconds (inclusive). */ + endTimeMs: number; + /** CloudWatch Logs filter pattern applied server-side. */ + filterPattern?: string; + /** Maximum number of events to yield. */ + limit?: number; +}; + +export interface CoreObservabilityClient { + resolveDeployedRuntime(project: Project, targetName: string): Promise; + /** Live-tails the runtime's log group until `signal` aborts. */ + streamRuntimeLogs( + input: StreamRuntimeLogsInput, + options: CoreOptions, + signal: AbortSignal, + ): AsyncGenerator; + /** Searches the runtime's log group over a time window, oldest to newest. */ + searchRuntimeLogs( + input: SearchRuntimeLogsInput, + options: CoreOptions, + signal?: AbortSignal, + ): AsyncGenerator; +} diff --git a/src/handlers/types.tsx b/src/handlers/types.tsx index 6194eaf45..3d76827b8 100644 --- a/src/handlers/types.tsx +++ b/src/handlers/types.tsx @@ -3,7 +3,7 @@ import type { CoreGatewayClient } from "./gateway/types.tsx"; import type { CoreHarnessClient } from "./harness/types.tsx"; import type { CoreIdentityClient } from "./identity/types.tsx"; import type { CoreMemoryClient } from "./memory/types.tsx"; -import type { CoreRuntimeClient } from "./runtime/types.tsx"; +import type { CoreObservabilityClient, CoreRuntimeClient } from "./runtime/types.tsx"; import type { Context } from "../router"; import type { ProjectManager } from "./project/types.ts"; import type { DescribeBedrockAgent } from "../core/project/bedrockAgent"; @@ -15,6 +15,7 @@ export interface Core { runtime: CoreRuntimeClient; gateway: CoreGatewayClient; eval: CoreEvalClient; + observability: CoreObservabilityClient; projectManager: ProjectManager; /** Describes a Bedrock Agent + alias for `--type import`. */ describeBedrockAgent: DescribeBedrockAgent; diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index d2ce65301..93d0b26d8 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -127,9 +127,14 @@ import type { } from "../handlers/identity/types"; import type { CoreMemoryClient } from "../handlers/memory/types"; import type { + CoreObservabilityClient, CoreRuntimeClient, + DeployedRuntime, RuntimeInvokeRequest, RuntimeInvokeResponse, + RuntimeLogEvent, + SearchRuntimeLogsInput, + StreamRuntimeLogsInput, } from "../handlers/runtime/types"; import type { BatchEvaluationDetail, @@ -160,7 +165,7 @@ import type { import { isTerminalStatus } from "../core/batchEvaluationResults"; import { abortable } from "../core/abortable"; import type { CoreOptions, CreateCloudFormationClient } from "../core/types"; -import type { ProjectManager } from "../handlers/project/types"; +import type { Project, ProjectManager } from "../handlers/project/types"; import type { Logger } from "../logging"; import type { ReadWriteJson } from "../io"; import { createSilentLogger } from "./logging"; @@ -2228,6 +2233,48 @@ export class TestEvalClient implements CoreEvalClient { } } +// TestObservabilityClient is a controllable CoreObservabilityClient: seed +// `logEvents` / `resolveDeployedRuntimeResponse`, or set `error` to force the +// next call to throw. Every call is recorded on `calls`. +export class TestObservabilityClient implements CoreObservabilityClient { + calls: { method: string; args: unknown[] }[] = []; + error: Error | undefined; + + resolveDeployedRuntimeResponse: DeployedRuntime = { + runtimeId: "project_runtime-0000000000", + region: "us-west-2", + stackName: "AgentCore-project-default", + targetName: "default", + }; + logEvents: RuntimeLogEvent[] = []; + + async resolveDeployedRuntime(project: Project, targetName: string): Promise { + this.calls.push({ method: "resolveDeployedRuntime", args: [project, targetName] }); + if (this.error) throw this.error; + return this.resolveDeployedRuntimeResponse; + } + + async *streamRuntimeLogs( + input: StreamRuntimeLogsInput, + options: CoreOptions, + signal: AbortSignal, + ): AsyncGenerator { + this.calls.push({ method: "streamRuntimeLogs", args: [input, options, signal] }); + if (this.error) throw this.error; + yield* this.logEvents; + } + + async *searchRuntimeLogs( + input: SearchRuntimeLogsInput, + options: CoreOptions, + signal?: AbortSignal, + ): AsyncGenerator { + this.calls.push({ method: "searchRuntimeLogs", args: [input, options, signal] }); + if (this.error) throw this.error; + yield* this.logEvents; + } +} + // TestCoreClient implements the Core contract with fully controllable sub-clients. export class TestCoreClient implements Core { readonly harness = new TestHarnessClient(); @@ -2236,6 +2283,7 @@ export class TestCoreClient implements Core { readonly runtime = new TestRuntimeClient(); readonly gateway = new TestGatewayClient(); readonly eval = new TestEvalClient(); + readonly observability = new TestObservabilityClient(); readonly projectManager: ProjectManager; // Commands the project manager would have run (npm install, git init, ...), From bd286124114480dbdc9ab06083bd991d0243e753 Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Mon, 31 Aug 2026 00:04:48 -0400 Subject: [PATCH 10/12] feat(runtime): traces list and get Adds `agentcore runtime traces list` and `agentcore runtime traces get `. list aggregates the runtime's telemetry with a Logs Insights `stats ... by traceId` query (newest first, default 12h window, --limit default 20) and renders a traceId/timestamp/sessionId table, or a single JSON document with --json; an empty result prints a stderr notice that traces take 2-3 minutes to appear. get validates the trace-id format, downloads every log record of the trace (@message JSON-parsed when possible), writes them to --output or agentcore/.cli/traces/ -.json inside a project (./.json outside), and prints the written path on stdout. Both leaves share `runtime logs`' addressing: --id anywhere, or automatic resolution of the project's deployed runtime from its stack outputs. The queries run through the shared Insights runner extracted in the earlier observability refactor. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- README.md | 7 + src/core/observability.test.ts | 148 ++++++++++++ src/core/observability.ts | 112 +++++++++ src/handlers/runtime/index.tsx | 6 +- src/handlers/runtime/runtime.test.tsx | 8 + src/handlers/runtime/traces/get/index.tsx | 81 +++++++ src/handlers/runtime/traces/get/outputPath.ts | 29 +++ src/handlers/runtime/traces/index.tsx | 15 ++ src/handlers/runtime/traces/list/index.tsx | 92 ++++++++ src/handlers/runtime/traces/traces.test.tsx | 217 ++++++++++++++++++ src/handlers/runtime/types.tsx | 39 ++++ src/testing/TestCoreClient.tsx | 22 ++ 12 files changed, 774 insertions(+), 2 deletions(-) create mode 100644 src/handlers/runtime/traces/get/index.tsx create mode 100644 src/handlers/runtime/traces/get/outputPath.ts create mode 100644 src/handlers/runtime/traces/index.tsx create mode 100644 src/handlers/runtime/traces/list/index.tsx create mode 100644 src/handlers/runtime/traces/traces.test.tsx diff --git a/README.md b/README.md index 9221f2bea..e17c47c16 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ agentcore # interactive TUI │ ├── list # list Runtimes (server-side paginated) │ ├── invoke # invoke a Runtime headlessly or in a persistent console │ ├── logs # follow a Runtime's logs live, or search a time window +│ ├── traces +│ │ ├── list # list a Runtime's recent traces +│ │ └── get # download a trace's log records to a JSON file │ ├── version │ │ ├── get # get a specific Runtime version │ │ └── list # list a Runtime's versions @@ -204,6 +207,10 @@ agentcore runtime logs --id --level error --query "database" agentcore runtime logs --id --since 1h --limit 100 agentcore runtime logs --id --since 2026-08-30T12:00:00Z --until now --json +# List recent traces (they take 2-3 minutes to appear), then download one +agentcore runtime traces list --id --since 30m +agentcore runtime traces get --id --output trace.json + # Inspect AgentCore Memories without project configuration or deployment agentcore memory get --id agentcore memory get --id --view without_decryption diff --git a/src/core/observability.test.ts b/src/core/observability.test.ts index 235577f82..3758af14d 100644 --- a/src/core/observability.test.ts +++ b/src/core/observability.test.ts @@ -537,3 +537,151 @@ describe("ObservabilityClient.streamRuntimeLogs", () => { ); }); }); + +// insightsLogs fakes the StartQuery/GetQueryResults protocol: every query +// completes immediately with `results`, and each StartQuery input is recorded. +function insightsLogs(results: { field: string; value: string }[][]) { + const queries: { + logGroupNames?: string[]; + queryString?: string; + startTime?: number; + endTime?: number; + }[] = []; + const logs = fakeLogs(async (command) => { + if (command instanceof StartQueryCommand) { + queries.push(command.input); + return { queryId: "q-traces" }; + } + expect(command).toBeInstanceOf(GetQueryResultsCommand); + return { status: "Complete", results }; + }); + return { logs, queries }; +} + +describe("ObservabilityClient.listRuntimeTraces", () => { + const INPUT = { + runtimeId: "my_agent-AbC123XyZ9", + startTimeMs: 1_700_000_000_123, + endTimeMs: 1_700_003_600_456, + limit: 5, + }; + + test("aggregates traces with a stats-by-traceId query over the runtime log group", async () => { + const { logs, queries } = insightsLogs([]); + + await clientWith(logs).listRuntimeTraces(INPUT, OPTIONS); + + expect(queries).toHaveLength(1); + expect(queries[0]!.logGroupNames).toEqual([ + "/aws/bedrock-agentcore/runtimes/my_agent-AbC123XyZ9-DEFAULT", + ]); + // Epoch ms narrows to whole seconds. + expect(queries[0]!.startTime).toBe(1_700_000_000); + expect(queries[0]!.endTime).toBe(1_700_003_600); + expect(queries[0]!.queryString).toBe( + 'filter ispresent(traceId) and traceId != ""\n' + + "| stats earliest(@timestamp) as firstSeen, latest(@timestamp) as lastSeen, " + + "count(*) as spanCount, earliest(attributes.session.id) as sessionId by traceId\n" + + "| sort lastSeen desc\n" + + "| limit 5", + ); + }); + + test("parses result rows into trace summaries, skipping rows without a trace id", async () => { + const { logs } = insightsLogs([ + [ + { field: "traceId", value: "abc123" }, + { field: "firstSeen", value: "1700000000000" }, + { field: "lastSeen", value: "1700000005000" }, + { field: "spanCount", value: "12" }, + { field: "sessionId", value: "session-1" }, + ], + [{ field: "lastSeen", value: "1700000001000" }], + [ + { field: "traceId", value: "def456" }, + { field: "firstSeen", value: "1700000002000" }, + ], + ]); + + const traces = await clientWith(logs).listRuntimeTraces(INPUT, OPTIONS); + + expect(traces).toEqual([ + { + traceId: "abc123", + timestamp: "1700000005000", + sessionId: "session-1", + spanCount: "12", + }, + // lastSeen falls back to firstSeen; sessionId/spanCount stay undefined. + { traceId: "def456", timestamp: "1700000002000", sessionId: undefined, spanCount: undefined }, + ]); + }); + + test("translates a missing log group into invoked-yet guidance", async () => { + const logs = fakeLogs(async () => { + throw new ResourceNotFoundException({ message: "no such group", $metadata: {} }); + }); + + await expect(clientWith(logs).listRuntimeTraces(INPUT, OPTIONS)).rejects.toThrow( + "Has the runtime been invoked yet?", + ); + }); +}); + +describe("ObservabilityClient.getRuntimeTrace", () => { + const INPUT = { + runtimeId: "my_agent-AbC123XyZ9", + traceId: "68b2fabc0000000000abcdef", + startTimeMs: 1_700_000_000_000, + endTimeMs: 1_700_003_600_000, + }; + + test("rejects a malformed trace id before querying", async () => { + const logs = fakeLogs(async () => { + throw new Error("must not be called"); + }); + + await expect( + clientWith(logs).getRuntimeTrace({ ...INPUT, traceId: "not'a$trace" }, OPTIONS), + ).rejects.toThrow("Invalid trace ID format. Expected a hex string (e.g., abc123def456)."); + }); + + test("downloads the trace's records with @message parsed when it is JSON", async () => { + const { logs, queries } = insightsLogs([ + [ + { field: "@timestamp", value: "2026-08-30 12:00:00.000" }, + { field: "@message", value: '{"traceId":"68b2fabc","body":"hello"}' }, + { field: "@ptr", value: "pointer-1" }, + ], + [ + { field: "@timestamp", value: "2026-08-30 12:00:01.000" }, + { field: "@message", value: "not json" }, + ], + ]); + + const records = await clientWith(logs).getRuntimeTrace(INPUT, OPTIONS); + + expect(queries[0]!.queryString).toBe( + "fields @timestamp, @message\n" + + "| filter traceId = '68b2fabc0000000000abcdef'\n" + + "| sort @timestamp asc\n" + + "| limit 10000", + ); + expect(records).toEqual([ + { + "@timestamp": "2026-08-30 12:00:00.000", + "@message": { traceId: "68b2fabc", body: "hello" }, + "@ptr": "pointer-1", + }, + { "@timestamp": "2026-08-30 12:00:01.000", "@message": "not json" }, + ]); + }); + + test("fails when the trace has no records", async () => { + const { logs } = insightsLogs([]); + + await expect(clientWith(logs).getRuntimeTrace(INPUT, OPTIONS)).rejects.toThrow( + "No trace data found for trace ID: 68b2fabc0000000000abcdef", + ); + }); +}); diff --git a/src/core/observability.ts b/src/core/observability.ts index d937f0096..81736d6e1 100644 --- a/src/core/observability.ts +++ b/src/core/observability.ts @@ -23,9 +23,13 @@ import type { Project } from "../handlers/project/types"; import type { CoreObservabilityClient, DeployedRuntime, + GetRuntimeTraceInput, + ListRuntimeTracesInput, RuntimeLogEvent, SearchRuntimeLogsInput, StreamRuntimeLogsInput, + TraceRecord, + TraceSummary, } from "../handlers/runtime/types"; import { AwsDeploymentTargetsSchema } from "../projectSchemas/aws-targets"; import { isStackNotFound } from "./project/backends/cdk/environment"; @@ -441,6 +445,114 @@ export class ObservabilityClient implements CoreObservabilityClient { nextToken = response.nextToken; } while (nextToken && (input.limit === undefined || yielded < input.limit)); } + + /** + * Lists the runtime's recent traces by aggregating its telemetry records with + * a Logs Insights `stats … by traceId` query (mirrors the old CLI's + * list-traces operation), newest first. + */ + async listRuntimeTraces( + input: ListRuntimeTracesInput, + options: CoreOptions, + ): Promise { + const logGroupName = runtimeLogGroup(input.runtimeId, DEFAULT_ENDPOINT_QUALIFIER); + // Infrastructure records carry an empty traceId; excluding them before the + // aggregation keeps them from occupying one of the `limit` buckets (the old + // CLI filtered afterwards, silently returning one trace fewer). + const queryString = + `filter ispresent(traceId) and traceId != ""\n` + + `| stats earliest(@timestamp) as firstSeen, latest(@timestamp) as lastSeen, ` + + `count(*) as spanCount, earliest(attributes.session.id) as sessionId by traceId\n` + + `| sort lastSeen desc\n` + + `| limit ${Math.floor(input.limit)}`; + + const rows = await this.runTraceQuery(input, logGroupName, queryString, options); + + const traces: TraceSummary[] = []; + for (const row of rows) { + const fields = fieldMap(row); + if (!fields.traceId) continue; + traces.push({ + traceId: fields.traceId, + timestamp: fields.lastSeen ?? fields.firstSeen ?? "unknown", + sessionId: fields.sessionId, + spanCount: fields.spanCount, + }); + } + return traces; + } + + /** + * Downloads every log record belonging to one trace, oldest first. The + * `@message` body is JSON-parsed when possible; other Insights fields pass + * through as returned. + */ + async getRuntimeTrace(input: GetRuntimeTraceInput, options: CoreOptions): Promise { + if (!TRACE_ID_PATTERN.test(input.traceId)) { + throw new InputValidationError( + "Invalid trace ID format. Expected a hex string (e.g., abc123def456).", + { meta: { traceId: input.traceId } }, + ); + } + + const logGroupName = runtimeLogGroup(input.runtimeId, DEFAULT_ENDPOINT_QUALIFIER); + const queryString = + `fields @timestamp, @message\n` + + `| filter traceId = '${sanitizeQueryValue(input.traceId)}'\n` + + `| sort @timestamp asc\n` + + `| limit 10000`; + + const rows = await this.runTraceQuery(input, logGroupName, queryString, options); + if (rows.length === 0) { + throw new ResourceNotFoundError(`No trace data found for trace ID: ${input.traceId}`, { + meta: { traceId: input.traceId }, + }); + } + + return rows.map((row) => { + const record: TraceRecord = fieldMap(row); + const message = record["@message"]; + if (typeof message === "string") { + try { + record["@message"] = JSON.parse(message); + } catch { + // Keep the original string when the body is not valid JSON. + } + } + return record; + }); + } + + private async runTraceQuery( + input: { runtimeId: string; startTimeMs: number; endTimeMs: number }, + logGroupName: string, + queryString: string, + options: CoreOptions, + ): Promise { + const logs = this.clients.logs(toClientConfig(options)); + const startSec = Math.floor(input.startTimeMs / 1000); + const endSec = Math.floor(input.endTimeMs / 1000); + try { + return await runInsightsQuery(logs, [logGroupName], queryString, startSec, endSec); + } catch (error) { + if (error instanceof ResourceNotFoundException) { + throw missingLogGroupError(input.runtimeId, logGroupName, error); + } + throw error; + } + } +} + +// Trace ids are hex strings, optionally dash-separated (mirrors the old CLI). +const TRACE_ID_PATTERN = /^[a-fA-F0-9-]+$/; + +// fieldMap flattens one Insights result row into a name -> value record. +function fieldMap(row: ResultField[]): Record { + const fields: Record = {}; + for (const field of row) { + if (field.field && field.value !== undefined) fields[field.field] = field.value; + } + return fields; } function missingLogGroupError( diff --git a/src/handlers/runtime/index.tsx b/src/handlers/runtime/index.tsx index 44bfbf339..471af6aba 100644 --- a/src/handlers/runtime/index.tsx +++ b/src/handlers/runtime/index.tsx @@ -8,6 +8,7 @@ import { createGetRuntimeHandler } from "./get"; import { createInvokeRuntimeHandler } from "./invoke"; import { createListRuntimesHandler } from "./list"; import { createRuntimeLogsHandler } from "./logs"; +import { createRuntimeTracesHandler } from "./traces"; import { createRuntimeVersionHandler } from "./version"; export function createRuntimeHandler(core: Core, io: AppIO): Router { @@ -15,8 +16,8 @@ export function createRuntimeHandler(core: Core, io: AppIO): Router { new Router("runtime", "inspect AgentCore Runtimes") .use(withTuiOnEmptyFlagsAndArgs(core, io)) .default(renderTui(core, io)) - // logs is headless-only: a bare `runtime logs` means "follow the project - // runtime's logs", so it must never fall into the TUI. + // logs and traces are headless-only: a bare `runtime logs` means "follow + // the project runtime's logs", so neither may fall into the TUI. .supportedTuiCommands("get", "list", "invoke", "version", "endpoint") .handler(createGetRuntimeHandler(core)) .handler(createListRuntimesHandler(core)) @@ -24,5 +25,6 @@ export function createRuntimeHandler(core: Core, io: AppIO): Router { .handler(createRuntimeVersionHandler(core, io)) .handler(createRuntimeEndpointHandler(core, io)) .handler(createRuntimeLogsHandler(core, io)) + .handler(createRuntimeTracesHandler(core, io)) ); } diff --git a/src/handlers/runtime/runtime.test.tsx b/src/handlers/runtime/runtime.test.tsx index 73d5ff90b..e11d67373 100644 --- a/src/handlers/runtime/runtime.test.tsx +++ b/src/handlers/runtime/runtime.test.tsx @@ -78,7 +78,15 @@ describe("runtime command hierarchy", () => { "version", "endpoint", "logs", + "traces", ]); + expect( + runtime + ?.children() + .find((child) => child.name() === "traces") + ?.children() + .map((child) => child.name()), + ).toEqual(["list", "get"]); expect( runtime ?.children() diff --git a/src/handlers/runtime/traces/get/index.tsx b/src/handlers/runtime/traces/get/index.tsx new file mode 100644 index 000000000..ab8da87bc --- /dev/null +++ b/src/handlers/runtime/traces/get/index.tsx @@ -0,0 +1,81 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import z from "zod"; +import { parseTimeString } from "../../../../core/observability"; +import { FileWriteError } from "../../../../errors"; +import type { AppIO } from "../../../../io"; +import { argument, createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import { JsonKey } from "../../../keys"; +import type { Core } from "../../../types"; +import { runtimeIdSchema } from "../../invoke/request"; +import { resolveRuntimeTarget } from "../../resolveRuntimeTarget"; +import { DEFAULT_TRACES_WINDOW_MS } from "../index"; +import { resolveTraceOutputPath } from "./outputPath"; + +export const createGetRuntimeTraceHandler = (core: Core, io: AppIO) => + createHandler({ + name: "get", + description: "download a trace's log records to a JSON file", + arguments: [argument("trace-id", "the trace ID to download", z.string().min(1))], + flags: [ + flag( + "id", + "the ID of the Runtime (defaults to the project's deployed runtime)", + runtimeIdSchema.optional(), + ), + flag( + "output", + "the output file path (default: agentcore/.cli/traces/-.json in a project)", + z.string().min(1, "requires a nonempty path").optional(), + ), + flag( + "since", + 'window start: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default 12h ago)', + z.string().min(1).optional(), + ), + flag( + "until", + 'window end: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default now)', + z.string().min(1).optional(), + ), + ], + handle: async (ctx, flags, args) => { + const traceId = args["trace-id"]; + const startTimeMs = + flags.since !== undefined + ? parseTimeString(flags.since) + : Date.now() - DEFAULT_TRACES_WINDOW_MS; + const endTimeMs = flags.until !== undefined ? parseTimeString(flags.until) : Date.now(); + + const target = await resolveRuntimeTarget(core, ctx, flags.id); + const records = await core.observability.getRuntimeTrace( + { runtimeId: target.runtimeId, traceId, startTimeMs, endTimeMs }, + target.options, + ); + + const filePath = resolveTraceOutputPath({ + output: flags.output, + project: target.project, + runtimeId: target.runtimeId, + traceId, + }); + try { + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, JSON.stringify(records, null, 2)); + } catch (error) { + throw new FileWriteError( + `Could not write the trace file at ${filePath}: ` + + `${error instanceof Error ? error.message : String(error)}`, + { cause: error, meta: { filePath } }, + ); + } + + if (ctx.require(JsonKey)) { + ctx.require(JsonRendererKey).renderJson({ filePath, recordCount: records.length }); + return; + } + io.stderr.write(`Saved ${records.length} records for trace ${traceId}\n`); + io.stdout.write(`${filePath}\n`); + }, + }); diff --git a/src/handlers/runtime/traces/get/outputPath.ts b/src/handlers/runtime/traces/get/outputPath.ts new file mode 100644 index 000000000..2eee13731 --- /dev/null +++ b/src/handlers/runtime/traces/get/outputPath.ts @@ -0,0 +1,29 @@ +import { join, resolve } from "node:path"; +import type { Project } from "../../../project/types"; + +/** + * Resolves where `runtime traces get` writes its JSON file: an explicit + * --output wins; inside a project the file lands under the project's + * `agentcore/.cli/traces/` (keyed by runtime and trace so downloads never + * collide); outside a project it lands in the working directory. + */ +export function resolveTraceOutputPath(config: { + output?: string; + project?: Project; + runtimeId: string; + traceId: string; + cwd?: string; +}): string { + const cwd = config.cwd ?? process.cwd(); + if (config.output) return resolve(cwd, config.output); + if (config.project) { + return join( + config.project.rootPath, + "agentcore", + ".cli", + "traces", + `${config.runtimeId}-${config.traceId}.json`, + ); + } + return resolve(cwd, `${config.traceId}.json`); +} diff --git a/src/handlers/runtime/traces/index.tsx b/src/handlers/runtime/traces/index.tsx new file mode 100644 index 000000000..a8f11cccb --- /dev/null +++ b/src/handlers/runtime/traces/index.tsx @@ -0,0 +1,15 @@ +import { Router } from "../../../router"; +import type { AppIO } from "../../../io"; +import type { Core } from "../../types"; +import { createGetRuntimeTraceHandler } from "./get"; +import { createListRuntimeTracesHandler } from "./list"; + +// The default window traces commands look back over when --since is omitted, +// matching the old CLI's 12h Insights lookback. +export const DEFAULT_TRACES_WINDOW_MS = 12 * 3_600_000; + +export function createRuntimeTracesHandler(core: Core, io: AppIO): Router { + return new Router("traces", "inspect a Runtime's traces") + .handler(createListRuntimeTracesHandler(core, io)) + .handler(createGetRuntimeTraceHandler(core, io)); +} diff --git a/src/handlers/runtime/traces/list/index.tsx b/src/handlers/runtime/traces/list/index.tsx new file mode 100644 index 000000000..36246cea1 --- /dev/null +++ b/src/handlers/runtime/traces/list/index.tsx @@ -0,0 +1,92 @@ +import z from "zod"; +import { parseTimeString } from "../../../../core/observability"; +import type { AppIO } from "../../../../io"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import { JsonKey } from "../../../keys"; +import type { Core } from "../../../types"; +import { runtimeIdSchema } from "../../invoke/request"; +import { resolveRuntimeTarget } from "../../resolveRuntimeTarget"; +import type { TraceSummary } from "../../types"; +import { DEFAULT_TRACES_WINDOW_MS } from "../index"; + +const TRACE_ID_WIDTH = 34; +const TIMESTAMP_WIDTH = 22; + +/** + * Renders a Logs Insights timestamp for the table. Aggregations return epoch + * milliseconds as a string; anything non-numeric passes through untouched. + */ +export function formatTraceTimestamp(timestamp: string): string { + const epochMs = Number(timestamp); + if (isNaN(epochMs)) return timestamp; + return new Date(epochMs) + .toISOString() + .replace("T", " ") + .replace(/\.\d+Z$/, "Z"); +} + +export function formatTraceTable(traces: TraceSummary[]): string { + const lines = [ + `${"TRACE ID".padEnd(TRACE_ID_WIDTH)}${"TIMESTAMP".padEnd(TIMESTAMP_WIDTH)}SESSION ID`, + ]; + for (const trace of traces) { + lines.push( + trace.traceId.padEnd(TRACE_ID_WIDTH) + + formatTraceTimestamp(trace.timestamp).padEnd(TIMESTAMP_WIDTH) + + (trace.sessionId ?? "-"), + ); + } + return lines.join("\n") + "\n"; +} + +export const createListRuntimeTracesHandler = (core: Core, io: AppIO) => + createHandler({ + name: "list", + description: "list a Runtime's recent traces", + flags: [ + flag( + "id", + "the ID of the Runtime (defaults to the project's deployed runtime)", + runtimeIdSchema.optional(), + ), + flag("limit", "maximum number of traces to display", z.number().int().positive().default(20)), + flag( + "since", + 'window start: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default 12h ago)', + z.string().min(1).optional(), + ), + flag( + "until", + 'window end: "5m", "1h", "2d", ISO 8601, epoch ms, or "now" (default now)', + z.string().min(1).optional(), + ), + ], + handle: async (ctx, flags) => { + const startTimeMs = + flags.since !== undefined + ? parseTimeString(flags.since) + : Date.now() - DEFAULT_TRACES_WINDOW_MS; + const endTimeMs = flags.until !== undefined ? parseTimeString(flags.until) : Date.now(); + + const target = await resolveRuntimeTarget(core, ctx, flags.id); + const traces = await core.observability.listRuntimeTraces( + { runtimeId: target.runtimeId, startTimeMs, endTimeMs, limit: flags.limit }, + target.options, + ); + + if (ctx.require(JsonKey)) { + ctx.require(JsonRendererKey).renderJson({ traces }); + return; + } + + if (traces.length === 0) { + io.stderr.write( + "No traces found in the specified time range. Traces take 2-3 minutes " + + "to appear after an invocation.\n", + ); + return; + } + io.stdout.write(formatTraceTable(traces)); + }, + }); diff --git a/src/handlers/runtime/traces/traces.test.tsx b/src/handlers/runtime/traces/traces.test.tsx new file mode 100644 index 000000000..9bbf34d99 --- /dev/null +++ b/src/handlers/runtime/traces/traces.test.tsx @@ -0,0 +1,217 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createSilentLogger, TestCoreClient, testIO } from "../../../testing"; +import { TestGlobalConfigAccessor } from "../../../testing/globalConfig"; +import { createRootHandler } from "../../index"; +import type { GetRuntimeTraceInput, ListRuntimeTracesInput } from "../types"; +import { formatTraceTable, formatTraceTimestamp } from "./list"; +import { resolveTraceOutputPath } from "./get/outputPath"; +import type { Project } from "../../project/types"; + +const REGION = "us-west-2"; +const SINCE_MS = 1_709_391_000_000; +const UNTIL_MS = 1_709_394_600_000; + +function testTracesCommand() { + const core = new TestCoreClient(); + const io = testIO(); + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + return { + core, + io, + route: (args: string[]) => root.route(["node", "agentcore", ...args, "--region", REGION]), + }; +} + +describe("runtime traces list", () => { + test("queries the window and renders a table", async () => { + const { core, io, route } = testTracesCommand(); + core.observability.traceSummaries = [ + { + traceId: "abc123", + timestamp: "1709391000000", + sessionId: "session-1", + spanCount: "7", + }, + { traceId: "def456", timestamp: "not-a-number" }, + ]; + + await route([ + "runtime", + "traces", + "list", + "--id", + "my_agent-AbC123XyZ9", + "--since", + `${SINCE_MS}`, + "--until", + `${UNTIL_MS}`, + "--limit", + "5", + ]); + + expect(core.observability.calls).toHaveLength(1); + const call = core.observability.calls[0]!; + expect(call.method).toBe("listRuntimeTraces"); + expect(call.args[0] as ListRuntimeTracesInput).toEqual({ + runtimeId: "my_agent-AbC123XyZ9", + startTimeMs: SINCE_MS, + endTimeMs: UNTIL_MS, + limit: 5, + }); + + const [header, first, second] = io.stdout().split("\n"); + expect(header).toMatch(/^TRACE ID\s+TIMESTAMP\s+SESSION ID$/); + expect(first).toMatch(/^abc123\s+2024-03-02 14:50:00Z\s+session-1$/); + // A non-numeric timestamp passes through; a missing session renders as "-". + expect(second).toMatch(/^def456\s+not-a-number\s+-$/); + }); + + test("defaults the limit to 20", async () => { + const { core, route } = testTracesCommand(); + + await route(["runtime", "traces", "list", "--id", "rt-1", "--since", `${SINCE_MS}`]); + + expect((core.observability.calls[0]!.args[0] as ListRuntimeTracesInput).limit).toBe(20); + }); + + test("--json renders a single JSON document", async () => { + const { core, io, route } = testTracesCommand(); + core.observability.traceSummaries = [{ traceId: "abc123", timestamp: "1709391000000" }]; + + await route(["runtime", "traces", "list", "--id", "rt-1", "--json"]); + + expect(JSON.parse(io.stdout())).toEqual({ + traces: [{ traceId: "abc123", timestamp: "1709391000000" }], + }); + }); + + test("an empty result prints a friendly notice on stderr", async () => { + const { io, route } = testTracesCommand(); + + await route(["runtime", "traces", "list", "--id", "rt-1"]); + + expect(io.stdout()).toBe(""); + expect(io.stderr()).toContain("No traces found in the specified time range"); + expect(io.stderr()).toContain("2-3 minutes"); + }); +}); + +describe("runtime traces get", () => { + test("downloads the records, writes the JSON file, and prints its path", async () => { + const { core, io, route } = testTracesCommand(); + core.observability.traceRecords = [ + { "@timestamp": "2026-08-30 12:00:00.000", "@message": { body: "hello" } }, + ]; + const output = join(mkdtempSync(join(tmpdir(), "trace-out-")), "nested", "trace.json"); + + await route([ + "runtime", + "traces", + "get", + "abc123def456", + "--id", + "my_agent-AbC123XyZ9", + "--since", + `${SINCE_MS}`, + "--output", + output, + ]); + + const call = core.observability.calls[0]!; + expect(call.method).toBe("getRuntimeTrace"); + expect(call.args[0] as GetRuntimeTraceInput).toMatchObject({ + runtimeId: "my_agent-AbC123XyZ9", + traceId: "abc123def456", + startTimeMs: SINCE_MS, + }); + + expect(io.stdout()).toBe(output); + expect(io.stderr()).toContain("Saved 1 records for trace abc123def456"); + expect(JSON.parse(await readFile(output, "utf8"))).toEqual([ + { "@timestamp": "2026-08-30 12:00:00.000", "@message": { body: "hello" } }, + ]); + }); + + test("--json reports the file path and record count", async () => { + const { core, io, route } = testTracesCommand(); + core.observability.traceRecords = [{ "@message": "a" }, { "@message": "b" }]; + const output = join(mkdtempSync(join(tmpdir(), "trace-out-")), "trace.json"); + + await route([ + "runtime", + "traces", + "get", + "abc123", + "--id", + "rt-1", + "--output", + output, + "--json", + ]); + + expect(JSON.parse(io.stdout())).toEqual({ filePath: output, recordCount: 2 }); + }); + + test("surfaces core errors (e.g. no trace data) unchanged", async () => { + const { core, route } = testTracesCommand(); + core.observability.error = new Error("No trace data found for trace ID: abc123"); + + await expect(route(["runtime", "traces", "get", "abc123", "--id", "rt-1"])).rejects.toThrow( + "No trace data found for trace ID: abc123", + ); + }); +}); + +describe("resolveTraceOutputPath", () => { + const project = { name: "Proj", rootPath: "/work/proj", spec: {} } as unknown as Project; + + test("an explicit --output wins, resolved against the cwd", () => { + expect( + resolveTraceOutputPath({ + output: "out/trace.json", + project, + runtimeId: "rt-1", + traceId: "abc", + cwd: "/work/elsewhere", + }), + ).toBe("/work/elsewhere/out/trace.json"); + }); + + test("inside a project the file lands under agentcore/.cli/traces", () => { + expect( + resolveTraceOutputPath({ project, runtimeId: "my_agent-AbC", traceId: "abc123", cwd: "/x" }), + ).toBe("/work/proj/agentcore/.cli/traces/my_agent-AbC-abc123.json"); + }); + + test("outside a project the file lands in the working directory", () => { + expect(resolveTraceOutputPath({ runtimeId: "rt-1", traceId: "abc123", cwd: "/tmp/x" })).toBe( + "/tmp/x/abc123.json", + ); + }); +}); + +describe("formatTraceTimestamp", () => { + test("renders epoch-ms strings as UTC timestamps and passes other text through", () => { + expect(formatTraceTimestamp("1709391000000")).toBe("2024-03-02 14:50:00Z"); + expect(formatTraceTimestamp("2026-08-30 12:00:00.000")).toBe("2026-08-30 12:00:00.000"); + }); +}); + +describe("formatTraceTable", () => { + test("pads columns and substitutes '-' for a missing session", () => { + const table = formatTraceTable([{ traceId: "abc", timestamp: "xyz" }]); + expect(table).toBe( + "TRACE ID TIMESTAMP SESSION ID\n" + + "abc xyz -\n", + ); + }); +}); diff --git a/src/handlers/runtime/types.tsx b/src/handlers/runtime/types.tsx index 6c8d74cda..45dcb5378 100644 --- a/src/handlers/runtime/types.tsx +++ b/src/handlers/runtime/types.tsx @@ -116,6 +116,41 @@ export type SearchRuntimeLogsInput = { limit?: number; }; +/** One trace aggregated from a runtime's telemetry, newest first. */ +export type TraceSummary = { + traceId: string; + /** Last-seen time as reported by Logs Insights (epoch ms rendered as a string). */ + timestamp: string; + sessionId?: string; + spanCount?: string; +}; + +/** + * One raw log record belonging to a trace. `@message` is the parsed JSON body + * when it parses, otherwise the original string; other Insights fields (e.g. + * `@timestamp`, `@ptr`) pass through as returned. + */ +export type TraceRecord = Record; + +export type ListRuntimeTracesInput = { + runtimeId: string; + /** Window start, epoch milliseconds. */ + startTimeMs: number; + /** Window end, epoch milliseconds. */ + endTimeMs: number; + /** Maximum number of traces to return. */ + limit: number; +}; + +export type GetRuntimeTraceInput = { + runtimeId: string; + traceId: string; + /** Window start, epoch milliseconds. */ + startTimeMs: number; + /** Window end, epoch milliseconds. */ + endTimeMs: number; +}; + export interface CoreObservabilityClient { resolveDeployedRuntime(project: Project, targetName: string): Promise; /** Live-tails the runtime's log group until `signal` aborts. */ @@ -130,4 +165,8 @@ export interface CoreObservabilityClient { options: CoreOptions, signal?: AbortSignal, ): AsyncGenerator; + /** Lists recent traces in the runtime's log group, newest first. */ + listRuntimeTraces(input: ListRuntimeTracesInput, options: CoreOptions): Promise; + /** Downloads every log record of one trace, oldest first. */ + getRuntimeTrace(input: GetRuntimeTraceInput, options: CoreOptions): Promise; } diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 93d0b26d8..a6da20a9e 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -130,11 +130,15 @@ import type { CoreObservabilityClient, CoreRuntimeClient, DeployedRuntime, + GetRuntimeTraceInput, + ListRuntimeTracesInput, RuntimeInvokeRequest, RuntimeInvokeResponse, RuntimeLogEvent, SearchRuntimeLogsInput, StreamRuntimeLogsInput, + TraceRecord, + TraceSummary, } from "../handlers/runtime/types"; import type { BatchEvaluationDetail, @@ -2273,6 +2277,24 @@ export class TestObservabilityClient implements CoreObservabilityClient { if (this.error) throw this.error; yield* this.logEvents; } + + traceSummaries: TraceSummary[] = []; + traceRecords: TraceRecord[] = []; + + async listRuntimeTraces( + input: ListRuntimeTracesInput, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "listRuntimeTraces", args: [input, options] }); + if (this.error) throw this.error; + return this.traceSummaries; + } + + async getRuntimeTrace(input: GetRuntimeTraceInput, options: CoreOptions): Promise { + this.calls.push({ method: "getRuntimeTrace", args: [input, options] }); + if (this.error) throw this.error; + return this.traceRecords; + } } // TestCoreClient implements the Core contract with fully controllable sub-clients. From 547ccd0cc41ad1f89e0850609016c5866e3dbb3e Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Mon, 31 Aug 2026 00:34:54 -0400 Subject: [PATCH 11/12] feat(tui): project create wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bare `agentcore project create` in a terminal now opens an interactive wizard: name (validated live against ProjectNameSchema, schema messages inline) → project type → harness model id or agent template (with the strands memory choice; hello-world skips it) → confirmation summary → live ProjectManager progress → success screen with next steps. The wizard builds the same CreateProjectInput as the flag-driven handler — resolveScaffoldHarnessInput (now exported) for the harness path, resolveRuntimeTemplateShortcut for templates — and iterates core.projectManager.create in the cwd, npm install and git init included. A create() failure renders and tears the TUI down via useApp().exit(error), so the process exits nonzero. Dispatch: only a bare, flagless invocation on a TTY opens the wizard (withTuiOnEmptyFlagsAndArgs, TTY-gated at registration). Any user-supplied flag, --json, or a non-TTY session stays headless. To make the middleware reachable, --name is optional at the Commander layer and enforced inside handle with the same "required option '--name ' not specified" wording — so `project create --defaults` and bare non-TTY invocations fail exactly as before. The other six project subcommands keep their not-implemented stubs. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- README.md | 13 +- src/components/Root.tsx | 20 +- .../project/create/create.screen.test.tsx | 509 ++++++++++++++++ src/handlers/project/create/index.ts | 29 +- src/handlers/project/create/screen.tsx | 571 ++++++++++++++++++ src/handlers/project/index.ts | 36 +- src/handlers/project/project.screen.test.tsx | 5 +- 7 files changed, 1147 insertions(+), 36 deletions(-) create mode 100644 src/handlers/project/create/create.screen.test.tsx create mode 100644 src/handlers/project/create/screen.tsx diff --git a/README.md b/README.md index e17c47c16..e43fe0988 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ It gives you two ways to work, from the same binary: JSON (`--json`), so it can be used by codeing agents and can drop cleanly into scripts, CI, and automation. - **An interactive TUI** — bare Harness, Runtime, Memory, Identity, and Gateway - branches and leaves open their corresponding menus and selection flows. + branches and leaves open their corresponding menus and selection flows, and a + bare `project create` opens a guided create wizard. ```bash agentcore # launch the interactive TUI @@ -27,7 +28,9 @@ responses. `agentcore` wraps all of that behind one ergonomic tool. ## Command surface Commands with operation flags run headlessly. Bare Harness, Runtime, Memory, -Identity, and Gateway branches and leaves open their interactive flows. +Identity, and Gateway branches and leaves open their interactive flows, as does +a bare `project create` in a terminal (any flag, `--json`, or a non-TTY stays +headless). ``` agentcore # interactive TUI @@ -110,7 +113,8 @@ agentcore # interactive TUI │ └── delete # delete an evaluator by id ├── project # manage an AgentCore project (scaffold → deploy) │ ├── create # create a project: a managed harness by default, -│ │ # or scaffolded runtime code via --template/--framework +│ │ # or scaffolded runtime code via --template/--framework; +│ │ # bare `project create` opens an interactive wizard │ ├── add # add a resource to the project (runtime, harness, memory, …) │ ├── export │ │ └── harness # convert a harness into an editable Strands runtime agent @@ -156,6 +160,9 @@ Global flags (declared at the root, available on every command): # tune it. agentcore project create --name MyAssistant cd MyAssistant && agentcore project deploy +# … or run `agentcore project create` bare in a terminal for the guided +# wizard (name → harness or template → confirm), which drives the same +# creation path. agentcore harness invoke --id --prompt "hello" # Scaffold runtime code instead (pass a template or framework flags). diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 76285c548..964690ef6 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -108,22 +108,14 @@ import { GatewayRuleListScreen } from "../handlers/gateway/rule/list/screen.tsx" import { GatewayRuleGetScreen } from "../handlers/gateway/rule/get/screen.tsx"; import { GatewayInvokeScreen } from "../handlers/gateway/invoke/screen.tsx"; import { ProjectScreen, ProjectCommandNotImplementedScreen } from "../handlers/project/screen.tsx"; +import { ProjectCreateScreen } from "../handlers/project/create/screen.tsx"; import { RootScreen, HelpScreen } from "../handlers/screen.tsx"; import type { Context } from "../router"; // PROJECT_COMMANDS are the `agentcore project` subcommands that are listed in -// the menu but have no screen of their own yet. Each is routed explicitly so -// selecting it reports "not implemented" error -const PROJECT_COMMANDS = [ - "create", - "add", - "export", - "remove", - "dev", - "deploy", - "status", - "build", -] as const; +// the menu but have no screen of their own yet (`create` has the wizard). Each +// is routed explicitly so selecting it reports "not implemented" error +const PROJECT_COMMANDS = ["add", "export", "remove", "dev", "deploy", "status", "build"] as const; export interface RootProps { // path is the command path to the executing node (e.g. "/agentcore"). @@ -752,6 +744,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) { element={} /> } /> + } + /> {PROJECT_COMMANDS.map((command) => ( { + const directory = await mkdtemp(join(tmpdir(), "agentcore-create-wizard-")); + tempDirectories.push(directory); + process.chdir(directory); + // cwd is the realpath (macOS tmpdir lives behind a /var -> /private/var + // symlink), matching the paths the manager derives from process.cwd(). + return process.cwd(); +} + +afterEach(async () => { + process.chdir(originalCwd); + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +// spyOnCreate records every CreateProjectInput handed to the manager while +// still running the real FsProjectManager underneath, so a test can assert +// both the exact input and the files it produced. +function spyOnCreate(core: TestCoreClient): CreateProjectInput[] { + const inputs: CreateProjectInput[] = []; + const manager = core.projectManager; + const original = manager.create.bind(manager); + manager.create = (input) => { + inputs.push(input); + return original(input); + }; + return inputs; +} + +const DEFAULT_MODEL_ID = "global.anthropic.claude-sonnet-4-6"; + +describe("project create wizard", () => { + test("harness default flow: name → type → model → review → created", async () => { + const directory = await inTempDirectory(); + const core = new TestCoreClient(); + const inputs = spyOnCreate(core); + const r = renderScreen("/agentcore/project/create", { core }); + + await waitForText(r.lastFrame, "name your project"); + await r.write("DemoApp"); + await r.press("return"); + + // Type step: harness is the preselected default. + await waitForText(r.lastFrame, "what should the project be built around?"); + expect(r.lastFrame()).toContain("● harness (recommended)"); + await r.press("return"); + + // Model step: prefilled with the default harness model. + await waitForText(r.lastFrame, "model id"); + expect(r.lastFrame()).toContain(DEFAULT_MODEL_ID); + await r.press("return"); + + // Review: the summary names the project, type, model, and directory. + await waitForText(r.lastFrame, "this project will be created"); + const review = r.lastFrame()!; + expect(review).toContain("DemoApp"); + expect(review).toContain("harness"); + expect(review).toContain(DEFAULT_MODEL_ID); + expect(review).toContain("./DemoApp"); + await r.press("return"); + + // Success: next steps point at the new directory and deploy. + await waitForText(r.lastFrame, "project created in ./DemoApp", 5000); + expect(r.lastFrame()).toContain("cd DemoApp"); + expect(r.lastFrame()).toContain("agentcore project deploy"); + + // The manager received exactly the input the flag-driven handler builds + // for `project create --name DemoApp`. + expect(inputs).toEqual([ + { + name: "DemoApp", + skipInstall: false, + skipGit: false, + scaffoldHarnessInput: { + name: "DemoApp", + model: { provider: "bedrock", modelId: DEFAULT_MODEL_ID }, + }, + }, + ]); + + // ... and really created the project in the cwd. + const spec = await Bun.file(join(directory, "DemoApp", "agentcore", "agentcore.json")).json(); + expect(spec.harnesses).toEqual([{ name: "DemoApp", path: "app/DemoApp" }]); + expect(spec.runtimes).toEqual([]); + r.unmount(); + }, 10000); + + test("an edited model id flows into the harness input", async () => { + await inTempDirectory(); + const core = new TestCoreClient(); + const inputs = spyOnCreate(core); + const r = renderScreen("/agentcore/project/create", { core }); + + await waitForText(r.lastFrame, "name your project"); + await r.write("TunedApp"); + await r.press("return"); + await waitForText(r.lastFrame, "what should the project be built around?"); + await r.press("return"); + + // The cursor starts at the end of the prefilled id; typing appends. + await waitForText(r.lastFrame, "model id"); + await r.write("-test"); + await r.press("return"); + await waitForText(r.lastFrame, "this project will be created"); + await r.press("return"); + await waitForText(r.lastFrame, "project created in ./TunedApp", 5000); + + expect(inputs[0]).toEqual({ + name: "TunedApp", + skipInstall: false, + skipGit: false, + scaffoldHarnessInput: { + name: "TunedApp", + model: { provider: "bedrock", modelId: `${DEFAULT_MODEL_ID}-test` }, + }, + }); + r.unmount(); + }, 10000); + + test("template flow: strands with the default memory choice", async () => { + const directory = await inTempDirectory(); + const core = new TestCoreClient(); + const inputs = spyOnCreate(core); + const r = renderScreen("/agentcore/project/create", { core }); + + await waitForText(r.lastFrame, "name your project"); + await r.write("StrandsApp"); + await r.press("return"); + + await waitForText(r.lastFrame, "what should the project be built around?"); + await r.press("down"); // scaffolded agent code + await waitForText(r.lastFrame, "● scaffolded agent code"); + await r.press("return"); + + // Template step: the three refactor-supported templates are offered. + await waitForText(r.lastFrame, "choose a template"); + expect(r.lastFrame()).toContain("hello-world-python"); + expect(r.lastFrame()).toContain("hello-world-python-container"); + expect(r.lastFrame()).toContain("● strands-python (recommended)"); + await r.press("return"); + + // Memory step: asked only for strands; long and short-term preselected. + await waitForText(r.lastFrame, "choose a memory configuration"); + expect(r.lastFrame()).toContain("● long and short-term"); + await r.press("return"); + + await waitForText(r.lastFrame, "this project will be created"); + expect(r.lastFrame()).toContain("strands-python"); + expect(r.lastFrame()).toContain("longAndShortTerm"); + await r.press("return"); + await waitForText(r.lastFrame, "project created in ./StrandsApp", 5000); + + // Identical to the flag-driven `--template strands-python` input. + expect(inputs).toEqual([ + { + name: "StrandsApp", + skipInstall: false, + skipGit: false, + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("strands-python"), + }, + ]); + + const spec = await Bun.file( + join(directory, "StrandsApp", "agentcore", "agentcore.json"), + ).json(); + expect(spec.runtimes.map((runtime: { name: string }) => runtime.name)).toEqual([ + "strands_agent", + ]); + expect(spec.memories).toHaveLength(1); + r.unmount(); + }, 10000); + + test("template flow: choosing no memory overrides the strands default", async () => { + await inTempDirectory(); + const core = new TestCoreClient(); + const inputs = spyOnCreate(core); + const r = renderScreen("/agentcore/project/create", { core }); + + await waitForText(r.lastFrame, "name your project"); + await r.write("BareStrands"); + await r.press("return"); + await waitForText(r.lastFrame, "what should the project be built around?"); + await r.press("down"); + await r.press("return"); + await waitForText(r.lastFrame, "choose a template"); + await r.press("return"); // strands-python is preselected + await waitForText(r.lastFrame, "choose a memory configuration"); + await r.press("up"); // short-term + await r.press("up"); // none + await waitForText(r.lastFrame, "● none"); + await r.press("return"); + await waitForText(r.lastFrame, "this project will be created"); + await r.press("return"); + await waitForText(r.lastFrame, "project created in ./BareStrands", 5000); + + expect(inputs[0]).toEqual({ + name: "BareStrands", + skipInstall: false, + skipGit: false, + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("strands-python", { memory: "none" }), + }); + r.unmount(); + }, 10000); + + test("template flow: hello-world skips the memory question", async () => { + await inTempDirectory(); + const core = new TestCoreClient(); + const inputs = spyOnCreate(core); + const r = renderScreen("/agentcore/project/create", { core }); + + await waitForText(r.lastFrame, "name your project"); + await r.write("HelloApp"); + await r.press("return"); + await waitForText(r.lastFrame, "what should the project be built around?"); + await r.press("down"); + await r.press("return"); + await waitForText(r.lastFrame, "choose a template"); + await r.press("up"); // hello-world-python-container + await r.press("up"); // hello-world-python + await waitForText(r.lastFrame, "● hello-world-python "); + await r.press("return"); + + // Straight to review: hello-world does not support memory. + await waitForText(r.lastFrame, "this project will be created"); + expect(r.lastFrame()).not.toContain("memory"); + await r.press("return"); + await waitForText(r.lastFrame, "project created in ./HelloApp", 5000); + + expect(inputs[0]).toEqual({ + name: "HelloApp", + skipInstall: false, + skipGit: false, + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("hello-world-python"), + }); + r.unmount(); + }, 10000); + + test("the name step shows the schema's messages and blocks continuing", async () => { + const r = renderScreen("/agentcore/project/create"); + + await waitForText(r.lastFrame, "name your project"); + // Submitting an empty name surfaces the schema's required message. + await r.press("return"); + await waitForText(r.lastFrame, "Project name is required"); + + // A name that starts with a digit shows the pattern message live. + await r.write("1abc"); + await waitForText(r.lastFrame, "must start with a letter"); + await r.press("return"); + // Still on the name step. + expect(r.lastFrame()).toContain("name your project"); + r.unmount(); + }); + + test("a reserved name is rejected with the schema's message", async () => { + const r = renderScreen("/agentcore/project/create"); + + await waitForText(r.lastFrame, "name your project"); + await r.write("bedrock"); + await waitForText(r.lastFrame, "conflicts with a reserved package dependency"); + await r.press("return"); + expect(r.lastFrame()).toContain("name your project"); + r.unmount(); + }); + + test("esc steps back through the flow and leaves from the first step", async () => { + const r = renderScreen("/agentcore/project/create"); + + await waitForText(r.lastFrame, "name your project"); + await r.write("DemoApp"); + await r.press("return"); + await waitForText(r.lastFrame, "what should the project be built around?"); + await r.press("escape"); + await waitForText(r.lastFrame, "name your project"); + // Esc on the first step lands on the project menu. + await r.press("escape"); + await waitForText(r.lastFrame, "manage an AgentCore project"); + r.unmount(); + }); + + test("selecting create from the project menu opens the wizard", async () => { + const r = renderScreen("/agentcore/project"); + + // `create` is the first menu item, so it is already selected. + await waitForText(r.lastFrame, "❯ create"); + await r.press("return"); + await waitForText(r.lastFrame, "name your project"); + r.unmount(); + }); + + test("an error from create() renders after the streamed progress", async () => { + const core = new TestCoreClient(); + core.projectManager.create = () => { + return (async function* () { + yield { message: "creating project directory" }; + throw new Error("disk full"); + })(); + }; + const r = renderScreen("/agentcore/project/create", { core }); + + await waitForText(r.lastFrame, "name your project"); + await r.write("DemoApp"); + await r.press("return"); + await waitForText(r.lastFrame, "what should the project be built around?"); + await r.press("return"); + await waitForText(r.lastFrame, "model id"); + await r.press("return"); + await waitForText(r.lastFrame, "this project will be created"); + await r.press("return"); + + // The error panel also requests app exit, which may unmount the screen; + // assert on the frame history rather than only the final frame. + await waitFor(() => + r.frames.some( + (frame) => frame.includes("✗ disk full") && frame.includes("creating project directory"), + ), + ); + r.unmount(); + }); + + test("a create() error tears the TUI down nonzero (renderTuiAt rejects)", async () => { + const core = new TestCoreClient(); + const created: CreateProjectInput[] = []; + core.projectManager.create = (input) => { + created.push(input); + return (async function* () { + yield { message: "creating project directory" }; + throw new Error("disk full"); + })(); + }; + const { streams, stdin } = ttyTestIO(); + + // The settlement handler is attached before any input is sent: the app + // exits (rejecting waitUntilExit) while keys are still being paced, and a + // bare rejected promise would trip bun's unhandled-rejection detection. + const caught: Promise = renderTuiAt( + "/agentcore/project/create", + ValueContext.EmptyContext(), + core, + streams.io, + ).then( + () => undefined, + (error: unknown) => error, + ); + + // Walk the shortest path (harness defaults) by raw key writes — frames are + // not observable here (Ink suppresses incremental frames under CI), so the + // pacing is tick-based. Writes are spaced out so consecutive keys cannot + // coalesce into one stdin chunk (Ink parses a merged "\r\r" as text, not as + // return presses); a slow trailing pump re-sends return as a recovery for a + // key that landed before its step's input handler subscribed. + await tick(50); + stdin.write("DemoApp"); + // One return per step: name → type → model → review → submit. + for (let press = 0; press < 4; press++) { + await tick(50); + stdin.write("\r"); + } + await waitFor( + () => { + if (created.length === 0) stdin.write("\r"); + return created.length > 0; + }, + 5000, + 150, + ); + + // exit(error) rejects waitUntilExit, so the error takes the normal CLI + // path and the process exits nonzero. + const error = await caught; + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("disk full"); + }, 10000); +}); + +describe("project create dispatch", () => { + function buildRoot(io: AppIO, core = new TestCoreClient()) { + return createRootHandler(core, { + io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + } + + test("bare create in a TTY session opens the wizard", async () => { + await inTempDirectory(); // hygiene: nothing must be created outside a temp dir + const { streams, stdin } = ttyTestIO(); + const root = buildRoot(streams.io); + + // outcome never rejects, so a mid-pump failure cannot trip bun's + // unhandled-rejection detection before the final assertion. + const outcome = root.route(["node", "agentcore", "project", "create"]).then( + () => ({ ok: true as const }), + (error: unknown) => ({ ok: false as const, error }), + ); + let settled = false; + void outcome.finally(() => { + settled = true; + }); + + // The wizard never finishes on its own; Ctrl+C (re-sent until the app + // reacts, slowly enough that repeats cannot coalesce into one chunk) + // closes it and resolves the route cleanly. The headless branch would + // instead reject with the missing --name usage error. + await waitFor( + () => { + if (!settled) stdin.write("\x03"); + return settled; + }, + 5000, + 150, + ); + expect(await outcome).toEqual({ ok: true }); + expect(streams.stderr()).not.toContain("required option"); + }, 10000); + + test("bare create without a TTY stays headless and reports the missing --name", async () => { + const io = testIO(); + const root = buildRoot(io.io); + + const error: unknown = await root + .route(["node", "agentcore", "project", "create"]) + .then(() => undefined) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(InputValidationError); + expect((error as Error).message).toContain("required option '--name ' not specified"); + }); + + test("any user-supplied flag stays headless even in a TTY", async () => { + const { streams } = ttyTestIO(); + const root = buildRoot(streams.io); + + const error: unknown = await root + .route(["node", "agentcore", "project", "create", "--defaults"]) + .then(() => undefined) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(InputValidationError); + expect((error as Error).message).toContain("required option '--name ' not specified"); + }); + + test("--json stays headless even in a TTY", async () => { + const { streams } = ttyTestIO(); + const root = buildRoot(streams.io); + + const error: unknown = await root + .route(["node", "agentcore", "project", "create", "--json"]) + .then(() => undefined) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(InputValidationError); + expect((error as Error).message).toContain("required option '--name ' not specified"); + }); + + test("flag-driven create still runs headless in a TTY session", async () => { + const directory = await inTempDirectory(); + const { streams } = ttyTestIO(); + const root = buildRoot(streams.io); + + await root.route([ + "node", + "agentcore", + "project", + "create", + "--name", + "FlagApp", + "--skip-install", + "--skip-git", + ]); + + expect(existsSync(join(directory, "FlagApp", "agentcore", "agentcore.json"))).toBe(true); + expect(streams.stderr()).toContain("Created project 'FlagApp'"); + }, 10000); +}); diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 405006ba7..4b874484d 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -68,7 +68,10 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = name: "create", description: "create a new AgentCore project", flags: [ - flag("name", "name of the project to create", ProjectNameSchema), + // Optional at the flag layer (and enforced in handle) so a bare + // interactive `project create` reaches the TUI wizard middleware instead + // of dying on Commander's mandatory-option check. + flag("name", "name of the project to create", ProjectNameSchema.optional()), flag( "defaults", "create a harness project with default settings (this is the default)", @@ -172,6 +175,11 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = flag("skip-git", "skip initializing a git repository", z.boolean().default(false)), ], handle: async (ctx, flags) => { + const name = flags["name"]; + if (name === undefined) { + throw new InputValidationError("required option '--name ' not specified"); + } + const presentRuntimeFlags: string[] = RUNTIME_PATH_FLAGS.filter( (f) => flags[f] !== undefined, ); @@ -231,19 +239,19 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = const createInput: CreateProjectInput = isRuntimePath ? { - name: flags["name"], + name, skipInstall: flags["skip-install"], skipGit: flags["skip-git"], scaffoldRuntimeInput: isImport - ? importScaffoldRuntimeInput(flags["runtime-name"] ?? flags["name"]) - : await resolveScaffoldRuntimeInput(config, flags), + ? importScaffoldRuntimeInput(flags["runtime-name"] ?? name) + : await resolveScaffoldRuntimeInput(config, { ...flags, name }), importBedrockAgent, } : { - name: flags["name"], + name, skipInstall: flags["skip-install"], skipGit: flags["skip-git"], - scaffoldHarnessInput: resolveScaffoldHarnessInput(flags), + scaffoldHarnessInput: resolveScaffoldHarnessInput({ ...flags, name }), }; if (!isRuntimePath && !flags["defaults"] && presentHarnessFlags.length === 0) { @@ -256,8 +264,8 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = config.io.stderr.write(`${event.message}\n`); } - config.io.stderr.write(`Created project '${flags["name"]}' in ./${flags["name"]}\n`); - config.io.stderr.write(`To deploy it: cd ${flags["name"]} && agentcore project deploy\n`); + config.io.stderr.write(`Created project '${name}' in ./${name}\n`); + config.io.stderr.write(`To deploy it: cd ${name} && agentcore project deploy\n`); }, }); @@ -319,8 +327,9 @@ async function resolveScaffoldRuntimeInput( // The harness input validates against the same schema `project add harness` // uses, before any file is written; the manager then scaffolds it through the -// same addResource path. -function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): ScaffoldHarnessInput { +// same addResource path. Exported so the TUI create wizard builds its harness +// input through the exact same translation as the flag-driven path. +export function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): ScaffoldHarnessInput { const additionalParams = parseJsonFlag>( "additional-params", flags["additional-params"], diff --git a/src/handlers/project/create/screen.tsx b/src/handlers/project/create/screen.tsx new file mode 100644 index 000000000..8413ea9ab --- /dev/null +++ b/src/handlers/project/create/screen.tsx @@ -0,0 +1,571 @@ +import { useEffect, useMemo, useState } from "react"; +import { Box, Text, useApp, useInput } from "ink"; +import { useNavigate } from "react-router"; +import { ProjectNameSchema } from "../../../projectSchemas/project"; +import type { ScreenProps } from "../../types"; +import type { CreateProjectInput } from "../types"; +import { DEFAULT_HARNESS_MODEL } from "../add/harness"; +import { + resolveRuntimeTemplateShortcut, + type MemoryShortcutName, + type RuntimeTemplateShortcutName, +} from "../shortcuts"; +import { resolveScaffoldHarnessInput } from "./index"; +import { Layout } from "../../../components/Layout"; +import { FormTextInput } from "../../../components/FormTextInput"; +import { FormRadioGroup, type FormRadioOption } from "../../../components/FormRadioGroup"; +import { KeyValueTable } from "../../../components/KeyValueTable"; +import { Stepper, type Step } from "../../../components/ui/stepper"; +import { Spinner } from "../../../components/ui/spinner"; +import { Divider } from "../../../components/ui/divider"; +import { darkTheme } from "../../../components/ui/_core.js"; + +const theme = darkTheme; + +// ─── form model ─────────────────────────────────────────────────────────────── + +// ProjectKind mirrors the headless dispatch: a project is created around either +// a harness (the default) or scaffolded runtime code. +type ProjectKind = "harness" | "agent"; + +interface CreateProjectFormValues { + name: string; + kind: ProjectKind; + // modelId configures the harness path; everything else uses defaults. + modelId: string; + // template + memory configure the agent path; memory applies to strands only. + template: RuntimeTemplateShortcutName; + memory: MemoryShortcutName; +} + +function emptyCreateProjectForm(): CreateProjectFormValues { + return { + name: "", + kind: "harness", + modelId: DEFAULT_HARNESS_MODEL.modelId, + template: "strands-python", + memory: "longAndShortTerm", + }; +} + +const PROJECT_KIND_OPTIONS: { kind: ProjectKind; label: string; description: string }[] = [ + { + kind: "harness", + label: "harness (recommended)", + description: "a managed agent configured by spec — no agent-loop code to maintain", + }, + { + kind: "agent", + label: "scaffolded agent code", + description: "generate runnable agent code from a template", + }, +]; + +const TEMPLATE_OPTIONS: { + template: RuntimeTemplateShortcutName; + label: string; + description: string; +}[] = [ + { + template: "hello-world-python", + label: "hello-world-python", + description: "minimal Python agent on Bedrock, no framework (CodeZip build)", + }, + { + template: "hello-world-python-container", + label: "hello-world-python-container", + description: "the hello-world agent packaged as a container image", + }, + { + template: "strands-python", + label: "strands-python (recommended)", + description: "Strands agent on Bedrock with memory (CodeZip build)", + }, +]; + +const MEMORY_OPTIONS: { memory: MemoryShortcutName; label: string; description: string }[] = [ + { memory: "none", label: "none", description: "no memory resources" }, + { + memory: "shortTerm", + label: "short-term", + description: "raw session events, 30-day expiry", + }, + { + memory: "longAndShortTerm", + label: "long and short-term", + description: "session events plus long-term memory strategies (recommended)", + }, +]; + +// buildCreateInput translates the form into the same CreateProjectInput the +// flag-driven `project create` builds: the harness path reuses its +// resolveScaffoldHarnessInput translation and the agent path resolves the same +// template shortcuts, so the wizard cannot drift from the headless CLI. +export function buildCreateInput(values: CreateProjectFormValues): CreateProjectInput { + if (values.kind === "harness") { + return { + name: values.name, + skipInstall: false, + skipGit: false, + scaffoldHarnessInput: resolveScaffoldHarnessInput({ + name: values.name, + "model-id": values.modelId, + }), + }; + } + return { + name: values.name, + skipInstall: false, + skipGit: false, + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut( + values.template, + // Memory is a strands question; the hello-world templates keep their own + // (memory-less) defaults, exactly like `--template` without `--memory`. + values.template === "strands-python" ? { memory: values.memory } : undefined, + ), + }; +} + +// summaryOf renders the review table: what will be created, and where. +function summaryOf(values: CreateProjectFormValues): Record { + const base = { project: values.name, directory: `./${values.name}` }; + if (values.kind === "harness") { + return { ...base, type: "harness", model: values.modelId }; + } + const withTemplate = { ...base, type: "agent code", template: values.template }; + return values.template === "strands-python" + ? { ...withTemplate, memory: values.memory } + : withTemplate; +} + +// ─── wizard shell ───────────────────────────────────────────────────────────── + +type WizardPhase = + { kind: "form" } | { kind: "running" } | { kind: "success" } | { kind: "error"; error: Error }; + +// ProjectCreateScreen is the interactive flow behind a bare `agentcore project +// create`: name → type → (model | template [→ memory]) → review, then the +// creation itself, streaming the ProjectManager's progress events. It drives +// core.projectManager.create with the same input the flag-driven handler +// builds, so both entry points scaffold identical projects — in the current +// working directory, npm install and git init included. +export function ProjectCreateScreen({ core }: ScreenProps) { + const navigate = useNavigate(); + const { exit } = useApp(); + + const [values, setValues] = useState(emptyCreateProjectForm); + const [stepIndex, setStepIndex] = useState(0); + const [phase, setPhase] = useState({ kind: "form" }); + const [events, setEvents] = useState([]); + + // The step list is dynamic: the branch chosen on the type step decides + // whether model or template (and, for strands, memory) questions follow. + const steps: Step[] = useMemo(() => { + const branch: Step[] = + values.kind === "harness" + ? [{ key: "model", title: "model" }] + : [ + { key: "template", title: "template" }, + ...(values.template === "strands-python" ? [{ key: "memory", title: "memory" }] : []), + ]; + return [ + { key: "name", title: "name" }, + { key: "type", title: "type" }, + ...branch, + { key: "review", title: "review" }, + ]; + }, [values.kind, values.template]); + + const stepKey = steps[stepIndex]!.key; + const patch = (update: Partial) => + setValues((current) => ({ ...current, ...update })); + + const next = () => setStepIndex((i) => Math.min(steps.length - 1, i + 1)); + const back = () => { + // Esc from the first step leaves the wizard for the project menu, the + // same place RouterScreen's esc goes; deeper steps step backwards. + if (stepIndex === 0) navigate("/agentcore/project"); + else setStepIndex((i) => i - 1); + }; + + const submit = async () => { + let input: CreateProjectInput; + try { + input = buildCreateInput(values); + } catch (error) { + setPhase({ kind: "error", error: toError(error) }); + return; + } + setPhase({ kind: "running" }); + try { + for await (const event of core.projectManager.create(input)) { + setEvents((current) => [...current, event.message]); + } + setPhase({ kind: "success" }); + } catch (error) { + setPhase({ kind: "error", error: toError(error) }); + } + }; + + return ( + + + {phase.kind === "form" && ( + <> + + step.key)} + /> + + + + + )} + {phase.kind !== "form" && ( + + + {phase.kind === "running" && } + {phase.kind === "success" && ( + exit()} /> + )} + {phase.kind === "error" && } + + )} + + + ); +} + +function toError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)); +} + +function hintsFor(stepKey: string, phase: WizardPhase): { key: string; label: string }[] { + if (phase.kind === "running") return [{ key: "ctl+c", label: "quit" }]; + if (phase.kind === "success") return [{ key: "enter", label: "exit" }]; + if (phase.kind === "error") return [{ key: "ctl+c", label: "quit" }]; + const base = [ + { key: "esc", label: "back" }, + { key: "ctl+c", label: "quit" }, + ]; + switch (stepKey) { + case "name": + case "model": + return [{ key: "enter", label: "continue" }, ...base]; + case "type": + case "template": + case "memory": + return [{ key: "↑↓", label: "choose" }, { key: "enter", label: "continue" }, ...base]; + case "review": + return [{ key: "enter", label: "create" }, ...base]; + default: + return base; + } +} + +// ─── steps ──────────────────────────────────────────────────────────────────── + +interface WizardStepProps { + stepKey: string; + values: CreateProjectFormValues; + patch: (update: Partial) => void; + onNext: () => void; + onBack: () => void; + onSubmit: () => void; +} + +function WizardStep({ stepKey, values, patch, onNext, onBack, onSubmit }: WizardStepProps) { + switch (stepKey) { + case "name": + return ( + patch({ name })} + onNext={onNext} + onBack={onBack} + /> + ); + case "type": + return ( + option.kind === values.kind)} + onSelect={(index) => patch({ kind: PROJECT_KIND_OPTIONS[index]!.kind })} + onNext={onNext} + onBack={onBack} + /> + ); + case "model": + return ( + patch({ modelId })} + onNext={onNext} + onBack={onBack} + /> + ); + case "template": + return ( + option.template === values.template, + )} + onSelect={(index) => patch({ template: TEMPLATE_OPTIONS[index]!.template })} + onNext={onNext} + onBack={onBack} + /> + ); + case "memory": + return ( + option.memory === values.memory)} + onSelect={(index) => patch({ memory: MEMORY_OPTIONS[index]!.memory })} + onNext={onNext} + onBack={onBack} + /> + ); + case "review": + return ; + default: + return null; + } +} + +// NameStep validates against ProjectNameSchema — the schema the flag-driven +// path enforces — showing the schema's own messages inline as the user types. +function NameStep({ + value, + onChange, + onNext, + onBack, +}: { + value: string; + onChange: (value: string) => void; + onNext: () => void; + onBack: () => void; +}) { + const [submitted, setSubmitted] = useState(false); + + const validation = ProjectNameSchema.safeParse(value); + const showError = !validation.success && (value !== "" || submitted); + const errorMessage = showError ? validation.error.issues[0]?.message : undefined; + + useInput((_input, key) => { + if (key.escape) { + onBack(); + return; + } + if (key.return) { + if (validation.success) onNext(); + else setSubmitted(true); + } + }); + + return ( + + { + onChange(next); + setSubmitted(false); + }} + /> + {errorMessage && {errorMessage}} + + ); +} + +// RadioStep is a single-choice step: the parent owns the selection, this owns +// the arrow/enter/esc handling around a FormRadioGroup. +function RadioStep({ + name, + helpText, + options, + selectedIndex, + onSelect, + onNext, + onBack, +}: { + name: string; + helpText: string; + options: FormRadioOption[]; + selectedIndex: number; + onSelect: (index: number) => void; + onNext: () => void; + onBack: () => void; +}) { + useInput((_input, key) => { + if (key.escape) { + onBack(); + return; + } + if (key.upArrow) { + onSelect(Math.max(0, selectedIndex - 1)); + return; + } + if (key.downArrow) { + onSelect(Math.min(options.length - 1, selectedIndex + 1)); + return; + } + if (key.return) onNext(); + }); + + return ( + + + + ); +} + +function ModelStep({ + value, + onChange, + onNext, + onBack, +}: { + value: string; + onChange: (value: string) => void; + onNext: () => void; + onBack: () => void; +}) { + const [error, setError] = useState(null); + + useInput((_input, key) => { + if (key.escape) { + onBack(); + return; + } + if (key.return) { + if (value.trim() === "") { + setError("enter a model id"); + return; + } + onNext(); + } + }); + + return ( + + { + onChange(next); + setError(null); + }} + /> + {error && {error}} + + ); +} + +function ReviewStep({ + values, + onSubmit, + onBack, +}: { + values: CreateProjectFormValues; + onSubmit: () => void; + onBack: () => void; +}) { + useInput((_input, key) => { + if (key.escape) { + onBack(); + return; + } + if (key.return) onSubmit(); + }); + + return ( + + this project will be created + + + + + enter scaffolds the project, installs dependencies, and initializes git + + + ); +} + +// ─── result panels ──────────────────────────────────────────────────────────── + +function EventLog({ events }: { events: string[] }) { + return ( + + {events.map((message, index) => ( + + ✓ {message} + + ))} + + ); +} + +function SuccessPanel({ name, onContinue }: { name: string; onContinue: () => void }) { + useInput((_input, key) => { + if (key.return || key.escape) onContinue(); + }); + + return ( + + + ✔ project created in ./{name} + + next steps + {` cd ${name}`} + {" agentcore project deploy"} + enter exits + + ); +} + +// ErrorPanel reports the failure and tears the TUI down through the same +// exit(error) pattern the not-implemented project stubs use: exit(error) +// rejects the waitUntilExit() that renderTuiAt awaits, so the error takes the +// normal CLI path and the process exits nonzero. +function ErrorPanel({ error }: { error: Error }) { + const { exit } = useApp(); + + useEffect(() => { + exit(error); + }, [exit, error]); + + return ✗ {error.message}; +} diff --git a/src/handlers/project/index.ts b/src/handlers/project/index.ts index 22c1d2a6b..094cbbd18 100644 --- a/src/handlers/project/index.ts +++ b/src/handlers/project/index.ts @@ -1,10 +1,10 @@ -import { Router } from "../../router"; +import { Router, type Handler } from "../../router"; import { checkPort, openBrowser, startHttpServer, watchFile, type AppIO } from "../../io"; import { CodeZipDevRunner } from "../../core/dev/codezip"; import { ContainerDevRunner } from "../../core/dev/container"; import { InspectorAssets } from "../../core/dev/inspectorAssets"; import { startOtelCollector } from "../../core/dev/otel/collector"; -import { withProject } from "../../middleware"; +import { withProject, withTuiOnEmptyFlagsAndArgs } from "../../middleware"; import { renderTui } from "../../tui"; import type { Core } from "../types"; import { createCreateProjectHandler } from "./create"; @@ -32,13 +32,31 @@ export function createProjectHandler({ core, io }: ProjectHandlerConfig): Router // and a usage exit code instead of the menu every sibling router opens. project.default(renderTui(core, io)); - project.handler( - createCreateProjectHandler({ - projectManager, - io, - describeBedrockAgent: core.describeBedrockAgent, - }), - ); + // A bare `agentcore project create` in an interactive session opens the TUI + // create wizard; any user-supplied flag or --json keeps the headless handler. + // The TTY gate wraps the middleware (rather than living inside it) so a + // piped/CI invocation also stays headless and reports the missing --name as + // a usage error instead of renderTui's "interactive mode requires a TTY". + const createProject = createCreateProjectHandler({ + projectManager, + io, + describeBedrockAgent: core.describeBedrockAgent, + }); + const createProjectWithWizard = withTuiOnEmptyFlagsAndArgs(core, io)(createProject); + const isInteractive = () => io.stdin.isTTY === true && io.stdout.isTTY === true; + const createProjectDispatch: Handler = { + name: () => createProject.name(), + description: () => createProject.description(), + flags: () => createProject.flags(), + arguments: () => createProject.arguments(), + doesSupportTui: () => createProject.doesSupportTui(), + children: () => createProject.children(), + handle: (ctx, flags, args) => + isInteractive() + ? createProjectWithWizard.handle(ctx, flags, args) + : createProject.handle(ctx, flags, args), + }; + project.handler(createProjectDispatch); project.handler(createAddProjectResourceHandler(config)); project.handler(createExportProjectResourceHandler({ projectManager, core, io })); project.handler( diff --git a/src/handlers/project/project.screen.test.tsx b/src/handlers/project/project.screen.test.tsx index cadb9461f..a73d0b013 100644 --- a/src/handlers/project/project.screen.test.tsx +++ b/src/handlers/project/project.screen.test.tsx @@ -76,8 +76,9 @@ describe("project subcommands without a screen", () => { // Reading the cases off the router also guards Root's hand-written // PROJECT_COMMANDS: an unrouted subcommand hits the catch-all, which resolves // instead of rejecting. Frames can't detect that — the catch-all exits before - // painting, so it and this screen both render empty. - test.each(projectSubcommands())( + // painting, so it and this screen both render empty. `create` is excluded: + // it has a real screen now (the create wizard, see create/screen.tsx). + test.each(projectSubcommands().filter((command) => command !== "create"))( "%s tears down the TUI with NotImplementedError", async (command) => { const { streams } = ttyTestIO(); From 07ffaf8a98271d00d543a9749bed15c99e827152 Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Mon, 31 Aug 2026 01:38:18 -0400 Subject: [PATCH 12/12] fix(ui): strip control bytes from coalesced text-input chunks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ink delivers rapidly typed keystrokes and every terminal paste as one multi-character useInput event whose key.return is false even when the chunk ends in "\r" — so TextInput appended the control byte into the value as an invisible character. In the create wizard's name step that left a visually valid name failing ProjectNameSchema with a message the user couldn't act on ("must ... contain only alphanumeric characters") and enter refusing to advance. Found by driving the wizard end-to-end in a real pty during the final live smoke. A single-line input must never store control bytes: strip C0 controls and DEL from appended text (the official ink-text-input strips \r/\n for the same reason). A stripped chunk's newline is deliberately not a submit — matching readline and browser single-line paste semantics. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XfADH54TZn2SSGUUixft8P --- src/components/ui/text-input/TextInput.tsx | 12 ++++++++++-- .../project/create/create.screen.test.tsx | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/components/ui/text-input/TextInput.tsx b/src/components/ui/text-input/TextInput.tsx index 83a303c5c..f9b1c3a12 100644 --- a/src/components/ui/text-input/TextInput.tsx +++ b/src/components/ui/text-input/TextInput.tsx @@ -135,8 +135,16 @@ const FocusedInput: React.FC = ({ } if (key.ctrl || key.meta || key.escape) return; - onChange(value.slice(0, cursor) + input + value.slice(cursor)); - setRawCursor(cursor + input.length); + // Rapid keystrokes and terminal pastes coalesce into one multi-character + // input event whose key.return is false even when the chunk carries a + // trailing "\r" — so control bytes would land in the value as invisible + // characters. A single-line input must never store them. + // eslint-disable-next-line no-control-regex + const text = input.replace(/[\u0000-\u001F\u007F]/g, ""); + if (text.length === 0) return; + + onChange(value.slice(0, cursor) + text + value.slice(cursor)); + setRawCursor(cursor + text.length); }); return ( diff --git a/src/handlers/project/create/create.screen.test.tsx b/src/handlers/project/create/create.screen.test.tsx index 5c2db2c88..c5f8232f9 100644 --- a/src/handlers/project/create/create.screen.test.tsx +++ b/src/handlers/project/create/create.screen.test.tsx @@ -297,6 +297,25 @@ describe("project create wizard", () => { r.unmount(); }); + test("a pasted chunk with a trailing return keeps the name clean", async () => { + const r = renderScreen("/agentcore/project/create"); + + await waitForText(r.lastFrame, "name your project"); + // A terminal paste (or keystrokes coalesced under load) arrives as one + // stdin chunk whose key.return is false even though it ends in "\r". The + // control byte must be stripped, not stored as an invisible character + // that fails validation with a message the user can't act on. + await r.write("PasteName\r"); + await waitForText(r.lastFrame, "PasteName"); + expect(r.lastFrame()).not.toContain("must start with a letter"); + + // The embedded "\r" is not a submit; a real enter advances with the + // clean value — which it could not do if the control byte had stuck. + await r.press("return"); + await waitForText(r.lastFrame, "what should the project be built around?"); + r.unmount(); + }); + test("esc steps back through the flow and leaves from the first step", async () => { const r = renderScreen("/agentcore/project/create");