$npx -y skills add github/awesome-copilot --skill agent-supply-chainVerify supply chain integrity for AI agent plugins, tools, and dependencies. Use this skill when: - Generating SHA-256 integrity manifests for agent plugins or tool packages - Verifying that installed plugins match their published manifests - Detecting tampered, modified, or untr
| 1 | # Agent Supply Chain Integrity |
| 2 | |
| 3 | Generate and verify integrity manifests for AI agent plugins and tools. Detect tampering, enforce version pinning, and establish supply chain provenance. |
| 4 | |
| 5 | ## Overview |
| 6 | |
| 7 | Agent plugins and MCP servers have the same supply chain risks as npm packages or container images — except the ecosystem has no equivalent of npm provenance, Sigstore, or SLSA. This skill fills that gap. |
| 8 | |
| 9 | ``` |
| 10 | Plugin Directory → Hash All Files (SHA-256) → Generate INTEGRITY.json |
| 11 | ↓ |
| 12 | Later: Plugin Directory → Re-Hash Files → Compare Against INTEGRITY.json |
| 13 | ↓ |
| 14 | Match? VERIFIED : TAMPERED |
| 15 | ``` |
| 16 | |
| 17 | ## When to Use |
| 18 | |
| 19 | - Before promoting a plugin from development to production |
| 20 | - During code review of plugin PRs |
| 21 | - As a CI step to verify no files were modified after review |
| 22 | - When auditing third-party agent tools or MCP servers |
| 23 | - Building a plugin marketplace with integrity requirements |
| 24 | |
| 25 | --- |
| 26 | |
| 27 | ## Pattern 1: Generate Integrity Manifest |
| 28 | |
| 29 | Create a deterministic `INTEGRITY.json` with SHA-256 hashes of all plugin files. |
| 30 | |
| 31 | ```python |
| 32 | import hashlib |
| 33 | import json |
| 34 | from datetime import datetime, timezone |
| 35 | from pathlib import Path |
| 36 | |
| 37 | EXCLUDE_DIRS = {".git", "__pycache__", "node_modules", ".venv", ".pytest_cache"} |
| 38 | EXCLUDE_FILES = {".DS_Store", "Thumbs.db", "INTEGRITY.json"} |
| 39 | |
| 40 | def hash_file(path: Path) -> str: |
| 41 | """Compute SHA-256 hex digest of a file.""" |
| 42 | h = hashlib.sha256() |
| 43 | with open(path, "rb") as f: |
| 44 | for chunk in iter(lambda: f.read(8192), b""): |
| 45 | h.update(chunk) |
| 46 | return h.hexdigest() |
| 47 | |
| 48 | def generate_manifest(plugin_dir: str) -> dict: |
| 49 | """Generate an integrity manifest for a plugin directory.""" |
| 50 | root = Path(plugin_dir) |
| 51 | files = {} |
| 52 | |
| 53 | for path in sorted(root.rglob("*")): |
| 54 | if not path.is_file(): |
| 55 | continue |
| 56 | if path.name in EXCLUDE_FILES: |
| 57 | continue |
| 58 | if any(part in EXCLUDE_DIRS for part in path.relative_to(root).parts): |
| 59 | continue |
| 60 | rel = path.relative_to(root).as_posix() |
| 61 | files[rel] = hash_file(path) |
| 62 | |
| 63 | # Chain hash: SHA-256 of all file hashes concatenated in sorted order |
| 64 | chain = hashlib.sha256() |
| 65 | for key in sorted(files.keys()): |
| 66 | chain.update(files[key].encode("ascii")) |
| 67 | |
| 68 | manifest = { |
| 69 | "plugin_name": root.name, |
| 70 | "generated_at": datetime.now(timezone.utc).isoformat(), |
| 71 | "algorithm": "sha256", |
| 72 | "file_count": len(files), |
| 73 | "files": files, |
| 74 | "manifest_hash": chain.hexdigest(), |
| 75 | } |
| 76 | return manifest |
| 77 | |
| 78 | # Generate and save |
| 79 | manifest = generate_manifest("my-plugin/") |
| 80 | Path("my-plugin/INTEGRITY.json").write_text( |
| 81 | json.dumps(manifest, indent=2) + "\n" |
| 82 | ) |
| 83 | print(f"Generated manifest: {manifest['file_count']} files, " |
| 84 | f"hash: {manifest['manifest_hash'][:16]}...") |
| 85 | ``` |
| 86 | |
| 87 | **Output (`INTEGRITY.json`):** |
| 88 | ```json |
| 89 | { |
| 90 | "plugin_name": "my-plugin", |
| 91 | "generated_at": "2026-04-01T03:00:00+00:00", |
| 92 | "algorithm": "sha256", |
| 93 | "file_count": 12, |
| 94 | "files": { |
| 95 | ".claude-plugin/plugin.json": "a1b2c3d4...", |
| 96 | "README.md": "e5f6a7b8...", |
| 97 | "skills/search/SKILL.md": "c9d0e1f2...", |
| 98 | "agency.json": "3a4b5c6d..." |
| 99 | }, |
| 100 | "manifest_hash": "7e8f9a0b1c2d3e4f..." |
| 101 | } |
| 102 | ``` |
| 103 | |
| 104 | --- |
| 105 | |
| 106 | ## Pattern 2: Verify Integrity |
| 107 | |
| 108 | Check that current files match the manifest. |
| 109 | |
| 110 | ```python |
| 111 | # Requires: hash_file() and generate_manifest() from Pattern 1 above |
| 112 | import json |
| 113 | from pathlib import Path |
| 114 | |
| 115 | def verify_manifest(plugin_dir: str) -> tuple[bool, list[str]]: |
| 116 | """Verify plugin files against INTEGRITY.json.""" |
| 117 | root = Path(plugin_dir) |
| 118 | manifest_path = root / "INTEGRITY.json" |
| 119 | |
| 120 | if not manifest_path.exists(): |
| 121 | return False, ["INTEGRITY.json not found"] |
| 122 | |
| 123 | manifest = json.loads(manifest_path.read_text()) |
| 124 | recorded = manifest.get("files", {}) |
| 125 | errors = [] |
| 126 | |
| 127 | # Check recorded files |
| 128 | for rel_path, expected_hash in recorded.items(): |
| 129 | full = root / rel_path |
| 130 | if not full.exists(): |
| 131 | errors.append(f"MISSING: {rel_path}") |
| 132 | continue |
| 133 | actual = hash_file(full) |
| 134 | if actual != expected_hash: |
| 135 | errors.append(f"MODIFIED: {rel_path}") |
| 136 | |
| 137 | # Check for new untracked files |
| 138 | current = generate_manifest(plugin_dir) |
| 139 | for rel_path in current["files"]: |
| 140 | if rel_path not in recorded: |