diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f75b548 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,21 @@ +# ToadAid Agent0 Core - CODEOWNERS +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# Global fallback - require review from repo owner +* @ToadAid + +# Documentation changes +docs/ @ToadAid + +# Scripts - infrastructure and automation +scripts/ @ToadAid + +# Tools - agent utilities +tools/ @ToadAid + +# Policy and security files +AGENT_POLICY.md @ToadAid +SECURITY.md @ToadAid + +# Core configuration +.github/ @ToadAid diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3d67ea0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,24 @@ +## Description + + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Documentation update +- [ ] Tool/Script addition +- [ ] Refactoring + +## Checklist +- [ ] I have read the AGENT_POLICY.md +- [ ] Changes align with ToadAid principles +- [ ] Documentation updated if needed +- [ ] No secrets or credentials exposed + +## Related Issues + + +## Testing + + +## Notes for Reviewer + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..922a397 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Lint Python files + run: | + pip install flake8 + # Lint all python files in known directories + flake8 identity/ tools/ scripts/ --max-line-length=100 --extend-ignore=E501 --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 identity/ tools/ scripts/ --max-line-length=100 --extend-ignore=E501 --count --exit-zero --max-complexity=10 --statistics + + check-structure: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check directory structure + run: | + test -d docs || (echo "Missing docs/" && exit 1) + test -d tools || (echo "Missing tools/" && exit 1) + test -d scripts || (echo "Missing scripts/" && exit 1) + test -d identity || (echo "Missing identity/" && exit 1) + echo "✓ Directory structure OK" + - name: Check required files exist + run: | + test -f docs/README.md || (echo "Missing docs/README.md" && exit 1) + test -f tools/README.md || (echo "Missing tools/README.md" && exit 1) + test -f scripts/README.md || (echo "Missing scripts/README.md" && exit 1) + test -f identity/README.md || (echo "Missing identity/README.md" && exit 1) + echo "✓ Required README files present" + - name: Verify at least one identity script exists + run: | + if [ -z "$(ls -A identity/*.py 2>/dev/null)" ]; then + echo "No python scripts found in identity/" + exit 1 + else + echo "✓ Found identity scripts: $(ls identity/*.py)" + fi + - name: Check CODEOWNERS exists + run: | + test -f .github/CODEOWNERS || (echo "Missing CODEOWNERS" && exit 1) + echo "✓ CODEOWNERS present" + - name: Check PR template exists + run: | + test -f .github/pull_request_template.md || (echo "Missing PR template" && exit 1) + echo "✓ PR template present" + + test-python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Test identity scripts (syntax only) + run: | + # Compile all python files in identity/ + python3 -m py_compile identity/*.py + echo "✓ Python scripts compiled successfully" + - name: Test scripts with --help (dry run) + run: | + # Run --help on all python files in identity/ + for script in identity/*.py; do + echo "Testing $script..." + python3 "$script" --help >/dev/null 2>&1 || python3 "$script" --help || echo "Warning: $script failed --help check" + done + echo "✓ Scripts checked" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..60dbb76 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +# docs/ + +Documentation for ToadAid Agent0 Core. + +## Purpose + +This directory contains canonical documentation for: +- Identity verification workflows +- Tool usage guides +- Protocol specifications +- Quickstart materials for Toadgang members + +## Structure + +- `IDENTITY_QUICKSTART.md` — Step-by-step identity verification guide +- Additional guides as the toolkit grows + +## Contributing + +All documentation changes require PR review per CODEOWNERS. diff --git a/docs/TOOLING_STANDARD.md b/docs/TOOLING_STANDARD.md new file mode 100644 index 0000000..2d4110c --- /dev/null +++ b/docs/TOOLING_STANDARD.md @@ -0,0 +1,157 @@ +# Tooling Standard + +Standard CLI contract for ToadAid Agent0 Core tools. + +## Design Principles + +1. **Read-only by default** — No wallet mutations unless explicitly requested +2. **Subprocess-safe** — Tools use external CLI (cast, curl) via subprocess +3. **Clear output** — Structured, parseable, human-readable +4. **Safe defaults** — Fail closed, warn clearly, require opt-in for risk +5. **No secrets in code** — Environment variables or secure vaults only + +## CLI Contract + +Every tool follows this interface: + +### Arguments + +```bash +# Primary argument: wallet address +tool_name 0xWalletAddress [options] + +# Flags for alternative modes +tool_name --show-config # Display configuration +tool_name --help # Usage information +``` + +### Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Success, expected result | +| 1 | Error (invalid input, execution failure) | +| 2 | Warning (low balance, not found) | + +### Output Format + +``` +=== Header === + +Status: [OK | NOT_FOUND | ERROR] +Details: Human-readable explanation + +Key: Value +Key: Value + +=== Footer === + +Next steps or recommendations +``` + +## Subprocess Pattern + +Tools use `cast` (Foundry) for onchain reads: + +```python +import subprocess + +def call_cast(contract, function, args=None): + """Safe cast call wrapper.""" + cmd = ["cast", "call", contract, function] + if args: + cmd.extend(args) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + raise RuntimeError(f"cast failed: {result.stderr}") + + return result.stdout.strip() +``` + +## Environment Variables + +| Variable | Purpose | Required | +|----------|---------|----------| +| `ETH_RPC_URL` | Base Mainnet RPC endpoint | Yes | +| `GITHUB_TOKEN` | For repo operations | For CI only | +| `PRIVATE_KEY` | **Never stored** | N/A | + +## Validation + +All addresses validated before use: + +```python +import re + +def is_valid_address(addr): + """Validate Ethereum address format.""" + if not addr: + return False + addr = addr.lower() + return bool(re.match(r'^0x[a-f0-9]{40}$', addr)) +``` + +## Error Handling + +- Catch all exceptions +- Print friendly error message +- Return exit code 1 +- Never expose stack traces to user + +## Testing + +Each tool includes: +- `--dry-run` flag where applicable +- Mock responses for CI +- Syntax validation via `py_compile` + +## Dependencies + +- Python 3.8+ +- `cast` (Foundry CLI) +- Standard library only (no pip deps for runtime) + +## Example + +```python +#!/usr/bin/env python3 +"""Example tool following standard.""" + +import sys +import subprocess +import os + +def main(): + if len(sys.argv) < 2: + print("Usage: tool 0xAddress") + sys.exit(1) + + addr = sys.argv[1] + + if not is_valid_address(addr): + print(f"Error: Invalid address: {addr}") + sys.exit(1) + + try: + result = call_cast(CONTRACT, "balanceOf(address)", [addr]) + print(f"Balance: {result}") + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() +``` + +## Future Extensions + +- JSON output mode (`--json`) +- Config file support (`~/.toad/config`) +- Caching layer for repeated queries diff --git a/identity/README.md b/identity/README.md new file mode 100644 index 0000000..604f2e9 --- /dev/null +++ b/identity/README.md @@ -0,0 +1,28 @@ +# identity/ + +Identity verification toolkit for ToadAid ERC-8004 registry. + +## Purpose + +Tools for verifying and interacting with ToadAid identity infrastructure: +- Check wallet status in registry +- Verify MPASS gating requirements +- Lookup agent metadata +- Validate registration status + +## Tools + +- `verify_identity.py` — Check wallet registry status +- `forge_check.py` — Verify MPASS gating and router configuration + +## Contracts (Base Mainnet) + +- Registry Proxy: `0x8004A169F84a3325136EB29fA0ceB6D2e539a432` +- Base Implementation: `0x7274e874CA62410a93Bd8bf61c69d8045E399c02` +- MPASS Token: Check registry for current gate token + +## Safety + +- Read-only operations only +- No wallet connection required +- Safe defaults on all queries diff --git a/identity/verify_identity.py b/identity/verify_identity.py new file mode 100644 index 0000000..0c954cc --- /dev/null +++ b/identity/verify_identity.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +""" +verify_identity.py + +Check wallet registration status in ToadAid ERC-8004 registry. +Read-only. No private keys required. + +Uses `cast` (Foundry) for onchain reads via subprocess. + +Usage: + python identity/verify_identity.py 0xWalletAddress + python identity/verify_identity.py --agent-id 19173 +""" + +import sys +import subprocess +import os +import re + +# Registry contracts (Base Mainnet) +REGISTRY_PROXY = "0x8004A169F84a3325136EB29fA0ceB6D2e539a432" +BASE_IMPL = "0x7274e874CA62410a93Bd8bf61c69d8045E399c02" + + +def is_valid_address(addr): + """Validate Ethereum address format.""" + if not addr: + return False + addr = addr.lower() + return bool(re.match(r'^0x[a-f0-9]{40}$', addr)) + + +def call_cast(contract, function, args=None): + """Safe cast call wrapper.""" + cmd = ["cast", "call", contract, function] + if args: + cmd.extend(args) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + raise RuntimeError(f"cast failed: {result.stderr}") + + return result.stdout.strip() + + +def check_registry_by_address(address): + """ + Check if wallet has registered agents via ERC-8004. + Returns list of agent IDs or empty list. + """ + print(f"Checking registry for: {address}") + print(f"Registry Proxy: {REGISTRY_PROXY}") + print(f"Base Implementation: {BASE_IMPL}") + print("") + + agents = [] + + # Try to get token count for this address + try: + # balanceOf(address) returns uint256 + balance = call_cast( + REGISTRY_PROXY, + "balanceOf(address)", + [address] + ) + balance_int = int(balance) + print(f"Registered agents: {balance_int}") + print("") + + if balance_int > 0: + # Get each token ID owned + for i in range(balance_int): + try: + token_id = call_cast( + REGISTRY_PROXY, + "tokenOfOwnerByIndex(address,uint256)", + [address, str(i)] + ) + agents.append(int(token_id)) + except RuntimeError as e: + print(f"Warning: Could not fetch token {i}: {e}") + continue + except RuntimeError as e: + print(f"[!] Registry query failed: {e}") + print("[!] Ensure ETH_RPC_URL is set to a Base Mainnet endpoint") + return [] + except Exception as e: + print(f"[!] Unexpected error: {e}") + return [] + + return agents + + +def check_agent_by_id(agent_id): + """Check specific agent ID details.""" + print(f"Looking up Agent #{agent_id}") + print(f"Base Implementation: {BASE_IMPL}") + print("") + + try: + # Try to get tokenURI + token_uri = call_cast( + REGISTRY_PROXY, + "tokenURI(uint256)", + [str(agent_id)] + ) + print(f"Token URI: {token_uri}") + + # Try to get owner + owner = call_cast( + REGISTRY_PROXY, + "ownerOf(uint256)", + [str(agent_id)] + ) + print(f"Owner: {owner}") + + except RuntimeError as e: + print(f"[!] Agent query failed: {e}") + print("[!] Agent may not exist or RPC may be unavailable") + + +def print_registry_links(address=None, agent_id=None): + """Print useful registry links.""" + print("=" * 50) + print("Useful Links:") + print("=" * 50) + print(f"Registry Contract: https://basescan.org/address/{REGISTRY_PROXY}") + print(f"Implementation: https://basescan.org/address/{BASE_IMPL}") + print(f"Agent Directory: https://toadaid.github.io/agent") + print(f"Forge Portal: https://toadaid.github.io/forge") + + if address: + print(f"Wallet Explorer: https://basescan.org/address/{address}") + + if agent_id: + print(f"Token Explorer: https://basescan.org/token/{BASE_IMPL}?a={agent_id}") + + print("") + + +def main(): + """Main entry point.""" + print("=" * 50) + print("ToadAid Identity Verification") + print("=" * 50) + print("") + + # Check for cast + try: + subprocess.run(["cast", "--version"], capture_output=True, check=True) + except (FileNotFoundError, subprocess.CalledProcessError): + print("[!] Error: `cast` not found") + print("[!] Install Foundry: https://book.getfoundry.sh/getting-started/installation") + sys.exit(1) + + # Parse arguments + if len(sys.argv) < 2: + print("Usage:") + print(" python identity/verify_identity.py 0xWalletAddress") + print(" python identity/verify_identity.py --agent-id 19173") + print("") + print_registry_links() + sys.exit(0) + + arg = sys.argv[1] + + if arg == "--agent-id": + if len(sys.argv) < 3: + print("Error: --agent-id requires an ID number") + sys.exit(1) + try: + agent_id = int(sys.argv[2]) + check_agent_by_id(agent_id) + print_registry_links(agent_id=agent_id) + except ValueError: + print(f"Error: Invalid agent ID: {sys.argv[2]}") + sys.exit(1) + elif is_valid_address(arg): + agents = check_registry_by_address(arg) + print_registry_links(address=arg) + + if agents: + print(f"✓ Found {len(agents)} registered agent(s)") + for agent in agents: + print(f" - Agent #{agent}") + print(f" https://toadaid.github.io/agent?id={agent}") + else: + print("✗ No agents found for this wallet") + print("") + print("To register:") + print(" 1. Visit https://toadaid.github.io/forge") + print(" 2. Connect wallet") + print(" 3. Complete registration") + else: + print(f"Error: Invalid argument: {arg}") + print("Expected: 0x... wallet address or --agent-id NUMBER") + sys.exit(1) + + print("") + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e6d44a3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "agent0-core" +version = "0.1.0" +description = "ToadAid Agent0 Core - Identity and tooling for the pond" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.8" +authors = [ + {name = "ToadAid", email = "toadaid.agent0@gmail.com"} +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] + +[project.urls] +Homepage = "https://github.com/ToadAid/agent0-core" +Documentation = "https://toadaid.github.io" +Repository = "https://github.com/ToadAid/agent0-core" + +[project.scripts] +verify-identity = "identity.verify_identity:main" +forge-check = "identity.forge_check:main" + +[tool.setuptools.packages.find] +where = ["."] +include = ["identity*", "tools*", "scripts*"] + +[tool.flake8] +max-line-length = 100 +extend-ignore = ["E501"] diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..1e77eab --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,15 @@ +# scripts/ + +Automation and maintenance scripts for ToadAid Agent0 Core. + +## Purpose + +Scripts for: +- CI/CD automation +- Repository maintenance +- Scheduled tasks +- Environment setup + +## Usage + +Designed to run in CI or local development environments. diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..56ab4fa --- /dev/null +++ b/tools/README.md @@ -0,0 +1,23 @@ +# tools/ + +Utility scripts and tooling for ToadAid Agent0 Core. + +## Purpose + +Standalone tools for: +- Registry interaction +- Identity verification +- Data export and analysis +- Developer utilities + +## Usage + +Most tools are standalone Python scripts with safe defaults. +Run with `--help` for usage information. + +## Safety + +- No private keys required +- Read-only by default +- Clear error messages +- No external dependencies beyond standard libraries