diff --git a/.github/harness/Dockerfile b/.github/harness/Dockerfile new file mode 100644 index 000000000..3deec1a46 --- /dev/null +++ b/.github/harness/Dockerfile @@ -0,0 +1,33 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Install GitHub CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +# Tokens are baked into the image at build time. This image must be treated as a +# secret and stored only in a registry with equivalent access controls. +ARG CLONE_TOKEN +ARG GITHUB_TOKEN + +# Configure git to use clone token for HTTPS clones +RUN git config --global url."https://${CLONE_TOKEN}@github.com/".insteadOf "https://github.com/" + +# Persist gh CLI auth so GITHUB_TOKEN doesn't need to be in the environment +RUN mkdir -p /root/.config/gh \ + && echo "github.com:" > /root/.config/gh/hosts.yml \ + && echo " oauth_token: ${GITHUB_TOKEN}" >> /root/.config/gh/hosts.yml \ + && echo " user: agentcore-cli-automation" >> /root/.config/gh/hosts.yml \ + && echo " git_protocol: https" >> /root/.config/gh/hosts.yml + +WORKDIR /opt/workspace diff --git a/.github/harness/README.md b/.github/harness/README.md new file mode 100644 index 000000000..d9ba15c61 --- /dev/null +++ b/.github/harness/README.md @@ -0,0 +1,39 @@ +# Harness Resources + +Container and scripts for AI-powered automation via +[AgentCore Harness](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html). + +## Structure + +``` +harness/ +├── Dockerfile # Container image for the harness runtime +├── harness_review.py # Invokes the harness to review PRs (SigV4 + event stream) +└── prompts/ + ├── system.md # System prompt (workspace context) + └── review.md # PR review task prompt +``` + +## Current: PR Reviewer + +Reviews pull requests on open/reopen via `.github/workflows/pr-ai-review.yml`. + +### Dual-token setup + +The Dockerfile takes two build args: + +- **`CLONE_TOKEN`** — baked into git config for cloning private repos +- **`GITHUB_TOKEN`** — baked into `gh` CLI auth for posting PR comments + +### Building the container + +```bash +finch build \ + --build-arg CLONE_TOKEN= \ + --build-arg GITHUB_TOKEN= \ + -t pr-reviewer .github/harness/ +``` + +## Future: Tester + +This directory will also house a harness-based test runner. diff --git a/.github/scripts/python/harness_review.py b/.github/harness/harness_review.py similarity index 62% rename from .github/scripts/python/harness_review.py rename to .github/harness/harness_review.py index fbfd0b0f9..2ee174266 100644 --- a/.github/scripts/python/harness_review.py +++ b/.github/harness/harness_review.py @@ -1,7 +1,7 @@ """Invoke Bedrock AgentCore Harness to review a GitHub PR. Reads PR_URL from the environment. Streams harness output to stdout. -Uses raw HTTP with SigV4 signing — no custom service model needed. +Uses the boto3 bedrock-agentcore client's invoke_harness API. """ import json @@ -11,11 +11,6 @@ import uuid import boto3 -from botocore.auth import SigV4Auth -from botocore.awsrequest import AWSRequest -from botocore.eventstream import EventStreamBuffer -from urllib.parse import quote -import urllib3 # ANSI color codes CYAN = "\033[36m" @@ -25,7 +20,7 @@ DIM = "\033[2m" RESET = "\033[0m" -SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..") +SCRIPTS_DIR = os.path.dirname(__file__) def read_prompt(filename): @@ -35,50 +30,37 @@ def read_prompt(filename): return f.read() -def invoke_harness(harness_arn, body, region): - """Send a SigV4-signed request to the harness invoke endpoint. Returns a streaming response. - - InvokeHarness is not in standard boto3, so we call the REST API directly. - boto3 is only used to resolve AWS credentials (from env vars, OIDC, etc.) - and sign the request with SigV4. The response is an AWS binary event stream. - """ - session = boto3.Session(region_name=region) - credentials = session.get_credentials().get_frozen_credentials() - url = f"https://bedrock-agentcore.{region}.amazonaws.com/harnesses/invoke?harnessArn={quote(harness_arn, safe='')}" - request = AWSRequest(method="POST", url=url, data=body, headers={ - "Content-Type": "application/json", - "Accept": "application/vnd.amazon.eventstream", - }) - SigV4Auth(credentials, "bedrock-agentcore", region).add_auth(request) - return urllib3.PoolManager().urlopen( - "POST", url, body=body, - headers=dict(request.headers), - preload_content=False, - timeout=urllib3.Timeout(connect=10, read=600), +def invoke_harness_streaming(harness_arn, session_id, system_prompt, messages, model_id, region): + """Call invoke_harness via boto3 and return the event stream.""" + client = boto3.client("bedrock-agentcore", region_name=region) + response = client.invoke_harness( + harnessArn=harness_arn, + runtimeSessionId=session_id, + systemPrompt=[{"text": system_prompt}], + messages=messages, + model={"bedrockModelConfig": {"modelId": model_id}}, ) - - -def parse_events(http_response): - """Yield (event_type, payload) tuples from the harness binary event stream. - - The response arrives as raw bytes in AWS binary event stream format. - EventStreamBuffer reassembles complete events from the 4KB chunks, - and we decode each event's JSON payload before yielding it. - """ - event_buffer = EventStreamBuffer() - for chunk in http_response.stream(4096): - event_buffer.add_data(chunk) - for event in event_buffer: - if event.headers.get(":message-type") == "exception": - payload = json.loads(event.payload.decode("utf-8")) - print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) - sys.exit(1) - event_type = event.headers.get(":event-type", "") - if event.payload: - yield event_type, json.loads(event.payload.decode("utf-8")) - - -def print_stream(http_response): + return response["stream"] + + +def parse_events(event_stream): + """Yield (event_type, payload) tuples from the boto3 event stream.""" + for event in event_stream: + if "contentBlockStart" in event: + yield "contentBlockStart", event["contentBlockStart"] + elif "contentBlockDelta" in event: + yield "contentBlockDelta", event["contentBlockDelta"] + elif "contentBlockStop" in event: + yield "contentBlockStop", event["contentBlockStop"] + elif "messageStop" in event: + yield "messageStop", event["messageStop"] + elif "internalServerException" in event: + yield "internalServerException", event["internalServerException"] + elif "runtimeClientError" in event: + yield "runtimeClientError", event["runtimeClientError"] + + +def print_stream(event_stream): """Display harness events with GitHub Actions log groups. The harness streams events as the agent works: @@ -112,7 +94,7 @@ def flush_text(): print(f"{DIM}{line}{RESET}", flush=True) text_buffer = "" - for event_type, payload in parse_events(http_response): + for event_type, payload in parse_events(event_stream): if event_type == "contentBlockStart": start = payload.get("start", {}) @@ -171,6 +153,11 @@ def flush_text(): print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) sys.exit(1) + elif event_type == "runtimeClientError": + close_group() + print(f"\n{RED}ERROR: {payload.get('message', payload)}{RESET}", file=sys.stderr) + sys.exit(1) + close_group() total = time.time() - start_time print(f"\n{GREEN}Review complete.{RESET} {DIM}({iteration} tool calls, {int(total)}s total){RESET}") @@ -200,18 +187,14 @@ def flush_text(): SYSTEM_PROMPT = read_prompt("system.md") REVIEW_PROMPT = read_prompt("review.md").format(pr_url=PR_URL) -request_body = json.dumps({ - "runtimeSessionId": SESSION_ID, - "systemPrompt": [{"text": SYSTEM_PROMPT}], - "messages": [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}], - "model": {"bedrockModelConfig": {"modelId": MODEL_ID}}, -}) +messages = [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}] -http_response = invoke_harness(HARNESS_ARN, request_body, REGION) - -if http_response.status != 200: - error = http_response.read().decode("utf-8") - print(f"{RED}ERROR: HTTP {http_response.status}: {error}{RESET}", file=sys.stderr) +try: + event_stream = invoke_harness_streaming( + HARNESS_ARN, SESSION_ID, SYSTEM_PROMPT, messages, MODEL_ID, REGION + ) +except Exception as e: + print(f"{RED}ERROR: Failed to invoke harness: {e}{RESET}", file=sys.stderr) sys.exit(1) -print_stream(http_response) +print_stream(event_stream) diff --git a/.github/scripts/prompts/review.md b/.github/harness/prompts/review.md similarity index 100% rename from .github/scripts/prompts/review.md rename to .github/harness/prompts/review.md diff --git a/.github/scripts/prompts/system.md b/.github/harness/prompts/system.md similarity index 100% rename from .github/scripts/prompts/system.md rename to .github/harness/prompts/system.md diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5dc0f86e6..80b17f987 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -87,7 +87,7 @@ jobs: - run: npm ci - run: npm run build --if-present - name: Run unit tests (shard ${{ matrix.shard }}) - run: npx vitest run --project unit --shard=${{ matrix.shard }} --reporter=blob --reporter=verbose + run: npx vitest run --project unit --shard=${{ matrix.shard }} --reporter=blob --reporter=verbose --coverage - name: Upload blob report if: always() uses: actions/upload-artifact@v7 @@ -144,6 +144,6 @@ jobs: with: json-summary-path: coverage/coverage-summary.json json-final-path: coverage/coverage-final.json - vite-config-path: vitest.unit.config.ts + vite-config-path: vitest.config.ts file-coverage-mode: none coverage-thresholds: '{ "lines": 50, "branches": 50, "functions": 50, "statements": 50 }' diff --git a/.github/workflows/e2e-tests-full.yml b/.github/workflows/e2e-tests-full.yml index 14809a587..e1ff4acab 100644 --- a/.github/workflows/e2e-tests-full.yml +++ b/.github/workflows/e2e-tests-full.yml @@ -27,6 +27,7 @@ jobs: fail-fast: false matrix: cdk-source: [npm, main] + shard: ['1/6', '2/6', '3/6', '4/6', '5/6', '6/6'] steps: - uses: actions/checkout@v6 with: @@ -70,7 +71,7 @@ jobs: CDK_REPO: ${{ secrets.CDK_REPO_NAME }} - name: Install CLI globally run: npm install -g "$(npm pack | tail -1)" - - name: Run E2E tests (${{ matrix.cdk-source }}) + - name: Run E2E tests (${{ matrix.cdk-source }}, shard ${{ matrix.shard }}) env: AWS_ACCOUNT_ID: ${{ steps.aws.outputs.account_id }} AWS_REGION: ${{ inputs.aws_region || 'us-east-1' }} @@ -78,7 +79,7 @@ jobs: OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }} GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }} CDK_TARBALL: ${{ env.CDK_TARBALL }} - run: npm run test:e2e + run: npx vitest run --project e2e --shard=${{ matrix.shard }} browser-tests: runs-on: ubuntu-latest environment: e2e-testing diff --git a/.github/workflows/pr-ai-review.yml b/.github/workflows/pr-ai-review.yml index 8ababddcc..26878d7a1 100644 --- a/.github/workflows/pr-ai-review.yml +++ b/.github/workflows/pr-ai-review.yml @@ -139,7 +139,7 @@ jobs: env: PR_URL: ${{ steps.pr-url.outputs.url }} HARNESS_ARN: ${{ secrets.HARNESS_ARN }} - run: python .github/scripts/python/harness_review.py + run: python .github/harness/harness_review.py - name: Remove agentcore-harness-reviewing label if: always() diff --git a/.github/workflows/release-main-and-preview.yml b/.github/workflows/release-main-and-preview.yml new file mode 100644 index 000000000..b3cfb5ee2 --- /dev/null +++ b/.github/workflows/release-main-and-preview.yml @@ -0,0 +1,496 @@ +name: Release Both (Main + Preview) + +on: + workflow_dispatch: + inputs: + main_bump_type: + description: 'Main branch version bump' + required: true + type: choice + options: + - patch + - minor + - major + preview_bump_type: + description: 'Preview branch version bump (prerelease with preview tag)' + required: true + type: choice + options: + - prerelease + main_changelog: + description: 'Main changelog entry (optional)' + required: false + type: string + preview_changelog: + description: 'Preview changelog entry (optional)' + required: false + type: string + dry_run: + description: 'Dry run — create PRs but skip npm publish' + required: false + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +jobs: + # ═══════════════════════════════════════════════════════════════════ + # Preflight — verify preview contains all of main + # ═══════════════════════════════════════════════════════════════════ + preflight: + name: Preflight Checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Verify running from main + run: | + if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then + echo "❌ This workflow must be run from the main branch." + exit 1 + fi + + - name: Verify preview contains all of main + run: | + git fetch origin preview + MAIN_SHA=$(git rev-parse HEAD) + MERGE_BASE=$(git merge-base HEAD origin/preview) + + if [[ "$MAIN_SHA" != "$MERGE_BASE" ]]; then + echo "❌ preview branch does not contain all of main." + echo "" + echo "Main HEAD: $MAIN_SHA" + echo "Merge base: $MERGE_BASE" + echo "" + echo "The sync-preview workflow should have merged automatically." + echo "If it failed due to conflicts, resolve manually:" + echo " git checkout preview && git merge main && git push origin preview" + echo "" + echo "Then re-run this workflow." + exit 1 + fi + + echo "✅ preview contains all of main" + + # ═══════════════════════════════════════════════════════════════════ + # Step 1 — Prepare main release (bump, PR) + # ═══════════════════════════════════════════════════════════════════ + prepare-main: + name: Prepare Main Release + needs: preflight + runs-on: ubuntu-latest + outputs: + version: ${{ steps.bump.outputs.version }} + branch: ${{ steps.bump.outputs.branch }} + + steps: + - name: Checkout main + uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 20.x + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - run: npm ci + + - name: Bump version + id: bump + env: + BUMP_TYPE: ${{ github.event.inputs.main_bump_type }} + CHANGELOG_INPUT: ${{ github.event.inputs.main_changelog }} + run: | + BUMP_CMD="npx tsx scripts/bump-version.ts $BUMP_TYPE" + if [ -n "$CHANGELOG_INPUT" ]; then + BUMP_CMD="$BUMP_CMD --changelog \"$CHANGELOG_INPUT\"" + fi + eval $BUMP_CMD + + NEW_VERSION=$(node -p "require('./package.json').version") + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "branch=release/v$NEW_VERSION" >> $GITHUB_OUTPUT + echo "📦 Main version: $NEW_VERSION" + + - name: Regenerate JSON schema + run: | + npm run build + node scripts/generate-schema.mjs + npx prettier --write schemas/ + + - name: Update snapshots + run: npm run test:update-snapshots + + - name: Create release branch and PR + env: + GH_TOKEN: ${{ github.token }} + NEW_VERSION: ${{ steps.bump.outputs.version }} + run: | + BRANCH_NAME="release/v$NEW_VERSION" + git ls-remote --exit-code --heads origin $BRANCH_NAME && git push origin --delete $BRANCH_NAME || true + git show-ref --verify --quiet refs/heads/$BRANCH_NAME && git branch -D $BRANCH_NAME || true + + git checkout -b $BRANCH_NAME + git add -A + git commit -m "chore: bump version to $NEW_VERSION" + git push origin $BRANCH_NAME + + gh pr create \ + --base main \ + --head "$BRANCH_NAME" \ + --title "Release v$NEW_VERSION" \ + --body "## Release v$NEW_VERSION (main) + + Part of a coordinated main + preview release. + + ### Checklist + - [ ] Review CHANGELOG.md + - [ ] All CI checks passing + - [ ] Merge this PR before approving the publish step" + + # ═══════════════════════════════════════════════════════════════════ + # Step 2 — Prepare preview release (bump, PR) + # ═══════════════════════════════════════════════════════════════════ + prepare-preview: + name: Prepare Preview Release + needs: preflight + runs-on: ubuntu-latest + outputs: + version: ${{ steps.bump.outputs.version }} + branch: ${{ steps.bump.outputs.branch }} + + steps: + - name: Checkout preview + uses: actions/checkout@v6 + with: + ref: preview + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 20.x + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - run: npm ci + + - name: Bump version + id: bump + env: + CHANGELOG_INPUT: ${{ github.event.inputs.preview_changelog }} + run: | + BUMP_CMD="npx tsx scripts/bump-version.ts prerelease --prerelease-tag preview" + if [ -n "$CHANGELOG_INPUT" ]; then + BUMP_CMD="$BUMP_CMD --changelog \"$CHANGELOG_INPUT\"" + fi + eval $BUMP_CMD + + NEW_VERSION=$(node -p "require('./package.json').version") + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "branch=release/v$NEW_VERSION" >> $GITHUB_OUTPUT + echo "📦 Preview version: $NEW_VERSION" + + - name: Regenerate JSON schema + run: | + npm run build + node scripts/generate-schema.mjs + npx prettier --write schemas/ + + - name: Update snapshots + run: npm run test:update-snapshots + + - name: Create release branch and PR + env: + GH_TOKEN: ${{ github.token }} + NEW_VERSION: ${{ steps.bump.outputs.version }} + run: | + BRANCH_NAME="release/v$NEW_VERSION" + git ls-remote --exit-code --heads origin $BRANCH_NAME && git push origin --delete $BRANCH_NAME || true + git show-ref --verify --quiet refs/heads/$BRANCH_NAME && git branch -D $BRANCH_NAME || true + + git checkout -b $BRANCH_NAME + git add -A + git commit -m "chore: bump version to $NEW_VERSION" + git push origin $BRANCH_NAME + + gh pr create \ + --base preview \ + --head "$BRANCH_NAME" \ + --title "Release v$NEW_VERSION (preview)" \ + --body "## Release v$NEW_VERSION (preview) + + Part of a coordinated main + preview release. + + ### Checklist + - [ ] Review CHANGELOG.md + - [ ] All CI checks passing + - [ ] Merge this PR before approving the publish step" + + # ═══════════════════════════════════════════════════════════════════ + # Step 3 — Build and test both + # ═══════════════════════════════════════════════════════════════════ + test-main: + name: Test Main + needs: prepare-main + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: release/v${{ needs.prepare-main.outputs.version }} + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + - run: curl -LsSf https://astral.sh/uv/install.sh | sh + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm run build + - run: npm run test:unit + + test-preview: + name: Test Preview + needs: prepare-preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: release/v${{ needs.prepare-preview.outputs.version }} + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + - run: curl -LsSf https://astral.sh/uv/install.sh | sh + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm run build + - run: npm run test:unit + + # ═══════════════════════════════════════════════════════════════════ + # Step 4 — Manual approval gate + # ═══════════════════════════════════════════════════════════════════ + release-approval: + name: Release Approval (Both) + needs: [test-main, test-preview, prepare-main, prepare-preview] + runs-on: ubuntu-latest + environment: + name: npm-publish-approval + steps: + - name: Approval checkpoint + env: + MAIN_VERSION: ${{ needs.prepare-main.outputs.version }} + PREVIEW_VERSION: ${{ needs.prepare-preview.outputs.version }} + run: | + echo "✅ Both builds and tests passed" + echo "" + echo "📦 Main version: $MAIN_VERSION (npm tag: latest)" + echo "📦 Preview version: $PREVIEW_VERSION (npm tag: preview)" + echo "" + echo "⚠️ MANUAL APPROVAL REQUIRED" + echo "" + echo "Before approving:" + echo "1. Merge the main release PR (release/v$MAIN_VERSION → main)" + echo "2. Merge the preview release PR (release/v$PREVIEW_VERSION → preview)" + echo "3. Verify both PRs are merged" + + # ═══════════════════════════════════════════════════════════════════ + # Step 5 — Verify both PRs merged before any publish + # ═══════════════════════════════════════════════════════════════════ + verify-merges: + name: Verify Both PRs Merged + needs: [prepare-main, prepare-preview, release-approval] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Verify main version + env: + EXPECTED: ${{ needs.prepare-main.outputs.version }} + run: | + git fetch origin main + ACTUAL=$(git show origin/main:package.json | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version") + if [ "$ACTUAL" != "$EXPECTED" ]; then + echo "❌ Main release PR not merged yet!" + echo "Expected: $EXPECTED, Got: $ACTUAL" + exit 1 + fi + echo "✅ Main version verified: $ACTUAL" + + - name: Verify preview version + env: + EXPECTED: ${{ needs.prepare-preview.outputs.version }} + run: | + git fetch origin preview + ACTUAL=$(git show origin/preview:package.json | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version") + if [ "$ACTUAL" != "$EXPECTED" ]; then + echo "❌ Preview release PR not merged yet!" + echo "Expected: $EXPECTED, Got: $ACTUAL" + exit 1 + fi + echo "✅ Preview version verified: $ACTUAL" + + # ═══════════════════════════════════════════════════════════════════ + # Step 6a — Publish main to npm (tag: latest) + # ═══════════════════════════════════════════════════════════════════ + publish-main: + name: Publish Main (@latest) + needs: [prepare-main, verify-merges] + runs-on: ubuntu-latest + environment: + name: npm-publish + url: https://www.npmjs.com/package/@aws/agentcore + permissions: + id-token: write + contents: write + + steps: + - name: Checkout main + uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 22.x + registry-url: 'https://registry.npmjs.org' + + - run: npm install -g npm@11.5.1 + - run: npm ci + - run: npm run build + + - name: Publish to npm + run: npm publish --access public --provenance --tag latest + + - name: Tag and release + env: + VERSION: ${{ needs.prepare-main.outputs.version }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v$VERSION" -m "Release v$VERSION" + git push origin "v$VERSION" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.prepare-main.outputs.version }} + name: AgentCore CLI v${{ needs.prepare-main.outputs.version }} + generate_release_notes: true + prerelease: false + body: | + ## Installation + + ```bash + npm install -g @aws/agentcore@${{ needs.prepare-main.outputs.version }} + ``` + + # ═══════════════════════════════════════════════════════════════════ + # Step 6b — Publish preview to npm (tag: preview) + # ═══════════════════════════════════════════════════════════════════ + publish-preview: + name: Publish Preview (@preview) + needs: [prepare-preview, verify-merges] + runs-on: ubuntu-latest + environment: + name: npm-publish + url: https://www.npmjs.com/package/@aws/agentcore + permissions: + id-token: write + contents: write + + steps: + - name: Checkout preview + uses: actions/checkout@v6 + with: + ref: preview + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 22.x + registry-url: 'https://registry.npmjs.org' + + - run: npm install -g npm@11.5.1 + - run: npm ci + - run: npm run build + + - name: Publish to npm + run: npm publish --access public --provenance --tag preview + + - name: Tag and release + env: + VERSION: ${{ needs.prepare-preview.outputs.version }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v$VERSION" -m "Release v$VERSION (preview)" + git push origin "v$VERSION" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.prepare-preview.outputs.version }} + name: AgentCore CLI v${{ needs.prepare-preview.outputs.version }} (Preview) + generate_release_notes: true + prerelease: true + body: | + ## Installation (Preview) + + ```bash + npm install -g @aws/agentcore@preview + ``` + + # ═══════════════════════════════════════════════════════════════════ + # Summary + # ═══════════════════════════════════════════════════════════════════ + summary: + name: Release Summary + needs: [prepare-main, prepare-preview, publish-main, publish-preview] + if: always() + runs-on: ubuntu-latest + steps: + - name: Summary + env: + MAIN_VERSION: ${{ needs.prepare-main.outputs.version }} + PREVIEW_VERSION: ${{ needs.prepare-preview.outputs.version }} + MAIN_STATUS: ${{ needs.publish-main.result }} + PREVIEW_STATUS: ${{ needs.publish-preview.result }} + run: | + echo "## Release Summary" + echo "" + echo "| Package | Version | npm Tag | Status |" + echo "|---------|---------|---------|--------|" + echo "| @aws/agentcore | $MAIN_VERSION | latest | $MAIN_STATUS |" + echo "| @aws/agentcore | $PREVIEW_VERSION | preview | $PREVIEW_STATUS |" diff --git a/.github/workflows/sync-preview.yml b/.github/workflows/sync-preview.yml new file mode 100644 index 000000000..61abcbc86 --- /dev/null +++ b/.github/workflows/sync-preview.yml @@ -0,0 +1,125 @@ +name: Sync Preview with Main + +on: + push: + branches: [main] + +concurrency: + group: sync-preview + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + name: Merge main into preview + runs-on: ubuntu-latest + steps: + - name: Checkout preview + uses: actions/checkout@v6 + with: + ref: preview + fetch-depth: 0 + + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Merge main into preview + id: merge + run: | + git fetch origin main + MAIN_SHA=$(git rev-parse origin/main) + MERGE_BASE=$(git merge-base HEAD origin/main) + + if [[ "$MAIN_SHA" == "$MERGE_BASE" ]]; then + echo "✅ preview already contains all of main" + echo "status=up-to-date" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "ℹ️ Merging main into preview..." + + if git merge origin/main --no-edit -m "chore: merge main into preview"; then + git push origin preview + echo "✅ main merged into preview and pushed" + echo "status=merged" >> $GITHUB_OUTPUT + else + git merge --abort + echo "status=conflict" >> $GITHUB_OUTPUT + fi + + - name: Get original commit author + if: steps.merge.outputs.status == 'conflict' + id: author + run: | + AUTHOR=$(git log origin/main -1 --format='%an') + GH_USER=$(git log origin/main -1 --format='%ae' | grep -oP '.*(?=@users\.noreply\.github\.com)' || echo "") + if [[ -z "$GH_USER" ]]; then + # Try to get GitHub username from the commit + GH_USER=$(gh api "/repos/${{ github.repository }}/commits/$(git rev-parse origin/main)" --jq '.author.login // empty' 2>/dev/null || echo "") + fi + echo "name=$AUTHOR" >> $GITHUB_OUTPUT + echo "gh_user=$GH_USER" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ github.token }} + + - name: Create PR for conflict resolution + if: steps.merge.outputs.status == 'conflict' + env: + GH_TOKEN: ${{ github.token }} + AUTHOR_NAME: ${{ steps.author.outputs.name }} + AUTHOR_GH: ${{ steps.author.outputs.gh_user }} + run: | + BRANCH="sync-preview/merge-main-$(date +%Y%m%d-%H%M%S)" + + # Check if there's already an open sync PR + EXISTING=$(gh pr list --base preview --search "sync-preview: merge main into preview" --state open --json number --jq 'length') + if [[ "$EXISTING" != "0" ]]; then + echo "ℹ️ Sync PR already open — skipping duplicate." + exit 0 + fi + + # Create a branch from preview with the conflict markers + git checkout -b "$BRANCH" + git merge origin/main --no-edit -m "chore: merge main into preview" || true + git add -A + git commit --no-edit -m "chore: merge main into preview (conflicts need resolution)" || true + git push origin "$BRANCH" + + # Build mention string + MENTION="" + if [[ -n "$AUTHOR_GH" ]]; then + MENTION="cc @${AUTHOR_GH}" + fi + + gh pr create \ + --base preview \ + --head "$BRANCH" \ + --title "sync-preview: merge main into preview" \ + --body "$(cat < + \`\`\` + 2. Search for conflict markers and resolve them: + \`\`\`bash + grep -rn '<<<<<<< HEAD' . + \`\`\` + 3. Keep preview-specific values (package version, preview tests, etc.) — accept main's changes for everything else. + 4. Commit and push, then merge this PR. + + This must be resolved before the next coordinated release. + + ${MENTION} + + _Opened automatically by the sync-preview workflow._ + BODY + )" diff --git a/.prettierignore b/.prettierignore index 8eda17e39..3b1452b18 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ CHANGELOG.md src/assets/**/*.md -.github/scripts/prompts/ +.github/harness/prompts/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f434ffef..b25ef328d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ All notable changes to this project will be documented in this file. +## [0.12.2] - 2026-04-30 + +### Added +- feat: add telemetry audit mode with FileSystemSink (#1014) (397c187) + +### Fixed +- fix: add Accept header to HTTP protocol invocation proxy (#1051) (821e4c3) + +### Other Changes +- fix(harness): add error handling for invoke_harness API call (#1056) (9a6a5d0) +- Merge pull request #1054 from aws/fix/remove-coauthor-reland (0afeaf5) +- refactor: move harness resources to .github/harness/ and use boto3 invoke_harness (ad2ba9b) +- Revert "refactor: move harness resources to .github/harness/ (#992)" (b8a90c9) +- refactor: move harness resources to .github/harness/ (#992) (aef3890) + +## [0.12.1] - 2026-04-29 + +### Added +- feat: add CloudWatch traces API for web UI (#997) (76b07aa) + +### Fixed +- fix: remove CONFIG_DIR exclusion from zip stage to preserve dependency agentcore/ packages (#1015) (d1e5241) + +### Other Changes +- fix(ci): install uv in release workflow prepare steps (#1038) (29ae8e5) +- fix(ci): move snapshot update after build in release workflow (#1036) (227c840) +- fix(ci): enable coverage collection in sharded unit test runs (#1034) (061b6b3) +- fix(ci): update snapshots after CDK version sync in release workflow (#1033) (d3b412f) +- chore(deps): bump @opentelemetry/sdk-metrics from 2.6.1 to 2.7.0 (#1030) (ad59fc0) +- chore(deps-dev): bump secretlint from 12.2.0 to 12.3.1 (#1029) (36755e9) +- chore(deps-dev): bump @secretlint/secretlint-rule-preset-recommend (#1028) (56a6d4c) +- chore(deps): bump @opentelemetry/resources from 2.6.1 to 2.7.0 (#1026) (ad482cf) +- chore(deps): bump the aws-cdk group with 2 updates (#1025) (1686e4d) +- chore(deps): bump the aws-sdk group with 14 updates (#1024) (1fc366c) +- ci: add coordinated main + preview release workflow (#995) (7e8cae4) +- fix(import): use GatewayNameSchema for gateway import name validation (#1011) (29b6522) +- test: remove 44 render-only and framework-testing tests (#998) (13b34a3) + ## [0.12.0] - 2026-04-28 ### Added diff --git a/browser-tests/tests/traces.test.ts b/browser-tests/tests/traces.test.ts index 77c1b5ad4..0ac481f04 100644 --- a/browser-tests/tests/traces.test.ts +++ b/browser-tests/tests/traces.test.ts @@ -1,22 +1,38 @@ import { expect, sendMessage, test } from '../fixtures'; test.describe('Traces', () => { - test('traces panel shows trace after invocation', async ({ page }) => { + test('traces panel shows span tree after invocation', async ({ page }) => { await page.goto('/'); await sendMessage(page, 'Say hello'); - await page.getByRole('tab', { name: 'Traces' }).click(); + const resourcePanel = page.getByTestId('resource-panel'); + await expect(resourcePanel).toBeVisible({ timeout: 10_000 }); - const traceList = page.getByTestId('trace-list'); + const tracesTab = resourcePanel.getByRole('tab', { name: 'Traces' }); + await tracesTab.click(); + + // Wait for trace list to populate + const traceList = resourcePanel.getByTestId('traces-trace-list'); await expect(traceList).toBeVisible({ timeout: 30_000 }); + // Click the first trace const traceButton = traceList.getByRole('button').first(); - await expect(traceButton).toBeVisible({ timeout: 30_000 }); - + await expect(traceButton).toBeVisible({ timeout: 10_000 }); await traceButton.click(); - const spanRow = page.locator('[role="button"]').filter({ hasText: /.+/ }); - await expect(spanRow.first()).toBeVisible({ timeout: 10_000 }); + // Verify span tree renders + const spanTree = resourcePanel.getByTestId('traces-span-tree'); + await expect(spanTree).toBeVisible({ timeout: 10_000 }); + + // Verify tree has at least one span row with a name + const spanRows = spanTree.getByRole('button'); + await expect(spanRows.first()).toBeVisible(); + + // Click a span to open log panel + await spanRows.first().click(); + + const logPanel = resourcePanel.getByTestId('traces-log-panel'); + await expect(logPanel).toBeVisible({ timeout: 5_000 }); }); }); diff --git a/docs/TESTING.md b/docs/TESTING.md index 700ab3aae..9c70af6b3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -415,6 +415,24 @@ Test configuration is in `vitest.config.ts` using Vitest projects: - Test timeout: 120 seconds - Hook timeout: 120 seconds +## Troubleshooting + +### `Cannot find module '@playwright/test'` + +Playwright is not installed. Run: + +```bash +npm install +``` + +### `browserType.launch: Executable doesn't exist` (Playwright browsers) + +Playwright browsers need to be downloaded after install. Run: + +```bash +npx playwright install chromium +``` + ## Integration Tests Integration tests require: diff --git a/e2e-tests/byo-custom-jwt.test.ts b/e2e-tests/byo-custom-jwt.test.ts index b7391a522..64e534e20 100644 --- a/e2e-tests/byo-custom-jwt.test.ts +++ b/e2e-tests/byo-custom-jwt.test.ts @@ -48,7 +48,7 @@ const region = process.env.AWS_REGION ?? 'us-east-1'; * Run the local CLI build without skipping install (needed for deploy). */ function runLocalCLI(args: string[], cwd: string): Promise { - return runCLI(args, cwd, /* skipInstall */ false); + return runCLI(args, cwd, { skipInstall: false }); } describe.sequential('e2e: BYO agent with CUSTOM_JWT auth', () => { diff --git a/e2e-tests/fixtures/import/cleanup_resources.py b/e2e-tests/fixtures/import/cleanup_resources.py index 0728b711e..429a36a1f 100644 --- a/e2e-tests/fixtures/import/cleanup_resources.py +++ b/e2e-tests/fixtures/import/cleanup_resources.py @@ -12,7 +12,7 @@ import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from common import REGION, RESOURCES_FILE, get_control_client, get_account_id +from common import REGION, RESOURCE_SUFFIX, RESOURCES_FILE, get_control_client, get_account_id import boto3 @@ -22,8 +22,12 @@ def cleanup_s3_code_objects(): account_id = get_account_id() bucket_name = f"bugbash-agentcore-code-{account_id}-{REGION}" s3 = boto3.client("s3", region_name=REGION) + prefix = f"bugbash-{RESOURCE_SUFFIX}/" if RESOURCE_SUFFIX else "" try: - resp = s3.list_objects_v2(Bucket=bucket_name) + list_args = {"Bucket": bucket_name} + if prefix: + list_args["Prefix"] = prefix + resp = s3.list_objects_v2(**list_args) objects = resp.get("Contents", []) if not objects: return @@ -31,7 +35,7 @@ def cleanup_s3_code_objects(): Bucket=bucket_name, Delete={"Objects": [{"Key": o["Key"]} for o in objects]}, ) - print(f"Deleted {len(objects)} object(s) from s3://{bucket_name}") + print(f"Deleted {len(objects)} object(s) from s3://{bucket_name}/{prefix}") except Exception as e: print(f"Could not clean up S3 objects: {e}") diff --git a/e2e-tests/fixtures/import/common.py b/e2e-tests/fixtures/import/common.py index 369ec0bb0..c786c0c01 100644 --- a/e2e-tests/fixtures/import/common.py +++ b/e2e-tests/fixtures/import/common.py @@ -8,9 +8,11 @@ import boto3 REGION = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "us-east-1" +RESOURCE_SUFFIX = os.environ.get("RESOURCE_SUFFIX", "") SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) APP_DIR = os.path.join(SCRIPT_DIR, "app") -RESOURCES_FILE = os.path.join(SCRIPT_DIR, "bugbash-resources.json") +_resources_name = f"bugbash-resources-{RESOURCE_SUFFIX}.json" if RESOURCE_SUFFIX else "bugbash-resources.json" +RESOURCES_FILE = os.path.join(SCRIPT_DIR, _resources_name) INLINE_POLICY_NAME = "bugbash-agentcore-permissions" @@ -35,6 +37,8 @@ def upload_code(prefix="bugbash"): """Zip APP_DIR and upload to S3. Returns (bucket, s3_key).""" bucket_name = get_code_bucket() s3 = boto3.client("s3", region_name=REGION) + if RESOURCE_SUFFIX: + prefix = f"{prefix}-{RESOURCE_SUFFIX}" # Create zip of app directory with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp: diff --git a/e2e-tests/import-gateway.test.ts b/e2e-tests/import-gateway.test.ts index 2aea04f02..fd80ae967 100644 --- a/e2e-tests/import-gateway.test.ts +++ b/e2e-tests/import-gateway.test.ts @@ -43,6 +43,7 @@ describe.sequential('e2e: import gateway', () => { const result = await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', 'setup_gateway.py'], fixtureDir, { AWS_REGION: region, + RESOURCE_SUFFIX: suffix, }); if (result.exitCode !== 0) { throw new Error( @@ -50,7 +51,7 @@ describe.sequential('e2e: import gateway', () => { ); } - const resourcesPath = join(fixtureDir, 'bugbash-resources.json'); + const resourcesPath = join(fixtureDir, `bugbash-resources-${suffix}.json`); const resources = JSON.parse(await readFile(resourcesPath, 'utf-8')) as Record; gatewayArn = resources.gateway!.arn; @@ -80,6 +81,7 @@ describe.sequential('e2e: import gateway', () => { try { await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', 'cleanup_resources.py'], fixtureDir, { AWS_REGION: region, + RESOURCE_SUFFIX: suffix, }); } catch { /* ignore — resources may already be deleted by CFN teardown */ diff --git a/e2e-tests/import-resources.test.ts b/e2e-tests/import-resources.test.ts index d51cbffac..72d9c253a 100644 --- a/e2e-tests/import-resources.test.ts +++ b/e2e-tests/import-resources.test.ts @@ -54,6 +54,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => { const result = await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', script], fixtureDir, { AWS_REGION: region, DEFAULT_EVALUATOR_MODEL, + RESOURCE_SUFFIX: suffix, }); if (result.exitCode !== 0) { throw new Error( @@ -63,7 +64,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => { } // 2. Read resource ARNs from bugbash-resources.json - const resourcesPath = join(fixtureDir, 'bugbash-resources.json'); + const resourcesPath = join(fixtureDir, `bugbash-resources-${suffix}.json`); const resources = JSON.parse(await readFile(resourcesPath, 'utf-8')) as Record; runtimeArn = resources['runtime-basic']!.arn; memoryArn = resources['memory-full']!.arn; @@ -102,6 +103,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => { try { await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', 'cleanup_resources.py'], fixtureDir, { AWS_REGION: region, + RESOURCE_SUFFIX: suffix, }); } catch { /* ignore — resources may already be deleted by CFN teardown */ diff --git a/integ-tests/add-remove-resources.test.ts b/integ-tests/add-remove-resources.test.ts index 57dd48483..a89c761dd 100644 --- a/integ-tests/add-remove-resources.test.ts +++ b/integ-tests/add-remove-resources.test.ts @@ -1,7 +1,10 @@ import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js'; import type { TestProject } from '../src/test-utils/index.js'; +import { createTelemetryHelper } from '../src/test-utils/telemetry-helper.js'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +const telemetry = createTelemetryHelper(); + describe('integration: add and remove resources', () => { let project: TestProject; @@ -16,13 +19,16 @@ describe('integration: add and remove resources', () => { afterAll(async () => { await project.cleanup(); + telemetry.destroy(); }); describe('memory lifecycle', () => { const memoryName = `IntegMem${Date.now().toString().slice(-6)}`; it('adds a memory resource', async () => { - const result = await runCLI(['add', 'memory', '--name', memoryName, '--json'], project.projectPath); + const result = await runCLI(['add', 'memory', '--name', memoryName, '--json'], project.projectPath, { + env: telemetry.env, + }); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); const json = JSON.parse(result.stdout); @@ -34,13 +40,17 @@ describe('integration: add and remove resources', () => { expect(memories, 'memories should exist').toBeDefined(); const found = memories!.some((m: Record) => m.name === memoryName); expect(found, `Memory "${memoryName}" should be in config`).toBe(true); + + // Verify telemetry + telemetry.assertMetricEmitted({ command: 'add.memory', exit_reason: 'success' }); }); it('adds a memory with EPISODIC strategy and verifies reflectionNamespaces', async () => { const episodicMemName = `EpiMem${Date.now().toString().slice(-6)}`; const result = await runCLI( ['add', 'memory', '--name', episodicMemName, '--strategies', 'EPISODIC', '--json'], - project.projectPath + project.projectPath, + { env: telemetry.env } ); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); @@ -61,6 +71,14 @@ describe('integration: add and remove resources', () => { expect(episodic!.reflectionNamespaces, 'Should have reflectionNamespaces').toBeDefined(); expect(episodic!.reflectionNamespaces!.length).toBeGreaterThan(0); + // Verify telemetry + telemetry.assertMetricEmitted({ + command: 'add.memory', + exit_reason: 'success', + strategy_count: '1', + strategy_episodic: 'true', + }); + // Clean up await runCLI(['remove', 'memory', '--name', episodicMemName, '--json'], project.projectPath); }); @@ -86,7 +104,8 @@ describe('integration: add and remove resources', () => { it('adds a credential resource', async () => { const result = await runCLI( ['add', 'credential', '--name', credentialName, '--api-key', 'test-key-integ-123', '--json'], - project.projectPath + project.projectPath, + { env: telemetry.env } ); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); @@ -99,6 +118,13 @@ describe('integration: add and remove resources', () => { expect(credentials, 'credentials should exist').toBeDefined(); const found = credentials!.some((c: Record) => c.name === credentialName); expect(found, `Credential "${credentialName}" should be in config`).toBe(true); + + // Verify telemetry + telemetry.assertMetricEmitted({ + command: 'add.credential', + exit_reason: 'success', + credential_type: 'api-key', + }); }); it('removes the credential resource', async () => { @@ -115,4 +141,30 @@ describe('integration: add and remove resources', () => { expect(found, `Credential "${credentialName}" should be removed from config`).toBe(false); }); }); + + describe('policy-engine', () => { + const engineName = `TestEngine${Date.now().toString().slice(-6)}`; + + it('adds a policy engine resource', async () => { + const result = await runCLI(['add', 'policy-engine', '--name', engineName, '--json'], project.projectPath, { + env: telemetry.env, + }); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + + telemetry.assertMetricEmitted({ + command: 'add.policy-engine', + exit_reason: 'success', + attach_gateway_count: '0', + }); + }); + + it('removes the policy engine resource', async () => { + const result = await runCLI(['remove', 'policy-engine', '--name', engineName, '--json'], project.projectPath); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + }); + }); }); diff --git a/integ-tests/create-no-agent.test.ts b/integ-tests/create-no-agent.test.ts index 4bcca2690..bcdf80eaa 100644 --- a/integ-tests/create-no-agent.test.ts +++ b/integ-tests/create-no-agent.test.ts @@ -32,7 +32,7 @@ describe('integration: create without agent', () => { it.skipIf(!hasNpm || !hasGit)('creates project with real npm install and git init', async () => { const name = `NoAgent${Date.now().toString().slice(-6)}`; - const result = await runCLI(['create', '--name', name, '--no-agent', '--json'], testDir, false); + const result = await runCLI(['create', '--name', name, '--no-agent', '--json'], testDir, { skipInstall: false }); expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); diff --git a/integ-tests/create-with-agent.test.ts b/integ-tests/create-with-agent.test.ts index 7fb20bdbf..69f0594b8 100644 --- a/integ-tests/create-with-agent.test.ts +++ b/integ-tests/create-with-agent.test.ts @@ -49,7 +49,7 @@ describe('integration: create with Python agent', () => { '--json', ], testDir, - false + { skipInstall: false } ); expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); diff --git a/integ-tests/dev-server.test.ts b/integ-tests/dev-server.test.ts index 5f60976e7..4b07b7284 100644 --- a/integ-tests/dev-server.test.ts +++ b/integ-tests/dev-server.test.ts @@ -60,7 +60,7 @@ describe('integration: dev server', () => { '--json', ], testDir, - false + { skipInstall: false } ); if (result.exitCode === 0) { diff --git a/integ-tests/help.test.ts b/integ-tests/help.test.ts index af99b8ce4..7e2176e2f 100644 --- a/integ-tests/help.test.ts +++ b/integ-tests/help.test.ts @@ -1,5 +1,9 @@ +import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; import { runCLI } from '../src/test-utils/index.js'; -import { describe, expect, it } from 'vitest'; +import { createTelemetryHelper } from '../src/test-utils/telemetry-helper.js'; +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { afterAll, describe, expect, it } from 'vitest'; const COMMANDS = [ 'create', @@ -38,3 +42,53 @@ describe('CLI help', () => { } }); }); + +describe('help modes telemetry', () => { + const telemetry = createTelemetryHelper(); + const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); + + afterAll(() => telemetry.destroy()); + + function run(args: string[], extraEnv: Record = {}) { + return spawnAndCollect('node', [cliPath, ...args], process.cwd(), { + AGENTCORE_SKIP_INSTALL: '1', + ...telemetry.env, + ...extraEnv, + }); + } + + it('writes JSONL audit file when audit is enabled via env var', async () => { + const result = await run(['help', 'modes']); + expect(result.exitCode).toBe(0); + + const entries = telemetry.readEntries(); + expect(entries).toHaveLength(1); + telemetry.assertMetricEmitted({ + command_group: 'help', + command: 'help.modes', + exit_reason: 'success', + }); + expect(entries[0]!.attrs['agentcore-cli.session_id']).toBeDefined(); + expect(entries[0]!.attrs['os.type']).toBeDefined(); + expect(entries[0]!.value).toBeGreaterThanOrEqual(0); + }); + + it('does not write audit file when audit is not enabled', async () => { + telemetry.clearEntries(); + + const noAuditCliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); + const result = await spawnAndCollect('node', [noAuditCliPath, 'help', 'modes'], process.cwd(), { + AGENTCORE_SKIP_INSTALL: '1', + AGENTCORE_CONFIG_DIR: telemetry.dir, + }); + expect(result.exitCode).toBe(0); + + const telemetryDir = join(telemetry.dir, 'telemetry'); + try { + const files = readdirSync(telemetryDir); + expect(files).toHaveLength(0); + } catch { + // telemetry dir doesn't exist — correct + } + }); +}); diff --git a/package-lock.json b/package-lock.json index 7497a04fa..a861d966d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aws/agentcore", - "version": "0.12.0", + "version": "0.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aws/agentcore", - "version": "0.12.0", + "version": "0.12.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -24,7 +24,7 @@ "@aws-sdk/client-sts": "^3.893.0", "@aws-sdk/client-xray": "^3.1003.0", "@aws-sdk/credential-providers": "^3.893.0", - "@aws/agent-inspector": "0.2.1", + "@aws/agent-inspector": "0.3.0", "@commander-js/extra-typings": "^14.0.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", @@ -242,9 +242,9 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "53.18.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.18.0.tgz", - "integrity": "sha512-/fa6rOpokkfa5tVIdhsaexQq5MVVTSsZSD1Tu45YcrdyGRusGrM9RlPMCPrwvMS1UfdVFBhcgO9dl9ODWAWOeQ==", + "version": "53.19.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.19.0.tgz", + "integrity": "sha512-pHtEeuiPwYu4vWJhqTqBCvOWy2w4BjYFW5W8L5lH5AzhKLLG92o+Ck0oLcKUkB0Ucl4ff49WL9K7sa7g35ionw==", "bundleDependencies": [ "jsonschema", "semver" @@ -377,14 +377,14 @@ } }, "node_modules/@aws-cdk/toolkit-lib": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/toolkit-lib/-/toolkit-lib-1.24.0.tgz", - "integrity": "sha512-tgtH0CJ8/N/CpT1/ebOBfUpxdAMSRsP9LTAjWfa+E0clX4Vuvx0w1J1bGYwtvKY9nQUbFIO4QfgNEHz8hVlMUA==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/toolkit-lib/-/toolkit-lib-1.25.0.tgz", + "integrity": "sha512-yTxQLcI7T8wfUAhWw8vO7dG//x0WL786AA5u4i/yRtSVSJd+0frHX6gD+njSv41OaNLhwP6EVYkFz9wTyCt64A==", "license": "Apache-2.0", "dependencies": { "@aws-cdk/cdk-assets-lib": "^1", "@aws-cdk/cloud-assembly-api": "2.2.2", - "@aws-cdk/cloud-assembly-schema": ">=53.18.0", + "@aws-cdk/cloud-assembly-schema": ">=53.19.0", "@aws-cdk/cloudformation-diff": "^2", "@aws-cdk/cx-api": "^2", "@aws-sdk/client-appsync": "^3", @@ -415,7 +415,7 @@ "@smithy/util-retry": "^4", "@smithy/util-waiter": "^4", "archiver": "^7.0.1", - "cdk-from-cfn": "^0.295.0", + "cdk-from-cfn": "^0.297.0", "chalk": "^4", "chokidar": "^4", "fast-deep-equal": "^3.1.3", @@ -687,24 +687,24 @@ } }, "node_modules/@aws-sdk/client-application-signals": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-application-signals/-/client-application-signals-3.1037.0.tgz", - "integrity": "sha512-xnGVyIWU1SXNSnnARvU3U3sic0QWH0wek/X3WpXFCpOm0NBzbTablWiAszNDU9RCvg9KUDm6Wdp0T4jnodXhEg==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-application-signals/-/client-application-signals-3.1038.0.tgz", + "integrity": "sha512-kUBaXESTxZO+JiMQIFU/Wv0Lop+KtuJP0xOv00dL/4a0RdcqNdAwbH7RnxXaL5z7toDLCfg9j93f0IQEdBLbXA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -712,7 +712,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -728,7 +728,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -788,25 +788,25 @@ } }, "node_modules/@aws-sdk/client-bedrock": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock/-/client-bedrock-3.1037.0.tgz", - "integrity": "sha512-XGuJ86vuuEsqp0Gq8fMCSMd/VNCwqTvKwFT99SU2OOLyNp31ChZ+LdIckJZl/A3jpUyZYpXjn7IxP/N/6UFiZA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock/-/client-bedrock-3.1038.0.tgz", + "integrity": "sha512-WY99Vodg7V4hxLQn7HOLawXHeVYv8Ys16Xx3CPpu8L7+1spvO/i4uykzTXH6GkojdAqNO2CSclhk31lb85nSWg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/token-providers": "3.1037.0", + "@aws-sdk/token-providers": "3.1038.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -814,7 +814,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -830,7 +830,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -839,24 +839,24 @@ } }, "node_modules/@aws-sdk/client-bedrock-agent": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agent/-/client-bedrock-agent-3.1037.0.tgz", - "integrity": "sha512-8Uc3zdwfxmjMrXb4qP69bByL054R+jWNaW3/Hq93E1jnag8vZz8YhJlz1x6jr1oXOoOLVErc1L/g8x0sMFZuRA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agent/-/client-bedrock-agent-3.1038.0.tgz", + "integrity": "sha512-cwOkwVhYs+GeW7SzNS/pmgkHZ/SAyv4wIkz62vew6sM6R+XgqqBbtEqDrf+FSsprFtx75edbduRg8boJt0veSg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -864,7 +864,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -880,7 +880,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -889,24 +889,24 @@ } }, "node_modules/@aws-sdk/client-bedrock-agentcore": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore/-/client-bedrock-agentcore-3.1037.0.tgz", - "integrity": "sha512-8WmZulMmFnCWFuX2rDBoZdebCMmmrAi1VABsLgm4O73w3+s7tcON1YgspG9gTevuVRtOVdk1B6TLw2Mo8NBHSQ==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore/-/client-bedrock-agentcore-3.1038.0.tgz", + "integrity": "sha512-HHsWj3AWsBnvihV0oUolOlnPxXum75kTdQHuYMX6kko4ek2iQWOe1WxUQG4TwOS7HUivU3Zf6CNti/K6wEkc7A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -917,7 +917,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -933,7 +933,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -943,24 +943,24 @@ } }, "node_modules/@aws-sdk/client-bedrock-agentcore-control": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore-control/-/client-bedrock-agentcore-control-3.1037.0.tgz", - "integrity": "sha512-tMfeMgohJ6L9ARRSdK8O7lbdYIggeRXtuRQFS+kISZTlvw+L4TjhUZ7TT5yIBO0UVkunoWsRQIH4VP4pgiVqQA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore-control/-/client-bedrock-agentcore-control-3.1038.0.tgz", + "integrity": "sha512-S+BtFwV7C5yUwjMZwExp0hAmdMCXLA2iG6KPvoS4zv8T2bMk1itrWfE4opqSZ1lgwDmnazcG7mmfAAUBFXvKXw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -968,7 +968,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -984,9 +984,9 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -994,28 +994,28 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1037.0.tgz", - "integrity": "sha512-Evla4DUdBf1pQpQa7pbfquj7jRaRktkI0qGoWBJBXWB9wQISzJ8OEI4sHugk/W6SF47C7hMP/o3Z/XBrfnejCw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1038.0.tgz", + "integrity": "sha512-oGiqs9v9WzPOdv7PDdm9iPibHgrbDvCDyNg43wFZn2PiiEUisFM+xUP2CRMsj41SmwZPhohmZkXiUu1+MghbAQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/token-providers": "3.1037.0", + "@aws-sdk/token-providers": "3.1038.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -1026,7 +1026,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1042,7 +1042,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -1051,42 +1051,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/token-providers": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1037.0.tgz", - "integrity": "sha512-csxa484KboWLs3f8jFQ5v9RwH8FVf0fQ+SO3GSXyu4Jtinhh4qXmOWLSVX30RBpB933dZaKGHGEXzEEY88NqRw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock/node_modules/@aws-sdk/token-providers": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1037.0.tgz", - "integrity": "sha512-csxa484KboWLs3f8jFQ5v9RwH8FVf0fQ+SO3GSXyu4Jtinhh4qXmOWLSVX30RBpB933dZaKGHGEXzEEY88NqRw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/client-cloudcontrol": { "version": "3.1036.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudcontrol/-/client-cloudcontrol-3.1036.0.tgz", @@ -1139,24 +1103,24 @@ } }, "node_modules/@aws-sdk/client-cloudformation": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.1037.0.tgz", - "integrity": "sha512-nLSwtmayv7tjjp6t8Lc20xZCeA+XJ5UzXvauQCnO3aRZVAxrgarQntZjS+eWlRYGRqLBjXSre4xL7XwUlObb2A==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.1038.0.tgz", + "integrity": "sha512-5c8u4magoWj8uAhiKoBY0/FXqCRqw3g05RXiQzUdNbeZDhfnv2b/r0aITVLlVEW/zfQUGi/WJfaV4vuffp6k1A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1164,7 +1128,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1180,9 +1144,9 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -1190,24 +1154,24 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1037.0.tgz", - "integrity": "sha512-W0TRDfyBikNR+DzOTBgBLT4TqVHCAasqx2Xu4G4PfTRCansUtEJRydq0CEVOpHlMfme4Va89O/r9sp/VoDsKRg==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1038.0.tgz", + "integrity": "sha512-OfdF+xZiiHWR5JL9rsc8jjlk1fNxnX2uS2iWWBM9vncVSXdhPAMb/rFNeHLkIx9IfmyiNi7kXUTAKgIUkVN0+w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -1218,7 +1182,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1234,7 +1198,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1293,24 +1257,24 @@ } }, "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1037.0.tgz", - "integrity": "sha512-/BQAyz98JRQFg3E8de3fGGydIYnsFRd6Cla4+zkviOe641fLCG0ZkPIk9D22HSi8qy9XKx+zk6ed2PcLO8uuPw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1038.0.tgz", + "integrity": "sha512-tTSXUZXzydM0VUoxcrM4YrhhQfFgepfpbRLEq460650rFAC8NsGhGQ6Ixo7UPV6TKEyI/jQcCnQVi4RVM4SkAg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1318,7 +1282,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1334,7 +1298,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1343,25 +1307,25 @@ } }, "node_modules/@aws-sdk/client-cognito-identity-provider": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.1037.0.tgz", - "integrity": "sha512-w0HuaMNtzcj6bErBX8/TVGbOz0a8JNCzPHLMq2u/ll4uuxl9Xut0njuy7vyY0/pYCuNE+no4uq+yHwn8U3ptgw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.1038.0.tgz", + "integrity": "sha512-E3/2sei5wiUvS+ZdlvQ93SXU2C9zdqq1mJqxik6B8GHtSnP2T4J97Hdu46FM6GK/HZJ+iFZFeIlM0B+P3uH2QQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1369,7 +1333,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1385,7 +1349,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1755,24 +1719,24 @@ } }, "node_modules/@aws-sdk/client-resource-groups-tagging-api": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-resource-groups-tagging-api/-/client-resource-groups-tagging-api-3.1037.0.tgz", - "integrity": "sha512-1+sv7vbSSRqVMBTvgFPFopXZ/SGdz3jP9aIHM8eOIuuZGYSmuqiXkNd7Ag5yxIQqEDO8sASevCqscEqN0f4OUw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-resource-groups-tagging-api/-/client-resource-groups-tagging-api-3.1038.0.tgz", + "integrity": "sha512-jEjVlhcAb4j658XKfxBPyyrbXJKmfg2bR3Xokqy5lhvZA80t6p47jeQ4s5yXIsaNzxnefJ8SuqrVft2KBHbDrQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1780,7 +1744,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1796,7 +1760,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1857,32 +1821,32 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1037.0.tgz", - "integrity": "sha512-DBmA1jAW8ST6C4srBxeL1/RLIir/d8WOm4s4mi59mGp6mBktHM59Kwb7GuURaCO60cotuce5zr0sKpMLPcBQyA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1038.0.tgz", + "integrity": "sha512-k60qm50bWkaqNfCJe1z28WaqgpztE0wbWVMZw6ZJcTOGfrWFhsJeLCEqtkH8w00iEozKx9GQwdQXz4G0sMGdKA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", "@aws-sdk/middleware-expect-continue": "^3.972.10", - "@aws-sdk/middleware-flexible-checksums": "^3.974.13", + "@aws-sdk/middleware-flexible-checksums": "^3.974.14", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-location-constraint": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-sdk-s3": "^3.972.34", + "@aws-sdk/middleware-sdk-s3": "^3.972.35", "@aws-sdk/middleware-ssec": "^3.972.10", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.23", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -1896,7 +1860,7 @@ "@smithy/md5-js": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1912,10 +1876,10 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -2074,25 +2038,25 @@ } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.1037.0.tgz", - "integrity": "sha512-Ye+BEvy1Fd/JtqfF1T9PiodIU52/Cd9sP4oBLnj8QQEyYRUcYG1OQ2xIFXF/gzAAMjfVN8HqGJo9LxdmScxZAQ==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.1038.0.tgz", + "integrity": "sha512-lboBAXDIr+ot5a357mQgaAwgMMYZW7EwO216LTASUHV3UN4YgqskrEcwsDV9765KH9wUDGxFt8rClS4ixaOgiA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.23", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -2100,7 +2064,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -2116,7 +2080,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2125,24 +2089,24 @@ } }, "node_modules/@aws-sdk/client-xray": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-xray/-/client-xray-3.1037.0.tgz", - "integrity": "sha512-cnic610qpFrbR3gNw0pFi6EFrkDhDJOHlOnQzpdjBQ38O3QXwkON6Kco8IyTs7TlyOr7HRmHnBiEYVDZrLVC0w==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-xray/-/client-xray-3.1038.0.tgz", + "integrity": "sha512-wPO92YHLQDbaHog82RFCmrGmMl5bdF6SOcejBr/RGcORsL4t6RXvaVP9sF73txgOTDRgUQh/EO1uy/JmTC9Dgg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -2150,7 +2114,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -2166,7 +2130,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2175,13 +2139,13 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.5.tgz", - "integrity": "sha512-lMPlYlYfQdNZhlkJgnkmESwrY+hNh3PljmZ+37oAqLNdJ6rnILAwFSyc6B3bJeDOtMORNnMQIej0aTRuOlDyhQ==", + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.6.tgz", + "integrity": "sha512-8Vu7zGxu+39ChR/s5J7nXBw3a2kMHAi0OfKT8ohgTVjX0qYed/8mIfdBb638oBmKrWCwwKjYAM5J/4gMJ8nAJA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.19", + "@aws-sdk/xml-builder": "^3.972.20", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", @@ -2191,7 +2155,7 @@ "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2213,12 +2177,12 @@ } }, "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.28.tgz", - "integrity": "sha512-UXhc4FfxbfNaIqycDnIZ+W8CMAoCtcJJfZkq+cWSUwQRN0V0d0uAoN2qCFyKZip8inlHeKJmNQsPliKKcElP8Q==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.29.tgz", + "integrity": "sha512-fklwtMw+9+1TRNa7KOCaaE9P9ubN6PdKCVlviX/vPRNtnMGIivAFrWcYsAcyw+sHPPioiSCSOHKKAhtOkO6IGg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", @@ -2229,12 +2193,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.31.tgz", - "integrity": "sha512-X/yGB73LmDW/6MdDJGCDzZBUXnM3ys4vs9l+5ZTJmiEswDdP1OjeoAFlFjVGS9o4KB2wZWQ9KOfdVNSSK6Ep3w==", + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.32.tgz", + "integrity": "sha512-7vA4GHg8NSmQxquJHSBcSM3RgB4ZaaRi6u4+zGFKOmOH6aqlgr2Sda46clkZDYzlirgfY96w15Zj0jh6PT48ng==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", @@ -2245,12 +2209,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.33.tgz", - "integrity": "sha512-c0ZF+lwoWVvX5iCaGKL5T/4DnIw88CGqxA0BcBs3U86mIp5EZYPVg+KSPkMXOyokmADvNewiMUfSG2uFwjRp0g==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.34.tgz", + "integrity": "sha512-vBrhWujFCLp1u8ptJRWYlipMutzPptb8pDQ00rKVH9q67T7rGd3VTWIj63aKrlLuY6qSsw1Rt5F/D/7wnNgryA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.6.1", @@ -2266,19 +2230,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.35.tgz", - "integrity": "sha512-jsU4u/cRkKFLKQS0k918FQ27fzXLG5ENiLWQMYE6581zLeI2hWh04ptlrvZMB3wJT/5d+vSzJk74X1CMFr4y8Q==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.36.tgz", + "integrity": "sha512-FBHyCmV8EB0gUvh1d+CZm87zt2PrdC7OyWexLRoH3I5zWSOUGa+9t58Y5jbxRfwUp3AWpHAFvKY6YzgR845sVA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-env": "^3.972.31", - "@aws-sdk/credential-provider-http": "^3.972.33", - "@aws-sdk/credential-provider-login": "^3.972.35", - "@aws-sdk/credential-provider-process": "^3.972.31", - "@aws-sdk/credential-provider-sso": "^3.972.35", - "@aws-sdk/credential-provider-web-identity": "^3.972.35", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-login": "^3.972.36", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -2291,13 +2255,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.35.tgz", - "integrity": "sha512-5oa3j0cA50jPqgNhZ9XdJVopuzUf1klRb28/2MfLYWWiPi9DRVvbrBWT+DidbHTT36520VuXZJahQwR+YgSjrg==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.36.tgz", + "integrity": "sha512-IFap01lJKxQc0C/OHmZwZQr/cKq0DhrcmKedRrdnnl42D+P0SImnnnWQjv07uIPqpEdtqmkPXb9TiPYTU+prxQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", @@ -2310,17 +2274,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.36.tgz", - "integrity": "sha512-4nT2T8Z7vH8KE9EdjEsuIlHpZSlcaK2PrKbQBjuUGU46BCCzF3WvP0u0Uiosni3Ykmmn4rWLVawoOCLotUtCbg==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.37.tgz", + "integrity": "sha512-/WFixFAAiw8WpmjZcI0l4t3DerXLmVinOIfuotmRZnu2qmsFPoqqmstASz0z8bi1pGdFXzeLzf6bwucM3mZcUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.31", - "@aws-sdk/credential-provider-http": "^3.972.33", - "@aws-sdk/credential-provider-ini": "^3.972.35", - "@aws-sdk/credential-provider-process": "^3.972.31", - "@aws-sdk/credential-provider-sso": "^3.972.35", - "@aws-sdk/credential-provider-web-identity": "^3.972.35", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-ini": "^3.972.36", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -2333,12 +2297,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.31.tgz", - "integrity": "sha512-eKeT4MXumpBJsrDLCYcSzIkFPVTFn/es7It2oogp2OhU/ic7P/+xzFpQx9ZhwtXS57Mc5S42BPWi7lHmvs/nYg==", + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.32.tgz", + "integrity": "sha512-uZp4tlGbpczV8QxmtIwOpSkcyGtBRR8/T4BAumRKfAt1nwCig3FSCZvrKl6ARDIDVRYn5p2oRcAsfFR01EgMGA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -2350,14 +2314,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.35.tgz", - "integrity": "sha512-bCuBdfnj0KGDMdLp6utMTLiJcFN2ek9EgZinxQZZSc3FxjJ/HSqeqab2cjbnoNfy8RM6suDCsRkmVY1izp9I+A==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.36.tgz", + "integrity": "sha512-DsLr0UHMyKzRJKe2bjlwU8q1cfoXg8TIJKV/xwvnalAemiZLOZunFzj/whGnFDZIBVLdnbLiwv5SvRf1+CSwkg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", - "@aws-sdk/token-providers": "3.1036.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/token-providers": "3.1038.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -2369,13 +2333,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.35.tgz", - "integrity": "sha512-swW6Bwvl8lanyEMtZOWE/oR6yqcRQH4HTQZUVsnDVgoXvRjRywpYpLv2BWwjUFyjPrqsdX6FeTkf4tMSe/qFTQ==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.36.tgz", + "integrity": "sha512-uzrURO7frJhHQVVNR5zBJcCYeMYflmXcWBK1+MiBym2Dfjh6nXATrMixrmGZi+97Q7ETZ+y/4lUwAy0Nfnznjw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -2387,23 +2351,23 @@ } }, "node_modules/@aws-sdk/credential-providers": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1037.0.tgz", - "integrity": "sha512-TPPoQzfNkWltNgjJn3RRY1S8VXffDvv49xGGs9K0DrYS9LZCLLsoHmSmShx9HQusPc/4Oz23rfRWTolCU19PdQ==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1038.0.tgz", + "integrity": "sha512-+B9BuRVPPKF0Q6msVS4vUGOsL4eUg7XYogikp56rUEQVoUVxn5ONyWlnNzsDMTv+BwuBgFo5N7gRZtEToAnSgg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-cognito-identity": "3.1037.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-cognito-identity": "^3.972.28", - "@aws-sdk/credential-provider-env": "^3.972.31", - "@aws-sdk/credential-provider-http": "^3.972.33", - "@aws-sdk/credential-provider-ini": "^3.972.35", - "@aws-sdk/credential-provider-login": "^3.972.35", - "@aws-sdk/credential-provider-node": "^3.972.36", - "@aws-sdk/credential-provider-process": "^3.972.31", - "@aws-sdk/credential-provider-sso": "^3.972.35", - "@aws-sdk/credential-provider-web-identity": "^3.972.35", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/client-cognito-identity": "3.1038.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.29", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-ini": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.36", + "@aws-sdk/credential-provider-node": "^3.972.37", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", @@ -2521,15 +2485,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.13.tgz", - "integrity": "sha512-b6QUe2hQX9XsnCzp6mtzVaERhganDKeb8lmGL6pVhr7rRVH9S9keDFW7uKytuuqmcY5943FixoGqn/QL+sbUBA==", + "version": "3.974.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.14.tgz", + "integrity": "sha512-mhTO3amGzYv/DQNbbqZo6UkHquBHlEEVRZwXmjeRqLmy1l9z3xCiFzglPL7n9JpVc2DZc9kjaraAn3JQrueZbw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/crc64-nvme": "^3.972.7", "@aws-sdk/types": "^3.973.8", "@smithy/is-array-buffer": "^4.2.2", @@ -2638,12 +2602,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.34.tgz", - "integrity": "sha512-/UL96JKjsjdodcRRMKl99tLQvK6Oi9ptLC9iU1yiTF/ruaDX0mtBBtnLNZDxIZRJOCVOtB49ed1YaTadqygk8Q==", + "version": "3.972.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.35.tgz", + "integrity": "sha512-lLppaNTAz+wNgLdi4FtHzrlwrGF0ODTnBWHBaFg85SKs0eJ+M+tP5ifrA8f/0lNd+Ak3MC1NGC6RavV3ny4HTg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.17", @@ -2677,18 +2641,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.35.tgz", - "integrity": "sha512-hOFWNOjVmOocpRlrU04nYxjMOeoe0Obu5AXEuhB8zblMCPl3cG1hdluQCZERRKFyhMQjwZnDbhSHjoMUjetFGw==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.36.tgz", + "integrity": "sha512-O2beToxguBvrZFFZ+fFgPbbae8MvyIBjQ6lImee4APHEXXNAD5ZJ2ayLF1mb7rsKw86TM81y5czg82bZncjSjg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "tslib": "^2.6.2" }, "engines": { @@ -2719,24 +2683,24 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.3.tgz", - "integrity": "sha512-SivE6GP228IVgfsrr2c/vqTg95X0Qj39Yw4uIrcddpkUzIltNMoNOR62leHOLhODfjv9K8X2mPTwS69A5kT0nQ==", + "version": "3.997.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.4.tgz", + "integrity": "sha512-4Sf+WY1lMJzXlw5MiyCMe/UzdILCwvuaHThbqMXS6dfh9gZy3No360I42RXquOI/ULUOhWy2HCyU0Fp20fQGPQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.23", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -2744,7 +2708,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -2760,7 +2724,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2785,12 +2749,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.22.tgz", - "integrity": "sha512-/rXhMXteD+BqhFd0nYprAgcZ/KtU+963uftPqd3tiFcFfooHZINXUGtOmo2SQjRVauCTNqIEzkwuSETdZFqTTA==", + "version": "3.996.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.23.tgz", + "integrity": "sha512-wBbys3Y53Ikly556vyADurKpYQHXS7Jjaskbz+Ga9PZCz7PB/9f3VdKbDlz7dqIzn+xwz7L/a6TR4iXcOi8IRw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.34", + "@aws-sdk/middleware-sdk-s3": "^3.972.35", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", @@ -2802,13 +2766,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1036.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1036.0.tgz", - "integrity": "sha512-aNSJ6jjDYayxN9ZA1JpycVScX93Lx03kKZ1EXt3DGOTahcWVLJj3oLAlop0xKP+vP2Ga2t49p1tEaMkTbCCaZA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1038.0.tgz", + "integrity": "sha512-Qniru+9oGGb/HNK/gGZWbV3jsD0k71ngE7qMQ/x6gYNYLd2EOwHCS6E2E6jfkaqO4i0d+nNKmfRy8bNcshKdGQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -2900,12 +2864,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.21.tgz", - "integrity": "sha512-Av4UHTcAWgdvbN0IP9pbtf4Qa1+6LtJqQdZWj5pLn5J67w0pnJJAZZ+7JPPcj2KN3378zD2JDM9DwJKEyvyMTQ==", + "version": "3.973.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.22.tgz", + "integrity": "sha512-YTYqTmOUrwbm1h99Ee4y/mVYpFRl0oSO/amtP5cc1BZZWdaAVWs9zj3TkyRHWvR9aI/ZS8m3mS6awXtYUlWyaw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", @@ -2939,9 +2903,9 @@ } }, "node_modules/@aws/agent-inspector": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@aws/agent-inspector/-/agent-inspector-0.2.1.tgz", - "integrity": "sha512-kyL6RBcTj1hYIchtrHDlDyeqm2viVYMBxhZKVn8wJn058YhI52GIDuUFlKD1avd57X+LJKlHr5VcKvBZp7Sg6A==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/agent-inspector/-/agent-inspector-0.3.0.tgz", + "integrity": "sha512-xD7QPr1WWkT9QWRWo6e9kq8kYxJLQ8egGscgSZ6jCyW3wNV5fcQ6THcAR/71hxxMFF2aleNUc3D8MoqgiS4DVw==", "license": "Apache-2.0", "dependencies": { "@ag-ui/core": "^0.0.52", @@ -4240,6 +4204,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-logs": { "version": "0.214.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", @@ -4258,6 +4238,22 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", + "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", @@ -4324,6 +4320,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-logs": { "version": "0.214.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", @@ -4342,6 +4354,22 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", + "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-trace-base": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", @@ -4428,12 +4456,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.0.tgz", + "integrity": "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4443,6 +4471,21 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", + "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-logs": { "version": "0.213.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.213.0.tgz", @@ -4493,13 +4536,13 @@ } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", - "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.0.tgz", + "integrity": "sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1" + "@opentelemetry/core": "2.7.0", + "@opentelemetry/resources": "2.7.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4508,6 +4551,21 @@ "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", + "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.0.tgz", @@ -4948,28 +5006,28 @@ "license": "MIT" }, "node_modules/@secretlint/config-creator": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-12.2.0.tgz", - "integrity": "sha512-enoydCMrJ8rmrM09qxDBd2XU1V3u9N9CfjRyUbYh3+m74G17u2PCTnlAw5UyeobewCb06d4Dym5t5ybCabATyA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-12.3.1.tgz", + "integrity": "sha512-CCRvPfrQLt2fPg3eWTIDGXNcVFQd6ZnvQCZ5lzclV9OF7iRqXQ4l5lfGO8NS68tIZx7YvBKhcO8/eVxdqm89HA==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/types": "12.2.0" + "@secretlint/types": "12.3.1" }, "engines": { "node": ">=22.0.0" } }, "node_modules/@secretlint/config-loader": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-12.2.0.tgz", - "integrity": "sha512-f7B9o6YF1jhTtd0ccJywcliCWkP02eYNM4efmua77AuztQTkXLVsw6eECXGAfZ9vh9uPHAK87Km6X4ta5hhtlA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-12.3.1.tgz", + "integrity": "sha512-PNrxz8tnAU/y5PmfOtKfVb+zEA3I+1iZqP1f9fXvIBtauBKs0h0Y+Cmvj0gG1a34kxaD1aQvFh8qHEhRV05gWg==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/profiler": "12.2.0", - "@secretlint/resolver": "12.2.0", - "@secretlint/types": "12.2.0", + "@secretlint/profiler": "12.3.1", + "@secretlint/resolver": "12.3.1", + "@secretlint/types": "12.3.1", "ajv": "^8.18.0", "debug": "^4.4.3", "rc-config-loader": "^4.1.4" @@ -4979,14 +5037,14 @@ } }, "node_modules/@secretlint/core": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-12.2.0.tgz", - "integrity": "sha512-ZT4irO8fPUg2810kcnfNQZ+AHIIYLFKyEqR91aSDl3g/RFOOLC66CAzGmMA1OuMc+sx9XE9TnM/IpLmLVvUSnA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-12.3.1.tgz", + "integrity": "sha512-ulcfARo1TANr8tWzDO/5cFxSNEEfRzgW6YPHYUijgpH3iYfwtUhWEU/r/BiFGl2PNaGzzVE1N9A6nZ74xbYvUQ==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/profiler": "12.2.0", - "@secretlint/types": "12.2.0", + "@secretlint/profiler": "12.3.1", + "@secretlint/types": "12.3.1", "debug": "^4.4.3", "structured-source": "^4.0.0" }, @@ -4995,14 +5053,14 @@ } }, "node_modules/@secretlint/formatter": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-12.2.0.tgz", - "integrity": "sha512-1KDSx4NgKObi8OQoPjBaGa41/sv9ZIrEMa94kQ3PhhVTPONP4N618W2c1CBVMuSNvRHilsKjXWZlKJKcIF4FlQ==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-12.3.1.tgz", + "integrity": "sha512-SXpTiRzuuFNbHa59zk0eUxFOB/LYxnuHSfqq7zU9lIt0z5rox6NrnN9WWkoQai2V9s7n3VqUVUpZqnhxQ2Jzpw==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/resolver": "12.2.0", - "@secretlint/types": "12.2.0", + "@secretlint/resolver": "12.3.1", + "@secretlint/types": "12.3.1", "@textlint/linter-formatter": "^15.5.4", "@textlint/module-interop": "^15.5.4", "@textlint/types": "^15.5.4", @@ -5031,18 +5089,18 @@ } }, "node_modules/@secretlint/node": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-12.2.0.tgz", - "integrity": "sha512-7hZxi49l2pkGjCT/BQf+ElKqFcbxooH9JslCThRfAMElyL3KGo14HhGfFFyWhhTLH2enAds1nKXczhWBI3otIQ==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-12.3.1.tgz", + "integrity": "sha512-1T08nqwWIJqSRrfkebk4Op5MwYgNnB6gwjv9v+X+V+HEIeG1GB/EgH8CJa8jK4uYdhUuaKyXpu36FIbjNa1wqA==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/config-loader": "12.2.0", - "@secretlint/core": "12.2.0", - "@secretlint/formatter": "12.2.0", - "@secretlint/profiler": "12.2.0", - "@secretlint/source-creator": "12.2.0", - "@secretlint/types": "12.2.0", + "@secretlint/config-loader": "12.3.1", + "@secretlint/core": "12.3.1", + "@secretlint/formatter": "12.3.1", + "@secretlint/profiler": "12.3.1", + "@secretlint/source-creator": "12.3.1", + "@secretlint/types": "12.3.1", "debug": "^4.4.3", "p-map": "^7.0.4" }, @@ -5051,23 +5109,23 @@ } }, "node_modules/@secretlint/profiler": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-12.2.0.tgz", - "integrity": "sha512-tB1NhUbCWH+32wSx6xE+Uj7nTUkidYEyW6B6pdGxsiZSM4SGz+FuKpr9OcylGsEphkkz1cQA3P9CjwCHcQqrnw==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-12.3.1.tgz", + "integrity": "sha512-lztyqJPTfkY0Ze9P7vNs3zm7p2Wq1+4ilFXVrxin0sDyFVXpkt+0+vsKsmdx9yBHabxrLDZgxa7fIsfV721cLw==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/resolver": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-12.2.0.tgz", - "integrity": "sha512-k3Mq4zeLpJtvBoEggEYstWhEiD23tL8qHbz/eYN+yQaQ2tItebIMd34qFX1jjeooiZdp/OuNWZA5JeyTw+SXcQ==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-12.3.1.tgz", + "integrity": "sha512-/QwcX5azKRdz9mBIbTBUsqp+cmWQZYGNdOHLbsMOBTLXa7KoEBffhmeaMSc0kNSrdgbgfu/7j+qeeaF4QwJf3A==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/secretlint-rule-preset-recommend": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-12.2.0.tgz", - "integrity": "sha512-n4qknL6vYRelmyrAyV/Z8I85c6jS6yF/ZxpgcqebjJuECIiel8OT2wIVIq9vk0MwlQN35skaQu0KvfM8uuGeyA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-12.3.1.tgz", + "integrity": "sha512-w9x9rIP1+qhV0k9k15aSIBo+6NrM4npAYIoNs7UmKIxuQSA6b91rhWpdMxgCv2/EQtym+gpuyN7rTSvG5wlYlw==", "dev": true, "license": "MIT", "engines": { @@ -5075,13 +5133,13 @@ } }, "node_modules/@secretlint/source-creator": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-12.2.0.tgz", - "integrity": "sha512-FYPtOmnm5daQnY4m2mgf/06bXkCL2oj1CIs+76tBu80kE1RDH0/ejsVKsiw6O5H3E2J1ruchRpSXTAlyQw1rYA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-12.3.1.tgz", + "integrity": "sha512-RCkmyKdoe6VFWMzzVm5a9W+a+ptJSusVX+YOrcNy/heklMIWLg0bL+HYFcyYCm8rU2dRq2HuSYTOamDjNs0LZg==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/types": "12.2.0", + "@secretlint/types": "12.3.1", "istextorbinary": "^9.5.0" }, "engines": { @@ -5089,9 +5147,9 @@ } }, "node_modules/@secretlint/types": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-12.2.0.tgz", - "integrity": "sha512-pIqhdWTFMN/cBfpZkAX1A8dqavsFvAdLobbxyMUHBUn/sUgXzyvUp7I52iyTr21EPc/BvOT9lDWdJBkcNz+n7Q==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-12.3.1.tgz", + "integrity": "sha512-Qv3fKvPkzUJpS9Ps6m2EPjC0RdxS2ZZrRfZAhIdl2u0zSjgf+Z0+AaCngmHRR+3Vtbw6s2FrCf4T6mLirm8Hgg==", "dev": true, "license": "MIT", "engines": { @@ -5393,19 +5451,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.5.tgz", - "integrity": "sha512-wnYOpB5vATFKWrY2Z9Alb0KhjZI6AbzU6Fbz3Hq2GnURdRYWB4q+qWivQtSTwXcmWUA3MZ6krfwL6Cq5MAbxsA==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", - "@smithy/service-error-classification": "^4.3.0", + "@smithy/service-error-classification": "^4.3.1", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -5525,9 +5583,9 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.0.tgz", - "integrity": "sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1" @@ -5748,12 +5806,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.4.tgz", - "integrity": "sha512-FY1UQQ1VFmMwiYp1GVS4MeaGD5O0blLNYK0xCRHU+mJgeoH/hSY8Ld8sJWKQ6uznkh14HveRGQJncgPyNl9J+A==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.6.tgz", + "integrity": "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.3.0", + "@smithy/service-error-classification": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -5806,9 +5864,9 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.16.tgz", - "integrity": "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", @@ -5838,24 +5896,24 @@ "license": "MIT" }, "node_modules/@textlint/ast-node-types": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.5.4.tgz", - "integrity": "sha512-bVtB6VEy9U9DpW8cTt25k5T+lz86zV5w6ImePZqY1AXzSuPhqQNT77lkMPxonXzUducEIlSvUu3o7sKw3y9+Sw==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.6.0.tgz", + "integrity": "sha512-CxZHFbYAU7J0A4izz31wV2ZZfySR6aVj2OSR6/3tppZm7VV6hM7nA7sutsLwIiBL/v4lsB1RM79l4Dc/VrH4qw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.5.4.tgz", - "integrity": "sha512-D9qJedKBLmAo+kiudop4UKgSxXMi4O8U86KrCidVXZ9RsK0NSVIw6+r2rlMUOExq79iEY81FRENyzmNVRxDBsg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.6.0.tgz", + "integrity": "sha512-IwHRhjwxs0a5t1eNAoKAdV224CDca38LyopPofXpwO/d0J75wBvzf/cBHXNl4TMsLKhYGtR83UprcLEKj/gZsA==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "15.5.4", - "@textlint/resolver": "15.5.4", - "@textlint/types": "15.5.4", + "@textlint/module-interop": "15.6.0", + "@textlint/resolver": "15.6.0", + "@textlint/types": "15.6.0", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", @@ -5898,27 +5956,27 @@ } }, "node_modules/@textlint/module-interop": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.5.4.tgz", - "integrity": "sha512-JyAUd26ll3IFF87LP0uGoa8Tzw5ZKiYvGs6v8jLlzyND1lUYCI4+2oIAslrODLkf0qwoCaJrBQWM3wsw+asVGQ==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.6.0.tgz", + "integrity": "sha512-MHY6pJx9i5kOlrvUSK51887tYZjHAV2qnr6unBm7LtBLGDFo93utdYqHyWep8r9QLsilQdeijWtufJI46z4v4w==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.5.4.tgz", - "integrity": "sha512-5GUagtpQuYcmhlOzBGdmVBvDu5lKgVTjwbxtdfoidN4OIqblIxThJHHjazU+ic+/bCIIzI2JcOjHGSaRmE8Gcg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.6.0.tgz", + "integrity": "sha512-T1l2Gd3455pwtm0cTewhX/LLy3bL9z6/Fu/am+jj+jjGfXVoknYkjfkZEKrjHlA7xzay0EfUKnu//teYemLeZw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.5.4.tgz", - "integrity": "sha512-mY28j2U7nrWmZbxyKnRvB8vJxJab4AxqOobLfb6iozrLelJbqxcOTvBQednadWPfAk9XWaZVMqUr9Nird3mutg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.6.0.tgz", + "integrity": "sha512-CvgYb1PiqF4BGyoZebGWzAJCZ4ChJAZ9gtWjpQIMKE4Xe2KlSwDA8m8MsiZIV321f5Ibx38BMjC1Z/2ZYP2GQg==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "15.5.4" + "@textlint/ast-node-types": "15.6.0" } }, "node_modules/@trivago/prettier-plugin-sort-imports": { @@ -7154,9 +7212,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.250.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.250.0.tgz", - "integrity": "sha512-8U8/S9VcmKSc3MHZWiB7P0IecgXoohI8Ya3dgtZMgbzC4mB+MEQmsYBeNgm4vzGQdRos8HjQLnFX1IBlZh7jQA==", + "version": "2.251.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.251.0.tgz", + "integrity": "sha512-H1Jfz2Oyejn+yG24i+By9fZpYfg+E3h1XnFCF2wnt/MyGOTIePRph7MRGkX73ap10ERSpmd0Ly58OLVykFoSQA==", "bundleDependencies": [ "@balena/dockerignore", "@aws-cdk/cloud-assembly-api", @@ -7176,8 +7234,8 @@ "dependencies": { "@aws-cdk/asset-awscli-v1": "2.2.273", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", - "@aws-cdk/cloud-assembly-api": "^2.2.0", - "@aws-cdk/cloud-assembly-schema": "^53.0.0", + "@aws-cdk/cloud-assembly-api": "^2.2.2", + "@aws-cdk/cloud-assembly-schema": "^53.18.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.3.3", @@ -7198,7 +7256,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { - "version": "2.2.0", + "version": "2.2.2", "bundleDependencies": [ "jsonschema", "semver" @@ -7214,7 +7272,7 @@ "node": ">= 18.0.0" }, "peerDependencies": { - "@aws-cdk/cloud-assembly-schema": ">=53.0.0" + "@aws-cdk/cloud-assembly-schema": ">=53.15.0" } }, "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/jsonschema": { @@ -7966,9 +8024,9 @@ } }, "node_modules/cdk-from-cfn": { - "version": "0.295.0", - "resolved": "https://registry.npmjs.org/cdk-from-cfn/-/cdk-from-cfn-0.295.0.tgz", - "integrity": "sha512-HNQu3TfNTHZNlxh/o0XxhMMSt3uDFDtMxxO2wZGvZpHwvjZLLFSCHooMbMGj75vtyqNmqKxQdR9WQSTcW3oIpg==", + "version": "0.297.0", + "resolved": "https://registry.npmjs.org/cdk-from-cfn/-/cdk-from-cfn-0.297.0.tgz", + "integrity": "sha512-ZyiugKPe9QYmfXNwbjBhc8sgbos7E0mQfo9P3/vrID8iKTsf5YZswPDu526sBTg4Fpx94N08UJZjLepRxaK4Ng==", "license": "MIT OR Apache-2.0" }, "node_modules/chai": { @@ -14501,17 +14559,17 @@ "license": "MIT" }, "node_modules/secretlint": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-12.2.0.tgz", - "integrity": "sha512-nIl6JNhywewJIJGHNeCpu0/NXs4zyhTriz9683SWNIjH6etDyN/Q/L2fJ4nCxqdl7iZM3MlVtQQMtPDomQINuw==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-12.3.1.tgz", + "integrity": "sha512-wv8TKCjU5hbBxo5jKEX8wIE78VAoL0Ux7pu18+TxtbICMZ2OCbu6EmO3OJLbUbyfUXSPVryNLNmGVgvwY6Z0xw==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/config-creator": "12.2.0", - "@secretlint/formatter": "12.2.0", - "@secretlint/node": "12.2.0", - "@secretlint/profiler": "12.2.0", - "@secretlint/resolver": "12.2.0", + "@secretlint/config-creator": "12.3.1", + "@secretlint/formatter": "12.3.1", + "@secretlint/node": "12.3.1", + "@secretlint/profiler": "12.3.1", + "@secretlint/resolver": "12.3.1", "debug": "^4.4.3", "globby": "^16.2.0", "read-pkg": "^10.1.0" diff --git a/package.json b/package.json index 7b0687a99..e88a3c25e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aws/agentcore", - "version": "0.12.0", + "version": "0.12.2", "description": "CLI for Amazon Bedrock AgentCore", "license": "Apache-2.0", "repository": { @@ -87,7 +87,7 @@ "@aws-sdk/client-sts": "^3.893.0", "@aws-sdk/client-xray": "^3.1003.0", "@aws-sdk/credential-providers": "^3.893.0", - "@aws/agent-inspector": "0.2.1", + "@aws/agent-inspector": "0.3.0", "@commander-js/extra-typings": "^14.0.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", diff --git a/src/cli/__tests__/global-config.test.ts b/src/cli/__tests__/global-config.test.ts index 2851a13a4..6e2038973 100644 --- a/src/cli/__tests__/global-config.test.ts +++ b/src/cli/__tests__/global-config.test.ts @@ -1,4 +1,9 @@ -import { getOrCreateInstallationId, readGlobalConfig, updateGlobalConfig } from '../global-config'; +import { + getOrCreateInstallationId, + readGlobalConfig, + readGlobalConfigSync, + updateGlobalConfig, +} from '../../lib/schemas/io/global-config'; import { createTempConfig } from './helpers/temp-config'; import { readFile, writeFile } from 'fs/promises'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; @@ -21,10 +26,29 @@ describe('global-config', () => { it('returns empty object when file is missing or invalid', async () => { expect(await readGlobalConfig(tmp.testDir + '/nonexistent.json')).toEqual({}); - await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: 'false' } })); + await writeFile(tmp.configFile, 'not json'); expect(await readGlobalConfig(tmp.configFile)).toEqual({}); }); + it('drops invalid fields while preserving valid ones', async () => { + await writeFile( + tmp.configFile, + JSON.stringify({ + transactionSearchIndexPercentage: 'not-a-number', + uvIndex: 'https://valid.url', + telemetry: { enabled: 'yes', endpoint: 'https://example.com' }, + }) + ); + + const config = await readGlobalConfig(tmp.configFile); + + expect(config).toEqual({ + transactionSearchIndexPercentage: undefined, + uvIndex: 'https://valid.url', + telemetry: { enabled: undefined, endpoint: 'https://example.com' }, + }); + }); + it('preserves unknown fields via passthrough', async () => { const full = { installationId: 'abc-123', @@ -39,6 +63,21 @@ describe('global-config', () => { }); }); + describe('readGlobalConfigSync', () => { + it('returns parsed config when file exists', async () => { + await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: false } })); + + expect(readGlobalConfigSync(tmp.configFile)).toEqual({ telemetry: { enabled: false } }); + }); + + it('returns empty object when file is missing or invalid', async () => { + expect(readGlobalConfigSync(tmp.testDir + '/nonexistent.json')).toEqual({}); + + await writeFile(tmp.configFile, 'not json'); + expect(readGlobalConfigSync(tmp.configFile)).toEqual({}); + }); + }); + describe('updateGlobalConfig', () => { it('creates directory and writes config when none exists', async () => { const fresh = createTempConfig('gc-fresh'); diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 72cf5c50d..88949ecfe 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,3 +1,4 @@ +import { getOrCreateInstallationId } from '../lib/schemas/io/global-config'; import { registerAdd } from './commands/add'; import { registerCreate } from './commands/create'; import { registerDeploy } from './commands/deploy'; @@ -19,8 +20,8 @@ import { registerTraces } from './commands/traces'; import { registerUpdate } from './commands/update'; import { registerValidate } from './commands/validate'; import { PACKAGE_VERSION } from './constants'; -import { getOrCreateInstallationId } from './global-config'; import { ALL_PRIMITIVES } from './primitives'; +import { TelemetryClientAccessor } from './telemetry'; import { App } from './tui/App'; import { LayoutProvider } from './tui/context'; import { COMMAND_DESCRIPTIONS } from './tui/copy'; @@ -222,7 +223,12 @@ export const main = async (argv: string[]) => { printTelemetryNotice(); } - await program.parseAsync(argv); + TelemetryClientAccessor.init(args[0] ?? 'unknown'); + try { + await program.parseAsync(argv); + } finally { + await TelemetryClientAccessor.shutdown(); + } // Telemetry notice already printed above; only run update check here. await printPostCommandNotices(false, updateCheck); diff --git a/src/cli/commands/dev/browser-mode.ts b/src/cli/commands/dev/browser-mode.ts index 3846dea85..c35113e6d 100644 --- a/src/cli/commands/dev/browser-mode.ts +++ b/src/cli/commands/dev/browser-mode.ts @@ -9,6 +9,8 @@ import { runWebUI, } from '../../operations/dev/web-ui'; import { listMemoryRecords, retrieveMemoryRecords } from '../../operations/memory'; +import { loadDeployedProjectConfig, resolveAgent } from '../../operations/resolve-agent'; +import { fetchTraceRecords, listTraces } from '../../operations/traces'; import path from 'node:path'; interface DeployedHandlers { @@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise { ? (agentNameParam, startTime, endTime) => collector.listTraces(agentNameParam, startTime, endTime) : undefined, onGetTrace: collector ? (agentNameParam, traceId) => collector.getTraceSpans(agentNameParam, traceId) : undefined, + onListCloudWatchTraces: async (agentName, _harnessName, startTime, endTime) => { + try { + const configIO = new ConfigIO({ baseDir }); + const context = await loadDeployedProjectConfig(configIO); + const resolved = resolveAgent(context, { runtime: agentName }); + if (!resolved.success) return { success: false, error: resolved.error }; + return listTraces({ + region: resolved.agent.region, + runtimeId: resolved.agent.runtimeId, + agentName: resolved.agent.agentName, + startTime, + endTime, + }); + } catch (err) { + return { + success: false, + error: `Failed to list CloudWatch traces: ${err instanceof Error ? err.message : String(err)}`, + }; + } + }, + onGetCloudWatchTrace: async (agentName, _harnessName, traceId, startTime, endTime) => { + try { + const configIO = new ConfigIO({ baseDir }); + const context = await loadDeployedProjectConfig(configIO); + const resolved = resolveAgent(context, { runtime: agentName }); + if (!resolved.success) return { success: false, error: resolved.error }; + return fetchTraceRecords({ + region: resolved.agent.region, + runtimeId: resolved.agent.runtimeId, + traceId, + startTime, + endTime, + includeSpans: true, + }); + } catch (err) { + return { + success: false, + error: `Failed to get CloudWatch trace: ${err instanceof Error ? err.message : String(err)}`, + }; + } + }, onListMemoryRecords: async (memoryName, namespace, strategyId) => { const deployed = await resolveDeployedHandlers(baseDir, onLog); if (!deployed.onListMemoryRecords) return { success: false, error: 'No deployed AgentCore Memory found' }; diff --git a/src/cli/commands/help/command.tsx b/src/cli/commands/help/command.tsx index 48d771854..338684d7e 100644 --- a/src/cli/commands/help/command.tsx +++ b/src/cli/commands/help/command.tsx @@ -1,3 +1,4 @@ +import { TelemetryClientAccessor } from '../../telemetry/client-accessor.js'; import type { Command } from '@commander-js/extra-typings'; const MODES_HELP = ` @@ -41,15 +42,23 @@ export const registerHelp = (program: Command) => { const helpCmd = program .command('help') .description('Display help topics') - .action(() => { - console.log('Available help topics: modes'); - console.log('Run `agentcore help ` for details.'); + .action(async () => { + const client = await TelemetryClientAccessor.get(); + await client.withCommandRun('help', () => { + console.log('Available help topics: modes'); + console.log('Run `agentcore help ` for details.'); + return {}; + }); }); helpCmd .command('modes') .description('Explain interactive vs non-interactive modes') - .action(() => { - console.log(MODES_HELP); + .action(async () => { + const client = await TelemetryClientAccessor.get(); + await client.withCommandRun('help.modes', () => { + console.log(MODES_HELP); + return {}; + }); }); }; diff --git a/src/cli/commands/import/__tests__/import-gateway-flow.test.ts b/src/cli/commands/import/__tests__/import-gateway-flow.test.ts index 5b322e942..183a6e63f 100644 --- a/src/cli/commands/import/__tests__/import-gateway-flow.test.ts +++ b/src/cli/commands/import/__tests__/import-gateway-flow.test.ts @@ -310,14 +310,13 @@ describe('handleImportGateway', () => { // ── Name validation ───────────────────────────────────────────────────── describe('Name validation', () => { - it('rejects invalid name starting with a number', async () => { - mockGetGatewayDetail.mockResolvedValue(makeGatewayDetail({ name: '123gateway' })); + it('rejects invalid name with special characters', async () => { + mockGetGatewayDetail.mockResolvedValue(makeGatewayDetail({ name: 'gateway_with_underscores!' })); const result = await handleImportGateway({ arn: GATEWAY_ARN }); expect(result.success).toBe(false); expect(result.error).toContain('Invalid name'); - expect(result.error).toContain('must start with a letter'); expect(mockConfigIOInstance.writeProjectSpec).not.toHaveBeenCalled(); }); diff --git a/src/cli/commands/import/import-gateway.ts b/src/cli/commands/import/import-gateway.ts index 491246438..3c2384e03 100644 --- a/src/cli/commands/import/import-gateway.ts +++ b/src/cli/commands/import/import-gateway.ts @@ -9,6 +9,7 @@ import type { GatewayPolicyEngineConfiguration, OutboundAuth, } from '../../../schema'; +import { GatewayNameSchema } from '../../../schema'; import type { GatewayDetail, GatewayTargetDetail } from '../../aws/agentcore-control'; import { getGatewayDetail, @@ -17,7 +18,7 @@ import { listAllGateways, } from '../../aws/agentcore-control'; import { isAccessDeniedError } from '../../errors'; -import { ANSI, NAME_REGEX } from './constants'; +import { ANSI } from './constants'; import { executeCdkImportPipeline } from './import-pipeline'; import { failResult, @@ -425,10 +426,11 @@ export async function handleImportGateway(options: ImportResourceOptions): Promi // 4. Validate name logger.startStep('Validate name'); let localName = options.name ?? gatewayDetail.name; - if (!NAME_REGEX.test(localName)) { + const nameResult = GatewayNameSchema.safeParse(localName); + if (!nameResult.success) { return failResult( logger, - `Invalid name "${localName}". Name must start with a letter and contain only letters, numbers, and underscores (max 48 chars).`, + `Invalid name "${localName}". ${nameResult.error.issues[0]?.message ?? 'Invalid gateway name'}`, 'gateway', localName ); diff --git a/src/cli/commands/telemetry/__tests__/telemetry.test.ts b/src/cli/commands/telemetry/__tests__/telemetry.test.ts index b0e615fcd..efdfd2f23 100644 --- a/src/cli/commands/telemetry/__tests__/telemetry.test.ts +++ b/src/cli/commands/telemetry/__tests__/telemetry.test.ts @@ -1,5 +1,5 @@ +import { readGlobalConfig } from '../../../../lib/schemas/io/global-config'; import { createTempConfig } from '../../../__tests__/helpers/temp-config'; -import { readGlobalConfig } from '../../../global-config'; import { handleTelemetryDisable, handleTelemetryEnable, handleTelemetryStatus } from '../actions'; import { chmod, mkdir, rm, writeFile } from 'fs/promises'; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; diff --git a/src/cli/commands/telemetry/actions.ts b/src/cli/commands/telemetry/actions.ts index 90750a0f6..696608e01 100644 --- a/src/cli/commands/telemetry/actions.ts +++ b/src/cli/commands/telemetry/actions.ts @@ -1,4 +1,4 @@ -import { GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_FILE, updateGlobalConfig } from '../../global-config.js'; +import { GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_FILE, updateGlobalConfig } from '../../../lib/schemas/io/global-config.js'; import { resolveTelemetryPreference } from '../../telemetry/config.js'; export async function handleTelemetryDisable( diff --git a/src/cli/operations/deploy/__tests__/post-deploy-observability.test.ts b/src/cli/operations/deploy/__tests__/post-deploy-observability.test.ts index 9155a699d..ba069f29e 100644 --- a/src/cli/operations/deploy/__tests__/post-deploy-observability.test.ts +++ b/src/cli/operations/deploy/__tests__/post-deploy-observability.test.ts @@ -1,23 +1,23 @@ import { setupTransactionSearch } from '../post-deploy-observability.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -const { mockEnableTransactionSearch, mockReadCliConfig } = vi.hoisted(() => ({ +const { mockEnableTransactionSearch, mockReadGlobalConfigSync } = vi.hoisted(() => ({ mockEnableTransactionSearch: vi.fn(), - mockReadCliConfig: vi.fn(), + mockReadGlobalConfigSync: vi.fn(), })); vi.mock('../../../aws/transaction-search', () => ({ enableTransactionSearch: mockEnableTransactionSearch, })); -vi.mock('../../../../lib/schemas/io/cli-config', () => ({ - readCliConfig: mockReadCliConfig, +vi.mock('../../../../lib/schemas/io/global-config', () => ({ + readGlobalConfigSync: mockReadGlobalConfigSync, })); describe('setupTransactionSearch', () => { beforeEach(() => { vi.clearAllMocks(); - mockReadCliConfig.mockReturnValue({}); + mockReadGlobalConfigSync.mockReturnValue({}); mockEnableTransactionSearch.mockResolvedValue({ success: true }); }); @@ -33,7 +33,7 @@ describe('setupTransactionSearch', () => { }); it('passes custom transactionSearchIndexPercentage from config', async () => { - mockReadCliConfig.mockReturnValue({ transactionSearchIndexPercentage: 25 }); + mockReadGlobalConfigSync.mockReturnValue({ transactionSearchIndexPercentage: 25 }); const result = await setupTransactionSearch({ region: 'us-east-1', @@ -57,7 +57,7 @@ describe('setupTransactionSearch', () => { }); it('skips when disableTransactionSearch is true in config', async () => { - mockReadCliConfig.mockReturnValue({ disableTransactionSearch: true }); + mockReadGlobalConfigSync.mockReturnValue({ disableTransactionSearch: true }); const result = await setupTransactionSearch({ region: 'us-east-1', diff --git a/src/cli/operations/deploy/post-deploy-observability.ts b/src/cli/operations/deploy/post-deploy-observability.ts index 295392629..0616a65dc 100644 --- a/src/cli/operations/deploy/post-deploy-observability.ts +++ b/src/cli/operations/deploy/post-deploy-observability.ts @@ -1,4 +1,4 @@ -import { readCliConfig } from '../../../lib/schemas/io/cli-config'; +import { readGlobalConfigSync } from '../../../lib/schemas/io/global-config'; import { enableTransactionSearch } from '../../aws/transaction-search'; export interface TransactionSearchSetupOptions { @@ -31,7 +31,7 @@ export async function setupTransactionSearch( return { success: true }; } - const config = readCliConfig(); + const config = readGlobalConfigSync(); if (config.disableTransactionSearch) { return { success: true }; } diff --git a/src/cli/operations/dev/web-ui/__tests__/cloudwatch-traces.test.ts b/src/cli/operations/dev/web-ui/__tests__/cloudwatch-traces.test.ts new file mode 100644 index 000000000..f0210f63a --- /dev/null +++ b/src/cli/operations/dev/web-ui/__tests__/cloudwatch-traces.test.ts @@ -0,0 +1,233 @@ +import { handleGetCloudWatchTrace, handleListCloudWatchTraces } from '../handlers/cloudwatch-traces.js'; +import type { RouteContext } from '../handlers/route-context.js'; +import type { IncomingMessage, ServerResponse } from 'http'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +function mockRes(): ServerResponse & { _status: number; _headers: Record; _body: string } { + const res = { + _status: 0, + _headers: {} as Record, + _body: '', + writeHead(status: number, headers?: Record) { + res._status = status; + if (headers) Object.assign(res._headers, headers); + return res; + }, + setHeader(name: string, value: string) { + res._headers[name] = value; + }, + end(body?: string) { + if (body) res._body = body; + }, + }; + return res as unknown as ServerResponse & { _status: number; _headers: Record; _body: string }; +} + +function mockReq(url: string): IncomingMessage { + return { url, headers: { host: 'localhost:8081' } } as unknown as IncomingMessage; +} + +function mockCtx(overrides: Partial = {}): RouteContext { + return { + options: { + mode: 'dev', + agents: [], + harnesses: [], + uiPort: 8081, + ...overrides, + }, + runningAgents: new Map(), + startingAgents: new Map(), + agentErrors: new Map(), + setCorsHeaders: vi.fn(), + readBody: vi.fn(), + } as RouteContext; +} + +describe('handleListCloudWatchTraces', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('returns 404 when no handler configured', async () => { + const ctx = mockCtx(); + const req = mockReq('/api/cloudwatch-traces?agentName=my-agent'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(404); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('not available'); + }); + + it('returns 400 when neither agentName nor harnessName provided', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('agentName'); + expect(body.error).toContain('harnessName'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('returns 400 when both agentName and harnessName provided', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('agentName'); + expect(body.error).toContain('harnessName'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('calls handler with agentName and returns traces', async () => { + const traces = [{ traceId: 't1' }, { traceId: 't2' }]; + const handler = vi.fn().mockResolvedValue({ success: true, traces }); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?agentName=my-agent'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(200); + expect(handler).toHaveBeenCalledWith('my-agent', undefined, undefined, undefined); + const body = JSON.parse(res._body); + expect(body.success).toBe(true); + expect(body.traces).toEqual(traces); + }); + + it('calls handler with harnessName', async () => { + const handler = vi.fn().mockResolvedValue({ success: true, traces: [] }); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?harnessName=my-harness'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(200); + expect(handler).toHaveBeenCalledWith(undefined, 'my-harness', undefined, undefined); + }); + + it('returns 500 when handler throws', async () => { + const handler = vi.fn().mockRejectedValue(new Error('boom')); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?agentName=my-agent'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(500); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('Failed to list CloudWatch traces'); + }); + + it('returns 400 for invalid startTime', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('startTime'); + expect(handler).not.toHaveBeenCalled(); + }); +}); + +describe('handleGetCloudWatchTrace', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('returns 404 when no handler configured', async () => { + const ctx = mockCtx(); + const req = mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(404); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('not available'); + }); + + it('returns 400 when traceId is missing', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onGetCloudWatchTrace: handler }); + const req = mockReq('/api/cloudwatch-traces/?agentName=my-agent'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('traceId'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('returns 400 when neither agentName nor harnessName provided', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onGetCloudWatchTrace: handler }); + const req = mockReq('/api/cloudwatch-traces/abc123'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('agentName'); + expect(body.error).toContain('harnessName'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('returns 500 when handler throws', async () => { + const handler = vi.fn().mockRejectedValue(new Error('boom')); + const ctx = mockCtx({ onGetCloudWatchTrace: handler }); + const req = mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(500); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('Failed to get CloudWatch trace'); + }); + + it('calls handler and returns records', async () => { + const records = [{ record: 'data1' }]; + const handler = vi.fn().mockResolvedValue({ success: true, records }); + const ctx = mockCtx({ onGetCloudWatchTrace: handler }); + const req = mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(200); + expect(handler).toHaveBeenCalledWith('my-agent', undefined, 'abc123', undefined, undefined); + const body = JSON.parse(res._body); + expect(body.success).toBe(true); + expect(body.records).toEqual(records); + }); +}); diff --git a/src/cli/operations/dev/web-ui/api-types.ts b/src/cli/operations/dev/web-ui/api-types.ts index 509d834ff..8ba57937e 100644 --- a/src/cli/operations/dev/web-ui/api-types.ts +++ b/src/cli/operations/dev/web-ui/api-types.ts @@ -8,6 +8,7 @@ * TODO: Extract these types into a shared package so both repos import * from a single source of truth instead of manually duplicating. */ +import type { CloudWatchSpanRecord, CloudWatchTraceRecord } from '../../traces/types'; // --------------------------------------------------------------------------- // GET /api/status @@ -279,6 +280,39 @@ export interface GetTraceResponse { error?: string; } +// --------------------------------------------------------------------------- +// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx +// --------------------------------------------------------------------------- + +/** A single trace entry returned by the CloudWatch traces list endpoint */ +export interface CloudWatchTraceEntry { + traceId: string; + timestamp: string; + sessionId?: string; + spanCount?: string; +} + +/** Response shape for GET /api/cloudwatch-traces */ +export interface ListCloudWatchTracesResponse { + success: boolean; + traces?: CloudWatchTraceEntry[]; + error?: string; +} + +// --------------------------------------------------------------------------- +// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx +// --------------------------------------------------------------------------- + +/** Response shape for GET /api/cloudwatch-traces/:traceId */ +export interface GetCloudWatchTraceResponse { + success: boolean; + records?: CloudWatchTraceRecord[]; + spans?: CloudWatchSpanRecord[]; + error?: string; +} + +export type { CloudWatchTraceRecord, CloudWatchSpanRecord } from '../../traces/types'; + // --------------------------------------------------------------------------- // GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz] // --------------------------------------------------------------------------- diff --git a/src/cli/operations/dev/web-ui/handlers/cloudwatch-traces.ts b/src/cli/operations/dev/web-ui/handlers/cloudwatch-traces.ts new file mode 100644 index 000000000..15759b766 --- /dev/null +++ b/src/cli/operations/dev/web-ui/handlers/cloudwatch-traces.ts @@ -0,0 +1,166 @@ +import type { RouteContext } from './route-context'; +import { parseRequestUrl } from './route-context'; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +/** + * GET /api/cloudwatch-traces?agentName=xxx or ?harnessName=xxx — list recent CloudWatch traces. + * Exactly one of agentName or harnessName must be provided. + */ +export async function handleListCloudWatchTraces( + ctx: RouteContext, + req: IncomingMessage, + res: ServerResponse, + origin?: string +): Promise { + const { param } = parseRequestUrl(req); + const handler = ctx.options.onListCloudWatchTraces; + + if (!handler) { + ctx.setCorsHeaders(res, origin); + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'CloudWatch traces are not available' })); + return; + } + + const agentName = param('agentName'); + const harnessName = param('harnessName'); + + if (!agentName && !harnessName) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Either agentName or harnessName query parameter is required' })); + return; + } + + if (agentName && harnessName) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + success: false, + error: 'Provide either agentName or harnessName, not both', + }) + ); + return; + } + + // Parse optional date range query params (epoch milliseconds) + const startTimeRaw = param('startTime'); + const endTimeRaw = param('endTime'); + const startTime = startTimeRaw ? Number(startTimeRaw) : undefined; + const endTime = endTimeRaw ? Number(endTimeRaw) : undefined; + + if (startTimeRaw && isNaN(startTime!)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'startTime must be a number (epoch milliseconds)' })); + return; + } + if (endTimeRaw && isNaN(endTime!)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'endTime must be a number (epoch milliseconds)' })); + return; + } + + try { + const result = await handler(agentName, harnessName, startTime, endTime); + ctx.setCorsHeaders(res, origin); + res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (err) { + ctx.options.onLog?.('error', `List CloudWatch traces error: ${err instanceof Error ? err.message : String(err)}`); + ctx.setCorsHeaders(res, origin); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Failed to list CloudWatch traces' })); + } +} + +/** + * GET /api/cloudwatch-traces/:traceId?agentName=xxx or ?harnessName=xxx — get full CloudWatch trace data. + * Exactly one of agentName or harnessName must be provided. + */ +export async function handleGetCloudWatchTrace( + ctx: RouteContext, + req: IncomingMessage, + res: ServerResponse, + origin?: string +): Promise { + const { pathname, param } = parseRequestUrl(req); + const handler = ctx.options.onGetCloudWatchTrace; + + if (!handler) { + ctx.setCorsHeaders(res, origin); + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'CloudWatch traces are not available' })); + return; + } + + const traceId = pathname.replace('/api/cloudwatch-traces/', ''); + const agentName = param('agentName'); + const harnessName = param('harnessName'); + + if (!traceId) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'traceId is required in the URL path' })); + return; + } + + if (!/^[a-fA-F0-9-]+$/.test(traceId)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid trace ID format' })); + return; + } + + if (!agentName && !harnessName) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Either agentName or harnessName query parameter is required' })); + return; + } + + if (agentName && harnessName) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + success: false, + error: 'Provide either agentName or harnessName, not both', + }) + ); + return; + } + + // Parse optional date range query params (epoch milliseconds) + const startTimeRaw = param('startTime'); + const endTimeRaw = param('endTime'); + const startTime = startTimeRaw ? Number(startTimeRaw) : undefined; + const endTime = endTimeRaw ? Number(endTimeRaw) : undefined; + + if (startTimeRaw && isNaN(startTime!)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'startTime must be a number (epoch milliseconds)' })); + return; + } + if (endTimeRaw && isNaN(endTime!)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'endTime must be a number (epoch milliseconds)' })); + return; + } + + try { + const result = await handler(agentName, harnessName, traceId, startTime, endTime); + ctx.setCorsHeaders(res, origin); + res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (err) { + ctx.options.onLog?.('error', `Get CloudWatch trace error: ${err instanceof Error ? err.message : String(err)}`); + ctx.setCorsHeaders(res, origin); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Failed to get CloudWatch trace' })); + } +} diff --git a/src/cli/operations/dev/web-ui/handlers/index.ts b/src/cli/operations/dev/web-ui/handlers/index.ts index 91d2d4d5d..0ae7b4f67 100644 --- a/src/cli/operations/dev/web-ui/handlers/index.ts +++ b/src/cli/operations/dev/web-ui/handlers/index.ts @@ -4,6 +4,7 @@ export { handleResources } from './resources'; export { handleStart } from './start'; export { handleInvocations } from './invocations'; export { handleListTraces, handleGetTrace } from './traces'; +export { handleListCloudWatchTraces, handleGetCloudWatchTrace } from './cloudwatch-traces'; export { handleListMemoryRecords, handleRetrieveMemoryRecords } from './memory'; export { handleMcpProxy } from './mcp-proxy'; export { handleA2AAgentCard } from './a2a-proxy'; diff --git a/src/cli/operations/dev/web-ui/handlers/invocations.ts b/src/cli/operations/dev/web-ui/handlers/invocations.ts index 4123a696d..3a6b70ed9 100644 --- a/src/cli/operations/dev/web-ui/handlers/invocations.ts +++ b/src/cli/operations/dev/web-ui/handlers/invocations.ts @@ -68,6 +68,7 @@ export async function handleInvocations( return new Promise((resolve, reject) => { const headers: Record = { 'Content-Type': 'application/json', + Accept: 'text/event-stream, */*', 'x-amzn-bedrock-agentcore-runtime-session-id': sessionId ?? randomUUID(), }; if (userId) { diff --git a/src/cli/operations/dev/web-ui/index.ts b/src/cli/operations/dev/web-ui/index.ts index 6901eb31a..b14949008 100644 --- a/src/cli/operations/dev/web-ui/index.ts +++ b/src/cli/operations/dev/web-ui/index.ts @@ -4,6 +4,8 @@ export { type StartHandler, type ListTracesHandler, type GetTraceHandler, + type ListCloudWatchTracesHandler, + type GetCloudWatchTraceHandler, type ListMemoryRecordsHandler, type RetrieveMemoryRecordsHandler, } from './web-server'; @@ -29,6 +31,11 @@ export type { InvocationRequest, ListTracesResponse, GetTraceResponse, + ListCloudWatchTracesResponse, + CloudWatchTraceEntry, + GetCloudWatchTraceResponse, + CloudWatchTraceRecord, + CloudWatchSpanRecord, ListMemoryRecordsResponse, MemoryRecordResponse, RetrieveMemoryRecordsRequest, diff --git a/src/cli/operations/dev/web-ui/web-server.ts b/src/cli/operations/dev/web-ui/web-server.ts index c3f9c6f36..2b20b2d07 100644 --- a/src/cli/operations/dev/web-ui/web-server.ts +++ b/src/cli/operations/dev/web-ui/web-server.ts @@ -4,8 +4,10 @@ import { type AgentError, type AgentInfo, WEB_UI_LOCAL_URL } from './constants'; import { type RouteContext, handleA2AAgentCard, + handleGetCloudWatchTrace, handleGetTrace, handleInvocations, + handleListCloudWatchTraces, handleListMemoryRecords, handleListTraces, handleMcpProxy, @@ -78,6 +80,29 @@ export type GetTraceHandler = ( endTime?: number ) => Promise<{ success: boolean; resourceSpans?: unknown[]; resourceLogs?: unknown[]; error?: string }>; +/** + * Custom handler for GET /api/cloudwatch-traces. + * Returns a list of recent CloudWatch traces for the given agent or harness. + */ +export type ListCloudWatchTracesHandler = ( + agentName: string | undefined, + harnessName: string | undefined, + startTime?: number, + endTime?: number +) => Promise<{ success: boolean; traces?: unknown[]; error?: string }>; + +/** + * Custom handler for GET /api/cloudwatch-traces/:traceId. + * Returns the full CloudWatch trace data for a specific trace. + */ +export type GetCloudWatchTraceHandler = ( + agentName: string | undefined, + harnessName: string | undefined, + traceId: string, + startTime?: number, + endTime?: number +) => Promise<{ success: boolean; records?: unknown[]; spans?: unknown[]; error?: string }>; + /** * Custom handler for GET /api/memory. * Returns a list of memory records for a given memory + namespace. @@ -124,6 +149,10 @@ export interface WebUIOptions { onListTraces?: ListTracesHandler; /** Custom handler for getting a single trace */ onGetTrace?: GetTraceHandler; + /** Custom handler for listing CloudWatch traces */ + onListCloudWatchTraces?: ListCloudWatchTracesHandler; + /** Custom handler for getting a single CloudWatch trace */ + onGetCloudWatchTrace?: GetCloudWatchTraceHandler; /** Custom handler for listing memory records */ onListMemoryRecords?: ListMemoryRecordsHandler; /** Custom handler for searching memory records */ @@ -291,6 +320,10 @@ export class WebUIServer { await handleGetTrace(ctx, req, res, origin); } else if (req.method === 'GET' && req.url?.startsWith('/api/traces')) { await handleListTraces(ctx, req, res, origin); + } else if (req.method === 'GET' && req.url?.startsWith('/api/cloudwatch-traces/')) { + await handleGetCloudWatchTrace(ctx, req, res, origin); + } else if (req.method === 'GET' && req.url?.startsWith('/api/cloudwatch-traces')) { + await handleListCloudWatchTraces(ctx, req, res, origin); } else if (req.method === 'POST' && req.url === '/api/start') { await handleStart(ctx, req, res, origin); } else if (req.method === 'POST' && req.url === '/invocations') { diff --git a/src/cli/operations/traces/__tests__/get-trace.test.ts b/src/cli/operations/traces/__tests__/get-trace.test.ts new file mode 100644 index 000000000..c6fda22f4 --- /dev/null +++ b/src/cli/operations/traces/__tests__/get-trace.test.ts @@ -0,0 +1,233 @@ +import { fetchTraceRecords, getTrace } from '../get-trace'; +import type { FetchTraceRecordsOptions } from '../types'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const { mockSend } = vi.hoisted(() => ({ + mockSend: vi.fn(), +})); + +vi.mock('@aws-sdk/client-cloudwatch-logs', () => ({ + CloudWatchLogsClient: class { + send = mockSend; + }, + StartQueryCommand: class { + constructor(public input: unknown) {} + }, + GetQueryResultsCommand: class { + constructor(public input: unknown) {} + }, +})); + +vi.mock('../../../aws', () => ({ + getCredentialProvider: vi.fn().mockReturnValue({}), +})); + +vi.mock('node:fs', () => ({ + default: { + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), + }, +})); + +const baseOptions: FetchTraceRecordsOptions = { + region: 'us-west-2', + runtimeId: 'runtime-123', + traceId: 'abc123def456', + startTime: 1000000, + endTime: 2000000, +}; + +describe('fetchTraceRecords', () => { + afterEach(() => vi.clearAllMocks()); + + it('returns parsed trace records from CloudWatch', async () => { + mockSend + .mockResolvedValueOnce({ queryId: 'q-1' }) // StartQueryCommand + .mockResolvedValueOnce({ + // GetQueryResultsCommand + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: '{"traceId":"abc123","spanId":"span1"}' }, + { field: '@ptr', value: 'ptr-value-1' }, + ], + [ + { field: '@timestamp', value: '2024-01-01T00:00:01Z' }, + { field: '@message', value: '{"traceId":"abc123","spanId":"span2"}' }, + ], + ], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(true); + expect(result.records).toHaveLength(2); + expect(result.records![0]).toEqual({ + '@timestamp': '2024-01-01T00:00:00Z', + '@message': { traceId: 'abc123', spanId: 'span1' }, + '@ptr': 'ptr-value-1', + }); + expect(result.records![1]).toEqual({ + '@timestamp': '2024-01-01T00:00:01Z', + '@message': { traceId: 'abc123', spanId: 'span2' }, + }); + }); + + it('returns error for invalid trace ID format', async () => { + const result = await fetchTraceRecords({ + ...baseOptions, + traceId: 'invalid!@#$', + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid trace ID format'); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it('returns error when no trace data found', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toContain('No trace data found'); + }); + + it('returns error when query fails to start', async () => { + mockSend.mockResolvedValueOnce({ queryId: undefined }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toContain('Failed to start CloudWatch Logs Insights query'); + }); + + it('returns error when query status is Failed', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ status: 'Failed' }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toContain('failed'); + }); + + it('preserves @ptr when present in CloudWatch response', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: '{"key":"val"}' }, + { field: '@ptr', value: 'cw-ptr-123' }, + ], + ], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(true); + expect(result.records).toHaveLength(1); + expect(result.records![0]!['@ptr']).toBe('cw-ptr-123'); + }); + + it('omits @ptr when not present in CloudWatch response', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: '{"key":"val"}' }, + ], + ], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(true); + expect(result.records![0]).not.toHaveProperty('@ptr'); + }); + + it('handles non-JSON @message gracefully', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: 'plain text message' }, + ], + ], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(true); + expect(result.records).toHaveLength(1); + expect(result.records![0]!['@message']).toBe('plain text message'); + }); + + it('handles ResourceNotFoundException', async () => { + const error = new Error('Not found'); + error.name = 'ResourceNotFoundException'; + mockSend.mockRejectedValueOnce(error); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toContain('Log group'); + expect(result.error).toContain('not found'); + }); +}); + +describe('getTrace', () => { + afterEach(() => vi.clearAllMocks()); + + it('calls fetchTraceRecords and writes result to disk', async () => { + const fs = await import('node:fs'); + + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: '{"traceId":"abc123"}' }, + ], + ], + }); + + const result = await getTrace({ + region: 'us-west-2', + runtimeId: 'runtime-123', + agentName: 'my-agent', + traceId: 'abc123def456', + outputPath: '/tmp/test-trace.json', + startTime: 1000000, + endTime: 2000000, + }); + + expect(result.success).toBe(true); + expect(result.filePath).toContain('test-trace.json'); + expect(fs.default.mkdirSync).toHaveBeenCalled(); + expect(fs.default.writeFileSync).toHaveBeenCalledWith('/tmp/test-trace.json', expect.stringContaining('"traceId"')); + }); + + it('returns error from fetchTraceRecords without writing file', async () => { + const fs = await import('node:fs'); + + const result = await getTrace({ + region: 'us-west-2', + runtimeId: 'runtime-123', + agentName: 'my-agent', + traceId: 'invalid!@#$', + startTime: 1000000, + endTime: 2000000, + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid trace ID format'); + expect(fs.default.writeFileSync).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/operations/traces/__tests__/list-traces.test.ts b/src/cli/operations/traces/__tests__/list-traces.test.ts new file mode 100644 index 000000000..0bbe884de --- /dev/null +++ b/src/cli/operations/traces/__tests__/list-traces.test.ts @@ -0,0 +1,135 @@ +import { listTraces } from '../list-traces'; +import type { ListTracesOptions } from '../types'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const { mockRunInsightsQuery } = vi.hoisted(() => ({ + mockRunInsightsQuery: vi.fn(), +})); + +vi.mock('../insights-query', () => ({ + runInsightsQuery: mockRunInsightsQuery, +})); + +const baseOptions: ListTracesOptions = { + region: 'us-west-2', + runtimeId: 'runtime-123', + agentName: 'my-agent', + startTime: 1000000, + endTime: 2000000, +}; + +describe('listTraces', () => { + afterEach(() => vi.clearAllMocks()); + + it('returns trace entries from query results', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: true, + rows: [ + { + traceId: 'trace-1', + lastSeen: '2024-01-01T00:05:00Z', + firstSeen: '2024-01-01T00:00:00Z', + spanCount: '12', + sessionId: 'sess-1', + }, + { traceId: 'trace-2', lastSeen: '2024-01-01T00:03:00Z', firstSeen: '2024-01-01T00:01:00Z', spanCount: '5' }, + ], + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(true); + expect(result.traces).toHaveLength(2); + expect(result.traces![0]).toEqual({ + traceId: 'trace-1', + timestamp: '2024-01-01T00:05:00Z', + sessionId: 'sess-1', + spanCount: '12', + }); + expect(result.traces![1]).toEqual({ + traceId: 'trace-2', + timestamp: '2024-01-01T00:03:00Z', + sessionId: undefined, + spanCount: '5', + }); + }); + + it('filters out rows without traceId', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: true, + rows: [ + { traceId: 'trace-1', lastSeen: '2024-01-01T00:00:00Z', spanCount: '3' }, + { lastSeen: '2024-01-01T00:00:00Z', spanCount: '1' }, + { traceId: '', lastSeen: '2024-01-01T00:00:00Z', spanCount: '2' }, + ], + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(true); + expect(result.traces).toHaveLength(1); + expect(result.traces![0]!.traceId).toBe('trace-1'); + }); + + it('falls back to firstSeen when lastSeen is missing', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: true, + rows: [{ traceId: 'trace-1', firstSeen: '2024-01-01T00:00:00Z', spanCount: '1' }], + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(true); + expect(result.traces![0]!.timestamp).toBe('2024-01-01T00:00:00Z'); + }); + + it('returns empty traces for empty query results', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: true, + rows: [], + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(true); + expect(result.traces).toHaveLength(0); + }); + + it('propagates errors from runInsightsQuery', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: false, + error: 'Log group not found', + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toBe('Log group not found'); + }); + + it('passes correct log group name and default limit', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ success: true, rows: [] }); + + await listTraces(baseOptions); + + expect(mockRunInsightsQuery).toHaveBeenCalledWith({ + region: 'us-west-2', + logGroupName: '/aws/bedrock-agentcore/runtimes/runtime-123-DEFAULT', + startTime: 1000000, + endTime: 2000000, + queryString: expect.stringContaining('limit 20'), + }); + }); + + it('respects custom limit', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ success: true, rows: [] }); + + await listTraces({ ...baseOptions, limit: 50 }); + + expect(mockRunInsightsQuery).toHaveBeenCalledWith( + expect.objectContaining({ + queryString: expect.stringContaining('limit 50'), + }) + ); + }); +}); diff --git a/src/cli/operations/traces/get-trace.ts b/src/cli/operations/traces/get-trace.ts index 85c4471be..a87f10a65 100644 --- a/src/cli/operations/traces/get-trace.ts +++ b/src/cli/operations/traces/get-trace.ts @@ -1,129 +1,186 @@ -import { getCredentialProvider } from '../../aws'; import { DEFAULT_ENDPOINT_NAME } from '../../constants'; -import { CloudWatchLogsClient, GetQueryResultsCommand, StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'; +import { runInsightsQuery } from './insights-query'; +import type { + CloudWatchSpanRecord, + CloudWatchTraceRecord, + FetchTraceRecordsOptions, + FetchTraceRecordsResult, + GetTraceOptions, + GetTraceResult, +} from './types'; import fs from 'node:fs'; import path from 'node:path'; -export interface GetTraceOptions { - region: string; - runtimeId: string; - agentName: string; - traceId: string; - outputPath?: string; - startTime?: number; - endTime?: number; -} +const SPANS_LOG_GROUP = 'aws/spans'; +const TRACE_ID_PATTERN = /^[a-fA-F0-9-]+$/; -export interface GetTraceResult { - success: boolean; - filePath?: string; - error?: string; +function runtimeLogGroup(runtimeId: string): string { + return `/aws/bedrock-agentcore/runtimes/${runtimeId}-${DEFAULT_ENDPOINT_NAME}`; } -/** - * Fetches a full trace from CloudWatch Logs and writes it to a JSON file. - * - * Log group naming convention: /aws/bedrock-agentcore/runtimes/{runtimeId}-DEFAULT - * Trace ID is stored in the @message JSON body as "traceId". - */ -export async function getTrace(options: GetTraceOptions): Promise { - const { region, runtimeId, agentName, traceId, outputPath } = options; - - if (!/^[a-fA-F0-9-]+$/.test(traceId)) { +async function fetchSpans( + region: string, + traceId: string, + startTime?: number, + endTime?: number +): Promise<{ success: boolean; spans?: CloudWatchSpanRecord[]; error?: string }> { + if (!TRACE_ID_PATTERN.test(traceId)) { return { success: false, error: 'Invalid trace ID format. Expected a hex string (e.g., abc123def456).' }; } - const client = new CloudWatchLogsClient({ - credentials: getCredentialProvider(), + const result = await runInsightsQuery({ region, + logGroupName: SPANS_LOG_GROUP, + startTime, + endTime, + queryString: `fields traceId, spanId, parentSpanId, name, kind, + startTimeUnixNano, endTimeUnixNano, durationNano, + status.code as statusCode, + resource.attributes.service.name as serviceName, + attributes.gen_ai.usage.input_tokens as inputTokens, + attributes.gen_ai.usage.output_tokens as outputTokens, + attributes.gen_ai.usage.total_tokens as totalTokens, + attributes.http.status_code as httpStatusCode, + attributes.session.id as sessionId +| filter ispresent(traceId) and ispresent(resource.attributes.service.name) +| filter resource.attributes.aws.service.type = "gen_ai_agent" +| filter traceId = '${traceId}' +| sort startTimeUnixNano asc`, }); - const logGroupName = `/aws/bedrock-agentcore/runtimes/${runtimeId}-${DEFAULT_ENDPOINT_NAME}`; + if (!result.success) return { success: false, error: result.error }; + + const spans: CloudWatchSpanRecord[] = (result.rows ?? []) + .filter(row => row.traceId && row.spanId) + .map(row => ({ + traceId: row.traceId!, + spanId: row.spanId!, + parentSpanId: row.parentSpanId ?? undefined, + name: row.name ?? undefined, + kind: row.kind ?? undefined, + startTimeUnixNano: row.startTimeUnixNano ?? undefined, + endTimeUnixNano: row.endTimeUnixNano ?? undefined, + durationNano: row.durationNano ?? undefined, + statusCode: row.statusCode ?? undefined, + serviceName: row.serviceName ?? undefined, + inputTokens: row.inputTokens ? Number(row.inputTokens) : undefined, + outputTokens: row.outputTokens ? Number(row.outputTokens) : undefined, + totalTokens: row.totalTokens ? Number(row.totalTokens) : undefined, + httpStatusCode: row.httpStatusCode ? Number(row.httpStatusCode) : undefined, + sessionId: row.sessionId ?? undefined, + })); + + return { success: true, spans }; +} + +/** + * Fetches trace records from CloudWatch Logs Insights for a given trace ID. + * Returns typed records for the web UI API. Use `getTrace()` to write raw + * results to a JSON file on disk. + */ +export async function fetchTraceRecords(options: FetchTraceRecordsOptions): Promise { + const { region, runtimeId, traceId, includeSpans } = options; - const now = Date.now(); - const endTime = options.endTime ?? now; - const startTime = options.startTime ?? endTime - 12 * 60 * 60 * 1000; // default: last 12 hours + if (!TRACE_ID_PATTERN.test(traceId)) { + return { success: false, error: 'Invalid trace ID format. Expected a hex string (e.g., abc123def456).' }; + } - try { - const startQuery = await client.send( - new StartQueryCommand({ - logGroupName, - startTime: Math.floor(startTime / 1000), - endTime: Math.floor(endTime / 1000), - queryString: `fields @timestamp, @message + const [recordsResult, spansResult] = await Promise.all([ + runInsightsQuery({ + region, + logGroupName: runtimeLogGroup(runtimeId), + startTime: options.startTime, + endTime: options.endTime, + queryString: `fields @timestamp, @message, @ptr | filter traceId = '${traceId}' | sort @timestamp asc -| limit 1000`, - }) - ); +| limit 10000`, + }), + includeSpans ? fetchSpans(region, traceId, options.startTime, options.endTime) : Promise.resolve(undefined), + ]); - if (!startQuery.queryId) { - return { success: false, error: 'Failed to start CloudWatch Logs Insights query' }; - } + if (!recordsResult.success) { + return { success: false, error: recordsResult.error }; + } - // Poll for results - let traceData: Record[] = []; - let queryStatus = 'Running'; - - for (let i = 0; i < 60; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); - - const queryResults = await client.send(new GetQueryResultsCommand({ queryId: startQuery.queryId })); - - queryStatus = queryResults.status ?? 'Unknown'; - - if (queryStatus === 'Complete' || queryStatus === 'Failed' || queryStatus === 'Cancelled') { - if (queryStatus !== 'Complete') { - return { success: false, error: `Query ${queryStatus.toLowerCase()}` }; - } - - traceData = (queryResults.results ?? []).map(row => { - const fields: Record = {}; - for (const field of row) { - if (field.field && field.value) { - fields[field.field] = field.value; - } - } - return fields; - }); - break; - } - } + const traceData = recordsResult.rows ?? []; - if (queryStatus === 'Running') { - return { success: false, error: 'Query timed out after 60 seconds' }; - } + if (traceData.length === 0 && (!spansResult || (spansResult.spans ?? []).length === 0)) { + return { success: false, error: `No trace data found for trace ID: ${traceId}` }; + } - if (traceData.length === 0) { - return { success: false, error: `No trace data found for trace ID: ${traceId}` }; + const records: CloudWatchTraceRecord[] = traceData.map(entry => { + let message: unknown = entry['@message'] ?? '{}'; + try { + message = JSON.parse(entry['@message'] ?? '{}'); + } catch { + // Keep original string if not valid JSON } - // Parse @message fields as JSON where possible - const parsedTrace = traceData.map(entry => { - try { - const parsed: unknown = JSON.parse(entry['@message'] ?? '{}'); - return { ...entry, '@message': parsed }; - } catch { - return entry; - } - }); - - // Write to file - const filePath = outputPath ?? path.join('agentcore', '.cli', 'traces', `${agentName}-${traceId}.json`); - - const dir = path.dirname(filePath); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(filePath, JSON.stringify(parsedTrace, null, 2)); - - return { success: true, filePath: path.resolve(filePath) }; - } catch (error: unknown) { - const err = error as Error; - if (err.name === 'ResourceNotFoundException') { - return { - success: false, - error: `Log group '${logGroupName}' not found. The agent may not have been invoked yet, or traces may not be enabled.`, - }; + const record: CloudWatchTraceRecord = { + '@timestamp': entry['@timestamp'] ?? '', + '@message': message, + }; + + if (entry['@ptr']) { + record['@ptr'] = entry['@ptr']; } - return { success: false, error: err.message ?? String(error) }; + + return record; + }); + + const result: FetchTraceRecordsResult = { success: true, records }; + + if (spansResult?.success && spansResult.spans) { + result.spans = spansResult.spans; } + + return result; +} + +/** + * Fetches a full trace from CloudWatch Logs and writes it to a JSON file. + * Preserves all raw CloudWatch Insights fields in the output file. + */ +export async function getTrace(options: GetTraceOptions): Promise { + const { region, runtimeId, agentName, traceId, outputPath } = options; + + if (!TRACE_ID_PATTERN.test(traceId)) { + return { success: false, error: 'Invalid trace ID format. Expected a hex string (e.g., abc123def456).' }; + } + + const result = await runInsightsQuery({ + region, + logGroupName: runtimeLogGroup(runtimeId), + startTime: options.startTime, + endTime: options.endTime, + queryString: `fields @timestamp, @message +| filter traceId = '${traceId}' +| sort @timestamp asc +| limit 10000`, + }); + if (!result.success) { + return { success: false, error: result.error }; + } + + const traceData = result.rows ?? []; + if (traceData.length === 0) { + return { success: false, error: `No trace data found for trace ID: ${traceId}` }; + } + + const parsedTrace = traceData.map(entry => { + try { + const parsed: unknown = JSON.parse(entry['@message'] ?? '{}'); + return { ...entry, '@message': parsed }; + } catch { + return entry; + } + }); + + const filePath = outputPath ?? path.join('agentcore', '.cli', 'traces', `${agentName}-${traceId}.json`); + const dir = path.dirname(filePath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(parsedTrace, null, 2)); + + return { success: true, filePath: path.resolve(filePath) }; } diff --git a/src/cli/operations/traces/index.ts b/src/cli/operations/traces/index.ts index bbb013439..cf19dbf9c 100644 --- a/src/cli/operations/traces/index.ts +++ b/src/cli/operations/traces/index.ts @@ -1,3 +1,15 @@ export { buildTraceConsoleUrl } from './trace-url'; -export { listTraces, type TraceEntry, type ListTracesOptions, type ListTracesResult } from './list-traces'; -export { getTrace, type GetTraceOptions, type GetTraceResult } from './get-trace'; +export { listTraces } from './list-traces'; +export { fetchTraceRecords, getTrace } from './get-trace'; +export { runInsightsQuery, type InsightsQueryOptions, type InsightsQueryResult } from './insights-query'; +export type { + CloudWatchSpanRecord, + CloudWatchTraceRecord, + FetchTraceRecordsOptions, + FetchTraceRecordsResult, + GetTraceOptions, + GetTraceResult, + ListTracesOptions, + ListTracesResult, + TraceEntry, +} from './types'; diff --git a/src/cli/operations/traces/insights-query.ts b/src/cli/operations/traces/insights-query.ts new file mode 100644 index 000000000..5a4da2031 --- /dev/null +++ b/src/cli/operations/traces/insights-query.ts @@ -0,0 +1,85 @@ +import { getCredentialProvider } from '../../aws'; +import { CloudWatchLogsClient, GetQueryResultsCommand, StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'; + +const DEFAULT_LOOKBACK_MS = 12 * 60 * 60 * 1000; + +export interface InsightsQueryOptions { + region: string; + logGroupName: string; + queryString: string; + startTime?: number; + endTime?: number; +} + +export interface InsightsQueryResult { + success: boolean; + rows?: Record[]; + error?: string; +} + +async function pollQueryResults(client: CloudWatchLogsClient, queryId: string): Promise { + for (let i = 0; i < 60; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); + + const queryResults = await client.send(new GetQueryResultsCommand({ queryId })); + const status = queryResults.status ?? 'Unknown'; + + if (status === 'Complete' || status === 'Failed' || status === 'Cancelled') { + if (status !== 'Complete') { + return { success: false, error: `Query ${status.toLowerCase()}` }; + } + + const rows = (queryResults.results ?? []).map(row => { + const fields: Record = {}; + for (const field of row) { + if (field.field && field.value) { + fields[field.field] = field.value; + } + } + return fields; + }); + return { success: true, rows }; + } + } + + return { success: false, error: 'Query timed out after 60 seconds' }; +} + +export async function runInsightsQuery(options: InsightsQueryOptions): Promise { + const { region, logGroupName, queryString } = options; + + const client = new CloudWatchLogsClient({ + credentials: getCredentialProvider(), + region, + }); + + const now = Date.now(); + const endTime = options.endTime ?? now; + const startTime = options.startTime ?? endTime - DEFAULT_LOOKBACK_MS; + + try { + const startQuery = await client.send( + new StartQueryCommand({ + logGroupName, + startTime: Math.floor(startTime / 1000), + endTime: Math.floor(endTime / 1000), + queryString, + }) + ); + + if (!startQuery.queryId) { + return { success: false, error: 'Failed to start CloudWatch Logs Insights query' }; + } + + return await pollQueryResults(client, startQuery.queryId); + } catch (error: unknown) { + const err = error as Error; + if (err.name === 'ResourceNotFoundException') { + return { + success: false, + error: `Log group '${logGroupName}' not found. The agent may not have been invoked yet, or traces may not be enabled.`, + }; + } + return { success: false, error: err.message ?? String(error) }; + } +} diff --git a/src/cli/operations/traces/list-traces.ts b/src/cli/operations/traces/list-traces.ts index 7bff6194a..e2d998578 100644 --- a/src/cli/operations/traces/list-traces.ts +++ b/src/cli/operations/traces/list-traces.ts @@ -1,28 +1,6 @@ -import { getCredentialProvider } from '../../aws'; import { DEFAULT_ENDPOINT_NAME } from '../../constants'; -import { CloudWatchLogsClient, GetQueryResultsCommand, StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'; - -export interface TraceEntry { - traceId: string; - timestamp: string; - sessionId?: string; - spanCount?: string; -} - -export interface ListTracesOptions { - region: string; - runtimeId: string; - agentName: string; - limit?: number; - startTime?: number; - endTime?: number; -} - -export interface ListTracesResult { - success: boolean; - traces?: TraceEntry[]; - error?: string; -} +import { runInsightsQuery } from './insights-query'; +import type { ListTracesOptions, ListTracesResult, TraceEntry } from './types'; /** * Lists recent traces for a deployed agent by querying CloudWatch Logs Insights. @@ -33,80 +11,33 @@ export interface ListTracesResult { export async function listTraces(options: ListTracesOptions): Promise { const { region, runtimeId, limit = 20 } = options; - const client = new CloudWatchLogsClient({ - credentials: getCredentialProvider(), - region, - }); - const logGroupName = `/aws/bedrock-agentcore/runtimes/${runtimeId}-${DEFAULT_ENDPOINT_NAME}`; - const now = Date.now(); - const endTime = options.endTime ?? now; - const startTime = options.startTime ?? endTime - 12 * 60 * 60 * 1000; // default: last 12 hours - - try { - const startQuery = await client.send( - new StartQueryCommand({ - logGroupName, - startTime: Math.floor(startTime / 1000), - endTime: Math.floor(endTime / 1000), - queryString: `stats earliest(@timestamp) as firstSeen, latest(@timestamp) as lastSeen, count(*) as spanCount, earliest(attributes.session.id) as sessionId by traceId + const result = await runInsightsQuery({ + region, + logGroupName, + startTime: options.startTime, + endTime: options.endTime, + queryString: `stats earliest(@timestamp) as firstSeen, latest(@timestamp) as lastSeen, count(*) as spanCount, earliest(attributes.session.id) as sessionId by traceId | sort lastSeen desc | limit ${limit}`, - }) - ); - - if (!startQuery.queryId) { - return { success: false, error: 'Failed to start CloudWatch Logs Insights query' }; - } - - // Poll for results - let status = 'Running'; - let results: TraceEntry[] = []; - - for (let i = 0; i < 60; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); - - const queryResults = await client.send(new GetQueryResultsCommand({ queryId: startQuery.queryId })); - - status = queryResults.status ?? 'Unknown'; - - if (status === 'Complete' || status === 'Failed' || status === 'Cancelled') { - if (status !== 'Complete') { - return { success: false, error: `Query ${status.toLowerCase()}` }; - } + }); - results = (queryResults.results ?? []).map(row => { - const fields: Record = {}; - for (const field of row) { - if (field.field && field.value) { - fields[field.field] = field.value; - } - } - return { - traceId: fields.traceId ?? 'unknown', - timestamp: fields.lastSeen ?? fields.firstSeen ?? 'unknown', - sessionId: fields.sessionId, - spanCount: fields.spanCount, - }; - }); - break; - } - } + if (!result.success) { + return { success: false, error: result.error }; + } - if (status === 'Running') { - return { success: false, error: 'Query timed out after 60 seconds' }; + const traces = (result.rows ?? []).reduce((acc, row) => { + if (row.traceId) { + acc.push({ + traceId: row.traceId, + timestamp: row.lastSeen ?? row.firstSeen ?? 'unknown', + sessionId: row.sessionId, + spanCount: row.spanCount, + }); } + return acc; + }, []); - return { success: true, traces: results }; - } catch (error: unknown) { - const err = error as Error; - if (err.name === 'ResourceNotFoundException') { - return { - success: false, - error: `Log group '${logGroupName}' not found. The agent may not have been invoked yet, or traces may not be enabled.`, - }; - } - return { success: false, error: err.message ?? String(error) }; - } + return { success: true, traces }; } diff --git a/src/cli/operations/traces/types.ts b/src/cli/operations/traces/types.ts new file mode 100644 index 000000000..fae88a83b --- /dev/null +++ b/src/cli/operations/traces/types.ts @@ -0,0 +1,77 @@ +export interface CloudWatchTraceRecord { + '@timestamp': string; + '@message': unknown; + '@ptr'?: string; +} + +export interface CloudWatchSpanRecord { + traceId: string; + spanId: string; + parentSpanId?: string; + name?: string; + kind?: string; + startTimeUnixNano?: string; + endTimeUnixNano?: string; + durationNano?: string; + statusCode?: string; + serviceName?: string; + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + httpStatusCode?: number; + sessionId?: string; +} + +export interface FetchTraceRecordsOptions { + region: string; + runtimeId: string; + traceId: string; + startTime?: number; + endTime?: number; + includeSpans?: boolean; +} + +export interface FetchTraceRecordsResult { + success: boolean; + records?: CloudWatchTraceRecord[]; + spans?: CloudWatchSpanRecord[]; + error?: string; +} + +export interface GetTraceOptions { + region: string; + runtimeId: string; + agentName: string; + traceId: string; + outputPath?: string; + startTime?: number; + endTime?: number; +} + +export interface GetTraceResult { + success: boolean; + filePath?: string; + error?: string; +} + +export interface TraceEntry { + traceId: string; + timestamp: string; + sessionId?: string; + spanCount?: string; +} + +export interface ListTracesOptions { + region: string; + runtimeId: string; + agentName: string; + limit?: number; + startTime?: number; + endTime?: number; +} + +export interface ListTracesResult { + success: boolean; + traces?: TraceEntry[]; + error?: string; +} diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index 4702633ed..8f354a43c 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -34,6 +34,19 @@ import { import { executeImportAgent } from '../operations/agent/import'; import { setupPythonProject } from '../operations/python'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; +import { + AgentType, + AuthorizerType, + Build, + Framework, + Language, + Memory, + ModelProvider as ModelProviderEnum, + NetworkMode as NetworkModeEnum, + Protocol, + standardize, +} from '../telemetry/schemas/common-shapes.js'; import { createRenderer } from '../templates'; import { requireTTY } from '../tui/guards/tty'; import type { GenerateConfig, MemoryOption } from '../tui/screens/generate/types'; @@ -264,92 +277,106 @@ export class AgentPrimitive extends BasePrimitive { + const validation = validateAddAgentOptions(cliOptions); + if (!validation.valid) { + throw new Error(validation.error); } - process.exit(1); - } - - // Parse custom claims JSON if provided (already validated by validateAddAgentOptions) - const customClaims = cliOptions.customClaims - ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) - : undefined; - - // Parse request header allowlist if provided - const requestHeaderAllowlist = cliOptions.requestHeaderAllowlist - ? parseAndNormalizeHeaders(cliOptions.requestHeaderAllowlist) - : undefined; - - const result = await this.add({ - name: cliOptions.name!, - type: cliOptions.type ?? 'create', - buildType: (cliOptions.build as BuildType) ?? 'CodeZip', - language: cliOptions.language!, - framework: cliOptions.framework!, - modelProvider: cliOptions.modelProvider!, - apiKey: cliOptions.apiKey, - memory: cliOptions.memory, - protocol: cliOptions.protocol, - networkMode: cliOptions.networkMode, - subnets: cliOptions.subnets, - securityGroups: cliOptions.securityGroups, - requestHeaderAllowlist, - codeLocation: cliOptions.codeLocation, - entrypoint: cliOptions.entrypoint, - bedrockAgentId: cliOptions.agentId, - bedrockAliasId: cliOptions.agentAliasId, - bedrockRegion: cliOptions.region, - authorizerType: cliOptions.authorizerType, - discoveryUrl: cliOptions.discoveryUrl, - allowedAudience: cliOptions.allowedAudience, - allowedClients: cliOptions.allowedClients, - allowedScopes: cliOptions.allowedScopes, - customClaims, - clientId: cliOptions.clientId, - clientSecret: cliOptions.clientSecret, - idleTimeout: cliOptions.idleTimeout ? Number(cliOptions.idleTimeout) : undefined, - maxLifetime: cliOptions.maxLifetime ? Number(cliOptions.maxLifetime) : undefined, - sessionStorageMountPath: cliOptions.sessionStorageMountPath, - }); - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added agent '${result.agentName}'`); - if (result.agentPath) { - console.log(`Agent code: ${result.agentPath}`); + // Parse custom claims JSON if provided (already validated by validateAddAgentOptions) + const customClaims = cliOptions.customClaims + ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) + : undefined; + + // Parse request header allowlist if provided + const requestHeaderAllowlist = cliOptions.requestHeaderAllowlist + ? parseAndNormalizeHeaders(cliOptions.requestHeaderAllowlist) + : undefined; + + const result = await this.add({ + name: cliOptions.name!, + type: cliOptions.type ?? 'create', + buildType: (cliOptions.build as BuildType) ?? 'CodeZip', + language: cliOptions.language!, + framework: cliOptions.framework!, + modelProvider: cliOptions.modelProvider!, + apiKey: cliOptions.apiKey, + memory: cliOptions.memory, + protocol: cliOptions.protocol, + networkMode: cliOptions.networkMode, + subnets: cliOptions.subnets, + securityGroups: cliOptions.securityGroups, + requestHeaderAllowlist, + codeLocation: cliOptions.codeLocation, + entrypoint: cliOptions.entrypoint, + bedrockAgentId: cliOptions.agentId, + bedrockAliasId: cliOptions.agentAliasId, + bedrockRegion: cliOptions.region, + authorizerType: cliOptions.authorizerType, + discoveryUrl: cliOptions.discoveryUrl, + allowedAudience: cliOptions.allowedAudience, + allowedClients: cliOptions.allowedClients, + allowedScopes: cliOptions.allowedScopes, + customClaims, + clientId: cliOptions.clientId, + clientSecret: cliOptions.clientSecret, + idleTimeout: cliOptions.idleTimeout ? Number(cliOptions.idleTimeout) : undefined, + maxLifetime: cliOptions.maxLifetime ? Number(cliOptions.maxLifetime) : undefined, + sessionStorageMountPath: cliOptions.sessionStorageMountPath, + }); + + if (!result.success) { + throw new Error(result.error); } - if (cliOptions.networkMode === 'VPC') { - console.log(`\x1b[33mNote: ${VPC_ENDPOINT_WARNING}\x1b[0m`); + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + console.log(`Added agent '${result.agentName}'`); + if (result.agentPath) { + console.log(`Agent code: ${result.agentPath}`); + } + if (cliOptions.networkMode === 'VPC') { + console.log(`\x1b[33mNote: ${VPC_ENDPOINT_WARNING}\x1b[0m`); + } } - } else { - console.error(result.error); - } - process.exit(result.success ? 0 : 1); + return { + language: standardize(Language, cliOptions.language), + framework: standardize(Framework, cliOptions.framework), + model_provider: standardize(ModelProviderEnum, cliOptions.modelProvider), + agent_type: standardize(AgentType, cliOptions.type ?? 'create'), + build: standardize(Build, cliOptions.build ?? 'CodeZip'), + protocol: standardize(Protocol, cliOptions.protocol ?? 'HTTP'), + network_mode: standardize(NetworkModeEnum, cliOptions.networkMode ?? 'PUBLIC'), + authorizer_type: standardize(AuthorizerType, cliOptions.authorizerType ?? 'NONE'), + memory: standardize(Memory, cliOptions.memory ?? 'none'), + }; + }); } else { - // TUI fallback — dynamic imports to avoid pulling ink (async) into registry - requireTTY(); - const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/add/AddFlow'), - ]); - const { clear, unmount } = render( - React.createElement(AddFlow, { - isInteractive: false, - initialResource: 'agent', - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); + try { + // TUI fallback — dynamic imports to avoid pulling ink (async) into registry + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'agent', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } } }); diff --git a/src/cli/primitives/CredentialPrimitive.tsx b/src/cli/primitives/CredentialPrimitive.tsx index 52f578235..9607094f8 100644 --- a/src/cli/primitives/CredentialPrimitive.tsx +++ b/src/cli/primitives/CredentialPrimitive.tsx @@ -4,6 +4,8 @@ import { CredentialSchema } from '../../schema'; import { validateAddCredentialOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; +import { CredentialType, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; import { computeDefaultCredentialEnvVarName } from './credential-utils'; @@ -273,23 +275,23 @@ export class CredentialPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - if ( - cliOptions.name || - cliOptions.apiKey || - cliOptions.json || - cliOptions.type || - cliOptions.discoveryUrl || - cliOptions.clientId || - cliOptions.clientSecret || - cliOptions.scopes - ) { - // CLI mode + if ( + cliOptions.name || + cliOptions.apiKey || + cliOptions.json || + cliOptions.type || + cliOptions.discoveryUrl || + cliOptions.clientId || + cliOptions.clientSecret || + cliOptions.scopes + ) { + // CLI mode + await cliCommandRun('add.credential', !!cliOptions.json, async () => { const validation = validateAddCredentialOptions({ name: cliOptions.name, type: cliOptions.type as 'api-key' | 'oauth' | undefined, @@ -301,12 +303,7 @@ export class CredentialPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - if (cliOptions.name || cliOptions.json) { - const fail = (error: string) => { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error })); - } else { - console.error(error); - } - process.exit(1); + if (cliOptions.name || cliOptions.json) { + await cliCommandRun('add.evaluator', !!cliOptions.json, async () => { + const fail = (error: string): never => { + throw new Error(error); }; if (!cliOptions.name || !cliOptions.level) { @@ -298,9 +295,13 @@ export class EvaluatorPrimitive extends BasePrimitive) => { const cliOptions = rawOptions as unknown as CLIAddGatewayOptions; - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + await cliCommandRun('add.gateway', !!cliOptions.json, async () => { const validation = validateAddGatewayOptions(cliOptions); if (!validation.valid) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: validation.error })); - } else { - console.error(validation.error); - } - process.exit(1); + throw new Error(validation.error); } // Parse custom claims JSON if provided (already validated) @@ -221,23 +217,30 @@ export class GatewayPrimitive extends BasePrimitive s.trim()) + .filter(Boolean).length + : 0; + return { + authorizer_type: standardize(AuthorizerType, cliOptions.authorizerType ?? 'NONE'), + has_policy_engine: !!cliOptions.policyEngine, + policy_engine_mode: standardize(PolicyEngineMode, cliOptions.policyEngineMode ?? 'log_only'), + semantic_search: cliOptions.semanticSearch !== false, + runtime_count: runtimeCount, + }; + }); }); removeCmd diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 41a2e6a75..e8a1da996 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -14,6 +14,13 @@ import { validateAddGatewayTargetOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovableGatewayTarget } from '../operations/remove/remove-gateway-target'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; +import { + GATEWAY_TARGET_TYPE_MAP, + GatewayTargetHost, + OutboundAuth, + standardize, +} from '../telemetry/schemas/common-shapes.js'; import { getTemplateToolDefinitions, renderGatewayTargetTemplate } from '../templates/GatewayTargetRenderer'; import { requireTTY } from '../tui/guards/tty'; import type { @@ -297,20 +304,15 @@ export class GatewayTargetPrimitive extends BasePrimitive { const validation = await validateAddGatewayTargetOptions(cliOptions); if (!validation.valid) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: validation.error })); - } else { - console.error(validation.error); - } - process.exit(1); + throw new Error(validation.error); } // Map CLI flag values to internal types @@ -321,6 +323,19 @@ export class GatewayTargetPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - if (cliOptions.name || cliOptions.json) { - // CLI mode + if (cliOptions.name || cliOptions.json) { + // CLI mode + await cliCommandRun('add.memory', !!cliOptions.json, async () => { const expiry = cliOptions.expiry ? parseInt(cliOptions.expiry, 10) : undefined; const validation = validateAddMemoryOptions({ name: cliOptions.name, @@ -203,12 +204,7 @@ export class MemoryPrimitive extends BasePrimitive s.trim().toUpperCase()) + .filter(Boolean); + return { + strategy_count: strategyList.length, + strategy_semantic: strategyList.includes('SEMANTIC'), + strategy_summarization: strategyList.includes('SUMMARIZATION'), + strategy_user_preference: strategyList.includes('USER_PREFERENCE'), + strategy_episodic: strategyList.includes('EPISODIC'), + }; + }); + } else { + try { // TUI fallback — dynamic imports to avoid pulling ink (async) into registry requireTTY(); const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ @@ -248,14 +259,10 @@ export class MemoryPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - if (cliOptions.name || cliOptions.json) { - // Merge --evaluator and --evaluator-arn into a single list - const allEvaluators = [...(cliOptions.evaluator ?? []), ...(cliOptions.evaluatorArn ?? [])]; + if (cliOptions.name || cliOptions.json) { + // Merge --evaluator and --evaluator-arn into a single list + const allEvaluators = [...(cliOptions.evaluator ?? []), ...(cliOptions.evaluatorArn ?? [])]; + await cliCommandRun('add.online-eval', !!cliOptions.json, async () => { if (!cliOptions.name || !cliOptions.runtime || allEvaluators.length === 0 || !cliOptions.samplingRate) { - const error = - '--name, --runtime, --evaluator (and/or --evaluator-arn), and --sampling-rate are all required in non-interactive mode'; - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error })); - } else { - console.error(error); - } - process.exit(1); + throw new Error( + '--name, --runtime, --evaluator (and/or --evaluator-arn), and --sampling-rate are all required in non-interactive mode' + ); } // Sampling rate as a percentage of requests to evaluate (0.01% to 100%) const samplingRate = parseFloat(cliOptions.samplingRate); if (isNaN(samplingRate) || samplingRate < 0.01 || samplingRate > 100) { - const error = `Invalid --sampling-rate "${cliOptions.samplingRate}". Must be a percentage between 0.01 and 100`; - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error })); - } else { - console.error(error); - } - process.exit(1); + throw new Error( + `Invalid --sampling-rate "${cliOptions.samplingRate}". Must be a percentage between 0.01 and 100` + ); } const result = await this.add({ @@ -162,15 +154,23 @@ export class OnlineEvalConfigPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - if (cliOptions.name || cliOptions.description || cliOptions.encryptionKeyArn || cliOptions.json) { + if (cliOptions.name || cliOptions.description || cliOptions.encryptionKeyArn || cliOptions.json) { + await cliCommandRun('add.policy-engine', !!cliOptions.json, async () => { if (!cliOptions.name) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: '--name is required' })); - } else { - console.error('--name is required'); - } - process.exit(1); + throw new Error('--name is required'); } const result = await this.add({ @@ -253,15 +250,29 @@ export class PolicyEnginePrimitive extends BasePrimitive s.trim()) + .filter(Boolean).length + : 0; + return { + attach_gateway_count: gatewayCount, + attach_mode: standardize(AttachMode, cliOptions.attachMode ?? 'log_only'), + }; + }); + } else { + try { requireTTY(); const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ import('ink'), @@ -278,14 +289,10 @@ export class PolicyEnginePrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - if ( - cliOptions.name || - cliOptions.engine || - cliOptions.source || - cliOptions.statement || - cliOptions.generate || - cliOptions.json - ) { + if ( + cliOptions.name || + cliOptions.engine || + cliOptions.source || + cliOptions.statement || + cliOptions.generate || + cliOptions.json + ) { + await cliCommandRun('add.policy', !!cliOptions.json, async () => { if (!cliOptions.name) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: '--name is required' })); - } else { - console.error('--name is required'); - } - process.exit(1); + throw new Error('--name is required'); } if (!cliOptions.engine) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: '--engine is required' })); - } else { - console.error('--engine is required'); - } - process.exit(1); + throw new Error('--engine is required'); } const result = await this.add({ @@ -335,15 +327,28 @@ export class PolicyPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + await cliCommandRun('add.runtime-endpoint', !!cliOptions.json, async () => { const result = await this.add({ runtime: cliOptions.runtime, endpoint: cliOptions.endpoint, @@ -261,23 +262,18 @@ export class RuntimeEndpointPrimitive extends BasePrimitive { expect(sink.metrics[0]!.attrs.check_only).toBe('true'); }); - it('silently drops invalid success payloads', async () => { + it('publishes metric with unknown defaults for incomplete success payloads', async () => { const sink = new InMemorySink(); const client = new TelemetryClient(sink); - // Missing required attrs for 'create' — should silently drop + // Missing required attrs for 'create' — should still publish with 'unknown' defaults await client.withCommandRun( 'create', // @ts-expect-error — intentionally incomplete async () => ({ language: 'python' }) ); - expect(sink.metrics).toHaveLength(0); + expect(sink.metrics).toHaveLength(1); + expect(sink.metrics[0]!.attrs).toMatchObject({ + exit_reason: 'success', + language: 'python', + framework: 'unknown', + model_provider: 'unknown', + }); + }); + + it('defaults invalid attrs to unknown while preserving valid ones', async () => { + const sink = new InMemorySink(); + const client = new TelemetryClient(sink); + + await client.withCommandRun( + 'create', + // @ts-expect-error — intentionally invalid enum value + async () => ({ + language: 'rust', // invalid enum + framework: 'strands', + model_provider: 'bedrock', + memory: 'shortterm', + protocol: 'mcp', + build: 'codezip', + agent_type: 'create', + network_mode: 'public', + has_agent: true, + }) + ); + + expect(sink.metrics).toHaveLength(1); + expect(sink.metrics[0]!.attrs.language).toBe('unknown'); + expect(sink.metrics[0]!.attrs.framework).toBe('strands'); }); it('records cancel when callback returns CANCELLED', async () => { diff --git a/src/cli/telemetry/__tests__/filesystem-sink.test.ts b/src/cli/telemetry/__tests__/filesystem-sink.test.ts new file mode 100644 index 000000000..50d8d4620 --- /dev/null +++ b/src/cli/telemetry/__tests__/filesystem-sink.test.ts @@ -0,0 +1,95 @@ +import { createTempConfig } from '../../__tests__/helpers/temp-config'; +import { resolveAuditFilePath } from '../config'; +import { FileSystemSink } from '../sinks/filesystem-sink'; +import { readFile } from 'fs/promises'; +import { join } from 'node:path'; +import { afterAll, beforeEach, describe, expect, it } from 'vitest'; + +const tmp = createTempConfig('fs-sink'); +const outputDir = join(tmp.configDir, 'telemetry'); + +function createSink(opts: { dir?: string; log?: (msg: string) => void } = {}) { + const filePath = join(opts.dir ?? outputDir, 'test-session.json'); + return new FileSystemSink({ filePath, log: opts.log }); +} + +function readJsonl(path: string): Promise { + return readFile(path, 'utf-8').then(data => + data + .trim() + .split('\n') + .map(line => JSON.parse(line)) + ); +} + +describe('FileSystemSink', () => { + beforeEach(() => tmp.setup()); + afterAll(() => tmp.cleanup()); + + it('writes each record as a JSONL line on disk', async () => { + const sink = createSink(); + sink.record(42, { command_group: 'deploy', command: 'deploy', exit_reason: 'success' }); + await sink.flush(); + + const entries = await readJsonl(join(outputDir, 'test-session.json')); + expect(entries).toHaveLength(1); + expect(entries[0]).toMatchObject({ + value: 42, + attrs: { command_group: 'deploy', command: 'deploy', exit_reason: 'success' }, + }); + }); + + it('appends multiple records as separate lines', async () => { + const sink = createSink(); + sink.record(10, { command_group: 'add', command: 'add.agent' }); + sink.record(20, { command_group: 'add', command: 'add.memory' }); + await sink.flush(); + + const entries = await readJsonl(join(outputDir, 'test-session.json')); + expect(entries).toHaveLength(2); + expect(entries[0]).toMatchObject({ value: 10 }); + expect(entries[1]).toMatchObject({ value: 20 }); + }); + + it('creates output directory if it does not exist', async () => { + const nested = join(tmp.testDir, 'deep', 'nested', 'telemetry'); + const filePath = join(nested, 'test.json'); + const sink = new FileSystemSink({ filePath }); + sink.record(1, { command_group: 'status', command: 'status' }); + await sink.flush(); + + const entries = await readJsonl(filePath); + expect(entries).toHaveLength(1); + }); + + it('flush is a no-op when no records exist', async () => { + const sink = createSink(); + await expect(sink.flush()).resolves.toBeUndefined(); + }); + + it('shutdown logs audit message when records were written', async () => { + const logged: string[] = []; + const sink = createSink({ log: msg => logged.push(msg) }); + sink.record(99, { command_group: 'invoke', command: 'invoke' }); + await sink.shutdown(); + + expect(logged).toHaveLength(1); + expect(logged[0]).toContain('[audit mode]'); + expect(logged[0]).toContain('test-session.json'); + }); + + it('shutdown does not log when no records were written', async () => { + const logged: string[] = []; + const sink = createSink({ log: msg => logged.push(msg) }); + await sink.shutdown(); + + expect(logged).toHaveLength(0); + }); +}); + +describe('resolveAuditFilePath', () => { + it('joins outputDir, entrypoint, and sessionId into a JSON file path', () => { + const path = resolveAuditFilePath('/home/user/.agentcore/telemetry', 'deploy', 'abc-123'); + expect(path).toBe('/home/user/.agentcore/telemetry/deploy-abc-123.json'); + }); +}); diff --git a/src/cli/telemetry/cli-command-run.ts b/src/cli/telemetry/cli-command-run.ts new file mode 100644 index 000000000..987f05730 --- /dev/null +++ b/src/cli/telemetry/cli-command-run.ts @@ -0,0 +1,71 @@ +import { getErrorMessage } from '../errors'; +import type { AddResult } from '../primitives/types.js'; +import { TelemetryClientAccessor } from './client-accessor.js'; +import type { Command, CommandAttrs } from './schemas/command-run.js'; + +/** + * Run a CLI command with telemetry, standardized error output, and process.exit. + * The callback should throw on failure and return telemetry attrs on success. + * + * If telemetry initialization fails, the command still runs without telemetry — + * telemetry must never block CLI behavior. + */ +export async function cliCommandRun( + command: C, + json: boolean, + fn: () => Promise> +): Promise { + try { + let client; + try { + client = await TelemetryClientAccessor.get(); + } catch { + // Telemetry init failed — run without it + await fn(); + process.exit(0); + } + // withCommandRun records success/failure telemetry, then re-throws on failure + await client.withCommandRun(command, fn); + process.exit(0); + } catch (error) { + if (json) { + console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + } else { + console.error(getErrorMessage(error)); + } + process.exit(1); + } +} + +/** + * Wrap a primitive .add() call with telemetry — used by TUI paths. + * CLI paths use {@link cliCommandRun} instead. + */ +export async function withAddTelemetry>( + command: C, + attrs: CommandAttrs, + fn: () => Promise> +): Promise> { + let client; + try { + client = await TelemetryClientAccessor.get(); + } catch { + return fn(); + } + + let result: AddResult | undefined; + try { + await client.withCommandRun(command, async () => { + result = await fn(); + if (!result.success) throw new Error(result.error); + return attrs; + }); + } catch (err) { + // withCommandRun re-throws after recording failure telemetry. + // result is set if fn() ran; if not, fn() itself threw. + if (!result) { + return { success: false, error: getErrorMessage(err) }; + } + } + return result!; +} diff --git a/src/cli/telemetry/client-accessor.ts b/src/cli/telemetry/client-accessor.ts new file mode 100644 index 000000000..04172dae5 --- /dev/null +++ b/src/cli/telemetry/client-accessor.ts @@ -0,0 +1,49 @@ +import { GLOBAL_CONFIG_DIR, readGlobalConfig } from '../../lib/schemas/io/global-config.js'; +import { TelemetryClient } from './client.js'; +import { resolveAuditFilePath, resolveResourceAttributes } from './config.js'; +import { FileSystemSink } from './sinks/filesystem-sink.js'; +import { CompositeSink } from './sinks/metric-sink.js'; +import { join } from 'path'; + +/** + * Manages a singleton TelemetryClient. Call init() at startup to configure, + * get() from command handlers to obtain the client, and shutdown() on exit. + * get() lazily initializes if init() was never called. + */ +export class TelemetryClientAccessor { + private static clientPromise: Promise | undefined; + + static init(entrypoint: string, mode: 'cli' | 'tui' = 'cli'): void { + this.clientPromise = createClient(entrypoint, mode); + } + + static get(): Promise { + this.clientPromise ??= createClient('unknown'); + return this.clientPromise; + } + + static async shutdown(): Promise { + if (this.clientPromise) { + const client = await this.clientPromise; + await client.shutdown(); + } + } +} + +async function createClient(entrypoint: string, mode: 'cli' | 'tui' = 'cli'): Promise { + const [resource, config] = await Promise.all([resolveResourceAttributes(mode), readGlobalConfig()]); + + const sinks = []; + const audit = process.env.AGENTCORE_TELEMETRY_AUDIT === '1' || config.telemetry?.audit === true; + + if (audit) { + const filePath = resolveAuditFilePath( + join(GLOBAL_CONFIG_DIR, 'telemetry'), + entrypoint, + resource['agentcore-cli.session_id'] + ); + sinks.push(new FileSystemSink({ filePath, resource })); + } + + return new TelemetryClient(new CompositeSink(sinks)); +} diff --git a/src/cli/telemetry/client.ts b/src/cli/telemetry/client.ts index 3228f45b1..91dffd94f 100644 --- a/src/cli/telemetry/client.ts +++ b/src/cli/telemetry/client.ts @@ -1,6 +1,6 @@ import { classifyError, isUserError } from './error-classification.js'; import { COMMAND_SCHEMAS, type Command, type CommandAttrs, deriveCommandGroup } from './schemas/command-run.js'; -import { type CommandResult, CommandResultSchema } from './schemas/common-shapes.js'; +import { type CommandResult, CommandResultSchema, resilientParse } from './schemas/common-shapes.js'; import type { MetricSink } from './sinks/metric-sink.js'; import { performance } from 'perf_hooks'; @@ -69,17 +69,24 @@ export class TelemetryClient { durationMs: number ): void { try { + // CommandResult is built internally — hard parse is intentional since + // a metric without a valid exit_reason is meaningless. CommandResultSchema.parse(result); - if (result.exit_reason !== 'failure' && result.exit_reason !== 'cancel') { - COMMAND_SCHEMAS[command].parse(attrs); - } + + // Validate command attrs resiliently: invalid fields default to 'unknown' + // instead of dropping the entire metric. + // On failure/cancel the callback attrs are empty so validation is skipped. + const validatedAttrs = + result.exit_reason !== 'failure' && result.exit_reason !== 'cancel' + ? resilientParse(COMMAND_SCHEMAS[command], attrs as Record) + : attrs; const otelAttrs: Record = { command_group: deriveCommandGroup(command), command, }; - for (const obj of [result, attrs]) { + for (const obj of [result, validatedAttrs]) { for (const [k, v] of Object.entries(obj)) { if (typeof v === 'boolean') { otelAttrs[k] = String(v); diff --git a/src/cli/telemetry/config.ts b/src/cli/telemetry/config.ts index 5bee94eff..fbaa3fb13 100644 --- a/src/cli/telemetry/config.ts +++ b/src/cli/telemetry/config.ts @@ -1,8 +1,9 @@ +import { getOrCreateInstallationId, readGlobalConfig } from '../../lib/schemas/io/global-config.js'; import { PACKAGE_VERSION } from '../constants.js'; -import { getOrCreateInstallationId, readGlobalConfig } from '../global-config.js'; import { type ResourceAttributes, ResourceAttributesSchema } from './schemas/common-attributes.js'; import { randomUUID } from 'crypto'; import os from 'os'; +import { join } from 'path'; // --------------------------------------------------------------------------- // Telemetry preference (opt-in / opt-out) @@ -59,3 +60,7 @@ export async function resolveResourceAttributes(mode: 'cli' | 'tui'): Promise { } }); }); + +describe('resilientParse', () => { + it('passes valid attrs through unchanged', () => { + const attrs = { + language: 'python', + framework: 'strands', + model_provider: 'bedrock', + memory: 'shortterm', + protocol: 'mcp', + build: 'codezip', + agent_type: 'create', + network_mode: 'public', + has_agent: true, + }; + expect(resilientParse(COMMAND_SCHEMAS.create, attrs)).toEqual(attrs); + }); + + it('defaults a single invalid enum field to unknown', () => { + const attrs = { + language: 'rust', // invalid + framework: 'strands', + model_provider: 'bedrock', + memory: 'shortterm', + protocol: 'mcp', + build: 'codezip', + agent_type: 'create', + network_mode: 'public', + has_agent: true, + }; + const result = resilientParse(COMMAND_SCHEMAS.create, attrs); + expect(result.language).toBe('unknown'); + expect(result.framework).toBe('strands'); + }); + + it('defaults missing required fields to unknown', () => { + const result = resilientParse(COMMAND_SCHEMAS.create, { language: 'python' }); + expect(result.language).toBe('python'); + expect(result.framework).toBe('unknown'); + expect(result.model_provider).toBe('unknown'); + }); + + it('defaults all fields to unknown when all are invalid', () => { + const result = resilientParse(COMMAND_SCHEMAS.create, {}); + for (const value of Object.values(result)) { + expect(value).toBe('unknown'); + } + }); + + it('returns empty object for no-attrs schemas', () => { + expect(resilientParse(COMMAND_SCHEMAS['telemetry.disable'], {})).toEqual({}); + }); +}); diff --git a/src/cli/telemetry/schemas/command-run.ts b/src/cli/telemetry/schemas/command-run.ts index f8a6df436..0acfcaf1b 100644 --- a/src/cli/telemetry/schemas/command-run.ts +++ b/src/cli/telemetry/schemas/command-run.ts @@ -157,6 +157,7 @@ export const COMMAND_SCHEMAS = { 'add.gateway-target': AddGatewayTargetAttrs, 'add.policy-engine': AddPolicyEngineAttrs, 'add.policy': AddPolicyAttrs, + 'add.runtime-endpoint': NoAttrs, // deploy deploy: DeployAttrs, @@ -193,6 +194,7 @@ export const COMMAND_SCHEMAS = { package: NoAttrs, validate: NoAttrs, 'help.modes': NoAttrs, + help: NoAttrs, 'remove.agent': NoAttrs, 'remove.memory': NoAttrs, 'remove.credential': NoAttrs, diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 5c5e56493..4624883cd 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -8,6 +8,40 @@ export function safeSchema>(shape: T) { return z.object(shape); } +/** + * Validate each field in a schema individually, defaulting to 'unknown' on failure. + * This ensures a single invalid attribute never blocks the entire metric from being published. + * Keys in attrs not present in the schema are omitted from the result. + */ +export function resilientParse( + schema: z.ZodObject, + attrs: Record +): Record { + const result: Record = {}; + for (const key of Object.keys(schema.shape)) { + const field = schema.shape[key] as z.ZodType; + const parsed = field.safeParse(attrs[key]); + result[key] = parsed.success ? parsed.data : 'unknown'; + } + return result; +} + +/** + * Lowercase a CLI value and parse it through a Zod enum, returning the narrowed type. + * The `as` cast on the failure branch is intentional: invalid values pass through to + * recordCommandRun, where COMMAND_SCHEMAS[command].parse(attrs) validates the full + * attr object in a try/catch — silently dropping the metric if any field is invalid. + * This ensures telemetry never crashes the CLI while keeping the happy-path type-safe. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function standardize>(schema: T, value: string | undefined): z.infer { + const lower = (value ?? '').toLowerCase(); + const result = schema.safeParse(lower); + // If the value doesn't match the enum, return the lowercased value anyway — + // recordCommandRun's try/catch will silently drop the invalid metric. + return (result.success ? result.data : lower) as z.infer; +} + // Primitive types export const Count = z.number().int().nonnegative(); @@ -41,7 +75,17 @@ export const GatewayTargetType = z.enum([ 'open-api-schema', 'smithy-model', 'lambda-function-arn', + 'unknown', ]); + +/** Map camelCase CLI target type to kebab-case telemetry enum value. */ +export const GATEWAY_TARGET_TYPE_MAP: Record> = { + apiGateway: 'api-gateway', + openApiSchema: 'open-api-schema', + smithyModel: 'smithy-model', + lambdaFunctionArn: 'lambda-function-arn', + mcpServer: 'mcp-server', +}; export const Language = z.enum(['python', 'typescript', 'other']); export const Level = z.enum(['session', 'trace', 'tool_call']); export const Memory = z.enum(['none', 'shortterm', 'longandshortterm']); diff --git a/src/cli/telemetry/sinks/filesystem-sink.ts b/src/cli/telemetry/sinks/filesystem-sink.ts new file mode 100644 index 000000000..a9868f2b9 --- /dev/null +++ b/src/cli/telemetry/sinks/filesystem-sink.ts @@ -0,0 +1,48 @@ +import type { MetricSink } from './metric-sink.js'; +import { appendFile, mkdir } from 'fs/promises'; +import { dirname } from 'path'; + +export interface FileSystemSinkConfig { + filePath: string; + resource?: Record; + log?: (message: string) => void; +} + +export class FileSystemSink implements MetricSink { + private readonly filePath: string; + private readonly resource: Record; + private readonly log: (message: string) => void; + private hasRecords = false; + + constructor(config: FileSystemSinkConfig) { + this.filePath = config.filePath; + this.resource = config.resource ?? {}; + this.log = config.log ?? (msg => console.log(msg)); + } + + record(value: number, attrs: Record): void { + this.hasRecords = true; + this.pendingWrite = this.pendingWrite.then(() => + this.appendEntry({ value, attrs: { ...this.resource, ...attrs } }) + ); + } + + async flush(): Promise { + await this.pendingWrite; + } + + async shutdown(): Promise { + await this.pendingWrite; + if (this.hasRecords) { + this.log(`[audit mode] Telemetry written to ${this.filePath}`); + } + } + + // Promise chain that serializes async writes so record() can stay synchronous. + private pendingWrite: Promise = Promise.resolve(); + + private async appendEntry(entry: { value: number; attrs: Record }): Promise { + await mkdir(dirname(this.filePath), { recursive: true }); + await appendFile(this.filePath, JSON.stringify(entry) + '\n'); + } +} diff --git a/src/cli/tui/hooks/useCreateEvaluator.ts b/src/cli/tui/hooks/useCreateEvaluator.ts index 6e1d8f052..f1cad666f 100644 --- a/src/cli/tui/hooks/useCreateEvaluator.ts +++ b/src/cli/tui/hooks/useCreateEvaluator.ts @@ -1,5 +1,7 @@ import type { EvaluatorConfig } from '../../../schema'; import { evaluatorPrimitive } from '../../primitives/registry'; +import { withAddTelemetry } from '../../telemetry/cli-command-run.js'; +import { Level, standardize } from '../../telemetry/schemas/common-shapes.js'; import { useCallback, useEffect, useState } from 'react'; interface CreateEvaluatorConfig { @@ -16,11 +18,19 @@ export function useCreateEvaluator() { const create = useCallback(async (config: CreateEvaluatorConfig) => { setStatus({ state: 'loading' }); try { - const addResult = await evaluatorPrimitive.add({ - name: config.name, - level: config.level as 'SESSION' | 'TRACE' | 'TOOL_CALL', - config: config.config, - }); + const addResult = await withAddTelemetry( + 'add.evaluator', + { + evaluator_type: config.config.codeBased ? 'code-based' : 'llm-as-a-judge', + level: standardize(Level, config.level), + }, + () => + evaluatorPrimitive.add({ + name: config.name, + level: config.level as 'SESSION' | 'TRACE' | 'TOOL_CALL', + config: config.config, + }) + ); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create evaluator'); } diff --git a/src/cli/tui/hooks/useCreateMcp.ts b/src/cli/tui/hooks/useCreateMcp.ts index 2b3b3b25a..ec91666d0 100644 --- a/src/cli/tui/hooks/useCreateMcp.ts +++ b/src/cli/tui/hooks/useCreateMcp.ts @@ -4,6 +4,8 @@ import { gatewayTargetPrimitive, policyEnginePrimitive, } from '../../primitives/registry'; +import { withAddTelemetry } from '../../telemetry/cli-command-run.js'; +import { AuthorizerType, PolicyEngineMode, standardize } from '../../telemetry/schemas/common-shapes.js'; import type { AddGatewayConfig } from '../screens/mcp/types'; import { useCallback, useEffect, useState } from 'react'; @@ -23,22 +25,33 @@ export function useCreateGateway() { const createGateway = useCallback(async (config: AddGatewayConfig) => { setStatus({ state: 'loading' }); try { - const addResult = await gatewayPrimitive.add({ - name: config.name, - description: config.description, - authorizerType: config.authorizerType, - discoveryUrl: config.jwtConfig?.discoveryUrl, - allowedAudience: config.jwtConfig?.allowedAudience?.join(','), - allowedClients: config.jwtConfig?.allowedClients?.join(','), - allowedScopes: config.jwtConfig?.allowedScopes?.join(','), - customClaims: config.jwtConfig?.customClaims, - clientId: config.jwtConfig?.clientId, - clientSecret: config.jwtConfig?.clientSecret, - enableSemanticSearch: config.enableSemanticSearch, - exceptionLevel: config.exceptionLevel, - policyEngine: config.policyEngineConfiguration?.policyEngineName, - policyEngineMode: config.policyEngineConfiguration?.mode, - }); + const addResult = await withAddTelemetry( + 'add.gateway', + { + authorizer_type: standardize(AuthorizerType, config.authorizerType ?? 'NONE'), + has_policy_engine: !!config.policyEngineConfiguration?.policyEngineName, + policy_engine_mode: standardize(PolicyEngineMode, config.policyEngineConfiguration?.mode ?? 'log_only'), + semantic_search: config.enableSemanticSearch !== false, + runtime_count: 0, + }, + () => + gatewayPrimitive.add({ + name: config.name, + description: config.description, + authorizerType: config.authorizerType, + discoveryUrl: config.jwtConfig?.discoveryUrl, + allowedAudience: config.jwtConfig?.allowedAudience?.join(','), + allowedClients: config.jwtConfig?.allowedClients?.join(','), + allowedScopes: config.jwtConfig?.allowedScopes?.join(','), + customClaims: config.jwtConfig?.customClaims, + clientId: config.jwtConfig?.clientId, + clientSecret: config.jwtConfig?.clientSecret, + enableSemanticSearch: config.enableSemanticSearch, + exceptionLevel: config.exceptionLevel, + policyEngine: config.policyEngineConfiguration?.policyEngineName, + policyEngineMode: config.policyEngineConfiguration?.mode, + }) + ); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create gateway'); } diff --git a/src/cli/tui/hooks/useCreateMemory.ts b/src/cli/tui/hooks/useCreateMemory.ts index 4345b4ead..d4196582f 100644 --- a/src/cli/tui/hooks/useCreateMemory.ts +++ b/src/cli/tui/hooks/useCreateMemory.ts @@ -2,6 +2,7 @@ import { ConfigIO } from '../../../lib'; import type { Memory } from '../../../schema'; import { getAvailableAgents } from '../../operations/attach'; import { memoryPrimitive } from '../../primitives/registry'; +import { withAddTelemetry } from '../../telemetry/cli-command-run.js'; import { useCallback, useEffect, useState } from 'react'; interface CreateMemoryConfig { @@ -24,13 +25,25 @@ export function useCreateMemory() { setStatus({ state: 'loading' }); try { const strategiesStr = config.strategies.map(s => s.type).join(','); - const addResult = await memoryPrimitive.add({ - name: config.name, - expiry: config.eventExpiryDuration, - strategies: strategiesStr || undefined, - dataStreamArn: config.streaming?.dataStreamArn, - contentLevel: config.streaming?.contentLevel, - }); + const strategyList = strategiesStr ? strategiesStr.split(',').map(s => s.trim().toUpperCase()) : []; + const addResult = await withAddTelemetry( + 'add.memory', + { + strategy_count: strategyList.length, + strategy_semantic: strategyList.includes('SEMANTIC'), + strategy_summarization: strategyList.includes('SUMMARIZATION'), + strategy_user_preference: strategyList.includes('USER_PREFERENCE'), + strategy_episodic: strategyList.includes('EPISODIC'), + }, + () => + memoryPrimitive.add({ + name: config.name, + expiry: config.eventExpiryDuration, + strategies: strategiesStr || undefined, + dataStreamArn: config.streaming?.dataStreamArn, + contentLevel: config.streaming?.contentLevel, + }) + ); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create memory'); } diff --git a/src/cli/tui/hooks/useCreateOnlineEval.ts b/src/cli/tui/hooks/useCreateOnlineEval.ts index 2d0190552..840b8ae5c 100644 --- a/src/cli/tui/hooks/useCreateOnlineEval.ts +++ b/src/cli/tui/hooks/useCreateOnlineEval.ts @@ -1,4 +1,5 @@ import { onlineEvalConfigPrimitive } from '../../primitives/registry'; +import { withAddTelemetry } from '../../telemetry/cli-command-run.js'; import { useCallback, useEffect, useState } from 'react'; interface CreateOnlineEvalConfig { @@ -17,13 +18,21 @@ export function useCreateOnlineEval() { const create = useCallback(async (config: CreateOnlineEvalConfig) => { setStatus({ state: 'loading' }); try { - const addResult = await onlineEvalConfigPrimitive.add({ - name: config.name, - agent: config.agent, - evaluators: config.evaluators, - samplingRate: config.samplingRate, - enableOnCreate: config.enableOnCreate, - }); + const addResult = await withAddTelemetry( + 'add.online-eval', + { + evaluator_count: config.evaluators.length, + enable_on_create: config.enableOnCreate ?? false, + }, + () => + onlineEvalConfigPrimitive.add({ + name: config.name, + agent: config.agent, + evaluators: config.evaluators, + samplingRate: config.samplingRate, + enableOnCreate: config.enableOnCreate, + }) + ); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create online eval config'); } diff --git a/src/cli/tui/screens/agent/useAddAgent.ts b/src/cli/tui/screens/agent/useAddAgent.ts index 3160e8712..273eff4aa 100644 --- a/src/cli/tui/screens/agent/useAddAgent.ts +++ b/src/cli/tui/screens/agent/useAddAgent.ts @@ -1,6 +1,5 @@ import { APP_DIR, ConfigIO, NoProjectError, findConfigRoot, setEnvVar } from '../../../../lib'; import type { AgentEnvSpec, DirectoryPath, FilePath } from '../../../../schema'; -import { getErrorMessage } from '../../../errors'; import { type PythonSetupResult, setupPythonProject } from '../../../operations'; import { mapGenerateConfigToRenderConfig, @@ -12,6 +11,19 @@ import { executeImportAgent } from '../../../operations/agent/import'; import { buildAuthorizerConfigFromJwtConfig, createManagedOAuthCredential } from '../../../primitives/auth-utils'; import { computeDefaultCredentialEnvVarName } from '../../../primitives/credential-utils'; import { credentialPrimitive } from '../../../primitives/registry'; +import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; +import { + AgentType as AgentTypeEnum, + AuthorizerType as AuthorizerTypeEnum, + Build, + Framework, + Language, + Memory as MemoryEnum, + ModelProvider, + NetworkMode, + Protocol, + standardize, +} from '../../../telemetry/schemas/common-shapes.js'; import { createRenderer } from '../../../templates'; import type { GenerateConfig } from '../generate/types'; import type { AddAgentConfig } from './types'; @@ -135,34 +147,25 @@ export function useAddAgent() { const addAgent = useCallback(async (config: AddAgentConfig): Promise => { setIsLoading(true); try { - const configBaseDir = findConfigRoot(); - if (!configBaseDir) { - return { ok: false, error: new NoProjectError().message }; - } - - const configIO = new ConfigIO({ baseDir: configBaseDir }); - - if (!configIO.configExists('project')) { - return { ok: false, error: new NoProjectError().message }; - } - - // Check for duplicate agent name - const project = await configIO.readProjectSpec(); - const existingAgent = project.runtimes.find(agent => agent.name === config.name); - if (existingAgent) { - return { ok: false, error: `Agent "${config.name}" already exists in this project.` }; - } - - // Branch based on agent type - if (config.agentType === 'import') { - return await handleImportPath(config, configBaseDir); - } else if (config.agentType === 'create') { - return await handleCreatePath(config, configBaseDir); - } else { - return await handleByoPath(config, configIO, configBaseDir); + const result = await withAddTelemetry( + 'add.agent', + { + language: standardize(Language, config.language), + framework: standardize(Framework, config.framework), + model_provider: standardize(ModelProvider, config.modelProvider), + agent_type: standardize(AgentTypeEnum, config.agentType), + build: standardize(Build, config.buildType), + protocol: standardize(Protocol, config.protocol ?? 'HTTP'), + network_mode: standardize(NetworkMode, config.networkMode ?? 'PUBLIC'), + authorizer_type: standardize(AuthorizerTypeEnum, config.authorizerType ?? 'NONE'), + memory: standardize(MemoryEnum, config.memory ?? 'none'), + }, + () => addAgentInner(config) + ); + if (!result.success) { + return { ok: false, error: result.error }; } - } catch (err) { - return { ok: false, error: getErrorMessage(err) }; + return result.outcome; } finally { setIsLoading(false); } @@ -175,6 +178,43 @@ export function useAddAgent() { return { addAgent, isLoading, reset }; } +type AddAgentInnerResult = + | { success: true; outcome: AddAgentCreateResult | AddAgentByoResult } + | { success: false; error: string }; + +async function addAgentInner(config: AddAgentConfig): Promise { + const configBaseDir = findConfigRoot(); + if (!configBaseDir) { + return { success: false, error: new NoProjectError().message }; + } + + const configIO = new ConfigIO({ baseDir: configBaseDir }); + + if (!configIO.configExists('project')) { + return { success: false, error: new NoProjectError().message }; + } + + const project = await configIO.readProjectSpec(); + const existingAgent = project.runtimes.find(agent => agent.name === config.name); + if (existingAgent) { + return { success: false, error: `Agent "${config.name}" already exists in this project.` }; + } + + let outcome: AddAgentCreateResult | AddAgentByoResult | AddAgentError; + if (config.agentType === 'import') { + outcome = await handleImportPath(config, configBaseDir); + } else if (config.agentType === 'create') { + outcome = await handleCreatePath(config, configBaseDir); + } else { + outcome = await handleByoPath(config, configIO, configBaseDir); + } + + if (!outcome.ok) { + return { success: false, error: outcome.error }; + } + return { success: true, outcome }; +} + /** * Handle the "create" path: generate agent from template and write to project. */ diff --git a/src/cli/tui/screens/identity/useCreateIdentity.ts b/src/cli/tui/screens/identity/useCreateIdentity.ts index 42aace21b..e214d1e67 100644 --- a/src/cli/tui/screens/identity/useCreateIdentity.ts +++ b/src/cli/tui/screens/identity/useCreateIdentity.ts @@ -2,6 +2,7 @@ import { ConfigIO } from '../../../../lib'; import type { Credential } from '../../../../schema'; import type { AddCredentialOptions } from '../../../primitives/CredentialPrimitive'; import { credentialPrimitive } from '../../../primitives/registry'; +import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; import { useCallback, useEffect, useState } from 'react'; interface CreateStatus { @@ -16,7 +17,13 @@ export function useCreateIdentity() { const create = useCallback(async (config: AddCredentialOptions) => { setStatus({ state: 'loading' }); try { - const result = await credentialPrimitive.add(config); + const result = await withAddTelemetry( + 'add.credential', + { + credential_type: config.authorizerType === 'OAuthCredentialProvider' ? 'oauth' : 'api-key', + }, + () => credentialPrimitive.add(config) + ); if (!result.success) { throw new Error(result.error ?? 'Failed to create credential'); } diff --git a/src/cli/tui/screens/policy/AddPolicyFlow.tsx b/src/cli/tui/screens/policy/AddPolicyFlow.tsx index 720984563..9b3542cb8 100644 --- a/src/cli/tui/screens/policy/AddPolicyFlow.tsx +++ b/src/cli/tui/screens/policy/AddPolicyFlow.tsx @@ -1,4 +1,6 @@ import { policyEnginePrimitive, policyPrimitive } from '../../../primitives/registry'; +import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; +import { AttachMode, ValidationMode, standardize } from '../../../telemetry/schemas/common-shapes.js'; import { ErrorPrompt, Panel, @@ -128,7 +130,14 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD }, []); const commitEngine = useCallback(async (engineName: string, gateways?: string[], mode?: 'LOG_ONLY' | 'ENFORCE') => { - const result = await policyEnginePrimitive.add({ name: engineName }); + const result = await withAddTelemetry( + 'add.policy-engine', + { + attach_gateway_count: gateways?.length ?? 0, + attach_mode: standardize(AttachMode, mode ?? 'log_only'), + }, + () => policyEnginePrimitive.add({ name: engineName }) + ); if (!result.success) { setFlow({ name: 'error', message: result.error }); return; @@ -155,13 +164,21 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD ); const handlePolicyComplete = useCallback(async (config: AddPolicyConfig) => { - const result = await policyPrimitive.add({ - name: config.name, - engine: config.engine, - statement: config.statement, - source: config.sourceFile || undefined, - validationMode: config.validationMode, - }); + const result = await withAddTelemetry( + 'add.policy', + { + source_type: config.sourceFile ? 'file' : config.sourceMethod === 'generate' ? 'generate' : 'statement', + validation_mode: standardize(ValidationMode, config.validationMode ?? 'FAIL_ON_ANY_FINDINGS'), + }, + () => + policyPrimitive.add({ + name: config.name, + engine: config.engine, + statement: config.statement, + source: config.sourceFile || undefined, + validationMode: config.validationMode, + }) + ); if (result.success) { setPolicyNames(prev => [...prev, config.name]); diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx index 2404109fc..83bda78e5 100644 --- a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx @@ -1,5 +1,6 @@ import { ConfigIO } from '../../../../lib'; import { runtimeEndpointPrimitive } from '../../../primitives/registry'; +import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; import { ErrorPrompt } from '../../components'; import { AddSuccessScreen } from '../add/AddSuccessScreen'; import { AddRuntimeEndpointScreen } from './AddRuntimeEndpointScreen'; @@ -78,24 +79,24 @@ export function AddRuntimeEndpointFlow({ }, [isInteractive, flow.name, onExit]); const handleCreateComplete = useCallback((config: RuntimeEndpointWizardConfig) => { - void runtimeEndpointPrimitive - .add({ + void withAddTelemetry('add.runtime-endpoint', {}, () => + runtimeEndpointPrimitive.add({ runtime: config.runtimeName, endpoint: config.endpointName, version: config.version, description: config.description, }) - .then(result => { - if (result.success) { - setFlow({ - name: 'create-success', - endpointName: config.endpointName, - runtimeName: config.runtimeName, - }); - return; - } - setFlow({ name: 'error', message: result.error ?? 'Unknown error' }); - }); + ).then(result => { + if (result.success) { + setFlow({ + name: 'create-success', + endpointName: config.endpointName, + runtimeName: config.runtimeName, + }); + return; + } + setFlow({ name: 'error', message: result.error ?? 'Unknown error' }); + }); }, []); if (flow.name === 'loading') { diff --git a/src/lib/packaging/__tests__/helpers.test.ts b/src/lib/packaging/__tests__/helpers.test.ts index a554edb0e..f27c171fa 100644 --- a/src/lib/packaging/__tests__/helpers.test.ts +++ b/src/lib/packaging/__tests__/helpers.test.ts @@ -477,8 +477,11 @@ describe('nested agentcore directory is preserved (issue #843)', () => { }); // ── createZipFromDir (async) ── + // The zip stage should NOT exclude agentcore/ — that's copySourceTree's job. + // When zipping a staging directory, any agentcore/ present is a legitimate + // Python package installed by uv, not the project config dir. - it('zip: excludes top-level agentcore/ but includes nested agentcore/', async () => { + it('zip: does not exclude agentcore/ directories (staging has no project config)', async () => { const src = buildFixture(join(root, 'zip-async')); const zipPath = join(root, 'zip-async.zip'); @@ -487,21 +490,17 @@ describe('nested agentcore directory is preserved (issue #843)', () => { const zipBuffer = await readFile(zipPath); const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); - // Top-level agentcore/ should NOT appear - expect(entries.some(e => e === 'agentcore/config.yaml')).toBe(false); - expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); - - // Nested agentcore/ SHOULD appear + // Both top-level and nested agentcore/ are preserved in the zip — + // the zip function zips everything; exclusion is copySourceTree's concern + expect(entries).toContain('agentcore/config.yaml'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); - - // Regular files present expect(entries).toContain('main.py'); }); // ── createZipFromDirSync ── - it('sync zip: excludes top-level agentcore/ but includes nested agentcore/', () => { + it('sync zip: does not exclude agentcore/ directories (staging has no project config)', () => { const src = buildFixture(join(root, 'zip-sync')); const zipPath = join(root, 'zip-sync.zip'); @@ -510,9 +509,61 @@ describe('nested agentcore directory is preserved (issue #843)', () => { const zipBuffer = readFileSync(zipPath); const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); - expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); + expect(entries).toContain('agentcore/config.yaml'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); expect(entries).toContain('main.py'); }); + + // ── Staging directory scenario (the actual bug) ── + // After uv installs deps into staging, copySourceTree copies user source on top. + // The staging dir may contain a top-level agentcore/ from a Python package. + // createZipFromDir must NOT strip it. + + it('zip preserves top-level agentcore/ Python package in staging dir', async () => { + const staging = join(root, 'staging-zip-async'); + mkdirSync(staging, { recursive: true }); + + // Simulate uv-installed dependency with top-level agentcore/ package + const agentcorePkg = join(staging, 'langgraph_checkpoint_aws', 'agentcore'); + mkdirSync(agentcorePkg, { recursive: true }); + writeFileSync(join(staging, 'langgraph_checkpoint_aws', '__init__.py'), '# init'); + writeFileSync(join(agentcorePkg, '__init__.py'), '# agentcore init'); + writeFileSync(join(agentcorePkg, 'saver.py'), 'class AgentCoreMemorySaver: pass'); + + // User source copied on top by copySourceTree + writeFileSync(join(staging, 'main.py'), 'print("hello")'); + + const zipPath = join(root, 'staging-async.zip'); + await createZipFromDir(staging, zipPath); + + const zipBuffer = await readFile(zipPath); + const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); + + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/__init__.py'); + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/saver.py'); + expect(entries).toContain('main.py'); + }); + + it('sync zip preserves top-level agentcore/ Python package in staging dir', () => { + const staging = join(root, 'staging-zip-sync'); + mkdirSync(staging, { recursive: true }); + + const agentcorePkg = join(staging, 'langgraph_checkpoint_aws', 'agentcore'); + mkdirSync(agentcorePkg, { recursive: true }); + writeFileSync(join(staging, 'langgraph_checkpoint_aws', '__init__.py'), '# init'); + writeFileSync(join(agentcorePkg, '__init__.py'), '# agentcore init'); + writeFileSync(join(agentcorePkg, 'saver.py'), 'class AgentCoreMemorySaver: pass'); + writeFileSync(join(staging, 'main.py'), 'print("hello")'); + + const zipPath = join(root, 'staging-sync.zip'); + createZipFromDirSync(staging, zipPath); + + const zipBuffer = readFileSync(zipPath); + const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); + + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/__init__.py'); + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/saver.py'); + expect(entries).toContain('main.py'); + }); }); diff --git a/src/lib/packaging/build-args.ts b/src/lib/packaging/build-args.ts index e5af34045..eadc35875 100644 --- a/src/lib/packaging/build-args.ts +++ b/src/lib/packaging/build-args.ts @@ -1,11 +1,11 @@ -import { readCliConfig } from '../schemas/io/cli-config'; +import { readGlobalConfigSync } from '../schemas/io/global-config'; /** * Return Docker --build-arg flags for UV index URLs configured in ~/.agentcore/config.json. * Returns an empty array when no custom indexes are configured. */ export function getUvBuildArgs(): string[] { - const config = readCliConfig(); + const config = readGlobalConfigSync(); const args: string[] = []; if (config.uvDefaultIndex) args.push('--build-arg', `UV_DEFAULT_INDEX=${config.uvDefaultIndex}`); if (config.uvIndex) args.push('--build-arg', `UV_INDEX=${config.uvIndex}`); diff --git a/src/lib/packaging/helpers.ts b/src/lib/packaging/helpers.ts index 31c74b298..36074395c 100644 --- a/src/lib/packaging/helpers.ts +++ b/src/lib/packaging/helpers.ts @@ -192,24 +192,23 @@ export async function createZipFromDir(sourceDir: string, outputZip: string): Pr await rm(outputZip, { force: true }); await mkdir(dirname(outputZip), { recursive: true }); - const files = await collectFiles(sourceDir, sourceDir); + const files = await collectFiles(sourceDir); const zipped = zipSync(files); await writeFile(outputZip, zipped); } -async function collectFiles(directory: string, rootDir: string, basePath = ''): Promise { +async function collectFiles(directory: string, basePath = ''): Promise { const result: Zippable = {}; const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (EXCLUDED_ENTRIES.has(entry.name)) continue; - if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue; const fullPath = join(directory, entry.name); const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { - Object.assign(result, await collectFiles(fullPath, rootDir, zipPath)); + Object.assign(result, await collectFiles(fullPath, zipPath)); } else if (entry.isFile()) { result[zipPath] = [await readFile(fullPath), { level: 6 }]; } @@ -325,19 +324,18 @@ export function ensureBinaryAvailableSync(binary: string, installHint?: string): throw new MissingDependencyError(binary, installHint); } -function collectFilesSync(directory: string, rootDir: string, basePath = ''): Zippable { +function collectFilesSync(directory: string, basePath = ''): Zippable { const result: Zippable = {}; const entries = readdirSync(directory, { withFileTypes: true }); for (const entry of entries) { if (EXCLUDED_ENTRIES.has(entry.name)) continue; - if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue; const fullPath = join(directory, entry.name); const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { - Object.assign(result, collectFilesSync(fullPath, rootDir, zipPath)); + Object.assign(result, collectFilesSync(fullPath, zipPath)); } else if (entry.isFile()) { result[zipPath] = [readFileSync(fullPath), { level: 6 }]; } @@ -349,7 +347,7 @@ export function createZipFromDirSync(sourceDir: string, outputZip: string): void rmSync(outputZip, { force: true }); mkdirSync(dirname(outputZip), { recursive: true }); - const files = collectFilesSync(sourceDir, sourceDir); + const files = collectFilesSync(sourceDir); const zipped = zipSync(files); writeFileSync(outputZip, zipped); } diff --git a/src/lib/schemas/io/cli-config.ts b/src/lib/schemas/io/cli-config.ts deleted file mode 100644 index aa36d82f1..000000000 --- a/src/lib/schemas/io/cli-config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; - -const CONFIG_FILE = join(homedir(), '.agentcore', 'config.json'); - -export interface CliConfig { - uvDefaultIndex?: string; - uvIndex?: string; - disableTransactionSearch?: boolean; - transactionSearchIndexPercentage?: number; -} - -/** - * Read the global CLI config from ~/.agentcore/config.json. - * Returns an empty object if the file doesn't exist or is malformed. - */ -export function readCliConfig(): CliConfig { - try { - const data = readFileSync(CONFIG_FILE, 'utf-8'); - const parsed: Record = JSON.parse(data) as Record; - const config: CliConfig = {}; - if (typeof parsed.uvDefaultIndex === 'string') config.uvDefaultIndex = parsed.uvDefaultIndex; - if (typeof parsed.uvIndex === 'string') config.uvIndex = parsed.uvIndex; - if (parsed.disableTransactionSearch === true) config.disableTransactionSearch = true; - if (typeof parsed.transactionSearchIndexPercentage === 'number') { - const pct = parsed.transactionSearchIndexPercentage; - if (pct >= 0 && pct <= 100) { - config.transactionSearchIndexPercentage = pct; - } - } - return config; - } catch { - return {}; - } -} diff --git a/src/cli/global-config.ts b/src/lib/schemas/io/global-config.ts similarity index 73% rename from src/cli/global-config.ts rename to src/lib/schemas/io/global-config.ts index 267ad6669..fc64eb39b 100644 --- a/src/cli/global-config.ts +++ b/src/lib/schemas/io/global-config.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'fs'; import { mkdir, readFile, writeFile } from 'fs/promises'; import { randomUUID } from 'node:crypto'; import { homedir } from 'os'; @@ -9,18 +10,19 @@ export const GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, 'config.json'); const GlobalConfigSchema = z .object({ - installationId: z.string().optional(), - uvDefaultIndex: z.string().optional(), - uvIndex: z.string().optional(), - disableTransactionSearch: z.boolean().optional(), - transactionSearchIndexPercentage: z.number().min(0).max(100).optional(), + installationId: z.string().optional().catch(undefined), + uvDefaultIndex: z.string().optional().catch(undefined), + uvIndex: z.string().optional().catch(undefined), + disableTransactionSearch: z.boolean().optional().catch(undefined), + transactionSearchIndexPercentage: z.number().int().min(0).max(100).optional().catch(undefined), telemetry: z .object({ - enabled: z.boolean().optional(), - endpoint: z.string().optional(), - audit: z.boolean().optional(), + enabled: z.boolean().optional().catch(undefined), + endpoint: z.string().optional().catch(undefined), + audit: z.boolean().optional().catch(undefined), }) - .optional(), + .optional() + .catch(undefined), }) .passthrough(); @@ -35,6 +37,15 @@ export async function readGlobalConfig(configFile = GLOBAL_CONFIG_FILE): Promise } } +export function readGlobalConfigSync(configFile = GLOBAL_CONFIG_FILE): GlobalConfig { + try { + const data = readFileSync(configFile, 'utf-8'); + return GlobalConfigSchema.parse(JSON.parse(data)); + } catch { + return {}; + } +} + export async function updateGlobalConfig( partial: GlobalConfig, configDir = GLOBAL_CONFIG_DIR, diff --git a/src/lib/schemas/io/index.ts b/src/lib/schemas/io/index.ts index e8ddddc0f..212468ffe 100644 --- a/src/lib/schemas/io/index.ts +++ b/src/lib/schemas/io/index.ts @@ -11,4 +11,3 @@ export { type PathConfig, } from './path-resolver'; export { ConfigIO, createConfigIO, getSchemaUrlForVersion } from './config-io'; -export { readCliConfig, type CliConfig } from './cli-config'; diff --git a/src/test-utils/cli-runner.ts b/src/test-utils/cli-runner.ts index 789624364..4526546c5 100644 --- a/src/test-utils/cli-runner.ts +++ b/src/test-utils/cli-runner.ts @@ -72,6 +72,14 @@ function getCLIPath(): string { * Run the AgentCore CLI via the local build (unit/integ tests). * Skips dependency installation by default for speed. */ -export async function runCLI(args: string[], cwd: string, skipInstall = true): Promise { - return spawnAndCollect('node', [getCLIPath(), ...args], cwd, skipInstall ? { AGENTCORE_SKIP_INSTALL: '1' } : {}); +export async function runCLI( + args: string[], + cwd: string, + options: { skipInstall?: boolean; env?: Record } = {} +): Promise { + const { skipInstall = true, env } = options; + return spawnAndCollect('node', [getCLIPath(), ...args], cwd, { + ...(skipInstall ? { AGENTCORE_SKIP_INSTALL: '1' } : {}), + ...env, + }); } diff --git a/src/test-utils/index.ts b/src/test-utils/index.ts index ff127a35e..1c032519c 100644 --- a/src/test-utils/index.ts +++ b/src/test-utils/index.ts @@ -4,6 +4,7 @@ */ export { runCLI, spawnAndCollect, cleanSpawnEnv, type RunResult } from './cli-runner.js'; +export { createTelemetryHelper, type TelemetryHelper, type TelemetryEntry } from './telemetry-helper.js'; export { exists } from './fs-helpers.js'; export { hasCommand, hasAwsCredentials, prereqs } from './prereqs.js'; export { createTestProject, type TestProject, type CreateTestProjectOptions } from './project-factory.js'; diff --git a/src/test-utils/project-factory.ts b/src/test-utils/project-factory.ts index 77c77e1c6..61525f6c5 100644 --- a/src/test-utils/project-factory.ts +++ b/src/test-utils/project-factory.ts @@ -65,7 +65,7 @@ export async function createTestProject(options: CreateTestProjectOptions = {}): args.push('--json'); - const result = await runCLI(args, testDir, skipInstall); + const result = await runCLI(args, testDir, { skipInstall }); if (result.exitCode !== 0) { // Clean up on failure diff --git a/src/test-utils/telemetry-helper.ts b/src/test-utils/telemetry-helper.ts new file mode 100644 index 000000000..e7fa58949 --- /dev/null +++ b/src/test-utils/telemetry-helper.ts @@ -0,0 +1,56 @@ +import { globSync } from 'glob'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { expect } from 'vitest'; + +export interface TelemetryEntry { + value: number; + attrs: Record; +} + +export interface TelemetryHelper { + /** Temp directory used as AGENTCORE_CONFIG_DIR */ + dir: string; + /** Env vars to pass to runCLI to enable audit mode */ + env: { AGENTCORE_TELEMETRY_AUDIT: '1'; AGENTCORE_CONFIG_DIR: string }; + /** Read all JSONL entries from the audit telemetry directory */ + readEntries: () => TelemetryEntry[]; + /** Assert a metric was emitted with attrs matching the given subset */ + assertMetricEmitted: (expected: Record) => void; + /** Delete telemetry entries only (keeps the config dir) */ + clearEntries: () => void; + /** Delete the entire config directory — call in afterAll */ + destroy: () => void; +} + +export function createTelemetryHelper(): TelemetryHelper { + const dir = mkdtempSync(join(tmpdir(), 'agentcore-audit-')); + const helper: TelemetryHelper = { + dir, + env: { AGENTCORE_TELEMETRY_AUDIT: '1', AGENTCORE_CONFIG_DIR: dir }, + readEntries() { + return globSync(join(dir, 'telemetry', '*.json')).flatMap(f => + readFileSync(f, 'utf-8') + .trim() + .split('\n') + .map(line => JSON.parse(line) as TelemetryEntry) + ); + }, + assertMetricEmitted(expected) { + const entries = helper.readEntries(); + const match = entries.find(e => Object.entries(expected).every(([k, v]) => String(e.attrs[k]) === String(v))); + expect( + match, + `No telemetry entry matching ${JSON.stringify(expected)}\nFound ${entries.length} entries:\n${entries.map(e => JSON.stringify(e.attrs)).join('\n')}` + ).toBeDefined(); + }, + clearEntries() { + rmSync(join(dir, 'telemetry'), { recursive: true, force: true }); + }, + destroy() { + rmSync(dir, { recursive: true, force: true }); + }, + }; + return helper; +}