Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/attest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ on:
schedule:
- cron: "17 4 * * *"
workflow_dispatch:
inputs:
max_targets:
description: "Attest at most N pending targets (0 = all). Use a small N for a smoke run."
required: false
default: "0"

permissions:
contents: read
Expand All @@ -38,9 +43,11 @@ jobs:
with:
python-version: "3.12"
- id: plan
env:
MAX_TARGETS: ${{ inputs.max_targets || '0' }}
run: |
pip install --quiet "pyyaml==6.0.3"
m=$(python3 tools/plan.py --attester "$ATTESTER_ID")
m=$(python3 tools/plan.py --attester "$ATTESTER_ID" --limit "$MAX_TARGETS")
n=$(printf '%s' "$m" | python3 -c 'import json,sys;print(len(json.load(sys.stdin)))')
echo "matrix=$m" >> "$GITHUB_OUTPUT"
echo "count=$n" >> "$GITHUB_OUTPUT"
Expand Down
21 changes: 17 additions & 4 deletions targets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ targets:
- node
- /cache/npm/@modelcontextprotocol__server-redis/node_modules/.bin/mcp-server-redis
- redis://127.0.0.1:6379
note: connects at startup
note: connects to Redis at startup (ECONNREFUSED under --network none); cannot list a surface without a live backend
skip: true
- coordinate: npm:@modelcontextprotocol/server-everart@0.6.2
spawn:
- node
Expand Down Expand Up @@ -129,6 +130,8 @@ targets:
- /cache/npm/@stripe__mcp/node_modules/.bin/mcp
- --tools=all
- --api-key=sk_test_DUMMY
skip: true
note: never completes the MCP handshake in the sandbox (30 s); needs investigation before attesting
- coordinate: npm:@sentry/mcp-server@0.39.0
spawn:
- node
Expand Down Expand Up @@ -174,7 +177,8 @@ targets:
- /cache/npm/mcp-server-kubernetes/node_modules/.bin/mcp-server-kubernetes
env:
KUBECONFIG: /tmp/kubeconfig
note: may need a cluster
note: crashes without a reachable cluster/kubeconfig
skip: true
- coordinate: npm:@agentdeskai/browser-tools-mcp@2.0.2
spawn:
- node
Expand Down Expand Up @@ -202,13 +206,17 @@ targets:
- from mcp_server_sqlite import main; main()
- --db-path
- /tmp/test.db
skip: true
note: "incompatible with mcp 2.x (Server.list_resources removed) \u2014 what uvx resolves today also crashes"
- coordinate: pypi:mcp-server-sentry@2025.1.14
spawn:
- python3
- -c
- from mcp_server_sentry import main; main()
- --auth-token
- DUMMY
skip: true
note: incompatible with mcp 2.x (McpError import)
- coordinate: pypi:mcp-atlassian@0.23.1
spawn:
- python3
Expand All @@ -226,6 +234,8 @@ targets:
- python3
- -c
- from mcp_server_tree_sitter.server import main; main()
skip: true
note: incompatible with mcp 2.x (mcp.server.fastmcp renamed)
- coordinate: pypi:arxiv-mcp-server@0.7.2
spawn:
- python3
Expand All @@ -249,6 +259,8 @@ targets:
env:
QDRANT_URL: http://127.0.0.1:6333
COLLECTION_NAME: x
skip: true
note: resolves a hostname at startup (fastembed model fetch) under --network none; cannot list a surface offline
- coordinate: pypi:duckduckgo-mcp-server@0.6.1
spawn:
- python3
Expand All @@ -259,14 +271,15 @@ targets:
- python3
- -c
- from mcp_server_docker import main; main()
note: needs a docker socket
note: requires Python >= 3.12; attester image is 3.11
skip: true
- coordinate: pypi:mcp-server-motherduck@1.0.8
spawn:
- python3
- -c
- from mcp_server_motherduck import main; main()
- --db-path
- ':memory:'
- /tmp/attest.duckdb
- coordinate: pypi:mcp-neo4j-cypher@0.6.0
spawn:
- python3
Expand Down
18 changes: 11 additions & 7 deletions tools/attest_one.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ set -euo pipefail
: "${TARGET_JSON:?TARGET_JSON (one plan.py entry) is required}"
: "${IMAGE:?IMAGE is required}"
OUT="${OUT:-$PWD/out}"; CACHE="${CACHE:-$PWD/cache}"
mkdir -p "$OUT" "$CACHE"
mkdir -p "$OUT" "$CACHE" "$CACHE/.tmp" # stage-1 scratch is disk-backed: dependency trees can exceed any sane tmpfs
coord=$(printf '%s' "$TARGET_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["coordinate"])')
lock_rel=$(printf '%s' "$TARGET_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["lock_rel"])')
mapfile -t spawn < <(printf '%s' "$TARGET_JSON" | python3 -c 'import json,sys;[print(a) for a in json.load(sys.stdin)["spawn"]]')
Expand All @@ -21,22 +21,26 @@ echo "== [$coord] stage 1: pre-fetch (network on, --ignore-scripts / --only-bina
case "$eco" in
npm)
docker run --rm --network bridge --user "$uid:$gid" -e HOME=/tmp -e npm_config_cache=/tmp/npm-cache \
--tmpfs /tmp:rw,nosuid,size=256m -v "$CACHE:/cache" "$IMAGE" \
-v "$CACHE/.tmp:/tmp" -v "$CACHE:/cache" "$IMAGE" \
npm install --prefix "/cache/npm/$segment" --ignore-scripts --no-audit --no-fund --no-package-lock --loglevel=error "$name@$version" ;;
pypi)
docker run --rm --network bridge --user "$uid:$gid" -e HOME=/tmp \
--tmpfs /tmp:rw,nosuid,size=256m -v "$CACHE:/cache" "$IMAGE" \
-v "$CACHE/.tmp:/tmp" -e TMPDIR=/tmp -v "$CACHE:/cache" "$IMAGE" \
pip install --quiet --no-cache-dir --only-binary :all: --target "/cache/pypi/$segment" "$name==$version" ;;
*) echo "unknown ecosystem $eco" >&2; exit 2 ;;
esac
echo "== [$coord] stage 2: sandboxed capture (network none, non-root, read-only, cap-drop ALL)"
envflags=(); for kv in "${envkv[@]:-}"; do [ -n "$kv" ] && envflags+=(-e "$kv"); done
# The MCP SDK spawns stdio servers with a MINIMAL default environment (HOME, PATH,
# USER…), not the parent's — so the target's env, and PYTHONPATH for PyPI targets, are
# delivered by wrapping the spawn in `env K=V …` inside the sandbox. The lock records
# `env` as the command; the surface digest consensus compares excludes `server`.
wrap=(env "HOME=/tmp" "PYTHONDONTWRITEBYTECODE=1" "PYTHONPATH=/cache/pypi/$segment" "NODE_PATH=/cache/npm/$segment/node_modules")
for kv in "${envkv[@]:-}"; do [ -n "$kv" ] && wrap+=("$kv"); done
docker run --rm --network none --user "$uid:$gid" --read-only --cap-drop ALL \
--security-opt no-new-privileges --pids-limit 256 --memory 512m \
--tmpfs /tmp:rw,noexec,nosuid,size=64m -e HOME=/tmp -e PYTHONDONTWRITEBYTECODE=1 \
-e "PYTHONPATH=/cache/pypi/$segment" -e "NODE_PATH=/cache/npm/$segment/node_modules" "${envflags[@]}" \
--tmpfs /tmp:rw,noexec,nosuid,size=64m -e HOME=/tmp \
-v "$CACHE:/cache:ro" -v "$OUT:/out" "$IMAGE" \
timeout 120 mcp-warden pin --lock "/out/$lock_rel" -- "${spawn[@]}"
timeout 120 mcp-warden pin --lock "/out/$lock_rel" -- "${wrap[@]}" "${spawn[@]}"
test -s "$lock_path"
if [ "${SIGN:-0}" = "1" ]; then
echo "== [$coord] stage 3: sign on host (v2 statement, ambient OIDC)"
Expand Down
3 changes: 3 additions & 0 deletions tools/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--attester", default="dse-nightly")
ap.add_argument("--all", action="store_true", help="ignore existing locks (dry runs)")
ap.add_argument("--limit", type=int, default=0, help="attest at most N targets (0 = all)")
ns = ap.parse_args()
doc = yaml.safe_load((ROOT / "targets.yaml").read_text(encoding="utf-8"))
out = []
Expand All @@ -49,6 +50,8 @@ def main() -> int:
"env": t.get("env", {}),
"lock_rel": str((d / f"{ns.attester}.lock").relative_to(ROOT)),
})
if ns.limit > 0:
out = out[: ns.limit]
json.dump(out, sys.stdout, separators=(",", ":"))
return 0

Expand Down