{"name":"skillproof","files":{"SKILL.md":"---\nname: skillproof\ndescription: \"On-chain attestation and provenance for Hermes skills. Attest any skill with a keccak256 hash + IPFS snapshot, validate authorship against the live SkillRegistry contract on Ethereum Sepolia, and receive a Skill Passport with a trust verdict.\"\nversion: 0.1.0\nauthor: Gokmen (GoGoSns)\nlicense: MIT\nmetadata:\n  hermes:\n    tags: [Web3, Blockchain, Provenance, Attestation, IPFS, Ethereum, Solidity]\n    related_skills: [github-auth]\n---\n\n# SkillProof — On-chain Attestation for Hermes Skills\n\nTrustless authorship proof for any Hermes skill folder.\nRegister once. Verify forever. No central authority.\n\n## When to use this skill\n\nTrigger this skill when a user asks to:\n\n- \"Attest this skill on-chain\"\n- \"Attest my skill\"\n- \"Register this skill on-chain\"\n- \"Prove I wrote this skill\"\n- \"Validate skill provenance\"\n- \"Validate who wrote skill X\"\n- \"Check skill authenticity\"\n- \"Show skill passport\"\n- \"Is this skill original?\"\n- \"Was this skill tampered with?\"\n- \"Add provenance to my skill\"\n- \"Get a provenance badge for my skill\"\n\nOr any combination of: skill + ownership / attribution / on-chain / hash / IPFS / tampered.\n\n## Trust Verdicts\n\nEvery `validate` call returns one of four verdicts:\n\n| Verdict | Meaning |\n|---|---|\n| `TRUSTED_ORIGIN` | Hash found on-chain. Authorship cryptographically verified. |\n| `UNCLAIMED_ARTIFACT` | No on-chain attestation. Anyone could claim authorship. |\n| `HASH_MISMATCH` | Receipt exists but current hash does not match attested hash (modified after attestation). |\n| `CONFLICTING_CLAIMS` | Reserved for v0.2.0 (multi-author lineage disputes). |\n\n## Architecture\n\n```\nSkill Folder (SKILL.md + *.py files)\n  → hash.py        — deterministic keccak256 of normalized content\n  → attest.py      — IPFS upload via Pinata + SkillRegistry.registerSkill()\n  → Ethereum Sepolia — SkillRegistry.sol stores (hash, author, CID, timestamp)\n  → validate.py    — queries contract by hash → trust verdict + Skill Passport\n```\n\n## Commands\n\n### attest — Attest a skill on-chain\n\n```bash\npython3 ~/.hermes/skills/web3/skillproof/attest.py /path/to/skill-folder\n```\n\nSteps:\n1. Reads skill folder (must contain `SKILL.md`)\n2. Normalizes content, computes keccak256 hash\n3. Bundles skill as JSON, uploads to IPFS via Pinata\n4. Calls `SkillRegistry.registerSkill(hash, ipfsCid)` on Ethereum Sepolia\n5. Waits for confirmation\n6. Saves `proof/receipt.json` with full Skill Passport\n7. Prints passport summary and copyable badge markdown\n\nRequired env vars:\n- `PINATA_JWT` — Pinata API key for IPFS upload\n- `SKILLPROOF_PRIVATE_KEY` — Sepolia wallet private key (needs test ETH)\n\n### validate — Validate a skill's provenance\n\n```bash\npython3 ~/.hermes/skills/web3/skillproof/validate.py /path/to/skill-folder\n```\n\nSteps:\n1. Computes local keccak256 hash\n2. Calls `SkillRegistry.getSkill(hash)` on Ethereum Sepolia\n3. Scans event logs for the registration transaction\n4. Returns trust verdict + full Skill Passport\n5. Saves `proof/receipt.json`\n\nNo private key required (read-only).\n\n### hash — Compute content hash only\n\n```bash\npython3 ~/.hermes/skills/web3/skillproof/hash.py /path/to/skill-folder\n```\n\nPrints the keccak256 hash and list of files included. No network calls.\n\n## Setup (one-time)\n\nCopy `.env.example` to `.env` and fill in:\n\n```dotenv\nPINATA_JWT=your_pinata_jwt_here\nSKILLPROOF_PRIVATE_KEY=your_sepolia_wallet_private_key_here\nSKILLPROOF_RPC_URL=https://sepolia.gateway.tenderly.co\nSKILLPROOF_CONTRACT_ADDRESS=0x9BaA24c3f0298423B6410C7b3a4b8Bc4B1c6919c\n```\n\nThe user needs:\n- A funded Ethereum Sepolia wallet (private key for `attest` only)\n- A Pinata account for IPFS upload (free tier works)\n\n## Live Contract\n\n- **Network**: Ethereum Sepolia (chainId: 11155111)\n- **Contract**: `0x9BaA24c3f0298423B6410C7b3a4b8Bc4B1c6919c`\n- **Etherscan**: https://sepolia.etherscan.io/address/0x9BaA24c3f0298423B6410C7b3a4b8Bc4B1c6919c\n\n## Receipt Format\n\nAfter every `attest` or `validate`, a receipt is written to `proof/receipt.json`:\n\n```json\n{\n  \"verdict\": \"TRUSTED_ORIGIN\",\n  \"passport\": {\n    \"identity\": \"<keccak256 hash>\",\n    \"author\": \"<wallet address>\",\n    \"born\": \"<ISO 8601 timestamp>\",\n    \"network\": \"sepolia\",\n    \"ipfsResidence\": \"<IPFS CID>\",\n    \"trustLevel\": \"TRUSTED_ORIGIN\",\n    \"parents\": \"none (original work)\",\n    \"children\": \"0 forks detected\"\n  },\n  \"evidence\": {\n    \"contractAddress\": \"0x9BaA24c3f0298423B6410C7b3a4b8Bc4B1c6919c\",\n    \"transactionHash\": \"<tx hash>\",\n    \"etherscanUrl\": \"https://sepolia.etherscan.io/tx/<tx>\",\n    \"ipfsGateway\": \"https://gateway.pinata.cloud/ipfs/<cid>\",\n    \"blockNumber\": \"<block>\"\n  },\n  \"meta\": {\n    \"toolVersion\": \"skillproof-v0.1.0\",\n    \"generatedAt\": \"<ISO 8601>\"\n  }\n}\n```\n\n## Limitations\n\n- Ethereum Sepolia only (testnet for hackathon)\n- Single-author model (lineage tracking planned for v0.2.0)\n- `CONFLICTING_CLAIMS` verdict reserved for v0.2.0\n\n## Author\n\nGokmen (GoGoSns) — Built for Nous Research Hermes Agent Creative Hackathon 2026.\n","attest.py":"\"\"\"SkillProof - Attest Skill On-chain\n\nComputes skill hash, uploads to IPFS via Pinata, writes attestation to\nSkillRegistry on Ethereum Sepolia. Generates a Skill Passport receipt.\n\nUsage:\n    python3 attest.py /path/to/skill-folder\n\"\"\"\n\nimport json\nimport os\nimport sys\nimport time\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nimport requests\nfrom dotenv import load_dotenv\nfrom web3 import Web3\n\nfrom hash import compute_skill_hash\n\nHERE = Path(__file__).resolve().parent\nload_dotenv(HERE / \".env\")\n\nCONTRACT_ADDRESS = os.getenv(\n    \"SKILLPROOF_CONTRACT_ADDRESS\",\n    \"0x9BaA24c3f0298423B6410C7b3a4b8Bc4B1c6919c\",\n)\nRPC_URL = os.getenv(\"SKILLPROOF_RPC_URL\", \"https://sepolia.gateway.tenderly.co\")\nPINATA_PIN_JSON = \"https://api.pinata.cloud/pinning/pinJSONToIPFS\"\nCHAIN_ID = 11155111  # Ethereum Sepolia\n\nABI = [\n    {\n        \"inputs\": [\n            {\"internalType\": \"bytes32\", \"name\": \"contentHash\", \"type\": \"bytes32\"},\n            {\"internalType\": \"string\", \"name\": \"ipfsCid\", \"type\": \"string\"},\n        ],\n        \"name\": \"registerSkill\",\n        \"outputs\": [],\n        \"stateMutability\": \"nonpayable\",\n        \"type\": \"function\",\n    },\n    {\n        \"inputs\": [{\"internalType\": \"bytes32\", \"name\": \"contentHash\", \"type\": \"bytes32\"}],\n        \"name\": \"getSkill\",\n        \"outputs\": [\n            {\n                \"components\": [\n                    {\"internalType\": \"address\", \"name\": \"author\", \"type\": \"address\"},\n                    {\"internalType\": \"bytes32\", \"name\": \"contentHash\", \"type\": \"bytes32\"},\n                    {\"internalType\": \"string\", \"name\": \"ipfsCid\", \"type\": \"string\"},\n                    {\"internalType\": \"uint256\", \"name\": \"registeredAt\", \"type\": \"uint256\"},\n                ],\n                \"internalType\": \"struct SkillRegistry.Skill\",\n                \"name\": \"\",\n                \"type\": \"tuple\",\n            }\n        ],\n        \"stateMutability\": \"view\",\n        \"type\": \"function\",\n    },\n]\n\nGREEN = \"\\033[92m\"\nYELLOW = \"\\033[93m\"\nCYAN = \"\\033[96m\"\nBOLD = \"\\033[1m\"\nDIM = \"\\033[2m\"\nRESET = \"\\033[0m\"\n\n\ndef short_addr(addr: str) -> str:\n    if not addr or len(addr) < 10:\n        return addr\n    return f\"{addr[:6]}...{addr[-4:]}\"\n\n\ndef get_pinata_jwt() -> str:\n    jwt = os.getenv(\"PINATA_JWT\", \"\").strip()\n    if not jwt or jwt.startswith(\"your_\"):\n        print(f\"{YELLOW}Error: PINATA_JWT not set in .env{RESET}\")\n        sys.exit(1)\n    return jwt\n\n\ndef get_private_key() -> str:\n    pk = os.getenv(\"SKILLPROOF_PRIVATE_KEY\", \"\").strip()\n    if not pk or pk.startswith(\"your_\") or len(pk) < 64:\n        print(f\"{YELLOW}Error: SKILLPROOF_PRIVATE_KEY not set in .env{RESET}\")\n        sys.exit(1)\n    return pk\n\n\ndef package_skill_as_json(skill_folder: Path) -> dict:\n    \"\"\"Bundle skill files into a JSON payload for IPFS.\"\"\"\n    files = {}\n    ignore = {\".venv\", \"__pycache__\", \".git\", \".env\", \".DS_Store\", \"proof\"}\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-attest-v0.1.0\",\n        },\n    }\n\n\ndef upload_to_pinata(payload: dict, jwt: str, skill_name: str) -> str:\n    \"\"\"Upload skill bundle to IPFS via Pinata. Returns CID.\"\"\"\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(f\"  {DIM}Uploading to Pinata...{RESET}\")\n    r = requests.post(PINATA_PIN_JSON, headers=headers, json=body, timeout=30)\n\n    if r.status_code != 200:\n        print(f\"  {YELLOW}Error: Pinata upload failed (HTTP {r.status_code}){RESET}\")\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 attest_on_chain(\n    w3: Web3, contract, content_hash: str, ipfs_cid: str, private_key: str\n) -> dict:\n    \"\"\"Submit registerSkill tx to Ethereum Sepolia. Returns tx info.\"\"\"\n    acct = w3.eth.account.from_key(private_key)\n    hash_bytes = bytes.fromhex(content_hash[2:])\n\n    skill = contract.functions.getSkill(hash_bytes).call()\n    author, _, _, _ = skill\n    zero = \"0x0000000000000000000000000000000000000000\"\n    if author.lower() != zero.lower():\n        return {\"alreadyRegistered\": True, \"author\": author, \"txHash\": None, \"blockNumber\": None}\n\n    nonce = w3.eth.get_transaction_count(acct.address)\n    tx = contract.functions.registerSkill(hash_bytes, ipfs_cid).build_transaction({\n        \"from\": acct.address,\n        \"nonce\": nonce,\n        \"chainId\": CHAIN_ID,\n        \"gas\": 300000,\n        \"maxFeePerGas\": w3.to_wei(\"30\", \"gwei\"),\n        \"maxPriorityFeePerGas\": w3.to_wei(\"2\", \"gwei\"),\n    })\n\n    print(f\"  {DIM}Signing and submitting transaction...{RESET}\")\n    signed = acct.sign_transaction(tx)\n    tx_hash_bytes = w3.eth.send_raw_transaction(signed.raw_transaction)\n    tx_hash_hex = \"0x\" + tx_hash_bytes.hex()\n\n    print(f\"  {CYAN}Tx: {tx_hash_hex}{RESET}\")\n    print(f\"  {DIM}Waiting for confirmation (up to 3 min)...{RESET}\")\n\n    receipt = w3.eth.wait_for_transaction_receipt(tx_hash_bytes, timeout=180)\n    return {\n        \"alreadyRegistered\": False,\n        \"author\": acct.address,\n        \"txHash\": tx_hash_hex,\n        \"blockNumber\": receipt.blockNumber,\n    }\n\n\ndef save_receipt(\n    skill_folder: Path,\n    verdict: str,\n    content_hash: str,\n    author: str,\n    ipfs_cid: str,\n    registered_at: int,\n    tx_hash: str | None,\n    block_number: int | None,\n) -> Path:\n    \"\"\"Write proof/receipt.json into the skill folder.\"\"\"\n    proof_dir = skill_folder / \"proof\"\n    proof_dir.mkdir(exist_ok=True)\n\n    born = None\n    if registered_at and registered_at > 0:\n        born = datetime.fromtimestamp(registered_at, tz=timezone.utc).isoformat()\n\n    receipt = {\n        \"verdict\": verdict,\n        \"passport\": {\n            \"identity\": content_hash,\n            \"author\": author or None,\n            \"born\": born,\n            \"network\": \"sepolia\",\n            \"ipfsResidence\": ipfs_cid or None,\n            \"trustLevel\": verdict,\n            \"parents\": \"none (original work)\",\n            \"children\": \"0 forks detected\",\n        },\n        \"evidence\": {\n            \"contractAddress\": CONTRACT_ADDRESS,\n            \"transactionHash\": tx_hash,\n            \"etherscanUrl\": f\"https://sepolia.etherscan.io/tx/{tx_hash}\" if tx_hash else None,\n            \"ipfsGateway\": (\n                f\"https://gateway.pinata.cloud/ipfs/{ipfs_cid}\" if ipfs_cid else None\n            ),\n            \"blockNumber\": block_number,\n        },\n        \"meta\": {\n            \"toolVersion\": \"skillproof-v0.1.0\",\n            \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n        },\n    }\n\n    receipt_path = proof_dir / \"receipt.json\"\n    receipt_path.write_text(json.dumps(receipt, indent=2))\n    return receipt_path\n\n\ndef print_passport(\n    verdict: str,\n    content_hash: str,\n    author: str,\n    ipfs_cid: str,\n    registered_at: int,\n    tx_hash: str | None,\n    block_number: int | None,\n    receipt_path: Path,\n) -> None:\n    \"\"\"Print the Skill Passport to terminal.\"\"\"\n    W = 64\n    color = GREEN\n    symbol = \"✓\"\n    born = (\n        datetime.fromtimestamp(registered_at, tz=timezone.utc).strftime(\n            \"%Y-%m-%d %H:%M:%S UTC\"\n        )\n        if registered_at\n        else \"just now\"\n    )\n\n    border = \"═\" * W\n    print()\n    print(f\"{color}{BOLD}╔{border}╗{RESET}\")\n    title = f\"  SKILL PASSPORT — {verdict}\"\n    print(f\"{color}{BOLD}║{title:^{W}}║{RESET}\")\n    print(f\"{color}{BOLD}╚{border}╝{RESET}\")\n    print()\n    print(f\"  {BOLD}IDENTITY {RESET}    {content_hash[:10]}...{content_hash[-6:]}\")\n    print(f\"  {BOLD}AUTHOR   {RESET}    {short_addr(author)}  ({author})\")\n    print(f\"  {BOLD}BORN     {RESET}    {born}\")\n    print(f\"  {BOLD}NETWORK  {RESET}    Ethereum Sepolia (chainId: {CHAIN_ID})\")\n    if ipfs_cid:\n        print(f\"  {BOLD}IPFS     {RESET}    {ipfs_cid}\")\n    print(f\"  {BOLD}TRUST    {RESET}    {color}{BOLD}{verdict} {symbol}{RESET}\")\n    print(f\"  {BOLD}PARENTS  {RESET}    none (original work)\")\n    print(f\"  {BOLD}CHILDREN {RESET}    0 forks detected\")\n    print()\n    print(f\"  {DIM}{'─' * (W - 2)}{RESET}\")\n    print(f\"  {BOLD}EVIDENCE{RESET}\")\n    print(f\"  {DIM}Contract     {CONTRACT_ADDRESS}{RESET}\")\n    if tx_hash:\n        print(f\"  {DIM}Transaction  {tx_hash}{RESET}\")\n        print(f\"  {DIM}Etherscan    https://sepolia.etherscan.io/tx/{tx_hash}{RESET}\")\n    if ipfs_cid:\n        print(f\"  {DIM}IPFS         https://gateway.pinata.cloud/ipfs/{ipfs_cid}{RESET}\")\n    if block_number:\n        print(f\"  {DIM}Block        {block_number}{RESET}\")\n    print()\n    print(f\"  Receipt saved → {receipt_path}\")\n    print()\n    print(f\"{color}{BOLD}╔{border}╗{RESET}\")\n    verdict_line = f\"  {symbol} VERDICT: {verdict} — Authorship cryptographically verified\"\n    print(f\"{color}{BOLD}║{verdict_line:<{W}}║{RESET}\")\n    print(f\"{color}{BOLD}╚{border}╝{RESET}\")\n    print()\n\n\ndef print_badge(tx_hash: str) -> None:\n    \"\"\"Print copyable shields.io badge markdown.\"\"\"\n    badge = (\n        f\"[![SkillProof Verified](https://img.shields.io/badge/SkillProof-Verified-brightgreen\"\n        f\"?logo=ethereum)](https://sepolia.etherscan.io/tx/{tx_hash})\"\n    )\n    print(f\"{BOLD}Badge (copy to your skill README):{RESET}\")\n    print()\n    print(badge)\n    print()\n\n\ndef main() -> None:\n    if len(sys.argv) != 2:\n        print(\"Usage: python3 attest.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    private_key = get_private_key()\n\n    print()\n    print(f\"{BOLD}{'=' * 60}{RESET}\")\n    print(f\"{BOLD}  SkillProof — Attest Skill{RESET}\")\n    print(f\"{BOLD}{'=' * 60}{RESET}\")\n    print()\n    print(f\"  Skill:    {skill_folder}\")\n    print(f\"  Network:  Ethereum Sepolia\")\n    print(f\"  Contract: {CONTRACT_ADDRESS}\")\n    print()\n\n    print(f\"{BOLD}Step 1/3{RESET}  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(f\"{BOLD}Step 2/3{RESET}  Packaging and uploading to IPFS...\")\n    payload = package_skill_as_json(skill_folder)\n    skill_name = payload[\"name\"]\n    print(f\"  Skill: {skill_name}\")\n    ipfs_cid = upload_to_pinata(payload, jwt, skill_name)\n    print(f\"  {GREEN}CID:   {ipfs_cid}{RESET}\")\n    print(f\"  {DIM}View:  https://gateway.pinata.cloud/ipfs/{ipfs_cid}{RESET}\")\n    print()\n\n    print(f\"{BOLD}Step 3/3{RESET}  Attesting on Ethereum Sepolia...\")\n    w3 = Web3(Web3.HTTPProvider(RPC_URL))\n    if not w3.is_connected():\n        print(f\"  {YELLOW}Warning: RPC not reachable ({RPC_URL}). Saving offline receipt.{RESET}\")\n        now = int(time.time())\n        receipt_path = save_receipt(\n            skill_folder, \"TRUSTED_ORIGIN\", content_hash,\n            \"0x0000000000000000000000000000000000000000\",\n            ipfs_cid, now, None, None,\n        )\n        print(f\"  Receipt: {receipt_path}\")\n        sys.exit(0)\n\n    contract = w3.eth.contract(\n        address=Web3.to_checksum_address(CONTRACT_ADDRESS), abi=ABI\n    )\n\n    result = attest_on_chain(w3, contract, content_hash, ipfs_cid, private_key)\n\n    if result[\"alreadyRegistered\"]:\n        print(f\"  {YELLOW}Already attested on-chain by {result['author']}{RESET}\")\n        hash_bytes = bytes.fromhex(content_hash[2:])\n        skill = contract.functions.getSkill(hash_bytes).call()\n        author, _, cid, ts = skill\n        receipt_path = save_receipt(\n            skill_folder, \"TRUSTED_ORIGIN\", content_hash,\n            author, cid, ts, None, None,\n        )\n        print_passport(\n            \"TRUSTED_ORIGIN\", content_hash, author, cid, ts, None, None, receipt_path\n        )\n        return\n\n    tx_hash = result[\"txHash\"]\n    block_number = result[\"blockNumber\"]\n    author = result[\"author\"]\n    registered_at = int(time.time())\n\n    print(f\"  {GREEN}Confirmed in block {block_number}{RESET}\")\n    print()\n\n    receipt_path = save_receipt(\n        skill_folder, \"TRUSTED_ORIGIN\", content_hash,\n        author, ipfs_cid, registered_at, tx_hash, block_number,\n    )\n\n    print_passport(\n        \"TRUSTED_ORIGIN\", content_hash, author, ipfs_cid,\n        registered_at, tx_hash, block_number, receipt_path,\n    )\n    print_badge(tx_hash)\n\n\nif __name__ == \"__main__\":\n    main()\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\", \".env\",\n    \"proof\",  # receipts generated by attest/validate\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(\".\"):\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","validate.py":"\"\"\"SkillProof - Validate Skill Provenance\n\nQueries the live SkillRegistry contract on Ethereum Sepolia to determine\na skill's trust verdict. Four possible outcomes:\n\n  TRUSTED_ORIGIN     — Hash found on-chain, authorship cryptographically verified\n  UNCLAIMED_ARTIFACT — No on-chain attestation; anyone could claim authorship\n  HASH_MISMATCH      — Receipt exists but local hash no longer matches (modified)\n  CONFLICTING_CLAIMS — Reserved for v0.2.0 (multi-author lineage disputes)\n\nUsage:\n    python3 validate.py /path/to/skill-folder\n\"\"\"\n\nimport json\nimport os\nimport sys\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nfrom dotenv import load_dotenv\nfrom web3 import Web3\n\nfrom hash import compute_skill_hash\n\nHERE = Path(__file__).resolve().parent\nload_dotenv(HERE / \".env\")\n\nCONTRACT_ADDRESS = os.getenv(\n    \"SKILLPROOF_CONTRACT_ADDRESS\",\n    \"0x9BaA24c3f0298423B6410C7b3a4b8Bc4B1c6919c\",\n)\nRPC_URL = os.getenv(\"SKILLPROOF_RPC_URL\", \"https://sepolia.gateway.tenderly.co\")\nCHAIN_ID = 11155111\n\n# keccak256(\"SkillRegistered(bytes32,address,string,uint256)\")\nEVENT_SIG = \"0x2d128174550918dd71a1594109e82c4eec30c56865662c0c321074ac7e8c1645\"\nDEPLOY_BLOCK = 10700000  # Conservative lower bound for log search\n\nABI = [\n    {\n        \"inputs\": [{\"internalType\": \"bytes32\", \"name\": \"contentHash\", \"type\": \"bytes32\"}],\n        \"name\": \"getSkill\",\n        \"outputs\": [\n            {\n                \"components\": [\n                    {\"internalType\": \"address\", \"name\": \"author\", \"type\": \"address\"},\n                    {\"internalType\": \"bytes32\", \"name\": \"contentHash\", \"type\": \"bytes32\"},\n                    {\"internalType\": \"string\", \"name\": \"ipfsCid\", \"type\": \"string\"},\n                    {\"internalType\": \"uint256\", \"name\": \"registeredAt\", \"type\": \"uint256\"},\n                ],\n                \"internalType\": \"struct SkillRegistry.Skill\",\n                \"name\": \"\",\n                \"type\": \"tuple\",\n            }\n        ],\n        \"stateMutability\": \"view\",\n        \"type\": \"function\",\n    },\n]\n\nGREEN = \"\\033[92m\"\nYELLOW = \"\\033[93m\"\nRED = \"\\033[91m\"\nCYAN = \"\\033[96m\"\nBOLD = \"\\033[1m\"\nDIM = \"\\033[2m\"\nRESET = \"\\033[0m\"\n\nZERO_ADDR = \"0x0000000000000000000000000000000000000000\"\n\n\ndef short_addr(addr: str) -> str:\n    if not addr or len(addr) < 10:\n        return addr\n    return f\"{addr[:6]}...{addr[-4:]}\"\n\n\ndef query_contract(w3: Web3, contract, content_hash: str) -> dict:\n    \"\"\"Call getSkill on the live contract.\"\"\"\n    hash_bytes = bytes.fromhex(content_hash[2:])\n    skill = contract.functions.getSkill(hash_bytes).call()\n    author, _, cid, ts = skill\n    return {\"author\": author, \"ipfsCid\": cid, \"registeredAt\": ts}\n\n\ndef get_registration_tx(w3: Web3, content_hash: str) -> dict:\n    \"\"\"Scan event logs to find the registration transaction hash.\"\"\"\n    hash_topic = \"0x\" + content_hash[2:].zfill(64).lower()\n    try:\n        logs = w3.eth.get_logs({\n            \"address\": CONTRACT_ADDRESS,\n            \"fromBlock\": DEPLOY_BLOCK,\n            \"toBlock\": \"latest\",\n            \"topics\": [EVENT_SIG, hash_topic],\n        })\n        if logs:\n            log = logs[0]\n            raw = log[\"transactionHash\"]\n            tx_hex = raw.hex() if hasattr(raw, \"hex\") else str(raw)\n            if not tx_hex.startswith(\"0x\"):\n                tx_hex = \"0x\" + tx_hex\n            return {\"transactionHash\": tx_hex, \"blockNumber\": log[\"blockNumber\"]}\n    except Exception:\n        pass\n    return {\"transactionHash\": None, \"blockNumber\": None}\n\n\ndef load_local_receipt(skill_folder: Path) -> dict | None:\n    \"\"\"Load any previously saved receipt from proof/receipt.json.\"\"\"\n    receipt_path = skill_folder / \"proof\" / \"receipt.json\"\n    if receipt_path.exists():\n        try:\n            return json.loads(receipt_path.read_text())\n        except Exception:\n            pass\n    return None\n\n\ndef determine_verdict(is_on_chain: bool, local_hash: str, local_receipt: dict | None) -> str:\n    if is_on_chain:\n        return \"TRUSTED_ORIGIN\"\n    if local_receipt:\n        receipt_hash = local_receipt.get(\"passport\", {}).get(\"identity\")\n        if receipt_hash and receipt_hash != local_hash:\n            return \"HASH_MISMATCH\"\n    return \"UNCLAIMED_ARTIFACT\"\n\n\ndef save_receipt(\n    skill_folder: Path,\n    verdict: str,\n    content_hash: str,\n    author: str | None,\n    ipfs_cid: str | None,\n    registered_at: int | None,\n    tx_hash: str | None,\n    block_number: int | None,\n) -> Path:\n    \"\"\"Write proof/receipt.json into the skill folder.\"\"\"\n    proof_dir = skill_folder / \"proof\"\n    proof_dir.mkdir(exist_ok=True)\n\n    born = None\n    if registered_at and registered_at > 0:\n        born = datetime.fromtimestamp(registered_at, tz=timezone.utc).isoformat()\n\n    receipt = {\n        \"verdict\": verdict,\n        \"passport\": {\n            \"identity\": content_hash,\n            \"author\": author,\n            \"born\": born,\n            \"network\": \"sepolia\",\n            \"ipfsResidence\": ipfs_cid,\n            \"trustLevel\": verdict,\n            \"parents\": \"none (original work)\",\n            \"children\": \"0 forks detected\",\n        },\n        \"evidence\": {\n            \"contractAddress\": CONTRACT_ADDRESS,\n            \"transactionHash\": tx_hash,\n            \"etherscanUrl\": f\"https://sepolia.etherscan.io/tx/{tx_hash}\" if tx_hash else None,\n            \"ipfsGateway\": (\n                f\"https://gateway.pinata.cloud/ipfs/{ipfs_cid}\" if ipfs_cid else None\n            ),\n            \"blockNumber\": block_number,\n        },\n        \"meta\": {\n            \"toolVersion\": \"skillproof-v0.1.0\",\n            \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n        },\n    }\n\n    receipt_path = proof_dir / \"receipt.json\"\n    receipt_path.write_text(json.dumps(receipt, indent=2))\n    return receipt_path\n\n\ndef print_passport(\n    verdict: str,\n    content_hash: str,\n    files: list,\n    author: str,\n    ipfs_cid: str,\n    registered_at: int,\n    tx_hash: str | None,\n    block_number: int | None,\n    receipt_path: Path,\n    skill_folder: Path,\n) -> None:\n    \"\"\"Print the Skill Passport to terminal.\"\"\"\n    W = 64\n\n    if verdict == \"TRUSTED_ORIGIN\":\n        color = GREEN\n        symbol = \"✓\"\n        verdict_desc = \"Authorship cryptographically verified on Ethereum Sepolia\"\n    elif verdict == \"HASH_MISMATCH\":\n        color = RED\n        symbol = \"✗\"\n        verdict_desc = \"Skill modified after its original on-chain attestation\"\n    else:\n        color = YELLOW\n        symbol = \"⚠\"\n        verdict_desc = \"No provenance record found on Ethereum Sepolia\"\n\n    border = \"═\" * W\n    print()\n    print(f\"{color}{BOLD}╔{border}╗{RESET}\")\n    title = f\"  SKILL PASSPORT — {verdict}\"\n    print(f\"{color}{BOLD}║{title:^{W}}║{RESET}\")\n    print(f\"{color}{BOLD}╚{border}╝{RESET}\")\n    print()\n\n    short_hash = f\"{content_hash[:10]}...{content_hash[-6:]}\"\n    print(f\"  {BOLD}IDENTITY {RESET}    {short_hash}\")\n    print(f\"  {BOLD}FILES    {RESET}    {len(files)} files hashed\")\n\n    if verdict == \"TRUSTED_ORIGIN\":\n        born = (\n            datetime.fromtimestamp(registered_at, tz=timezone.utc).strftime(\n                \"%Y-%m-%d %H:%M:%S UTC\"\n            )\n            if registered_at\n            else \"unknown\"\n        )\n        print(f\"  {BOLD}AUTHOR   {RESET}    {short_addr(author)}  ({author})\")\n        print(f\"  {BOLD}BORN     {RESET}    {born}\")\n        print(f\"  {BOLD}NETWORK  {RESET}    Ethereum Sepolia (chainId: {CHAIN_ID})\")\n        if ipfs_cid:\n            print(f\"  {BOLD}IPFS     {RESET}    {ipfs_cid}\")\n        print(f\"  {BOLD}TRUST    {RESET}    {color}{BOLD}{verdict} {symbol}{RESET}\")\n        print(f\"  {BOLD}PARENTS  {RESET}    none (original work)\")\n        print(f\"  {BOLD}CHILDREN {RESET}    0 forks detected\")\n        print()\n        print(f\"  {DIM}{'─' * (W - 2)}{RESET}\")\n        print(f\"  {BOLD}EVIDENCE{RESET}\")\n        print(f\"  {DIM}Contract     {CONTRACT_ADDRESS}{RESET}\")\n        if tx_hash:\n            print(f\"  {DIM}Transaction  {tx_hash}{RESET}\")\n            print(f\"  {DIM}Etherscan    https://sepolia.etherscan.io/tx/{tx_hash}{RESET}\")\n        if ipfs_cid:\n            print(f\"  {DIM}IPFS         https://gateway.pinata.cloud/ipfs/{ipfs_cid}{RESET}\")\n        if block_number:\n            print(f\"  {DIM}Block        {block_number}{RESET}\")\n\n    elif verdict == \"HASH_MISMATCH\":\n        print(f\"  {BOLD}TRUST    {RESET}    {color}{BOLD}{verdict} {symbol}{RESET}\")\n        print()\n        print(f\"  {RED}WARNING: A receipt exists for this skill path but the{RESET}\")\n        print(f\"  {RED}current hash does not match the attested hash.{RESET}\")\n        print(f\"  {DIM}The skill was modified after attestation.{RESET}\")\n        print(f\"  {DIM}Run attest.py again to create a fresh attestation.{RESET}\")\n\n    else:  # UNCLAIMED_ARTIFACT\n        print(f\"  {BOLD}TRUST    {RESET}    {color}{BOLD}{verdict} {symbol}{RESET}\")\n        print()\n        print(f\"  {YELLOW}No on-chain attestation found for this skill.{RESET}\")\n        print(f\"  {DIM}Anyone could claim authorship.{RESET}\")\n        print()\n        print(f\"  To claim ownership, run:\")\n        print(f\"  {CYAN}python3 attest.py {skill_folder}{RESET}\")\n\n    print()\n    print(f\"  Receipt saved → {receipt_path}\")\n    print()\n    print(f\"{color}{BOLD}╔{border}╗{RESET}\")\n    verdict_line = f\"  {symbol} VERDICT: {verdict} — {verdict_desc}\"\n    print(f\"{color}{BOLD}║{verdict_line:<{W}}║{RESET}\")\n    print(f\"{color}{BOLD}╚{border}╝{RESET}\")\n    print()\n\n\ndef main() -> None:\n    if len(sys.argv) != 2:\n        print(\"Usage: python3 validate.py /path/to/skill-folder\")\n        sys.exit(1)\n\n    skill_folder = Path(sys.argv[1]).resolve()\n\n    try:\n        local_hash, files = compute_skill_hash(skill_folder)\n    except (FileNotFoundError, NotADirectoryError) as e:\n        print(f\"Error: {e}\")\n        sys.exit(1)\n\n    print()\n    print(f\"{BOLD}{'=' * 60}{RESET}\")\n    print(f\"{BOLD}  SkillProof — Validate Provenance{RESET}\")\n    print(f\"{BOLD}{'=' * 60}{RESET}\")\n    print()\n    print(f\"  Skill:    {skill_folder}\")\n    print(f\"  Hash:     {local_hash}\")\n    print(f\"  Files:    {len(files)}\")\n    print(f\"  Network:  Ethereum Sepolia\")\n    print(f\"  Contract: {CONTRACT_ADDRESS}\")\n    print()\n\n    print(f\"{DIM}Connecting to {RPC_URL}...{RESET}\")\n\n    w3 = Web3(Web3.HTTPProvider(RPC_URL))\n    if not w3.is_connected():\n        print(f\"{YELLOW}Warning: Could not connect to RPC. Using offline check.{RESET}\")\n        local_receipt = load_local_receipt(skill_folder)\n        verdict = \"UNCLAIMED_ARTIFACT\"\n        if local_receipt:\n            receipt_hash = local_receipt.get(\"passport\", {}).get(\"identity\")\n            if receipt_hash == local_hash and local_receipt.get(\"verdict\") == \"TRUSTED_ORIGIN\":\n                verdict = \"TRUSTED_ORIGIN\"\n            elif receipt_hash and receipt_hash != local_hash:\n                verdict = \"HASH_MISMATCH\"\n        receipt_path = save_receipt(\n            skill_folder, verdict, local_hash, None, None, None, None, None\n        )\n        print_passport(\n            verdict, local_hash, files, \"\", \"\", 0, None, None, receipt_path, skill_folder\n        )\n        return\n\n    contract = w3.eth.contract(\n        address=Web3.to_checksum_address(CONTRACT_ADDRESS), abi=ABI\n    )\n\n    print(f\"{DIM}Querying contract for hash {local_hash[:10]}...{RESET}\")\n    on_chain = query_contract(w3, contract, local_hash)\n    is_on_chain = on_chain[\"author\"].lower() != ZERO_ADDR.lower()\n\n    local_receipt = load_local_receipt(skill_folder)\n    verdict = determine_verdict(is_on_chain, local_hash, local_receipt)\n\n    tx_hash = None\n    block_number = None\n    if is_on_chain:\n        print(f\"{DIM}Fetching registration event from logs...{RESET}\")\n        tx_info = get_registration_tx(w3, local_hash)\n        tx_hash = tx_info[\"transactionHash\"]\n        block_number = tx_info[\"blockNumber\"]\n\n    receipt_path = save_receipt(\n        skill_folder,\n        verdict,\n        local_hash,\n        on_chain[\"author\"] if is_on_chain else None,\n        on_chain[\"ipfsCid\"] if is_on_chain else None,\n        on_chain[\"registeredAt\"] if is_on_chain else None,\n        tx_hash,\n        block_number,\n    )\n\n    print_passport(\n        verdict,\n        local_hash,\n        files,\n        on_chain[\"author\"] if is_on_chain else \"\",\n        on_chain[\"ipfsCid\"] if is_on_chain else \"\",\n        on_chain[\"registeredAt\"] if is_on_chain else 0,\n        tx_hash,\n        block_number,\n        receipt_path,\n        skill_folder,\n    )\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":1777706804,"totalFiles":7,"tool":"skillproof-attest-v0.1.0"}}