mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
security: supply chain hardening — CI pinning, dep pinning, and code fixes (#9801)
CI/CD Hardening:
- Pin all 12 GitHub Actions to full commit SHAs (was mutable @vN tags)
- Add explicit permissions: {contents: read} to 4 workflows
- Pin CI pip installs to exact versions (pyyaml==6.0.2, httpx==0.28.1)
- Extend supply-chain-audit.yml to scan workflow, Dockerfile, dependency
manifest, and Actions version changes
Dependency Pinning:
- Pin git-based Python deps to commit SHAs (atroposlib, tinker, yc-bench)
- Pin WhatsApp Baileys from mutable branch to commit SHA
Tool Registry:
- Reject tool name shadowing from different tool families (plugins/MCP
cannot overwrite built-in tools). MCP-to-MCP overwrites still allowed.
MCP Security:
- Add tool description content scanning for prompt injection patterns
- Log detailed change diff on dynamic tool refresh at WARNING level
Skill Manager:
- Fix dangerous verdict bug: agent-created skills with dangerous
findings were silently allowed (ask->None->allow). Now blocked.
This commit is contained in:
parent
9bbf7659e9
commit
eed891f1bb
14 changed files with 214 additions and 53 deletions
5
.github/workflows/contributor-check.yml
vendored
5
.github/workflows/contributor-check.yml
vendored
|
|
@ -9,11 +9,14 @@ on:
|
||||||
- '**/*.py'
|
- '**/*.py'
|
||||||
- '.github/workflows/contributor-check.yml'
|
- '.github/workflows/contributor-check.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-attribution:
|
check-attribution:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Full history needed for git log
|
fetch-depth: 0 # Full history needed for git log
|
||||||
|
|
||||||
|
|
|
||||||
12
.github/workflows/deploy-site.yml
vendored
12
.github/workflows/deploy-site.yml
vendored
|
|
@ -28,20 +28,20 @@ jobs:
|
||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deploy.outputs.page_url }}
|
url: ${{ steps.deploy.outputs.page_url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: website/package-lock.json
|
cache-dependency-path: website/package-lock.json
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Install PyYAML for skill extraction
|
- name: Install PyYAML for skill extraction
|
||||||
run: pip install pyyaml httpx
|
run: pip install pyyaml==6.0.2 httpx==0.28.1
|
||||||
|
|
||||||
- name: Extract skill metadata for dashboard
|
- name: Extract skill metadata for dashboard
|
||||||
run: python3 website/scripts/extract-skills.py
|
run: python3 website/scripts/extract-skills.py
|
||||||
|
|
@ -73,10 +73,10 @@ jobs:
|
||||||
echo "hermes-agent.nousresearch.com" > _site/CNAME
|
echo "hermes-agent.nousresearch.com" > _site/CNAME
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||||
with:
|
with:
|
||||||
path: _site
|
path: _site
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deploy
|
id: deploy
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||||
|
|
|
||||||
14
.github/workflows/docker-publish.yml
vendored
14
.github/workflows/docker-publish.yml
vendored
|
|
@ -23,21 +23,21 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
|
|
||||||
# Build amd64 only so we can `load` the image for smoke testing.
|
# Build amd64 only so we can `load` the image for smoke testing.
|
||||||
# `load: true` cannot export a multi-arch manifest to the local daemon.
|
# `load: true` cannot export a multi-arch manifest to the local daemon.
|
||||||
# The multi-arch build follows on push to main / release.
|
# The multi-arch build follows on push to main / release.
|
||||||
- name: Build image (amd64, smoke test)
|
- name: Build image (amd64, smoke test)
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
|
|
@ -56,14 +56,14 @@ jobs:
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'release'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'release'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Push multi-arch image (main branch)
|
- name: Push multi-arch image (main branch)
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
|
|
@ -75,7 +75,7 @@ jobs:
|
||||||
|
|
||||||
- name: Push multi-arch image (release)
|
- name: Push multi-arch image (release)
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
|
|
|
||||||
9
.github/workflows/docs-site-checks.yml
vendored
9
.github/workflows/docs-site-checks.yml
vendored
|
|
@ -7,13 +7,16 @@ on:
|
||||||
- '.github/workflows/docs-site-checks.yml'
|
- '.github/workflows/docs-site-checks.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docs-site-checks:
|
docs-site-checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
@ -23,7 +26,7 @@ jobs:
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: website
|
working-directory: website
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
|
|
|
||||||
5
.github/workflows/nix.yml
vendored
5
.github/workflows/nix.yml
vendored
|
|
@ -14,6 +14,9 @@ on:
|
||||||
- 'run_agent.py'
|
- 'run_agent.py'
|
||||||
- 'acp_adapter/**'
|
- 'acp_adapter/**'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: nix-${{ github.ref }}
|
group: nix-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
@ -26,7 +29,7 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
|
- uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@565684385bcd71bad329742eefe8d12f2e765b39 # v13
|
- uses: DeterminateSystems/magic-nix-cache-action@565684385bcd71bad329742eefe8d12f2e765b39 # v13
|
||||||
- name: Check flake
|
- name: Check flake
|
||||||
|
|
|
||||||
22
.github/workflows/skills-index.yml
vendored
22
.github/workflows/skills-index.yml
vendored
|
|
@ -20,14 +20,14 @@ jobs:
|
||||||
if: github.repository == 'NousResearch/hermes-agent'
|
if: github.repository == 'NousResearch/hermes-agent'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install httpx pyyaml
|
run: pip install httpx==0.28.1 pyyaml==6.0.2
|
||||||
|
|
||||||
- name: Build skills index
|
- name: Build skills index
|
||||||
env:
|
env:
|
||||||
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
run: python scripts/build_skills_index.py
|
run: python scripts/build_skills_index.py
|
||||||
|
|
||||||
- name: Upload index artifact
|
- name: Upload index artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: skills-index
|
name: skills-index
|
||||||
path: website/static/api/skills-index.json
|
path: website/static/api/skills-index.json
|
||||||
|
|
@ -53,25 +53,25 @@ jobs:
|
||||||
# Only deploy on schedule or manual trigger (not on every push to the script)
|
# Only deploy on schedule or manual trigger (not on every push to the script)
|
||||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: skills-index
|
name: skills-index
|
||||||
path: website/static/api/
|
path: website/static/api/
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: website/package-lock.json
|
cache-dependency-path: website/package-lock.json
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Install PyYAML for skill extraction
|
- name: Install PyYAML for skill extraction
|
||||||
run: pip install pyyaml
|
run: pip install pyyaml==6.0.2
|
||||||
|
|
||||||
- name: Extract skill metadata for dashboard
|
- name: Extract skill metadata for dashboard
|
||||||
run: python3 website/scripts/extract-skills.py
|
run: python3 website/scripts/extract-skills.py
|
||||||
|
|
@ -92,10 +92,10 @@ jobs:
|
||||||
echo "hermes-agent.nousresearch.com" > _site/CNAME
|
echo "hermes-agent.nousresearch.com" > _site/CNAME
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||||
with:
|
with:
|
||||||
path: _site
|
path: _site
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deploy
|
id: deploy
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||||
|
|
|
||||||
58
.github/workflows/supply-chain-audit.yml
vendored
58
.github/workflows/supply-chain-audit.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|
@ -149,6 +149,62 @@ jobs:
|
||||||
"
|
"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- CI/CD workflow files modified ---
|
||||||
|
WORKFLOW_HITS=$(git diff --name-only "$BASE".."$HEAD" | grep -E '\.github/workflows/.*\.ya?ml$' || true)
|
||||||
|
if [ -n "$WORKFLOW_HITS" ]; then
|
||||||
|
FINDINGS="${FINDINGS}
|
||||||
|
### ⚠️ WARNING: CI/CD workflow files modified
|
||||||
|
Changes to workflow files can alter build pipelines, inject steps, or modify permissions. Verify no unauthorized actions or secrets access were added.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
\`\`\`
|
||||||
|
${WORKFLOW_HITS}
|
||||||
|
\`\`\`
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Dockerfile / container build files modified ---
|
||||||
|
DOCKER_HITS=$(git diff --name-only "$BASE".."$HEAD" | grep -iE '(Dockerfile|\.dockerignore|docker-compose)' || true)
|
||||||
|
if [ -n "$DOCKER_HITS" ]; then
|
||||||
|
FINDINGS="${FINDINGS}
|
||||||
|
### ⚠️ WARNING: Container build files modified
|
||||||
|
Changes to Dockerfiles or compose files can alter base images, add build steps, or expose ports. Verify base image pins and build commands.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
\`\`\`
|
||||||
|
${DOCKER_HITS}
|
||||||
|
\`\`\`
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Dependency manifest files modified ---
|
||||||
|
DEP_HITS=$(git diff --name-only "$BASE".."$HEAD" | grep -E '(pyproject\.toml|requirements.*\.txt|package\.json|Gemfile|go\.mod|Cargo\.toml)$' || true)
|
||||||
|
if [ -n "$DEP_HITS" ]; then
|
||||||
|
FINDINGS="${FINDINGS}
|
||||||
|
### ⚠️ WARNING: Dependency manifest files modified
|
||||||
|
Changes to dependency files can introduce new packages or change version pins. Verify all dependency changes are intentional and from trusted sources.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
\`\`\`
|
||||||
|
${DEP_HITS}
|
||||||
|
\`\`\`
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- GitHub Actions version unpinning (mutable tags instead of SHAs) ---
|
||||||
|
ACTIONS_UNPIN=$(echo "$DIFF" | grep -n '^\+' | grep 'uses:' | grep -v '#' | grep -E '@v[0-9]' | head -10 || true)
|
||||||
|
if [ -n "$ACTIONS_UNPIN" ]; then
|
||||||
|
FINDINGS="${FINDINGS}
|
||||||
|
### ⚠️ WARNING: GitHub Actions with mutable version tags
|
||||||
|
Actions should be pinned to full commit SHAs (not \`@v4\`, \`@v5\`). Mutable tags can be retargeted silently if a maintainer account is compromised.
|
||||||
|
|
||||||
|
**Matches:**
|
||||||
|
\`\`\`
|
||||||
|
${ACTIONS_UNPIN}
|
||||||
|
\`\`\`
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Output results ---
|
# --- Output results ---
|
||||||
if [ -n "$FINDINGS" ]; then
|
if [ -n "$FINDINGS" ]; then
|
||||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
|
||||||
11
.github/workflows/tests.yml
vendored
11
.github/workflows/tests.yml
vendored
|
|
@ -6,6 +6,9 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
# Cancel in-progress runs for the same PR/branch
|
# Cancel in-progress runs for the same PR/branch
|
||||||
concurrency:
|
concurrency:
|
||||||
group: tests-${{ github.ref }}
|
group: tests-${{ github.ref }}
|
||||||
|
|
@ -17,13 +20,13 @@ jobs:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt-get update && sudo apt-get install -y ripgrep
|
run: sudo apt-get update && sudo apt-get install -y ripgrep
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
||||||
|
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
run: uv python install 3.11
|
run: uv python install 3.11
|
||||||
|
|
@ -49,10 +52,10 @@ jobs:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
||||||
|
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
run: uv python install 3.11
|
run: uv python install 3.11
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,13 @@ dingtalk = ["dingtalk-stream>=0.1.0,<1"]
|
||||||
feishu = ["lark-oapi>=1.5.3,<2"]
|
feishu = ["lark-oapi>=1.5.3,<2"]
|
||||||
web = ["fastapi>=0.104.0,<1", "uvicorn[standard]>=0.24.0,<1"]
|
web = ["fastapi>=0.104.0,<1", "uvicorn[standard]>=0.24.0,<1"]
|
||||||
rl = [
|
rl = [
|
||||||
"atroposlib @ git+https://github.com/NousResearch/atropos.git",
|
"atroposlib @ git+https://github.com/NousResearch/atropos.git@c20c85256e5a45ad31edf8b7276e9c5ee1995a30",
|
||||||
"tinker @ git+https://github.com/thinking-machines-lab/tinker.git",
|
"tinker @ git+https://github.com/thinking-machines-lab/tinker.git@30517b667f18a3dfb7ef33fb56cf686d5820ba2b",
|
||||||
"fastapi>=0.104.0,<1",
|
"fastapi>=0.104.0,<1",
|
||||||
"uvicorn[standard]>=0.24.0,<1",
|
"uvicorn[standard]>=0.24.0,<1",
|
||||||
"wandb>=0.15.0,<1",
|
"wandb>=0.15.0,<1",
|
||||||
]
|
]
|
||||||
yc-bench = ["yc-bench @ git+https://github.com/collinear-ai/yc-bench.git ; python_version >= '3.12'"]
|
yc-bench = ["yc-bench @ git+https://github.com/collinear-ai/yc-bench.git@bfb0c88062450f46341bd9a5298903fc2e952a5c ; python_version >= '3.12'"]
|
||||||
all = [
|
all = [
|
||||||
"hermes-agent[modal]",
|
"hermes-agent[modal]",
|
||||||
"hermes-agent[daytona]",
|
"hermes-agent[daytona]",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"start": "node bridge.js"
|
"start": "node bridge.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@whiskeysockets/baileys": "WhiskeySockets/Baileys#fix/abprops-abt-fetch",
|
"@whiskeysockets/baileys": "WhiskeySockets/Baileys#01047debd81beb20da7b7779b08edcb06aa03770",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"pino": "^9.0.0"
|
"pino": "^9.0.0"
|
||||||
|
|
|
||||||
|
|
@ -2837,7 +2837,7 @@ class TestRegistryCollisionWarning:
|
||||||
"""registry.register() warns when a tool name is overwritten by a different toolset."""
|
"""registry.register() warns when a tool name is overwritten by a different toolset."""
|
||||||
|
|
||||||
def test_overwrite_different_toolset_logs_warning(self, caplog):
|
def test_overwrite_different_toolset_logs_warning(self, caplog):
|
||||||
"""Overwriting a tool from a different toolset emits a warning."""
|
"""Overwriting a tool from a different toolset is REJECTED with an error."""
|
||||||
from tools.registry import ToolRegistry
|
from tools.registry import ToolRegistry
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
@ -2847,11 +2847,13 @@ class TestRegistryCollisionWarning:
|
||||||
|
|
||||||
reg.register(name="my_tool", toolset="builtin", schema=schema, handler=handler)
|
reg.register(name="my_tool", toolset="builtin", schema=schema, handler=handler)
|
||||||
|
|
||||||
with caplog.at_level(logging.WARNING, logger="tools.registry"):
|
with caplog.at_level(logging.ERROR, logger="tools.registry"):
|
||||||
reg.register(name="my_tool", toolset="mcp-ext", schema=schema, handler=handler)
|
reg.register(name="my_tool", toolset="mcp-ext", schema=schema, handler=handler)
|
||||||
|
|
||||||
assert any("collision" in r.message.lower() for r in caplog.records)
|
assert any("rejected" in r.message.lower() for r in caplog.records)
|
||||||
assert any("builtin" in r.message and "mcp-ext" in r.message for r in caplog.records)
|
assert any("builtin" in r.message and "mcp-ext" in r.message for r in caplog.records)
|
||||||
|
# The original tool should still be from 'builtin', not overwritten
|
||||||
|
assert reg.get_toolset_for_tool("my_tool") == "builtin"
|
||||||
|
|
||||||
def test_overwrite_same_toolset_no_warning(self, caplog):
|
def test_overwrite_same_toolset_no_warning(self, caplog):
|
||||||
"""Re-registering within the same toolset is silent (e.g. reconnect)."""
|
"""Re-registering within the same toolset is silent (e.g. reconnect)."""
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,58 @@ def _sanitize_error(text: str) -> str:
|
||||||
return _CREDENTIAL_PATTERN.sub("[REDACTED]", text)
|
return _CREDENTIAL_PATTERN.sub("[REDACTED]", text)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MCP tool description content scanning
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Patterns that indicate potential prompt injection in MCP tool descriptions.
|
||||||
|
# These are WARNING-level — we log but don't block, since false positives
|
||||||
|
# would break legitimate MCP servers.
|
||||||
|
_MCP_INJECTION_PATTERNS = [
|
||||||
|
(re.compile(r"ignore\s+(all\s+)?previous\s+instructions", re.I),
|
||||||
|
"prompt override attempt ('ignore previous instructions')"),
|
||||||
|
(re.compile(r"you\s+are\s+now\s+a", re.I),
|
||||||
|
"identity override attempt ('you are now a...')"),
|
||||||
|
(re.compile(r"your\s+new\s+(task|role|instructions?)\s+(is|are)", re.I),
|
||||||
|
"task override attempt"),
|
||||||
|
(re.compile(r"system\s*:\s*", re.I),
|
||||||
|
"system prompt injection attempt"),
|
||||||
|
(re.compile(r"<\s*(system|human|assistant)\s*>", re.I),
|
||||||
|
"role tag injection attempt"),
|
||||||
|
(re.compile(r"do\s+not\s+(tell|inform|mention|reveal)", re.I),
|
||||||
|
"concealment instruction"),
|
||||||
|
(re.compile(r"(curl|wget|fetch)\s+https?://", re.I),
|
||||||
|
"network command in description"),
|
||||||
|
(re.compile(r"base64\.(b64decode|decodebytes)", re.I),
|
||||||
|
"base64 decode reference"),
|
||||||
|
(re.compile(r"exec\s*\(|eval\s*\(", re.I),
|
||||||
|
"code execution reference"),
|
||||||
|
(re.compile(r"import\s+(subprocess|os|shutil|socket)", re.I),
|
||||||
|
"dangerous import reference"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_mcp_description(server_name: str, tool_name: str, description: str) -> List[str]:
|
||||||
|
"""Scan an MCP tool description for prompt injection patterns.
|
||||||
|
|
||||||
|
Returns a list of finding strings (empty = clean).
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
if not description:
|
||||||
|
return findings
|
||||||
|
for pattern, reason in _MCP_INJECTION_PATTERNS:
|
||||||
|
if pattern.search(description):
|
||||||
|
findings.append(reason)
|
||||||
|
if findings:
|
||||||
|
logger.warning(
|
||||||
|
"MCP server '%s' tool '%s': suspicious description content — %s. "
|
||||||
|
"Description: %.200s",
|
||||||
|
server_name, tool_name, "; ".join(findings),
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
def _prepend_path(env: dict, directory: str) -> dict:
|
def _prepend_path(env: dict, directory: str) -> dict:
|
||||||
"""Prepend *directory* to env PATH if it is not already present."""
|
"""Prepend *directory* to env PATH if it is not already present."""
|
||||||
updated = dict(env or {})
|
updated = dict(env or {})
|
||||||
|
|
@ -798,6 +850,9 @@ class MCPServerTask:
|
||||||
from toolsets import TOOLSETS
|
from toolsets import TOOLSETS
|
||||||
|
|
||||||
async with self._refresh_lock:
|
async with self._refresh_lock:
|
||||||
|
# Capture old tool names for change diff
|
||||||
|
old_tool_names = set(self._registered_tool_names)
|
||||||
|
|
||||||
# 1. Fetch current tool list from server
|
# 1. Fetch current tool list from server
|
||||||
tools_result = await self.session.list_tools()
|
tools_result = await self.session.list_tools()
|
||||||
new_mcp_tools = tools_result.tools if hasattr(tools_result, "tools") else []
|
new_mcp_tools = tools_result.tools if hasattr(tools_result, "tools") else []
|
||||||
|
|
@ -817,10 +872,26 @@ class MCPServerTask:
|
||||||
self.name, self, self._config
|
self.name, self, self._config
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
# 5. Log what changed (user-visible notification)
|
||||||
"MCP server '%s': dynamically refreshed %d tool(s)",
|
new_tool_names = set(self._registered_tool_names)
|
||||||
self.name, len(self._registered_tool_names),
|
added = new_tool_names - old_tool_names
|
||||||
)
|
removed = old_tool_names - new_tool_names
|
||||||
|
changes = []
|
||||||
|
if added:
|
||||||
|
changes.append(f"added: {', '.join(sorted(added))}")
|
||||||
|
if removed:
|
||||||
|
changes.append(f"removed: {', '.join(sorted(removed))}")
|
||||||
|
if changes:
|
||||||
|
logger.warning(
|
||||||
|
"MCP server '%s': tools changed dynamically — %s. "
|
||||||
|
"Verify these changes are expected.",
|
||||||
|
self.name, "; ".join(changes),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"MCP server '%s': dynamically refreshed %d tool(s) (no changes)",
|
||||||
|
self.name, len(self._registered_tool_names),
|
||||||
|
)
|
||||||
|
|
||||||
async def _run_stdio(self, config: dict):
|
async def _run_stdio(self, config: dict):
|
||||||
"""Run the server using stdio transport."""
|
"""Run the server using stdio transport."""
|
||||||
|
|
@ -1838,6 +1909,10 @@ def _register_server_tools(name: str, server: MCPServerTask, config: dict) -> Li
|
||||||
if not _should_register(mcp_tool.name):
|
if not _should_register(mcp_tool.name):
|
||||||
logger.debug("MCP server '%s': skipping tool '%s' (filtered by config)", name, mcp_tool.name)
|
logger.debug("MCP server '%s': skipping tool '%s' (filtered by config)", name, mcp_tool.name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Scan tool description for prompt injection patterns
|
||||||
|
_scan_mcp_description(name, mcp_tool.name, mcp_tool.description or "")
|
||||||
|
|
||||||
schema = _convert_mcp_schema(name, mcp_tool)
|
schema = _convert_mcp_schema(name, mcp_tool)
|
||||||
tool_name_prefixed = schema["name"]
|
tool_name_prefixed = schema["name"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,11 +117,27 @@ class ToolRegistry:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
existing = self._tools.get(name)
|
existing = self._tools.get(name)
|
||||||
if existing and existing.toolset != toolset:
|
if existing and existing.toolset != toolset:
|
||||||
logger.warning(
|
# Allow MCP-to-MCP overwrites (legitimate: server refresh,
|
||||||
"Tool name collision: '%s' (toolset '%s') is being "
|
# or two MCP servers with overlapping tool names).
|
||||||
"overwritten by toolset '%s'",
|
both_mcp = (
|
||||||
name, existing.toolset, toolset,
|
existing.toolset.startswith("mcp-")
|
||||||
|
and toolset.startswith("mcp-")
|
||||||
)
|
)
|
||||||
|
if both_mcp:
|
||||||
|
logger.debug(
|
||||||
|
"Tool '%s': MCP toolset '%s' overwriting MCP toolset '%s'",
|
||||||
|
name, toolset, existing.toolset,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Reject shadowing — prevent plugins/MCP from overwriting
|
||||||
|
# built-in tools or vice versa.
|
||||||
|
logger.error(
|
||||||
|
"Tool registration REJECTED: '%s' (toolset '%s') would "
|
||||||
|
"shadow existing tool from toolset '%s'. Deregister the "
|
||||||
|
"existing tool first if this is intentional.",
|
||||||
|
name, toolset, existing.toolset,
|
||||||
|
)
|
||||||
|
return
|
||||||
self._tools[name] = ToolEntry(
|
self._tools[name] = ToolEntry(
|
||||||
name=name,
|
name=name,
|
||||||
toolset=toolset,
|
toolset=toolset,
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,11 @@ def _security_scan_skill(skill_dir: Path) -> Optional[str]:
|
||||||
report = format_scan_report(result)
|
report = format_scan_report(result)
|
||||||
return f"Security scan blocked this skill ({reason}):\n{report}"
|
return f"Security scan blocked this skill ({reason}):\n{report}"
|
||||||
if allowed is None:
|
if allowed is None:
|
||||||
# "ask" — allow but include the warning so the user sees the findings
|
# "ask" verdict — for agent-created skills this means dangerous
|
||||||
|
# findings were detected. Block the skill and include the report.
|
||||||
report = format_scan_report(result)
|
report = format_scan_report(result)
|
||||||
logger.warning("Agent-created skill has security findings: %s", reason)
|
logger.warning("Agent-created skill blocked (dangerous findings): %s", reason)
|
||||||
# Don't block — return None to allow, but log the warning
|
return f"Security scan blocked this skill ({reason}):\n{report}"
|
||||||
return None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Security scan failed for %s: %s", skill_dir, e, exc_info=True)
|
logger.warning("Security scan failed for %s: %s", skill_dir, e, exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue