Implement governed auto-claim loop on task_plane:
- SchedulabilityProof model (5-segment persisted diagnostic snapshot) + ProofBuilder
- authoritative_claim_recheck (A23: cross-tenant/task/generation/lease/capacity)
- SchedulingService wiring TaskScheduler to HTTP long_poll/poll/schedule
- governed_claim single-transaction (proof→recheck→claim→receipt→event)
- HttpRunnerTransport (real transport, renew with claim_id self-resolution)
- AgentPullRunner discover_once handles idle/no-task (REQ-03c)
- receipt/readback endpoints (/proofs, /receipts, /tasks/{id}/receipts)
Pure stdlib, no new deps; 25 tests pass (22 R2 + 2 heartbeat + 1 idle). Base: 3c320ae.
79 lines
2.0 KiB
Python
79 lines
2.0 KiB
Python
"""Claim/proof receipts for ANNEX-2.3 R2 governed claim audit read-back.
|
|
|
|
A receipt is a minimal, tamper-evident record generated alongside a governed
|
|
claim. Its digest is sha256(canonical_json(payload)) so the payload can be
|
|
re-verified on read-back, consistent with the evidence and event chain.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from .models import compute_sha256_hex
|
|
|
|
|
|
def receipt_digest(payload: dict[str, Any]) -> str:
|
|
return compute_sha256_hex(payload)
|
|
|
|
|
|
@dataclass
|
|
class ClaimReceipt:
|
|
receipt_id: str
|
|
kind: str
|
|
task_id: str
|
|
worker_id: str
|
|
claim_id: str | None
|
|
proof_id: str | None
|
|
digest: str
|
|
payload: dict[str, Any]
|
|
created_at: float
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"receipt_id": self.receipt_id,
|
|
"kind": self.kind,
|
|
"task_id": self.task_id,
|
|
"worker_id": self.worker_id,
|
|
"claim_id": self.claim_id,
|
|
"proof_id": self.proof_id,
|
|
"digest": self.digest,
|
|
"payload": self.payload,
|
|
"created_at": self.created_at,
|
|
}
|
|
|
|
|
|
def build_receipt(
|
|
*,
|
|
kind: str,
|
|
task_id: str,
|
|
worker_id: str,
|
|
claim_id: str | None = None,
|
|
proof_id: str | None = None,
|
|
extra: dict[str, Any] | None = None,
|
|
created_at: float | None = None,
|
|
) -> ClaimReceipt:
|
|
"""Build a ClaimReceipt whose digest covers the full digestable payload."""
|
|
extra = extra or {}
|
|
payload = {
|
|
"kind": kind,
|
|
"task_id": task_id,
|
|
"worker_id": worker_id,
|
|
"claim_id": claim_id,
|
|
"proof_id": proof_id,
|
|
**extra,
|
|
}
|
|
return ClaimReceipt(
|
|
receipt_id=f"rct-{uuid.uuid4()}",
|
|
kind=kind,
|
|
task_id=task_id,
|
|
worker_id=worker_id,
|
|
claim_id=claim_id,
|
|
proof_id=proof_id,
|
|
digest=receipt_digest(payload),
|
|
payload=payload,
|
|
created_at=created_at if created_at is not None else time.time(),
|
|
)
|