Skill: mirni/gh-bridge (version 1.0.0)

--- name: gh-bridge description: "Agent-to-Human (A2H) verification and escrow platform. Agents request physical-world tasks from humans, define verification criteria (GPS, photos, timestamps, signatures), and Bridge handles escrow, proof verification, and dispute resolution. Escrow-by-Invariants: payment released only when ALL formal criteria pass." metadata: {"openclaw":{"emoji":"🌉","requires":{"bins":["python"]},"install":[{"id":"pip","kind":"uv","packages":["fastapi","uvicorn","pydantic"]}]}} --- # Bridge Request human help for physical-world tasks. Bridge verifies the work was done before releasing payment. ## Start the server ```bash uvicorn bridge.app:app --port 8015 ``` ## Create a task with verification criteria ```bash curl -s -X POST http://localhost:8015/v1/tasks \ -H "Content-Type: application/json" \ -d '{ "description": "Pick up package at 123 Main St, photograph it", "budget_usdc": "25.00", "verification_criteria": [ {"type": "gps_proof", "description": "At pickup location", "params": {"latitude": 37.7749, "longitude": -122.4194, "radius_m": 100}}, {"type": "photo_proof", "description": "Photo of package", "params": {"min_photos": 1}} ] }' | jq ``` Escrow is locked automatically. Fee is 5% of budget. ## Submit proof and verify ```bash curl -s -X POST http://localhost:8015/v1/tasks/TASK_ID/verify \ -H "Content-Type: application/json" \ -d '{ "worker_id": "worker-1", "proofs": [ {"type": "gps_proof", "data": {"latitude": 37.7749, "longitude": -122.4194}}, {"type": "photo_proof", "data": {"photo_hashes": ["sha256:abc123"]}} ] }' | jq ``` If ALL criteria pass → escrow released. If any fail → escrow held. ## Dispute a task ```bash curl -s -X POST http://localhost:8015/v1/tasks/TASK_ID/dispute \ -H "Content-Type: application/json" \ -d '{"reason": "GPS proof appears faked"}' | jq ``` Freezes escrow. No verification possible while disputed. ## Other endpoints ```bash curl -s http://localhost:8015/v1/tasks | jq # List all tasks curl -s http://localhost:8015/v1/tasks?status=posted | jq # Filter by status curl -s http://localhost:8015/v1/platforms | jq # Available platforms curl -s http://localhost:8015/v1/stats | jq # Platform statistics ``` ## Verification types | Type | What it proves | How | |------|---------------|-----| | `gps_proof` | Worker was at location | Haversine distance < radius | | `photo_proof` | Photos submitted | Unique hash count >= min | | `timestamp_proof` | Done within deadline | Elapsed hours < max | | `signature_proof` | Cryptographic signature | Non-empty signature present | ## Escrow model - **Locked** at task creation (budget held) - **Released** only if ALL criteria pass - **Frozen** on dispute - **Refunded** if deadline expires with no proof

path: bridge/app.py
content: """
Bridge API — Agent-to-Human verification & escrow platform.

Endpoints:
  POST   /v1/tasks              — Request human actuation
  GET    /v1/tasks               — List tasks (filter by status)
  GET    /v1/tasks/{id}          — Get task by ID
  POST   /v1/tasks/{id}/verify   — Submit proof and verify
  POST   /v1/tasks/{id}/dispute  — Dispute a task
  GET    /v1/platforms           — List available platforms
  GET    /v1/stats               — Platform statistics
"""

from fastapi import FastAPI, HTTPException, Query

from .models import (
    BridgeStatsResponse,
    CreateTaskRequest,
    DisputeRequest,
    DisputeResponse,
    PlatformInfo,
    PlatformsResponse,
    TaskListResponse,
    TaskResponse,
    VerifyRequest,
    VerifyResponse,
)
from .state import bridge_stats, create_task, dispute_task, get_task, list_tasks, update_task_status
from .verifier import verify_all

app = FastAPI(
    title="Bridge API",
    description="Agent-to-Human verification & escrow platform.",
    version="0.1.0",
)


def _task_to_response(task) -> TaskResponse:
    return TaskResponse(
        id=task.id,
        description=task.description,
        status=task.status,
        budget_usdc=task.budget_usdc,
        fee_usdc=task.fee_usdc,
        escrow_status=task.escrow_status,
        location=task.location,
        verification_criteria=task.verification_criteria,
        priority=task.priority,
        deadline_hours=task.deadline_hours,
        created_at=task.created_at,
        worker_id=task.worker_id,
    )


@app.post("/v1/tasks", response_model=TaskResponse)
async def create(request: CreateTaskRequest) -> TaskResponse:
    """Request human actuation — create a task with escrow."""
    task = create_task(
        description=request.description,
        budget_usdc=request.budget_usdc,
        location=request.location,
        verification_criteria=request.verification_criteria,
        priority=request.priority,
        deadline_hours=request.deadline_hours,
    )
    return _task_to_response(task)


@app.get("/v1/tasks", response_model=TaskListResponse)
async def list_all(status: str | None = Query(default=None)) -> TaskListResponse:
    """List all tasks, optionally filtered by status."""
    tasks = list_tasks(status)
    return TaskListResponse(
        tasks=[_task_to_response(t) for t in tasks],
        total=len(tasks),
    )


@app.get("/v1/tasks/{task_id}", response_model=TaskResponse)
async def get_by_id(task_id: str) -> TaskResponse:
    """Get a task by ID."""
    task = get_task(task_id)
    if task is None:
        raise HTTPException(status_code=404, detail=f"Task not found: {task_id}")
    return _task_to_response(task)


@app.post("/v1/tasks/{task_id}/verify", response_model=VerifyResponse)
async def verify(task_id: str, request: VerifyRequest) -> VerifyResponse:
    """Submit proof and verify against criteria."""
    task = get_task(task_id)
    if task is None:
        raise HTTPException(status_code=404, detail=f"Task not found: {task_id}")
    if task.status == "disputed":
        raise HTTPException(status_code=409, detail="Task is disputed — cannot verify")

    update_task_status(task_id, "proof_submitted", worker_id=request.worker_id)

    all_passed, results = verify_all(task.verification_criteria, request.proofs)

    if all_passed:
        update_task_status(task_id, "completed", escrow_status="released")
        escrow_action = "release"
    else:
        update_task_status(task_id, "failed", escrow_status="locked")
        escrow_action = "hold"

    return VerifyResponse(
        task_id=task_id,
        verification_status="PASSED" if all_passed else "FAILED",
        criteria_results=results,
        escrow_action=escrow_action,
    )


@app.post("/v1/tasks/{task_id}/dispute", response_model=DisputeResponse)
async def file_dispute(task_id: str, request: DisputeRequest) -> DisputeResponse:
    """Dispute a task — freezes escrow."""
    task = dispute_task(task_id, request.reason)
    if task is None:
        raise HTTPException(status_code=404, detail=f"Task not found: {task_id}")
    return DisputeResponse(
        task_id=task_id,
        status=task.status,
        escrow_status=task.escrow_status,
        dispute_reason=task.dispute_reason,
    )


@app.get("/v1/platforms", response_model=PlatformsResponse)
async def get_platforms() -> PlatformsResponse:
    """List available human-work platforms."""
    return PlatformsResponse(platforms=[
        PlatformInfo(name="rentahuman", status="active", description="RentAHuman.ai — crypto-native A2H marketplace"),
        PlatformInfo(name="meatlayer", status="coming_soon", description="MeatLayer — physical world API layer"),
        PlatformInfo(name="manual", status="active", description="Manual fulfillment via Telegram notification"),
    ])


@app.get("/v1/stats", response_model=BridgeStatsResponse)
async def stats() -> BridgeStatsResponse:
    """Get platform statistics."""
    s = bridge_stats()
    return BridgeStatsResponse(**s)


path: bridge/models.py
content: """
Pydantic models for Bridge API — A2H verification & escrow.
"""

from decimal import Decimal
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field


# ── Location ────────────────────────────────────────────────────────────────

class Location(BaseModel):
    model_config = ConfigDict(extra="forbid")

    latitude: Decimal = Field(..., ge=Decimal("-90"), le=Decimal("90"))
    longitude: Decimal = Field(..., ge=Decimal("-180"), le=Decimal("180"))
    radius_km: Decimal = Field(default=Decimal("10"), gt=Decimal("0"))


# ── Verification Criteria ───────────────────────────────────────────────────

class VerificationCriterion(BaseModel):
    model_config = ConfigDict(extra="forbid")

    type: Literal["gps_proof", "photo_proof", "timestamp_proof", "signature_proof"]
    description: str = Field(..., min_length=1, max_length=500)
    params: dict[str, Any] = Field(default_factory=dict)


# ── Task Creation ───────────────────────────────────────────────────────────

class CreateTaskRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    description: str = Field(..., min_length=1, max_length=5000)
    location: Location | None = Field(default=None)
    verification_criteria: list[VerificationCriterion] = Field(default_factory=list, max_length=10)
    budget_usdc: Decimal = Field(..., gt=Decimal("0"), le=Decimal("10000"))
    priority: Literal["standard", "urgent", "critical"] = Field(default="standard")
    deadline_hours: int = Field(default=24, gt=0, le=720)


# ── Proof Submission ────────────────────────────────────────────────────────

class ProofItem(BaseModel):
    model_config = ConfigDict(extra="forbid")

    type: str = Field(...)
    data: dict[str, Any] = Field(...)


class VerifyRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    worker_id: str = Field(..., min_length=1, max_length=200)
    proofs: list[ProofItem] = Field(..., min_length=1, max_length=20)


# ── Criterion Result ────────────────────────────────────────────────────────

class CriterionResult(BaseModel):
    model_config = ConfigDict(extra="forbid")

    type: str
    passed: bool
    detail: str = Field(default="")


# ── Responses ───────────────────────────────────────────────────────────────

TaskStatus = Literal["posted", "accepted", "proof_submitted", "verified", "disputed", "completed", "refunded", "failed"]
EscrowStatus = Literal["locked", "released", "refunded", "frozen"]


class TaskResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    id: str
    description: str
    status: TaskStatus
    budget_usdc: Decimal
    fee_usdc: Decimal
    escrow_status: EscrowStatus
    location: Location | None = None
    verification_criteria: list[VerificationCriterion]
    priority: str
    deadline_hours: int
    created_at: str
    worker_id: str | None = None


class VerifyResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    task_id: str
    verification_status: Literal["PASSED", "FAILED"]
    criteria_results: list[CriterionResult]
    escrow_action: Literal["release", "hold", "freeze"]


class DisputeRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    reason: str = Field(..., min_length=1, max_length=2000)


class DisputeResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    task_id: str
    status: TaskStatus
    escrow_status: EscrowStatus
    dispute_reason: str


class TaskListResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    tasks: list[TaskResponse]
    total: int = Field(..., ge=0)


class PlatformInfo(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: str
    status: Literal["active", "coming_soon"]
    description: str


class PlatformsResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    platforms: list[PlatformInfo]


class BridgeStatsResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    total_tasks: int = Field(..., ge=0)
    posted: int = Field(..., ge=0)
    completed: int = Field(..., ge=0)
    disputed: int = Field(..., ge=0)
    total_budget_usdc: Decimal = Field(..., ge=Decimal("0"))
    total_fees_usdc: Decimal = Field(..., ge=Decimal("0"))


path: bridge/state.py
content: """
In-memory state for Bridge tasks and escrow.
"""

import time
import uuid
from dataclasses import dataclass, field
from decimal import ROUND_HALF_UP, Decimal
from typing import Any


FEE_RATE = Decimal("0.05")  # 5% matchmaking fee


@dataclass
class BridgeTask:
    id: str = field(default_factory=lambda: f"bridge-{uuid.uuid4().hex[:8]}")
    description: str = ""
    status: str = "posted"  # posted, accepted, proof_submitted, verified, disputed, completed, refunded, failed
    budget_usdc: Decimal = Decimal("0")
    fee_usdc: Decimal = Decimal("0")
    escrow_status: str = "locked"  # locked, released, refunded, frozen
    location: Any = None
    verification_criteria: list = field(default_factory=list)
    priority: str = "standard"
    deadline_hours: int = 24
    created_at: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
    worker_id: str | None = None
    dispute_reason: str = ""


_tasks: dict[str, BridgeTask] = {}


def create_task(
    description: str,
    budget_usdc: Decimal,
    location: Any = None,
    verification_criteria: list | None = None,
    priority: str = "standard",
    deadline_hours: int = 24,
) -> BridgeTask:
    fee = (budget_usdc * FEE_RATE).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    task = BridgeTask(
        description=description,
        budget_usdc=budget_usdc,
        fee_usdc=fee,
        location=location,
        verification_criteria=verification_criteria or [],
        priority=priority,
        deadline_hours=deadline_hours,
    )
    _tasks[task.id] = task
    return task


def get_task(task_id: str) -> BridgeTask | None:
    return _tasks.get(task_id)


def list_tasks(status: str | None = None) -> list[BridgeTask]:
    tasks = list(_tasks.values())
    if status:
        tasks = [t for t in tasks if t.status == status]
    return tasks


def update_task_status(task_id: str, status: str, escrow_status: str | None = None, worker_id: str | None = None) -> BridgeTask | None:
    task = _tasks.get(task_id)
    if task is None:
        return None
    task.status = status
    if escrow_status:
        task.escrow_status = escrow_status
    if worker_id:
        task.worker_id = worker_id
    return task


def dispute_task(task_id: str, reason: str) -> BridgeTask | None:
    task = _tasks.get(task_id)
    if task is None:
        return None
    task.status = "disputed"
    task.escrow_status = "frozen"
    task.dispute_reason = reason
    return task


def bridge_stats() -> dict:
    tasks = list(_tasks.values())
    return {
        "total_tasks": len(tasks),
        "posted": sum(1 for t in tasks if t.status == "posted"),
        "completed": sum(1 for t in tasks if t.status == "completed"),
        "disputed": sum(1 for t in tasks if t.status == "disputed"),
        "total_budget_usdc": sum((t.budget_usdc for t in tasks), Decimal("0")),
        "total_fees_usdc": sum((t.fee_usdc for t in tasks), Decimal("0")),
    }


path: bridge/verifier.py
content: """
Verification engine for Bridge tasks.

Validates proof submissions against formal criteria.
Each proof type has a deterministic verification function.
"""

import math

from .models import CriterionResult, ProofItem, VerificationCriterion


def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    """Calculate distance in meters between two GPS coordinates."""
    R = 6371000  # Earth radius in meters
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dphi = math.radians(lat2 - lat1)
    dlambda = math.radians(lon2 - lon1)
    a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2) ** 2
    return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))


def verify_gps_proof(criterion: VerificationCriterion, proof: ProofItem) -> CriterionResult:
    """Verify worker was at the required location."""
    params = criterion.params
    proof_data = proof.data

    required_lat = float(params.get("latitude", 0))
    required_lon = float(params.get("longitude", 0))
    radius_m = float(params.get("radius_m", 100))

    submitted_lat = float(proof_data.get("latitude", 0))
    submitted_lon = float(proof_data.get("longitude", 0))

    distance = _haversine(required_lat, required_lon, submitted_lat, submitted_lon)
    passed = distance <= radius_m

    return CriterionResult(
        type="gps_proof",
        passed=passed,
        detail=f"Distance: {distance:.0f}m (max: {radius_m:.0f}m)",
    )


def verify_photo_proof(criterion: VerificationCriterion, proof: ProofItem) -> CriterionResult:
    """Verify required number of photos submitted with unique hashes."""
    min_photos = int(criterion.params.get("min_photos", 1))
    hashes = proof.data.get("photo_hashes", [])
    unique_hashes = set(hashes)

    passed = len(unique_hashes) >= min_photos

    return CriterionResult(
        type="photo_proof",
        passed=passed,
        detail=f"Photos: {len(unique_hashes)} unique (min: {min_photos})",
    )


def verify_timestamp_proof(criterion: VerificationCriterion, proof: ProofItem) -> CriterionResult:
    """Verify task completed within time limit."""
    max_hours = float(criterion.params.get("max_hours", 24))

    accepted = proof.data.get("accepted_at", "")
    completed = proof.data.get("completed_at", "")

    if not accepted or not completed:
        return CriterionResult(type="timestamp_proof", passed=False, detail="Missing timestamps")

    # Simple hour calculation from ISO strings
    from datetime import datetime
    try:
        t_accepted = datetime.fromisoformat(accepted.replace("Z", "+00:00"))
        t_completed = datetime.fromisoformat(completed.replace("Z", "+00:00"))
        elapsed_hours = (t_completed - t_accepted).total_seconds() / 3600
        passed = elapsed_hours <= max_hours
        return CriterionResult(
            type="timestamp_proof",
            passed=passed,
            detail=f"Elapsed: {elapsed_hours:.1f}h (max: {max_hours:.1f}h)",
        )
    except (ValueError, TypeError):
        return CriterionResult(type="timestamp_proof", passed=False, detail="Invalid timestamp format")


def verify_signature_proof(criterion: VerificationCriterion, proof: ProofItem) -> CriterionResult:
    """Verify cryptographic signature is present (simplified — checks non-empty)."""
    signature = proof.data.get("signature", "")
    passed = len(signature) > 0
    return CriterionResult(
        type="signature_proof",
        passed=passed,
        detail="Signature present" if passed else "No signature provided",
    )


_VERIFIERS = {
    "gps_proof": verify_gps_proof,
    "photo_proof": verify_photo_proof,
    "timestamp_proof": verify_timestamp_proof,
    "signature_proof": verify_signature_proof,
}


def verify_all(
    criteria: list[VerificationCriterion],
    proofs: list[ProofItem],
) -> tuple[bool, list[CriterionResult]]:
    """Verify all criteria against submitted proofs.

    Returns (all_passed, results).
    """
    proof_by_type = {p.type: p for p in proofs}
    results: list[CriterionResult] = []

    for criterion in criteria:
        proof = proof_by_type.get(criterion.type)
        if proof is None:
            results.append(CriterionResult(
                type=criterion.type,
                passed=False,
                detail=f"No proof submitted for {criterion.type}",
            ))
            continue

        verifier = _VERIFIERS.get(criterion.type)
        if verifier is None:
            results.append(CriterionResult(
                type=criterion.type,
                passed=False,
                detail=f"Unknown proof type: {criterion.type}",
            ))
            continue

        results.append(verifier(criterion, proof))

    all_passed = all(r.passed for r in results)
    return all_passed, results


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)