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 f09c95470..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 @@ -65,6 +68,10 @@ 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 +│ ├── 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 @@ -104,9 +111,37 @@ 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; +│ │ # 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 +│ ├── 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 └── 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 | @@ -118,6 +153,29 @@ 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 +# … 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). +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 # Create a harness; a default execution role is created for you. agentcore harness create \ @@ -148,6 +206,18 @@ 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 + +# 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 @@ -208,6 +278,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/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/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/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/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/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/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..964690ef6 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -108,13 +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", "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"). @@ -743,6 +744,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) { element={} /> } /> + } + /> {PROJECT_COMMANDS.map((command) => ( >({ 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/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/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/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/index.tsx b/src/core/index.tsx index 87948c358..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, @@ -21,6 +23,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 +45,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 @@ -67,8 +71,10 @@ export class CoreClient implements AwsClients { readonly runtime: RuntimeClient; readonly gateway: GatewayClient; readonly eval: EvalClient; + readonly observability: ObservabilityClient; readonly projectManager: ProjectManager; + readonly describeBedrockAgent: DescribeBedrockAgent; constructor(config: CoreClientConfig) { this.createControlClient = config.createControlClient; @@ -89,10 +95,20 @@ 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, }); + this.describeBedrockAgent = config.describeBedrockAgent ?? describeBedrockAgent; } // control returns the control-plane client for `config`, creating and caching it diff --git a/src/core/observability.test.ts b/src/core/observability.test.ts new file mode 100644 index 000000000..3758af14d --- /dev/null +++ b/src/core/observability.test.ts @@ -0,0 +1,687 @@ +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 { 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", () => { + 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); + }); +}); + +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?", + ); + }); +}); + +// 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 new file mode 100644 index 000000000..81736d6e1 --- /dev/null +++ b/src/core/observability.ts @@ -0,0 +1,568 @@ +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, + GetRuntimeTraceInput, + ListRuntimeTracesInput, + RuntimeLogEvent, + SearchRuntimeLogsInput, + StreamRuntimeLogsInput, + TraceRecord, + TraceSummary, +} 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 +// (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".`, + ); +} + +/** + * 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)); + } + + /** + * 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( + 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/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/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/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.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.test.ts b/src/core/project/manager.test.ts index 89c435159..90f3fe88d 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -2,17 +2,24 @@ 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, type ProjectEvent, - type TeardownConfirmationHandler, } from "../../handlers/project/types"; import { createSilentLogger } from "../../testing"; import type { DeployBackendInput, ProjectBackend } from "./backends/types"; @@ -347,8 +354,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 +369,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 +404,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 +462,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 +505,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", () => { @@ -543,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 fae997d59..82dc14ab1 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -1,17 +1,20 @@ 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, ProjectEvent, ProjectResource, RemoveResourceInput, + RemoveResourceResult, } from "../../handlers/project/types"; import type { Logger } from "../../logging"; import { @@ -26,9 +29,21 @@ 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 { 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"; @@ -38,13 +53,23 @@ import { enclosingProjectRoot, projectSpecPath } from "./fsUtils"; import { AgentCoreCLIError, InputValidationError, + InvalidEnvironmentError, + MalformedServiceResponseError, NotImplementedError, ProjectStateError, + ResourceNotFoundError, } 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 +86,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 +105,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 +123,7 @@ export class FsProjectManager implements ProjectManager { }), }; this.templateRenderer = config.templateRenderer ?? new HandlebarsTemplateRenderer(); + this.resolveAccount = config.resolveAccount ?? resolveAwsAccount; } public async resolve(input: ResolveProjectInput): Promise { @@ -122,10 +155,26 @@ 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); + // 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) { @@ -133,9 +182,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); @@ -386,7 +437,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); @@ -427,22 +481,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); @@ -452,23 +512,296 @@ 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 newProjectSpec = await this.commitSpec(agentCoreSpecPath, newSpec, envFile); + + return { + project: { ...project, spec: newProjectSpec }, + removedEnvKeys, + }; + } + + 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: { ...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; + } + } + + 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 newSpecParseResult = ProjectSpecSchema.safeParse(newSpec); + 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; + }, + }, + ); - if (!newSpecParseResult.success) - throw new InputValidationError(z.prettifyError(newSpecParseResult.error), { - cause: newSpecParseResult.error, + // 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}`); }); - const newProjectSpec = await this.json.write(agentCoreSpecPath, newSpecParseResult.data); + 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 { - ...project, - spec: newProjectSpec, + harnessName, + agentName: targetAgentName, + agentPath: agentDir, + notesPath: join(agentDir, EXPORT_NOTES_FILENAME), + notes: plan.notes, }; } @@ -497,24 +830,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 +873,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) { @@ -610,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/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..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]+/, "") @@ -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") @@ -125,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; }, @@ -150,6 +197,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/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/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/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/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/create.screen.test.tsx b/src/handlers/project/create/create.screen.test.tsx new file mode 100644 index 000000000..c5f8232f9 --- /dev/null +++ b/src/handlers/project/create/create.screen.test.tsx @@ -0,0 +1,528 @@ +import { test, expect, describe, afterEach } 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"; +import { + renderScreen, + waitForText, + cleanupScreens, + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, + ttyTestIO, + tick, + waitFor, +} from "../../../testing"; +import { renderTuiAt } from "../../../tui"; +import { createRootHandler } from "../../index"; +import { ValueContext } from "../../../router"; +import { InputValidationError } from "../../../errors"; +import type { AppIO } from "../../../io"; +import { resolveRuntimeTemplateShortcut } from "../shortcuts"; +import type { CreateProjectInput } from "../types"; + +afterEach(cleanupScreens); + +// The wizard scaffolds into process.cwd() exactly like the flag-driven path, +// so creation tests run inside a temp directory (same pattern as +// project.test.ts / manager.test.ts). +const originalCwd = process.cwd(); +const tempDirectories: string[] = []; + +async function inTempDirectory(): Promise { + 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("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"); + + 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 69441eff5..4b874484d 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -11,22 +11,72 @@ 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"; +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) +// 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", + "type", + "agent-id", + "agent-alias-id", +] 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), + // 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)", + z.boolean().default(false), + ), flag( "template", "a preset of flags for scaffolding the runtime; compatible flags override preset values", @@ -64,6 +114,59 @@ 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", + "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)", @@ -71,19 +174,33 @@ 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; + handle: async (ctx, flags) => { + const name = flags["name"]; + if (name === undefined) { + throw new InputValidationError("required option '--name ' not specified"); + } - 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 +208,174 @@ 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 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, + skipInstall: flags["skip-install"], + skipGit: flags["skip-git"], + scaffoldRuntimeInput: isImport + ? importScaffoldRuntimeInput(flags["runtime-name"] ?? name) + : await resolveScaffoldRuntimeInput(config, { ...flags, name }), + importBedrockAgent, + } + : { + name, + skipInstall: flags["skip-install"], + skipGit: flags["skip-git"], + scaffoldHarnessInput: resolveScaffoldHarnessInput({ ...flags, name }), + }; + + 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(`Created project '${name}' in ./${name}\n`); + config.io.stderr.write(`To deploy it: cd ${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. 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"], + ); + + 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/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/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/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/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..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"; @@ -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; @@ -24,15 +25,40 @@ 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 })); + // 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( withProject({ projectManager: config.projectManager })( createRemoveProjectHandler({ projectManager: config.projectManager, io: config.io }), 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(); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 9c642432d..55081b20d 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -64,14 +64,219 @@ 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("--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 + // 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 +1095,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"]); @@ -918,8 +1124,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/remove/index.test.ts b/src/handlers/project/remove/index.test.ts index e791aa481..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, @@ -43,7 +51,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; @@ -85,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: [ @@ -101,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(); @@ -113,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", @@ -314,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 e40db8e71..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"; @@ -6,7 +8,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"; @@ -58,10 +60,25 @@ 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; + /** 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; + } + ); /** A progress step reported while a long-running project operation runs. */ export type ProjectEvent = { @@ -85,6 +102,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; }; @@ -193,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: @@ -224,6 +274,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 */ @@ -243,6 +300,28 @@ 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; + + /** + * 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; } diff --git a/src/handlers/runtime/index.tsx b/src/handlers/runtime/index.tsx index edcc9e4a7..471af6aba 100644 --- a/src/handlers/runtime/index.tsx +++ b/src/handlers/runtime/index.tsx @@ -7,15 +7,24 @@ import { createRuntimeEndpointHandler } from "./endpoint"; 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 { - 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 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)) + .handler(createInvokeRuntimeHandler(core, io)) + .handler(createRuntimeVersionHandler(core, io)) + .handler(createRuntimeEndpointHandler(core, io)) + .handler(createRuntimeLogsHandler(core, io)) + .handler(createRuntimeTracesHandler(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..e11d67373 100644 --- a/src/handlers/runtime/runtime.test.tsx +++ b/src/handlers/runtime/runtime.test.tsx @@ -77,7 +77,16 @@ describe("runtime command hierarchy", () => { "invoke", "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 a7ff1c38a..45dcb5378 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,92 @@ 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; +}; + +/** 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. */ + 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; + /** 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/handlers/types.tsx b/src/handlers/types.tsx index f129805a8..3d76827b8 100644 --- a/src/handlers/types.tsx +++ b/src/handlers/types.tsx @@ -3,9 +3,10 @@ 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"; export interface Core { harness: CoreHarnessClient; @@ -14,7 +15,10 @@ export interface Core { runtime: CoreRuntimeClient; gateway: CoreGatewayClient; eval: CoreEvalClient; + observability: CoreObservabilityClient; 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/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/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); } } diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 8fcce7a46..a6da20a9e 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -127,9 +127,18 @@ import type { } from "../handlers/identity/types"; import type { CoreMemoryClient } from "../handlers/memory/types"; import type { + CoreObservabilityClient, CoreRuntimeClient, + DeployedRuntime, + GetRuntimeTraceInput, + ListRuntimeTracesInput, RuntimeInvokeRequest, RuntimeInvokeResponse, + RuntimeLogEvent, + SearchRuntimeLogsInput, + StreamRuntimeLogsInput, + TraceRecord, + TraceSummary, } from "../handlers/runtime/types"; import type { BatchEvaluationDetail, @@ -160,11 +169,16 @@ 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"; 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"; @@ -1209,6 +1223,7 @@ type TestCoreClientOptions = { json?: ReadWriteJson; backends?: Partial>; createCloudFormationClient?: CreateCloudFormationClient; + resolveAccount?: (region: string) => Promise; }; export class TestIdentityClient implements CoreIdentityClient { @@ -2222,6 +2237,66 @@ 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; + } + + 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. export class TestCoreClient implements Core { readonly harness = new TestHarnessClient(); @@ -2230,12 +2305,29 @@ 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, ...), // 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(), @@ -2246,6 +2338,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"), }); } } 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, 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"] }