{"name":"skillproof","files":{"SKILL.md":"---\nname: skillproof\ndescription: \"On-chain provenance for Hermes skills. Register skills with content hash + IPFS, verify authorship, track evolution.\"\nversion: 0.1.0\nauthor: Gokmen (GoGoSns)\nlicense: MIT\nmetadata:\n  hermes:\n    tags: [Web3, Blockchain, Provenance, Attribution, IPFS, Base, Solidity]\n    related_skills: [github-auth]\n---\n\n# SkillProof - On-chain Provenance for Hermes Skills\n\nThis skill lets users prove they wrote a Hermes skill by registering its content hash + IPFS pointer on the Base Sepolia blockchain. Once registered, anyone can verify authorship cryptographically.\n\n## When to use this skill\n\nTrigger this skill when a user asks to:\n- \"Register this skill on-chain\"\n- \"Prove I wrote this skill\"\n- \"Verify who wrote skill X\"\n- \"Add provenance to my skill\"\n- \"Check if this skill is original\"\n\nOr when they mention any combination of: skill + ownership, skill + attribution, skill + on-chain.\n\n## Architecture\n\nUser skill folder (any folder with SKILL.md)\n  -> hash.py computes keccak256 hash of normalized content\n  -> register.py uploads to IPFS via Pinata + calls smart contract\n  -> SkillRegistry on Base Sepolia stores (hash, author, IPFS CID, timestamp)\n  -> verify.py queries contract by hash to prove authorship\n\n## Commands\n\n### register - Register a new skill on-chain\n\nbash:\npython3 ~/.hermes/skills/web3/skillproof/register.py /path/to/skill-folder\n\nWhat happens:\n1. Reads the skill folder (must contain SKILL.md)\n2. Normalizes content (strips whitespace, sorts files)\n3. Computes keccak256 hash\n4. Uploads skill folder to IPFS via Pinata\n5. Calls SkillRegistry.registerSkill(hash, ipfsCid) on Base Sepolia\n6. Prints transaction hash + IPFS CID + BaseScan link\n\n### verify - Verify a skill's provenance\n\nbash:\npython3 ~/.hermes/skills/web3/skillproof/verify.py /path/to/skill-folder\n\nWhat happens:\n1. Computes hash of the skill (same way as register)\n2. Queries SkillRegistry on Base Sepolia\n3. Returns: author wallet address, IPFS CID, registration timestamp\n4. Compares local hash vs on-chain hash\n\nIf unregistered: \"Not on-chain. Anyone can claim authorship.\"\nIf registered: \"Verified. Written by 0xABC...XYZ on 2026-04-30.\"\n\n## Setup (one-time)\n\nThe user needs:\n- A funded Base Sepolia wallet (private key in ~/.hermes/.env as SKILLPROOF_PRIVATE_KEY)\n- A Pinata account (JWT in ~/.hermes/.env as PINATA_JWT)\n- The deployed SkillRegistry contract address (already in code)\n\n## Limitations\n\n- Currently Base Sepolia only (testnet)\n- Single-author model\n- Evolution tracking planned for v0.2.0\n\n## Future (post-hackathon)\n\n- Multi-chain support\n- Skill evolution chains (v1 -> v2 fork tracking)\n- Optional x402 tip layer\n- Reputation scoring\n\n## Author\n\nGokmen (GoGoSns) - Built for Nous Research Hermes Agent Creative Hackathon 2026.\n\n","hash.py":"\"\"\"\nSkillProof - Content Hashing\n============================\n\nComputes a deterministic keccak256 hash of a Hermes skill folder.\n\nWhy deterministic?\n- Same content = same hash, always\n- Even if files are in different order, whitespace differs, etc.\n- The hash is the skill's \"fingerprint\" on the blockchain.\n\nHow it works:\n1. Find all files in the skill folder (excluding .venv, __pycache__, etc.)\n2. Sort them by filename (deterministic order)\n3. Read each file, normalize whitespace\n4. Concatenate: filename + content for each file\n5. Hash the result with keccak256 (same algorithm Ethereum uses)\n\nUsage:\n    python3 hash.py /path/to/skill-folder\n\"\"\"\n\nimport sys\nimport os\nfrom pathlib import Path\nfrom eth_utils import keccak\n\n\n# Files/folders we ignore when hashing\nIGNORE_PATTERNS = {\n    \".venv\", \"__pycache__\", \".git\", \"node_modules\",\n    \".DS_Store\", \".pytest_cache\", \"*.pyc\"\n}\n\n\ndef should_ignore(path: Path) -> bool:\n    \"\"\"Check if a path should be ignored when hashing.\"\"\"\n    for part in path.parts:\n        if part in IGNORE_PATTERNS:\n            return True\n        if part.startswith(\".\") and part not in {\".env\"}:\n            return True\n    return False\n\n\ndef normalize_content(content: bytes) -> bytes:\n    \"\"\"\n    Normalize file content for deterministic hashing.\n    - Convert CRLF (Windows) to LF (Unix)\n    - Strip trailing whitespace from each line\n    - Strip leading/trailing whitespace from whole file\n    \"\"\"\n    text = content.decode(\"utf-8\", errors=\"replace\")\n    # Normalize line endings\n    text = text.replace(\"\\r\\n\", \"\\n\").replace(\"\\r\", \"\\n\")\n    # Strip trailing whitespace from each line\n    lines = [line.rstrip() for line in text.split(\"\\n\")]\n    # Rejoin and strip whole file\n    text = \"\\n\".join(lines).strip()\n    return text.encode(\"utf-8\")\n\n\ndef compute_skill_hash(skill_folder: Path) -> tuple[str, list[str]]:\n    \"\"\"\n    Compute deterministic keccak256 hash of a skill folder.\n\n    Returns:\n        (hash_hex, list_of_files_included)\n    \"\"\"\n    if not skill_folder.exists():\n        raise FileNotFoundError(f\"Folder not found: {skill_folder}\")\n\n    if not skill_folder.is_dir():\n        raise NotADirectoryError(f\"Not a folder: {skill_folder}\")\n\n    # SKILL.md is mandatory\n    skill_md = skill_folder / \"SKILL.md\"\n    if not skill_md.exists():\n        raise FileNotFoundError(\n            f\"No SKILL.md found in {skill_folder}. \"\n            f\"Hermes skills require a SKILL.md manifest.\"\n        )\n\n    # Collect all files (recursively), sorted for determinism\n    all_files = []\n    for path in sorted(skill_folder.rglob(\"*\")):\n        if path.is_file() and not should_ignore(path.relative_to(skill_folder)):\n            all_files.append(path)\n\n    # Build the content stream\n    stream = b\"\"\n    files_included = []\n    for path in all_files:\n        rel_path = path.relative_to(skill_folder).as_posix()\n        files_included.append(rel_path)\n\n        # Add filename to stream (so renaming a file changes the hash)\n        stream += rel_path.encode(\"utf-8\") + b\"\\n\"\n\n        # Add normalized content\n        with open(path, \"rb\") as f:\n            content = f.read()\n        stream += normalize_content(content) + b\"\\n---END-OF-FILE---\\n\"\n\n    # Compute keccak256\n    hash_bytes = keccak(stream)\n    hash_hex = \"0x\" + hash_bytes.hex()\n\n    return hash_hex, files_included\n\n\ndef main():\n    if len(sys.argv) != 2:\n        print(\"Usage: python3 hash.py /path/to/skill-folder\")\n        sys.exit(1)\n\n    skill_folder = Path(sys.argv[1]).resolve()\n\n    try:\n        hash_hex, files = compute_skill_hash(skill_folder)\n    except (FileNotFoundError, NotADirectoryError) as e:\n        print(f\"Error: {e}\")\n        sys.exit(1)\n\n    print(f\"Skill folder: {skill_folder}\")\n    print(f\"Files included: {len(files)}\")\n    for f in files:\n        print(f\"  - {f}\")\n    print(f\"\\nKeccak256 hash:\")\n    print(f\"  {hash_hex}\")\n    print(f\"\\nThis is the skill's unique fingerprint.\")\n    print(f\"Any change to any file changes the hash.\")\n\n\nif __name__ == \"__main__\":\n    main()\n","mock_registry.py":"\"\"\"SkillProof - Mock Registry (development only)\n\nThis file simulates the on-chain SkillRegistry contract for local testing.\nIt will be replaced by real on-chain queries (web3.py) once the contract is\ndeployed to Base Sepolia.\n\nFormat matches the contract's Skill struct:\n  {\n    \"author\": \"0x...\" (wallet address),\n    \"contentHash\": \"0x...\" (keccak256),\n    \"ipfsCid\": \"Qm...\" (IPFS CID),\n    \"registeredAt\": <unix_timestamp>\n  }\n\"\"\"\n\nimport time\n\nMOCK_REGISTRY = {\n    # Pre-populated for demo: a \"registered\" skill\n    \"0xdeadbeef0000000000000000000000000000000000000000000000000000beef\": {\n        \"author\": \"0xA8DBF18e67779C7B7dC839370B85940FF506185d\",\n        \"contentHash\": \"0xdeadbeef0000000000000000000000000000000000000000000000000000beef\",\n        \"ipfsCid\": \"QmExampleMockCidForDemoOnly\",\n        \"registeredAt\": int(time.time()) - 86400,  # 1 day ago\n    }\n}\n\n\ndef get_skill(content_hash: str):\n    \"\"\"Mock equivalent of contract's getSkill(bytes32) function.\"\"\"\n    skill = MOCK_REGISTRY.get(content_hash.lower())\n    if skill is None:\n        # Contract returns zero-filled struct for unregistered hashes\n        return {\n            \"author\": \"0x0000000000000000000000000000000000000000\",\n            \"contentHash\": \"0x\" + \"00\" * 32,\n            \"ipfsCid\": \"\",\n            \"registeredAt\": 0,\n        }\n    return skill\n\n\ndef is_registered(content_hash: str) -> bool:\n    \"\"\"Helper: true if this hash is on-chain (or in mock).\"\"\"\n    skill = get_skill(content_hash)\n    return skill[\"author\"] != \"0x0000000000000000000000000000000000000000\"\n","register.py":"\"\"\"SkillProof - Register Skill On-chain (with IPFS upload)\n\nThis script:\n1. Computes the deterministic hash of a Hermes skill folder\n2. Uploads the skill content to IPFS via Pinata\n3. Records the (hash, ipfs_cid, author) in the registry\n\nUsage:\n    python3 register.py /path/to/skill-folder\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\nimport requests\nfrom dotenv import load_dotenv\n\nfrom hash import compute_skill_hash\nfrom mock_registry import MOCK_REGISTRY, is_registered\n\n\nPINATA_PIN_JSON = \"https://api.pinata.cloud/pinning/pinJSONToIPFS\"\n\nHERE = Path(__file__).resolve().parent\nload_dotenv(HERE / \".env\")\n\n\ndef get_pinata_jwt():\n    jwt = os.getenv(\"PINATA_JWT\", \"\").strip()\n    if not jwt or jwt == \"PASTE_YOUR_JWT_HERE\":\n        print(\"Error: PINATA_JWT not set in .env file\")\n        print(f\"Edit: {HERE / '.env'}\")\n        sys.exit(1)\n    return jwt\n\n\ndef package_skill_as_json(skill_folder):\n    files = {}\n    ignore = {\".venv\", \"__pycache__\", \".git\", \".env\", \".DS_Store\"}\n\n    for path in sorted(skill_folder.rglob(\"*\")):\n        rel = path.relative_to(skill_folder)\n        if any(part in ignore or part.startswith(\".\") for part in rel.parts):\n            continue\n        if not path.is_file():\n            continue\n        try:\n            content = path.read_text(encoding=\"utf-8\")\n        except UnicodeDecodeError:\n            continue\n        files[rel.as_posix()] = content\n\n    skill_name = skill_folder.name\n    skill_md = skill_folder / \"SKILL.md\"\n    if skill_md.exists():\n        for line in skill_md.read_text(encoding=\"utf-8\").splitlines()[:20]:\n            if line.startswith(\"name:\"):\n                skill_name = line.split(\":\", 1)[1].strip()\n                break\n\n    return {\n        \"name\": skill_name,\n        \"files\": files,\n        \"metadata\": {\n            \"packagedAt\": int(time.time()),\n            \"totalFiles\": len(files),\n            \"tool\": \"skillproof-register-v0.1.0\",\n        },\n    }\n\n\ndef upload_to_pinata(payload, jwt, skill_name):\n    headers = {\n        \"Authorization\": f\"Bearer {jwt}\",\n        \"Content-Type\": \"application/json\",\n    }\n    body = {\n        \"pinataContent\": payload,\n        \"pinataMetadata\": {\n            \"name\": f\"skillproof-{skill_name}\",\n            \"keyvalues\": {\"skillName\": skill_name, \"tool\": \"skillproof\"},\n        },\n        \"pinataOptions\": {\"cidVersion\": 1},\n    }\n\n    print(\"  Uploading to Pinata...\")\n    r = requests.post(PINATA_PIN_JSON, headers=headers, json=body, timeout=30)\n\n    if r.status_code != 200:\n        print(f\"Error: Pinata upload failed (HTTP {r.status_code})\")\n        print(f\"Response: {r.text}\")\n        sys.exit(1)\n\n    data = r.json()\n    cid = data.get(\"IpfsHash\") or data.get(\"cid\")\n    if not cid:\n        print(f\"Error: no CID in Pinata response: {data}\")\n        sys.exit(1)\n    return cid\n\n\ndef register_in_registry(content_hash, author, ipfs_cid):\n    if is_registered(content_hash):\n        print(f\"  Already registered (hash: {content_hash[:10]}...)\")\n        return\n    MOCK_REGISTRY[content_hash] = {\n        \"author\": author,\n        \"contentHash\": content_hash,\n        \"ipfsCid\": ipfs_cid,\n        \"registeredAt\": int(time.time()),\n    }\n    print(\"  Registered in mock registry\")\n\n\ndef main():\n    if len(sys.argv) != 2:\n        print(\"Usage: python3 register.py /path/to/skill-folder\")\n        sys.exit(1)\n\n    skill_folder = Path(sys.argv[1]).resolve()\n    if not skill_folder.is_dir():\n        print(f\"Error: not a folder: {skill_folder}\")\n        sys.exit(1)\n\n    jwt = get_pinata_jwt()\n    DEV_WALLET = os.getenv(\n        \"SKILLPROOF_DEV_WALLET\",\n        \"0xA8DBF18e67779C7B7dC839370B85940FF506185d\",\n    )\n\n    print()\n    print(\"=\" * 60)\n    print(\"  SkillProof - Register Skill\")\n    print(\"=\" * 60)\n    print()\n    print(f\"Skill folder: {skill_folder}\")\n    print(f\"Dev wallet:   {DEV_WALLET}\")\n    print()\n\n    print(\"Step 1/3: Computing deterministic hash...\")\n    content_hash, files = compute_skill_hash(skill_folder)\n    print(f\"  Hash: {content_hash}\")\n    print(f\"  Files: {len(files)}\")\n    print()\n\n    print(\"Step 2/3: Packaging and uploading to IPFS...\")\n    payload = package_skill_as_json(skill_folder)\n    skill_name = payload[\"name\"]\n    print(f\"  Skill name: {skill_name}\")\n    cid = upload_to_pinata(payload, jwt, skill_name)\n    print(f\"  IPFS CID: {cid}\")\n    print(f\"  Gateway:  https://gateway.pinata.cloud/ipfs/{cid}\")\n    print()\n\n    print(\"Step 3/3: Registering in registry...\")\n    register_in_registry(content_hash, DEV_WALLET, cid)\n    print()\n\n    print(\"=\" * 60)\n    print(\"  REGISTRATION COMPLETE\")\n    print(\"=\" * 60)\n    print(f\"Hash:      {content_hash}\")\n    print(f\"Author:    {DEV_WALLET}\")\n    print(f\"IPFS:      ipfs://{cid}\")\n    print(f\"View:      https://gateway.pinata.cloud/ipfs/{cid}\")\n    print(f\"Registry:  mock (will be on-chain after Base Sepolia deploy)\")\n    print(\"=\" * 60)\n    print()\n\n\nif __name__ == \"__main__\":\n    main()\n","verify.py":"\"\"\"SkillProof - Verify Skill Authorship\n\nVerifies that a Hermes skill is registered on-chain (or in mock registry\nduring development). Returns the author wallet, IPFS CID, and timestamp.\n\nUsage:\n    python3 verify.py /path/to/skill-folder\n\nOutput:\n    - If registered: shows author, IPFS CID, registered date\n    - If not registered: warning message\n\"\"\"\n\nimport sys\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nfrom hash import compute_skill_hash\nfrom mock_registry import get_skill, is_registered\n\n\ndef short_addr(addr: str) -> str:\n    \"\"\"Format an address as 0xABC...XYZ for display.\"\"\"\n    if not addr or len(addr) < 10:\n        return addr\n    return f\"{addr[:6]}...{addr[-4:]}\"\n\n\ndef format_timestamp(ts: int) -> str:\n    \"\"\"Convert unix timestamp to readable date.\"\"\"\n    if ts == 0:\n        return \"never\"\n    dt = datetime.fromtimestamp(ts, tz=timezone.utc)\n    return dt.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n\n\ndef verify_skill(skill_folder: Path) -> dict:\n    \"\"\"\n    Verify a skill's on-chain provenance.\n\n    Returns a dict with:\n      - hash: the computed local hash\n      - registered: bool\n      - author, ipfsCid, registeredAt: on-chain data (if registered)\n    \"\"\"\n    # Step 1: compute local hash\n    local_hash, files = compute_skill_hash(skill_folder)\n\n    # Step 2: query registry (mock for now, real contract later)\n    skill = get_skill(local_hash)\n    registered = is_registered(local_hash)\n\n    return {\n        \"hash\": local_hash,\n        \"files\": files,\n        \"registered\": registered,\n        \"author\": skill[\"author\"],\n        \"ipfsCid\": skill[\"ipfsCid\"],\n        \"registeredAt\": skill[\"registeredAt\"],\n    }\n\n\ndef print_report(result: dict, skill_folder: Path):\n    \"\"\"Pretty-print the verification result.\"\"\"\n    print()\n    print(\"=\" * 60)\n    print(\"  SkillProof Verification Report\")\n    print(\"=\" * 60)\n    print()\n    print(f\"Skill folder:    {skill_folder}\")\n    print(f\"Files included:  {len(result['files'])}\")\n    for f in result[\"files\"]:\n        print(f\"  - {f}\")\n    print()\n    print(f\"Local hash:      {result['hash']}\")\n    print()\n\n    if result[\"registered\"]:\n        print(\"Status:          REGISTERED ON-CHAIN\")\n        print()\n        print(f\"  Author:        {result['author']}\")\n        print(f\"                 ({short_addr(result['author'])})\")\n        print(f\"  IPFS CID:      {result['ipfsCid']}\")\n        print(f\"  Registered:    {format_timestamp(result['registeredAt'])}\")\n        print()\n        print(\"  This skill has verifiable on-chain provenance.\")\n        print(\"  Authorship can be proved cryptographically.\")\n    else:\n        print(\"Status:          NOT REGISTERED\")\n        print()\n        print(\"  This skill has no on-chain provenance.\")\n        print(\"  Anyone could claim authorship.\")\n        print()\n        print(\"  To register, run:\")\n        print(f\"  python3 register.py {skill_folder}\")\n\n    print()\n    print(\"=\" * 60)\n    print()\n\n\ndef main():\n    if len(sys.argv) != 2:\n        print(\"Usage: python3 verify.py /path/to/skill-folder\")\n        sys.exit(1)\n\n    skill_folder = Path(sys.argv[1]).resolve()\n\n    try:\n        result = verify_skill(skill_folder)\n    except FileNotFoundError as e:\n        print(f\"Error: {e}\")\n        sys.exit(1)\n\n    print_report(result, skill_folder)\n\n\nif __name__ == \"__main__\":\n    main()\n"},"metadata":{"packagedAt":1777636601,"totalFiles":5,"tool":"skillproof-register-v0.1.0"}}