diff --git a/.bandit-baseline.json b/.bandit-baseline.json new file mode 100644 index 00000000..b5efe48a --- /dev/null +++ b/.bandit-baseline.json @@ -0,0 +1,20 @@ +{ + "errors": [], + "generated_at": "2026-02-05T22:50:21Z", + "metrics": { + "_totals": { + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 0, + "nosec": 0, + "skipped_tests": 0 + } + }, + "results": [] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1492992a..d62dc067 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,22 +1,36 @@ { - "name": "awslabs-agent-plugins", - "owner": { - "name": "Amazon Web Services", - "email": "aws-agent-plugins@amazon.com" - }, "metadata": { "description": "Official AWS plugins for Claude Code, Cursor, and AI coding assistants.", "version": "1.0.0" }, + "name": "awslabs-agent-plugins", + "owner": { + "email": "aws-agent-plugins@amazon.com", + "name": "Amazon Web Services" + }, "plugins": [ { + "category": "deployment", + "description": "Deploy any application to AWS. Get architecture recommendations, cost estimates, and one-command deployment.", + "keywords": [ + "aws", + "aws agent skills", + "amazon", + "deploy", + "cdk", + "cloudformation", + "infrastructure", + "pricing" + ], "name": "deploy-on-aws", "source": "./plugins/deploy-on-aws", - "description": "Deploy any application to AWS. Get architecture recommendations, cost estimates, and one-command deployment.", - "version": "1.0.0", - "category": "deployment", - "tags": ["aws", "deploy", "infrastructure", "cdk"], - "keywords": ["aws", "aws agent skills", "amazon", "deploy", "cdk", "cloudformation", "infrastructure", "pricing"] + "tags": [ + "aws", + "deploy", + "infrastructure", + "cdk" + ], + "version": "1.0.0" } ] } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ea6d6056 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,33 @@ +# [CODEOWNERS](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#about-code-owners) + +## Default owners for everything in the repo + +* @awslabs/agent-plugins-admins @awslabs/agent-plugins-maintainers + +## Adminstrators + +.bandit-baseline.json @awslabs/agent-plugins-admins +.claude-plugin/ @awslabs/agent-plugins-admins +.github/ @awslabs/agent-plugins-admins +.gitignore @awslabs/agent-plugins-admins +.gitleaks-baseline.json @awslabs/agent-plugins-admins +.gitleaks.toml @awslabs/agent-plugins-admins +.gitleaksignore @awslabs/agent-plugins-admins +.markdownlint-cli2.yaml @awslabs/agent-plugins-admins +AGENTS.md @awslabs/agent-plugins-admins +CLAUDE.md @awslabs/agent-plugins-admins +CODE_OF_CONDUCT.md @awslabs/agent-plugins-admins +CONTRIBUTING.md @awslabs/agent-plugins-admins +DEVELOPMENT_GUIDE.md @awslabs/agent-plugins-admins +dprint.json @awslabs/agent-plugins-admins +LICENSE @awslabs/agent-plugins-admins +mise.toml @awslabs/agent-plugins-admins +NOTICE @awslabs/agent-plugins-admins +plugins/ @awslabs/agent-plugins-admins +README.md @awslabs/agent-plugins-admins +schemas/ @awslabs/agent-plugins-admins +tools/ @awslabs/agent-plugins-admins + +## File must end with CODEOWNERS file + +.github/CODEOWNERS @awslabs/agent-plugins-admins diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4ffbdf0a..5ca9b637 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -30,7 +30,7 @@ body: label: Current Behavior description: | What actually happened? - + Please include full errors, uncaught exceptions, stack traces, and relevant logs. If service responses are relevant, please include wire logs. validations: @@ -42,7 +42,7 @@ body: description: | Provide a self-contained, concise snippet of code that can be used to reproduce the issue. For more complex issues provide a repo with the smallest sample that reproduces the bug. - + Avoid including business logic or unrelated code, it makes diagnosis more difficult. The code sample should be an SSCCE. See http://sscce.org/ for details. In short, please provide a code sample that we can copy/paste, run and reproduce. validations: diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml index 166f3161..75d8a001 100644 --- a/.github/ISSUE_TEMPLATE/documentation.yml +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -24,4 +24,4 @@ body: description: | Include links to affected documentation page(s). validations: - required: true \ No newline at end of file + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..b4b467d4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,13 @@ + + +#### Related + + + +#### Changes + + + +#### Acknowledgment + +By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the [project license](https://github.com/awslabs/agent-plugins/blob/main/LICENSE). diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..ffef0135 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,117 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +permissions: + actions: none + attestations: none + checks: none + contents: none + deployments: none + discussions: none + id-token: none + issues: none + models: none + packages: none + pages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '23 14 * * 3' + workflow_dispatch: +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..7572f3b2 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,16 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + with: + config-file: amazon-ospo/dependency-review-config/default/dependency-review-config.yml@main + # # distlib + # allow-dependencies-licenses: "pkg:pypi/distlib@0.4.0" diff --git a/.github/workflows/merge-prevention.yml b/.github/workflows/merge-prevention.yml new file mode 100644 index 00000000..087e01da --- /dev/null +++ b/.github/workflows/merge-prevention.yml @@ -0,0 +1,116 @@ +--- +# This workflow is to prevent unintentional merges that cannot be accomplished by rulesets or other settings. +name: Merge Prevention +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + - labeled + - unlabeled + merge_group: + types: + - checks_requested +permissions: + actions: none + attestations: none + checks: none + contents: none + deployments: none + discussions: none + id-token: none + issues: none + models: none + packages: none + pages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none +env: + DO_NOT_MERGE_LABEL: ${{ vars.DO_NOT_MERGE_LABEL || 'do-not-merge' }} + HALT_MERGES: ${{ vars.HALT_MERGES || '0' }} +jobs: + get-pr-info: + permissions: + contents: read + pull-requests: read + # id-token: write + runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.get-pr.outputs.pr-number }} + pr_labels: ${{ steps.get-pr.outputs.pr-labels }} + env: + GH_TOKEN: ${{ github.token }} + PR_LABELS_JSON: ${{ toJson(github.event.pull_request.labels.*.name) }} + steps: + - name: Get PR info + id: get-pr + run: | + if [ "${{ github.event_name }}" == "merge_group" ]; then + PR_NUMBER=$(echo "${{ github.ref }}" | grep -oP '(?<=/pr-)\d+' || echo "") + PR_LABELS=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER | jq -c '[.labels[].name] // []') + echo "::group::Getting Information" + gh api repos/${{ github.repository }}/pulls/$PR_NUMBER + echo "::endgroup::" + elif [ "${{ github.event_name }}" == "pull_request" ]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + PR_LABELS=$(echo "$PR_LABELS_JSON" | jq -c '.') + fi + echo "::group::Debug Output Values" + echo "PR_NUMBER: $PR_NUMBER" + echo "PR_LABELS: $PR_LABELS" + echo "::endgroup::" + echo "pr-number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "pr-labels=$PR_LABELS" >> $GITHUB_OUTPUT + check-merge-status: + runs-on: ubuntu-latest + needs: get-pr-info + if: always() + steps: + - run: | + PR_NUMBER="${{ needs.get-pr-info.outputs.pr_number }}" + # Default to 0 (allow all) if not set + if [ -z "$HALT_MERGES" ]; then + HALT_MERGES=0 + fi + echo "::debug::HALT_MERGES value: $HALT_MERGES" + echo "::debug::This PR number: $PR_NUMBER" + if [ "$HALT_MERGES" = "0" ]; then + echo "::debug::✅ All merges are allowed (HALT_MERGES=0)" + exit 0 + elif [ "$HALT_MERGES" = "$PR_NUMBER" ]; then + echo "::debug::✅ This PR #$PR_NUMBER is explicitly allowed" + exit 0 + else + echo "::debug::🛑 Merges are blocked. HALT_MERGES is set to $HALT_MERGES" + if [ "$HALT_MERGES" -lt 0 ]; then + echo "::error::All merges are blocked" + else + echo "::warning::Only PR #$HALT_MERGES is allowed to merge" + fi + exit 1 + fi + fail-by-label: + runs-on: ubuntu-latest + needs: get-pr-info + if: always() + steps: + - run: | + echo "::group::Debug Output Values" + echo "PR_LABELS: ${{ needs.get-pr-info.outputs.pr_labels }}" + echo "::endgroup::" + - name: When PR has the "${{ env.DO_NOT_MERGE_LABEL }}" label + id: pr-has-label + if: contains(needs.get-pr-info.outputs.pr_labels, env.DO_NOT_MERGE_LABEL) + run: | + echo "::error::❌ The label \"${{ env.DO_NOT_MERGE_LABEL }}\" is used to prevent merging." + exit 1 + - name: When PR does not have the "${{ env.DO_NOT_MERGE_LABEL }}" label + id: pr-missing-label + if: ! contains(needs.get-pr-info.outputs.pr_labels, env.DO_NOT_MERGE_LABEL) + run: | + echo "::debug::✅ The label \"${{ env.DO_NOT_MERGE_LABEL }}\" is absent" + exit 0 diff --git a/.github/workflows/pull-request-lint.yml b/.github/workflows/pull-request-lint.yml new file mode 100644 index 00000000..94e3e397 --- /dev/null +++ b/.github/workflows/pull-request-lint.yml @@ -0,0 +1,66 @@ +name: Pull Request Validation + +on: + pull_request_target: + branches: [ "main" ] + types: + - labeled + - opened + - synchronize + - reopened + - ready_for_review + - edited + merge_group: {} + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Validate PR title + runs-on: ubuntu-latest + permissions: + pull-requests: read + if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 #v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: |- + fix + feat + build + chore + ci + docs + style + refactor + perf + test + requireScope: false + + contributorStatement: + name: Require Contributor Statement + runs-on: ubuntu-latest + permissions: + pull-requests: read + env: + PR_BODY: ${{ github.event.pull_request.body }} + EXPECTED: By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the [project license](https://github.com/${{ github.repository }}/blob/main/LICENSE). + HELP: Contributor statement missing from PR description. Please include the following text in the PR description + if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && !(github.event.pull_request.user.login == 'awslabs-mcp' || github.event.pull_request.user.login == 'dependabot[bot]') + steps: + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + with: + script: |- + const actual = process.env.PR_BODY.replace(/\r?\n/g, "\n"); + const expected = process.env.EXPECTED.replace(/\r?\n/g, "\n"); + if (!actual.includes(expected)) { + console.log("%j", actual); + console.log("%j", expected); + core.setFailed(`${process.env.HELP}: ${expected}`); + } diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 4ef28e68..dfb3804b 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -47,5 +47,8 @@ jobs: - name: Lint Markdown (includes SKILL.md validation) run: mise run lint:md + - name: Validate Cross Reference manifests + run: mise run lint:cross-refs + - name: Validate JSON manifests run: mise run lint:manifests diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml new file mode 100644 index 00000000..dff49c79 --- /dev/null +++ b/.github/workflows/scorecard-analysis.yml @@ -0,0 +1,54 @@ +name: Scorecard Analysis +on: + push: + branches: + - main + schedule: + # Weekly on Mondays at 3am Pacific (11:00 UTC) + - cron: '0 11 * * 1' + +permissions: {} + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: scorecard-results.sarif + results_format: sarif + # Scorecard team runs a weekly scan of public GitHub repos, + # see https://github.com/ossf/scorecard#public-data. + # Setting `publish_results: true` helps us scale by leveraging your workflow to + # extract the results instead of relying on our own infrastructure to run scans. + # And it's free for you! + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable + # uploads of run results in SARIF format to the repository Actions tab. + # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts + - name: "Upload artifact" + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: SARIF file + path: scorecard-results.sarif + retention-days: 14 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@7434149006143a4d75b82a2f411ef15b03ccc2d7 # v4.31.9 + with: + sarif_file: scorecard-results.sarif diff --git a/.github/workflows/security-scanners.yml b/.github/workflows/security-scanners.yml new file mode 100644 index 00000000..182752c6 --- /dev/null +++ b/.github/workflows/security-scanners.yml @@ -0,0 +1,306 @@ +name: Security Scanners +on: + schedule: + # Daily at 15:12 UTC (random time to avoid GitHub Actions load spikes) + - cron: '12 15 * * *' + push: + pull_request: + workflow_dispatch: +permissions: + actions: none + attestations: none + checks: none + contents: none + deployments: none + discussions: none + id-token: none + issues: none + models: none + packages: none + pages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none +jobs: + gitleaks: + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + env: + GITLEAKS_VERSION: "8.30.0" + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + - name: Install gitleaks + run: | + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar -xz; + sudo mv gitleaks /usr/local/bin/; + gitleaks --version; + - name: Run gitleaks (full history) + run: | + gitleaks git --config=.gitleaks.toml --baseline-path=.gitleaks-baseline.json --report-path=gitleaks-report_sarif.json --report-format=sarif . || true + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + if: always() + with: + name: gitleaks.sarif + path: gitleaks-report_sarif.json + if-no-files-found: ignore + - uses: github/codeql-action/upload-sarif@57eebf61a2246ab60a0c2f5a85766db783ad3553 # v3.28.15 + continue-on-error: true + with: + sarif_file: gitleaks-report_sarif.json + bandit: + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: PyCQA/bandit-action@cd2700ff8e8a10b277288e068d0c207c614c46ee # main + continue-on-error: true + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + if: always() + with: + name: bandit.sarif + path: results.sarif + if-no-files-found: error + grype: + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - run: | + curl -sSfL https://raw.githubusercontent.com/anchore/grype/43e7e3246ed01b1ec0ff54f9b054201ccbe78e3a/install.sh | sh -s -- -b /usr/local/bin v0.104.3 + grype --version + - run: | + grype --output sarif . | tee grype.sarif.json + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: grype.sarif + path: grype.sarif.json + if-no-files-found: error + - uses: github/codeql-action/upload-sarif@57eebf61a2246ab60a0c2f5a85766db783ad3553 # v3.28.15 + continue-on-error: true + with: + sarif_file: grype.sarif.json + checkov: + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + - uses: bridgecrewio/checkov-action@5051a5cfc7e4c71d95199f81ffafbb490c7e6213 # v12.3079.0 + with: + output_format: cli,sarif + output_file_path: console,results.sarif + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@b2ff80ddacba59b60f4e0cf3b699baaea3230cd9 # v4.31.9 + if: success() || failure() + with: + sarif_file: results.sarif + semgrep: + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 # Required for baseline comparison + - run: | + echo "semgrep==1.149.0" > requirements.txt + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: '3.13' + cache: 'pip' + - run: | + pip install -r requirements.txt + rm requirements.txt + - name: Run semgrep + id: semgrep + env: + # For PRs: base SHA; for push to default branch: empty + BASELINE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} + run: | + BASELINE_ARGS="" + if [ -n "$BASELINE_SHA" ]; then + BASELINE_ARGS="--baseline-commit $BASELINE_SHA" + fi + + # Run semgrep, capture exit code + set +e + semgrep scan --oss-only --verbose --metrics=off --config=r/all --sarif-output semgrep.sarif.json $BASELINE_ARGS + SEMGREP_EXIT=$? + set -e + + echo "exit_code=$SEMGREP_EXIT" >> "$GITHUB_OUTPUT" + + # Exit 0 for now to allow artifact upload + exit 0 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: semgrep.sarif + path: semgrep.sarif.json + if-no-files-found: error + - uses: github/codeql-action/upload-sarif@57eebf61a2246ab60a0c2f5a85766db783ad3553 # v3.28.15 + continue-on-error: true + with: + sarif_file: semgrep.sarif.json + - if: steps.semgrep.outputs.exit_code != '0' + run: | + echo "::error::semgrep found new security issues" + exit ${{ steps.semgrep.outputs.exit_code }} + clamav: + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + services: + clamav: + image: clamav/clamav@sha256:a56287b4ffa299bde2ef09234cb8b6134d591d0be05b63f5065932dc93cb2435 + ports: + - 3310:3310 + options: >- + --health-cmd "/usr/local/bin/clamdcheck.sh" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Wait for ClamAV service + run: timeout 300 bash -c 'until echo > /dev/tcp/localhost/3310; do sleep 5; done' 2>/dev/null + - run: | + sudo apt-get update || true + sudo rm -f /var/lib/man-db/auto-update + sudo apt-get install -y --no-install-recommends clamdscan + sudo mkdir -p /etc/clamav + cat << EOF | sudo tee /etc/clamav/clamd.conf + TCPSocket 3310 + TCPAddr 127.0.0.1 + EOF + clamdscan --version + - run: | + clamdscan --verbose --log=clamdscan.txt --stream --fdpass --multiscan . + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + if: always() + with: + name: clamdscan.txt + path: clamdscan.txt + if-no-files-found: error + sonarqube: + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + services: + sonarqube: + image: sonarqube:community@sha256:48dd0e946ad6481dde43bb31d1a7af09c22f59be6399b195dcce7b87d82c5f40 + ports: + - 9000:9000 + env: + SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: true + SONAR_WEB_SYSTEMPASSCODE: passcode # is it possible to make this dynamic? + options: >- + --health-cmd "status=$(wget -qO- http://localhost:9000/api/system/status | grep -oP '(?<=status\":\").*' | sed -e 's|\"}||' | tr -d '\n'); echo -n \"$status\"; test \"$status\" = \"UP\"" + --health-interval 10s + --health-timeout 5s + --health-retries 30 + steps: + # - if: always() + # run: | + # docker inspect $(docker ps -qf "ancestor=sonarqube:community@sha256:48dd0e946ad6481dde43bb31d1a7af09c22f59be6399b195dcce7b87d82c5f40") | jq '.[] | {"test": .Config.Healthcheck.Test, "state": .State.Health}' + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + - name: Wait for SonarQube to be ready + run: | + until curl --silent http://localhost:9000/api/system/status | jq -r '.status' | grep -q 'UP'; do + echo "Waiting for SonarQube..." + sleep 5 + done + echo "SonarQube is ready!" + - name: Get SonarQube token + run: | + # Login and get token (default credentials: admin/admin) + SONAR_TOKEN=$(curl -s -u admin:admin -X POST "http://localhost:9000/api/user_tokens/generate?name=github-actions" \ + | jq -r '.token') + echo "SONAR_TOKEN=$SONAR_TOKEN" >> $GITHUB_ENV + - name: SonarQube Info + run: | + curl --silent --request GET \ + --url 'http://localhost:9000/api/server/version' \ + --header 'X-Sonar-Passcode: passcode' + curl --silent --request GET \ + --url 'http://localhost:9000/api/system/health' \ + --header 'X-Sonar-Passcode: passcode' | jq + curl --silent --request GET \ + --url 'http://localhost:9000/api/system/info' \ + --header 'Authorization: Bearer ${{ env.SONAR_TOKEN }}' | jq + + # TODO: Shouldn't the project be matching the repo and name? + - name: Create SonarQube project + run: | + # Create project + curl --silent --request POST \ + --url "http://localhost:9000/api/projects/create?project=my-project&name=MyProject" \ + --header 'Authorization: Bearer ${{ env.SONAR_TOKEN }}' + + - uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6.0.0 + id: sonarqube-scan-action + env: + SONAR_HOST_URL: http://localhost:9000 + SONAR_TOKEN: ${{ env.SONAR_TOKEN }} + with: + args: > + "-Dsonar.projectName=MyProject" + -Dsonar.projectKey=my-project + -Dsonar.sources=. + -Dsonar.verbose=true + + # If you wish to fail your job when the Quality Gate is red, uncomment the + # following lines. This would typically be used to fail a deployment. + - uses: SonarSource/sonarqube-quality-gate-action@cf038b0e0cdecfa9e56c198bbb7d21d751d62c3b # v1.2.0 + id: sonarqube-quality-gate-check + timeout-minutes: 5 # why 4 minutes? + env: + SONAR_HOST_URL: http://localhost:9000 + SONAR_TOKEN: ${{ env.SONAR_TOKEN }} + + # Use the output from the Quality Gate in another step. + # The possible outputs of the `quality-gate-status` variable are `PASSED`, `WARN`, or `FAILED`. + - name: "Example show SonarQube Quality Gate Status value" + if: always() + run: | + echo "The Quality Gate status is ${{ steps.sonarqube-quality-gate-check.outputs.quality-gate-status }}" + find . -name "report-task.txt" -print0 | xargs -0 -I{} cat {} + + - name: Get Issues + run: | + curl --silent --request GET \ + --url 'http://localhost:9000/api/issues/search?componentKeys=my-project&ps=500&p=1' \ + --header 'Authorization: Bearer ${{ env.SONAR_TOKEN }}' | tee sonar-issues.json | jq || exit 1 + + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + if: always() + with: + name: sonar-issues.json + path: sonar-issues.json + if-no-files-found: error + + # TODO: write a loop and write the JSON artifact out + # TODO: convert the artifact to a SARIF format + # http://localhost:9000/api/issues/search?componentKeys=AWS-Labs-MCP-Servers + # https://next.sonarqube.com/sonarqube/web_api/api/project_analyses/search diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..cb338a13 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +name: stale +on: + schedule: + - cron: 0 1 * * * + workflow_dispatch: {} +permissions: {} +jobs: + stale: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + with: + days-before-stale: -1 + days-before-close: -1 + days-before-pr-stale: 14 + days-before-pr-close: 2 + stale-pr-message: This pull request is now marked as stale because it hasn't seen activity for a while. Add a comment or it will be closed soon. If you wish to exclude this issue from being marked as stale, add the "backlog" label. + close-pr-message: Closing this pull request as it hasn't seen activity for a while. Please add a comment @mentioning a maintainer to reopen. If you wish to exclude this issue from being marked as stale, add the "backlog" label. + stale-pr-label: stale + exempt-pr-labels: backlog + days-before-issue-stale: 60 + days-before-issue-close: 7 + stale-issue-message: This issue is now marked as stale because it hasn't seen activity for a while. Add a comment or it will be closed soon. If you wish to exclude this issue from being marked as stale, add the "backlog" label. + close-issue-message: Closing this issue as it hasn't seen activity for a while. Please add a comment @mentioning a maintainer to reopen. If you wish to exclude this issue from being marked as stale, add the "backlog" label. + stale-issue-label: stale + exempt-issue-labels: backlog diff --git a/.gitignore b/.gitignore index 7d2b28a6..3c6f9ca9 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ Thumbs.db # Environment .env .env.local + +# Claude +.claude/settings.local.json diff --git a/.gitleaks-baseline.json b/.gitleaks-baseline.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/.gitleaks-baseline.json @@ -0,0 +1 @@ +[] diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..2d2cb5a4 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,29 @@ +# Gitleaks Configuration +# https://github.com/gitleaks/gitleaks + +title = "gitleaks config" + +# Extend the default gitleaks configuration +[extend] +useDefault = true + +# Global allowlist patterns +[allowlist] +description = "Global allowlist" +paths = [ + '''\.gitleaksignore$''', + '''\.gitleaks-baseline\.json$''', +] + +# Example: Add custom rules or override defaults +# [[rules]] +# id = "custom-api-key" +# description = "Custom API Key Pattern" +# regex = '''(?i)custom[_-]?api[_-]?key\s*[=:]\s*['"]?([a-zA-Z0-9]{32,})['"]?''' +# keywords = ["custom", "api", "key"] + +# Example: Rule-specific allowlist +# [[rules.allowlists]] +# description = "Allow example keys in tests" +# paths = ['''test/.*''', '''.*_test\.go$'''] +# regexTarget = "match" diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..8f60d8c6 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,5 @@ +# Gitleaks ignore file +# https://github.com/gitleaks/gitleaks#gitleaksignore + +# SonarQube default credentials for local CI container (not real secrets) +.github/workflows/scanners.yml:curl-auth-user:198 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f2400f3..d3c0ede7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,12 @@ Thank you for your interest in contributing to Agent Plugins for AWS. +## Role Guides + +Depending on your role, please review the appropriate guide for repository-specific instructions: + +- [Development Guide](DEVELOPMENT_GUIDE.md) - For contributors and developers + ## Reporting Bugs/Feature Requests We welcome you to use the GitHub issue tracker to report bugs or suggest features. diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md new file mode 100644 index 00000000..c618b76f --- /dev/null +++ b/DEVELOPMENT_GUIDE.md @@ -0,0 +1,29 @@ +# Development Guide + +## Security Scanning + +### Gitleaks - Secret Detection + +This repository uses [gitleaks](https://github.com/gitleaks/gitleaks) to detect secrets and sensitive information in the codebase. + +#### Handling False Positives + +If gitleaks reports a false positive (e.g., example API keys in documentation, test fixtures), you can add it to the baseline file to suppress future warnings. + +1. Run gitleaks locally to generate the baseline: + + ```bash + gitleaks git --config=.gitleaks.toml --report-format=json . > .gitleaks-baseline.json + ``` + +2. Review the generated file to ensure only legitimate false positives are included. + +3. Commit the updated `.gitleaks-baseline.json` file. + +#### Configuration + +Custom rules and allowlists are defined in `.gitleaks.toml`. Common customizations include: + +- Excluding paths (vendor directories, generated files) +- Allowlisting specific patterns or files +- Adding custom secret detection rules diff --git a/dprint.json b/dprint.json index 94b35b25..865eaef7 100644 --- a/dprint.json +++ b/dprint.json @@ -1,25 +1,25 @@ { "$schema": "https://dprint.dev/schemas/v0.json", - "lineWidth": 100, - "indentWidth": 2, - "useTabs": false, - "newLineKind": "lf", - "markdown": { - "lineWidth": 100, - "textWrap": "maintain" - }, - "json": { - "indentWidth": 2, - "lineWidth": 120 - }, "excludes": [ "**/node_modules/**", "**/.git/**", "**/dist/**", "**/.claude/docs/**" ], + "indentWidth": 2, + "json": { + "indentWidth": 2, + "lineWidth": 120 + }, + "lineWidth": 100, + "markdown": { + "lineWidth": 100, + "textWrap": "maintain" + }, + "newLineKind": "lf", "plugins": [ "https://plugins.dprint.dev/markdown-0.17.8.wasm", "https://plugins.dprint.dev/json-0.19.4.wasm" - ] + ], + "useTabs": false } diff --git a/mise.toml b/mise.toml index bf4904bb..9463f0b6 100644 --- a/mise.toml +++ b/mise.toml @@ -1,7 +1,7 @@ # mise.toml - Tool versions and tasks for Agent Plugins for AWS # See: https://mise.jdx.dev -min_version = "2024.11.1" +min_version = "2026.2.4" [tools] node = "24" diff --git a/plugins/deploy-on-aws/.claude-plugin/plugin.json b/plugins/deploy-on-aws/.claude-plugin/plugin.json index 3a20c039..8b378627 100644 --- a/plugins/deploy-on-aws/.claude-plugin/plugin.json +++ b/plugins/deploy-on-aws/.claude-plugin/plugin.json @@ -1,12 +1,19 @@ { - "name": "deploy-on-aws", - "description": "Deploy any application to AWS. Get architecture recommendations, cost estimates, and one-command deployment.", - "version": "1.0.0", "author": { "name": "Amazon Web Services" }, + "description": "Deploy any application to AWS. Get architecture recommendations, cost estimates, and one-command deployment.", "homepage": "https://github.com/awslabs/agent-plugins", - "repository": "https://github.com/awslabs/agent-plugins", + "keywords": [ + "aws", + "deploy", + "infrastructure", + "cdk", + "cloudformation", + "pricing" + ], "license": "Apache-2.0", - "keywords": ["aws", "deploy", "infrastructure", "cdk", "cloudformation", "pricing"] + "name": "deploy-on-aws", + "repository": "https://github.com/awslabs/agent-plugins", + "version": "1.0.0" } diff --git a/plugins/deploy-on-aws/.mcp.json b/plugins/deploy-on-aws/.mcp.json index 1173851e..0e201a27 100644 --- a/plugins/deploy-on-aws/.mcp.json +++ b/plugins/deploy-on-aws/.mcp.json @@ -1,22 +1,26 @@ { "mcpServers": { "awsiac": { - "command": "uvx", - "args": ["awslabs.aws-iac-mcp-server@latest"] + "args": [ + "awslabs.aws-iac-mcp-server@latest" + ], + "command": "uvx" + }, + "awsknowledge": { + "type": "http", + "url": "https://knowledge-mcp.global.api.aws" }, "awspricing": { - "type": "stdio", + "args": [ + "awslabs.aws-pricing-mcp-server@latest" + ], "command": "uvx", - "args": ["awslabs.aws-pricing-mcp-server@latest"], + "disabled": false, "env": { "FASTMCP_LOG_LEVEL": "ERROR" }, "timeout": 120000, - "disabled": false - }, - "awsknowledge": { - "url": "https://knowledge-mcp.global.api.aws", - "type": "http" + "type": "stdio" } } } diff --git a/schemas/marketplace.schema.json b/schemas/marketplace.schema.json index 836813b2..d42670c1 100644 --- a/schemas/marketplace.schema.json +++ b/schemas/marketplace.schema.json @@ -1,47 +1,55 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://awslabs.github.io/agent-plugins/schemas/marketplace.schema.json", - "title": "Marketplace Registry", + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Schema for marketplace.json registry file", - "type": "object", - "required": ["name", "owner", "metadata", "plugins"], "properties": { - "name": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" - }, - "owner": { - "type": "object", - "required": ["name"], + "metadata": { "properties": { - "name": { + "description": { "type": "string" }, - "email": { + "version": { "type": "string" } - } + }, + "type": "object" }, - "metadata": { - "type": "object", + "name": { + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" + }, + "owner": { "properties": { - "description": { + "email": { "type": "string" }, - "version": { + "name": { "type": "string" } - } + }, + "required": [ + "name" + ], + "type": "object" }, "plugins": { - "type": "array", "items": { - "type": "object", - "required": ["name", "source"], "properties": { + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, "name": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" }, "source": { "oneOf": [ @@ -49,7 +57,6 @@ "type": "string" }, { - "type": "object", "properties": { "github": { "type": "string" @@ -57,33 +64,36 @@ "url": { "type": "string" } - } + }, + "type": "object" } ] }, - "description": { - "type": "string" - }, - "version": { - "type": "string" - }, - "category": { - "type": "string" - }, "tags": { - "type": "array", "items": { "type": "string" - } + }, + "type": "array" }, - "keywords": { - "type": "array", - "items": { - "type": "string" - } + "version": { + "type": "string" } - } - } + }, + "required": [ + "name", + "source" + ], + "type": "object" + }, + "type": "array" } - } + }, + "required": [ + "name", + "owner", + "metadata", + "plugins" + ], + "title": "Marketplace Registry", + "type": "object" } diff --git a/schemas/mcp.schema.json b/schemas/mcp.schema.json index c5f878df..a1ebc0c5 100644 --- a/schemas/mcp.schema.json +++ b/schemas/mcp.schema.json @@ -1,70 +1,95 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://awslabs.github.io/agent-plugins/schemas/mcp.schema.json", - "title": "MCP Configuration", + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Schema for .mcp.json MCP server definitions", - "type": "object", - "required": ["mcpServers"], "properties": { "mcpServers": { - "type": "object", "additionalProperties": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["stdio", "http"], - "default": "stdio" - }, - "command": { - "type": "string", - "description": "Command to execute (stdio type)" + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "stdio" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "command" + ] + } }, + { + "if": { + "properties": { + "type": { + "const": "http" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "url" + ] + } + } + ], + "properties": { "args": { - "type": "array", + "description": "Command arguments (stdio type)", "items": { "type": "string" }, - "description": "Command arguments (stdio type)" + "type": "array" }, - "url": { - "type": "string", - "description": "HTTP endpoint (http type)" + "command": { + "description": "Command to execute (stdio type)", + "type": "string" + }, + "disabled": { + "default": false, + "type": "boolean" }, "env": { - "type": "object", "additionalProperties": { "type": "string" }, - "description": "Environment variables" + "description": "Environment variables", + "type": "object" }, "timeout": { - "type": "integer", + "description": "Timeout in milliseconds", "minimum": 1000, - "description": "Timeout in milliseconds" + "type": "integer" }, - "disabled": { - "type": "boolean", - "default": false - } - }, - "allOf": [ - { - "if": { - "properties": { "type": { "const": "stdio" } }, - "required": ["type"] - }, - "then": { "required": ["command"] } + "type": { + "default": "stdio", + "enum": [ + "stdio", + "http" + ], + "type": "string" }, - { - "if": { - "properties": { "type": { "const": "http" } }, - "required": ["type"] - }, - "then": { "required": ["url"] } + "url": { + "description": "HTTP endpoint (http type)", + "type": "string" } - ] - } + }, + "type": "object" + }, + "type": "object" } - } + }, + "required": [ + "mcpServers" + ], + "title": "MCP Configuration", + "type": "object" } diff --git a/schemas/plugin.schema.json b/schemas/plugin.schema.json index e6e197a8..0d7417dc 100644 --- a/schemas/plugin.schema.json +++ b/schemas/plugin.schema.json @@ -1,56 +1,58 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://awslabs.github.io/agent-plugins/schemas/plugin.schema.json", - "title": "Plugin Manifest", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": true, "description": "Schema for plugin.json files in Agent Plugins for AWS", - "type": "object", - "required": ["name"], "properties": { - "name": { - "type": "string", - "description": "Plugin identifier (kebab-case)", - "pattern": "^[a-z][a-z0-9-]*$", - "minLength": 1, - "maxLength": 64 - }, - "description": { - "type": "string", - "description": "Brief plugin description", - "maxLength": 500 - }, - "version": { - "type": "string", - "description": "Semantic version", - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[a-zA-Z0-9.-]+)?$" - }, "author": { - "type": "object", "properties": { - "name": { + "email": { "type": "string" }, - "email": { + "name": { "type": "string" } - } + }, + "type": "object" }, - "homepage": { + "description": { + "description": "Brief plugin description", + "maxLength": 500, "type": "string" }, - "repository": { + "homepage": { "type": "string" }, - "license": { - "type": "string", - "description": "SPDX license identifier" - }, "keywords": { - "type": "array", "items": { "type": "string" }, + "type": "array", "uniqueItems": true + }, + "license": { + "description": "SPDX license identifier", + "type": "string" + }, + "name": { + "description": "Plugin identifier (kebab-case)", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" + }, + "repository": { + "type": "string" + }, + "version": { + "description": "Semantic version", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[a-zA-Z0-9.-]+)?$", + "type": "string" } }, - "additionalProperties": true + "required": [ + "name" + ], + "title": "Plugin Manifest", + "type": "object" } diff --git a/schemas/skill-frontmatter.schema.json b/schemas/skill-frontmatter.schema.json index 61350aeb..b1c56cee 100644 --- a/schemas/skill-frontmatter.schema.json +++ b/schemas/skill-frontmatter.schema.json @@ -1,53 +1,58 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://awslabs.github.io/agent-plugins/schemas/skill-frontmatter.schema.json", - "title": "SKILL.md Frontmatter", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": true, "description": "Schema for YAML frontmatter in SKILL.md files", - "type": "object", - "required": ["name", "description"], "properties": { - "name": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$", - "maxLength": 64, - "description": "Skill name (kebab-case)" + "agent": { + "description": "Subagent type (when context: fork)", + "type": "string" }, - "description": { - "type": "string", - "minLength": 20, - "description": "When to use this skill (for auto-triggering)" + "allowed-tools": { + "description": "Comma-separated list of allowed tools", + "type": "string" }, "argument-hint": { - "type": "string", - "description": "Hint for expected arguments" + "description": "Hint for expected arguments", + "type": "string" }, - "disable-model-invocation": { - "type": "boolean", - "default": false, - "description": "Prevent Claude from auto-loading" + "context": { + "description": "Run in forked subagent context", + "enum": [ + "fork" + ], + "type": "string" }, - "user-invocable": { - "type": "boolean", - "default": true, - "description": "Show in slash command menu" + "description": { + "description": "When to use this skill (for auto-triggering)", + "minLength": 20, + "type": "string" }, - "allowed-tools": { - "type": "string", - "description": "Comma-separated list of allowed tools" + "disable-model-invocation": { + "default": false, + "description": "Prevent Claude from auto-loading", + "type": "boolean" }, "model": { - "type": "string", - "description": "Model to use when skill is active" + "description": "Model to use when skill is active", + "type": "string" }, - "context": { - "type": "string", - "enum": ["fork"], - "description": "Run in forked subagent context" + "name": { + "description": "Skill name (kebab-case)", + "maxLength": 64, + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" }, - "agent": { - "type": "string", - "description": "Subagent type (when context: fork)" + "user-invocable": { + "default": true, + "description": "Show in slash command menu", + "type": "boolean" } }, - "additionalProperties": true + "required": [ + "name", + "description" + ], + "title": "SKILL.md Frontmatter", + "type": "object" } diff --git a/tools/validate-cross-refs.cjs b/tools/validate-cross-refs.cjs index 0b076cc3..7f82b55e 100644 --- a/tools/validate-cross-refs.cjs +++ b/tools/validate-cross-refs.cjs @@ -76,9 +76,9 @@ function validatePlugin(plugin) { info(`Validating plugin: ${pluginName}`); // Determine plugin directory path - const source = plugin.source || `./${pluginName}`; - const normalizedSource = source.replace(/^\.\//, "").replace(/\/$/, ""); - const pluginDir = path.join(PLUGINS_ROOT, normalizedSource); + // source is relative to repo root (e.g., "./plugins/deploy-on-aws") + const source = plugin.source || `${PLUGINS_ROOT}/${pluginName}`; + const pluginDir = source.replace(/^\.\//, "").replace(/\/$/, ""); // Check 1: Plugin directory exists if (!fs.existsSync(pluginDir)) {