Skip to content

Agent-trust layer: dual-signed receipts, ERC-8004 identity, x401 shadow, caller binding (all flag-gated) - #56

Open
Svaag wants to merge 7 commits into
mainfrom
feat/agent-trust-layer
Open

Agent-trust layer: dual-signed receipts, ERC-8004 identity, x401 shadow, caller binding (all flag-gated)#56
Svaag wants to merge 7 commits into
mainfrom
feat/agent-trust-layer

Conversation

@Svaag

@Svaag Svaag commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Implements the accepted trust-layer plan (docs/plans/2026-07-10-agent-trust-layer.md) across every x402 service: ERC-8004 identifies the service, x401 proves authority when necessary, x402 proves payment, dual-signed receipts attest outcomes — Hyrule stays the deterministic execution engine.

Everything lands dark: all TRUST_* flags default off, and a guard test pins /.well-known/x402.json byte-identical to today's announced surface while they stay off (protects #53). Rollout is #54.

What's here (one commit per milestone)

  • M1 — receipts core (0e6abb3): hyrule_cloud/trust/ (mypy-strict). Every settled payment on every paid route mints a receipt signed twice over the same canonical bytes — ES256 compact JWS (offline-verifiable via /.well-known/jwks.json) + EIP-712 ReceiptDigest (secp256k1, verifiable by EVM tooling / usable as ERC-8004 feedback evidence). Minted inside PaymentGate's settle paths (incl. settle_verified + dev-bypass), so all 13 intel routers, proxy, VM, domain, jobs — and future speedtest/mail — are covered with zero per-route changes. HYRULE-RECEIPT response header, GET /v1/receipts/{id}, migration 015 (+ ix_payment_events_tx_hash duplicate-settle detection — deliberately not a uniqueness constraint on the best-effort ledger). With TRUST_RECEIPTS_ENABLED=true the app refuses to boot unless both keys validate.
  • M2 — fulfillment + refund receipts (9521017): orchestrator (provisioned/failed/simulated, with provision timings), extend, domain register, BGP job completion (artifact hash evidence), native BTC/XMR intents at first SETTLED, and both refund choke points (soft-linked to their refund_owed ledger event, idempotent). GET /v1/vm/{id}/receipts, hyrule_receipts_total + hyrule_payment_duplicate_settled_tx metrics, MCP/client tools, and the x402 canary now fails if an advertised receipt doesn't verify. Privacy invariant (test-pinned): native-rail receipts carry no deposit address and no txid, even hashed — correlation is the unguessable intent id.
  • M3 — discovery-only ERC-8004 (156675b): registration document at /.well-known/agent-registration.json (spec pinned 2026-07-10 — the draft's filename, not the A2A agent-card guess), flag-gated manifest identity/receipts blocks, and scripts/erc8004_register.py (raw JSON-RPC + eth-account, no web3 dep; prints the ops-repo eth_getLogs monitoring query). The app runtime never reads chain state.
  • M5+M6 — x401 (a0c6a59): v0.2.0 pinned (PROOF-REQUEST/PROOF-RESPONSE/PROOF-RESULT, base64url JSON, DCQL). TRUST_X401_MODE=shadow logs what enforcement WOULD require (migration 016) with HTTP responses byte-identical to off (test-pinned); the 402 gains an advisory x401 extension. Enforce (ships off) is proof-first-then-pay on /v1/vm/create only: 401 + PROOF-REQUEST before reservation and before the gate — a payment header without a valid proof never reaches verify/settle (gate call count 0, test-pinned). POST /v1/x401/proof issues TTL-bounded hyr_pf_ tokens scope-bound to quote/amount/route/method; the structural verifier only satisfies under a test-only flag and honestly 503s otherwise.
  • M7 — caller binding (094a5e6): RFC 9421 → did:web, observe-only. Verified principals flow into ledger extras + receipt agent fields; SSRF-guarded resolution with positive/negative caches; every failure degrades to unverified/absent with the request untouched.
  • Docs (5597613): docs/trust-layer.md, the open profile docs/x402-compute-fulfillment-receipt.md, docs/runbooks/trust-keys.md (ceremonies, rotation, monitoring), scripts/verify_receipt.py, plan copy, CLAUDE.md refresh.

Verification

  • 381 tests pass (+56 new across 5 trust test files), mypy strict clean (new modules not in the override list), ruff clean.
  • Migrations 015/016: up → down → up clean (001–014 are Postgres-only ARRAY, so chain-tested from a stamped 014; prod runs the real chain via ExecStartPre=alembic upgrade head as usual).
  • Live loop with flags on: paid /v1/dns/lookupHYRULE-RECEIPT header → both signatures verified offline via scripts/verify_receipt.py; receipt soft-links its ledger event; 402 carries bazaar + x401 advisory; agent-registration/JWKS/manifest blocks all serve. Flags off: no new headers, no manifest keys, agent-registration 404, JWKS empty.
  • The 7 test_launch_blockers failures on main are pre-existing (stale x402.org facilitator in fixtures) — filed as Stale x402.org facilitator in .env.example era: 7 test_launch_blockers tests fail on clean main #55, untouched here.

Follow-ups

🤖 Generated with Claude Code

Svaag and others added 6 commits July 10, 2026 11:16
…t M1)

Every settled x402 payment across all paid routes now mints a receipt
carrying an ES256 compact JWS (offline-verifiable via /.well-known/jwks.json)
plus an EIP-712 secp256k1 signature over the sha256 of the same canonical
payload. New hyrule_cloud/trust/ package (mypy-strict), TrustConfig (TRUST_*,
default off), fulfillment_receipts table (migration 015, plus a duplicate-
settlement detection index on payment_events.tx_hash), HYRULE-RECEIPT
response header, and GET /v1/receipts/{id} retrieval.

Minting is best-effort and bounded (2s) inside PaymentGate's settle paths —
check_payment, settle_verified, and both dev-bypass branches — so a broken
or disabled trust layer never changes payment behavior; native BTC/XMR
receipts never disclose deposit addresses or txids, even hashed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fulfillment-kind receipts now cover the async outcomes: VM provisioned
(real + simulated, with provision timing), VM failed, VM extended, domain
registered/failed, BGP job completed (with artifact evidence), and native
BTC/XMR intents at first SETTLED observation. Refund-kind receipts are
minted at both refund choke points (RefundService.record_owed and the
atomic native REFUND_MANUAL flip), soft-linked to their refund_owed ledger
event, and idempotent.

Payment receipts gain order correlation via request.state.receipt_
correlation (quote/vm/domain set before the gate call). New GET
/v1/vm/{id}/receipts (management-gated), client + MCP tools (get_receipt,
list_vm_receipts), /metrics gains hyrule_receipts_total and a duplicate-
settled-tx gauge, and the x402 canary now verifies both receipt signatures
offline whenever a paid response advertises one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st M3)

Discovery-only ERC-8004 surfaces, pinned to the current Draft (verified
2026-07-10): the registration document at /.well-known/agent-registration.json
(type registration-v1, services, x402Support, registrations[] from config
once the on-chain ceremony has run), flag-gated manifest `identity` and
`receipts` blocks, and scripts/erc8004_register.py — a one-shot
register/setAgentURI ceremony tool over raw JSON-RPC + eth-account (no web3
dep) that also prints the eth_getLogs monitoring query for the ops repo.

The app runtime never reads chain state, so registry outages cannot affect
service. A guard test pins /.well-known/x402.json byte-identical to the
pre-trust manifest while all TRUST_* flags are off, protecting the pending
launch announcement surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t M5+M6)

x401 v0.2.0 identity-proof layer, pinned to the current draft (verified
2026-07-10): PROOF-REQUEST/PROOF-RESPONSE/PROOF-RESULT headers carrying
base64url JSON, OpenID4VP DCQL predicates, header-authoritative semantics.

TRUST_X401_MODE=off|shadow|enforce. Shadow evaluates the step-up policy
(VM create/extend over duration/amount thresholds) and writes decisions to
the new x401_proof_log (migration 016) — HTTP responses are test-pinned
byte-identical to mode=off. The 402 challenge gains an advisory x401
extension while the mode is not off.

Enforce (ships OFF; flipping is a human-controlled ops event) is
proof-first-then-pay on /v1/vm/create only: a step-up order without a
valid proof gets 401 + PROOF-REQUEST before any reservation and before
the payment gate is ever invoked (gate call count 0, test-pinned).
POST /v1/x401/proof verifies a Result Artifact via a pluggable verifier
(v1 StructuralVerifier satisfies only under the test-only
TRUST_X401_ACCEPT_STRUCTURAL flag — it honestly 503s otherwise) and issues
a TTL-bounded hyr_pf_ token sha256-at-rest, scope-bound to the exact
quote/amount/route/method. /metrics gains hyrule_x401_decisions_total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…trust M7)

TRUST_PRINCIPAL_MODE=observe resolves RFC 9421 HTTP Message Signatures
(minimal subset: @method + @target-uri, created/expires/keyid/alg params,
ed25519 + ecdsa-p256-sha256) whose keyid is a did:web key. DID documents
are fetched behind the existing SSRF guards (public-address pre-flight,
2s timeout, 64KB cap) with positive + negative TTL caches; ports and
non-web DID methods are rejected.

The resulting AgentPrincipal (did, key_id, verified) is recorded on
request.state, merged into settled payment_events extras, and stamped
into receipts' agent field + agent_did column. It never blocks, never
authorizes, and a resolver outage or private DID host degrades to an
unverified/absent principal with the request untouched (test-pinned).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion runbook

- docs/x402-compute-fulfillment-receipt.md — the open receipt profile:
  payload schema, JCS-subset canonicalization, dual-signature rules
  (ES256 JWS + EIP-712 ReceiptDigest), per-rail semantics incl. the
  native-rail privacy rules and a reserved Zcash slot.
- docs/trust-layer.md — components, flag matrix, test-pinned invariants,
  spec pins (ERC-8004 draft + registry addresses, x401 v0.2.0), and the
  remaining ops roadmap (key ceremony/registration, Circle Gateway spike,
  receipt-backed feedback).
- docs/runbooks/trust-keys.md — key generation/rotation ceremonies, the
  Sepolia→mainnet ERC-8004 registration procedure with human sign-off,
  and registry-event/duplicate-settlement monitoring for the ops repo.
- scripts/verify_receipt.py — customer-facing offline verifier (fetches
  JWKS from the receipt's own origin so staging verification works).
- docs/plans/2026-07-10-agent-trust-layer.md — the executed plan with
  progress + spec-pin deviations recorded.
- CLAUDE.md — trust package in the file map; refreshed the not-yet-built
  list (refund recording shipped in #44; trust ops rollout is the open item).

Verified end-to-end on a live app: paid call → HYRULE-RECEIPT header →
/v1/receipts/{id} → both signatures verified offline; manifest/agent-
registration/JWKS blocks present with flags on and byte-absent with flags
off; migrations 015/016 up→down→up clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

53 - Partially compliant

Compliant requirements:

Non-compliant requirements:

  • All other requirements are ops-gated or not code changes in this PR

Requires further human verification:

  • No further human verification needed from code review

54 - Partially compliant

Compliant requirements:

  • Implementation of dual-signed receipts (M1) with offline verification
  • Fulfillment + refund receipts (M2)
  • x401 shadow mode, policy engine, proof tokens (M5/M6)
  • ERC-8004 identity scaffolding (agent card, registration)
  • Receipt core integration into PaymentGate, orchestrator, intents, domain
  • All TRUST_* flags default off, guard test for manifest

Non-compliant requirements:

  • Key ceremony, Vault storage, flag flips, ERC-8004 mainnet registration, network ops monitoring, Circle Gateway spike, external trust features

Requires further human verification:

  • All not-compliant items are operator-gated and require human ceremony

55 - Partially compliant

Compliant requirements:

  • (none)

Non-compliant requirements:

  • The PR does not address the stale facilitator URL in test fixtures

Requires further human verification:

  • (none)
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🏅 Score: 90
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@github-actions

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Soft-fail proof token issuance

The issue_proof_token method leaks the cleartext token in the response and has no
timeout/bound on the database write. Since the token is returned exactly once and
can be reused within TTL, if the database write succeeds but the response fails
(e.g. network hiccup), the token is lost forever and the caller cannot retry. Wrap
the commit and return in a soft-fail pattern similar to _log_decision: catch
exceptions, log a warning, and return None to avoid breaking the calling flow. This
is critical because an unhandled failure here would raise a 500 error during the
proof flow, even though the documentation states that "a broken x401 store must
never change a payment or provisioning outcome."

hyrule_cloud/trust/x401.py [416-445]

 async def issue_proof_token(
     self,
     *,
     bound_quote_hash: str,
     route: str,
     method: str,
-    ...
-) -> str:
+    claims: dict[str, str] | None = None,
+    agent_did: str | None = None,
+) -> str | None:
     if self._session_factory is None:
-        raise RuntimeError("x401 proof tokens need a session factory")
-    cleartext = generate_proof_token()
-    row = X401ProofTokenRow(
-        token_hash=_hash_token(cleartext),
-        quote_hash=bound_quote_hash,
-        ...
-    )
-    async with self._session_factory() as session:
-        session.add(row)
-        await session.commit()
-    return cleartext
+        return None
+    try:
+        cleartext = generate_proof_token()
+        row = X401ProofTokenRow(
+            token_hash=_hash_token(cleartext),
+            quote_hash=bound_quote_hash,
+            route=route[:256],
+            method=method[:8],
+            claims=claims or None,
+            agent_did=agent_did[:256] if agent_did else None,
+            expires_at=datetime.now(UTC)
+            + timedelta(seconds=self.config.x401_proof_token_ttl_seconds),
+        )
+        async with self._session_factory() as session:
+            session.add(row)
+            await session.commit()
+        return cleartext
+    except Exception:
+        log.warning("x401_issue_token_failed", exc_info=True)
+        return None
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that a database failure in issue_proof_token would raise an unhandled exception, violating the documented invariant that a broken x401 store must never change the payment/provisioning outcome. Adding a try/except block with a logged warning and returning None aligns with the soft-fail pattern used elsewhere in the same class. The change is relevant and improves robustness.

Medium
Return True on DB lookup failure

The check_proof_token method logs a warning on DB failure but returns a hard False,
which in enforce mode would reject a legitimate caller. According to the docstring,
"a broken x401 store must never change a payment or provisioning outcome (in shadow
mode by definition; in enforce mode a log failure still never blocks)". However, a
DB transient failure during check_proof_token would cause a false negative and block
(or fail) the request. Change the except block to return True when the lookup fails,
preserving the invariant that store failures never change the outcome.

hyrule_cloud/trust/x401.py [447-482]

-async def check_proof_token(
-    self,
-    cleartext: str,
-    *,
-    bound_quote_hash: str,
-    route: str,
-    method: str,
-) -> bool:
-    if self._session_factory is None or not cleartext.startswith("hyr_pf_"):
-        return False
-    try:
-        async with self._session_factory() as session:
-            row = (
-                await session.execute(
-                    select(X401ProofTokenRow).where(
-                        X401ProofTokenRow.token_hash == _hash_token(cleartext)
-                    )
-                )
-            ).scalar_one_or_none()
-    except Exception:
-        log.warning("x401_token_lookup_failed", exc_info=True)
-        return False
-    if row is None:
-        return False
-    expires = row.expires_at
-    if expires.tzinfo is None:
-        expires = expires.replace(tzinfo=UTC)
-    if expires < datetime.now(UTC):
-        return False
-    return (
-        row.quote_hash == bound_quote_hash
-        and row.route == route[:256]
-        and row.method == method[:8]
-    )
+except Exception:
+    log.warning("x401_token_lookup_failed", exc_info=True)
+    return True
Suggestion importance[1-10]: 5

__

Why: The suggestion aims to prevent a DB failure from blocking a legitimate request by returning True instead of False. While this respects the invariant that store failures should not break the flow, it could introduce a security bypass if an attacker triggers a transient DB error. A more balanced approach might be to raise a warning and treat the token as invalid, but the suggestion is not wrong. Score reflects moderate impact and potential trade-off.

Low

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5597613306

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +274 to +277
async with httpx.AsyncClient(
timeout=_HTTP_TIMEOUT_SECONDS, follow_redirects=False
) as client:
response = await client.get(url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin DID document fetches to the vetted address

When TRUST_PRINCIPAL_MODE=observe and a request supplies a did:web keyid, this preflight checks the hostname with resolve_public_addresses, but client.get(url) performs a fresh DNS lookup. A DNS-rebinding DID host can return a public IP for the check and then a private/link-local IP for the actual HTTPS GET, turning signature observation into an SSRF to port 443 despite the guard. Reuse the vetted address or a transport that pins it while preserving Host/SNI.

Useful? React with 👍 / 👎.

Comment on lines +1248 to +1252
await x401.observe(
route=request.url.path,
method="POST",
amount=total,
duration_days=body.days,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce x401 on advertised VM extensions

When TRUST_X401_MODE=enforce and an extension exceeds the duration/amount threshold, this code logs the STEP_UP decision and then proceeds to gate.check_payment without requiring PROOF-RESPONSE. Since the x401 policy/advisory advertises /v1/vm/{vm_id}/extend as a required-conditionally step-up route, clients can pay for elevated extensions without the proof the server says is required. Apply _x401_enforce_or_none here or stop advertising extend as enforced.

Useful? React with 👍 / 👎.

Comment on lines 169 to +173
row.status = "completed"
row.artifact_snapshot_id = body.artifact_snapshot_id
row.completed_at = datetime.now(UTC)
job_payer, job_tx, job_price = row.owner_wallet, row.payment_tx, row.price_usd
completed = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid minting duplicate job completion receipts

If the internal worker retries /v1/internal/bgp/jobs/{job_id}/complete after losing the response, the row is already completed but this path still sets completed = True, so the receipt block below mints another fulfillment receipt for the same paid job/artifact. That inflates receipt metrics and exposes multiple attestations for one completion; only mint when transitioning from a non-completed status.

Useful? React with 👍 / 👎.

Comment on lines +205 to +208
if int(created) > now + _CLOCK_SKEW_SECONDS:
return unverified
expires = params.get("expires", "")
if expires.isdigit() and int(expires) < now - _CLOCK_SKEW_SECONDS:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject stale caller signatures

When TRUST_PRINCIPAL_MODE=observe, a captured signature with an old created value and no expires is still accepted because only future timestamps and expired expires values are rejected. This contradicts the stated ±300s skew rule and lets a replayed signature for the same method/URI be recorded as a verified agent in ledger rows and receipts long after it was signed; also reject created < now - _CLOCK_SKEW_SECONDS.

Useful? React with 👍 / 👎.

Comment on lines +988 to +990
request.state.receipt_correlation = {
"quote_id": quote_row.quote_id if quote_row is not None else None,
"vm_id": reservation_row.vm_id if reservation_row is not None else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid stamping payment receipts with provisional VM ids

When a paid VM create uses a pre-payment reservation, the payment receipt is minted during gate.check_payment, before the reservation is known to survive. If the reservation is later released or replaced (for example a consumed-quote race or a reservation swept during a very slow settlement), the returned HYRULE-RECEIPT points at a VM id that was deleted or never fulfilled. Only correlate the payment receipt to the stable quote/order here, and let fulfillment receipts carry the final VM id.

Useful? React with 👍 / 👎.

Bring #69's ed25519 response-body signing under the canonical trust
layer instead of shipping a parallel signing stack. Measurements attest
the DATA; receipts attest the transaction — independent flags, one key
namespace, one JWKS.

- trust/measurements.py: MeasurementSigner (ed25519 detached signature
  over exact 2xx JSON body bytes), verify_measurement_signature,
  load_measurement_signer, enforce_measurement_key_guard (fail-closed
  boot guard mirroring receipts), measurement_jwks_entries.
- middleware/signing.py: ResponseSigningMiddleware — signs paid 2xx JSON
  from the enabled catalog, Hyrule-Signature/-Key headers + CORS expose;
  passthrough when unconfigured (soft-fail).
- config TrustConfig: TRUST_MEASUREMENT_SIGNING_ENABLED/KEY/KEY_ID/
  RETIRED_JWKS_JSON (default off).
- Unified JWKS: build_jwks appends the OKP/Ed25519 key (active + retired)
  so one /.well-known/jwks.json verifies receipts AND measurements.
- Advertise under agent-registration signedMeasurements only — NOT the
  x402 manifest, preserving the byte-identical-off invariant.
- app.py: enforce_measurement_key_guard + middleware install.
- x402_canary.py: --verify-signature re-derives the pubkey from jwks.
- Docs: trust-layer component row, trust-keys runbook section + rotation,
  .env.example block. 9 new tests; 397 pass, mypy strict + ruff clean.

Supersedes and closes #69.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Svaag

Svaag commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Folded #69 (ed25519 signed measurements) into this PR — commit b46e429. Signed measurements now live under the same trust umbrella: one key namespace (TRUST_MEASUREMENT_SIGNING_*), the shared /.well-known/jwks.json (OKP/Ed25519 entry alongside the receipt keys), advertised only in agent-registration (signedMeasurements) so the x402 manifest stays byte-identical with flags off. New files: trust/measurements.py, middleware/signing.py, tests/test_trust_measurements.py (9 tests). Full suite 397 pass, mypy strict + ruff clean.

⚠️ This PR is CONFLICTING with main — it is behind by several merged commits (CDP switch, domain-reselling) with genuine conflicts in the routes/orchestrator area. Needs a rebase onto origin/main before it can merge. Flagging separately from this fold since the conflicts are in domain code, not the trust layer.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b46e429339

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +51 to +60
def _is_paid_operation(scope: Scope) -> bool:
method = scope.get("method", "").upper()
path = (scope.get("path", "") or "").rstrip("/") or "/"
for op_method, template, regex in _matchers():
if op_method != method or not regex.fullmatch(path):
continue
# discovery_for re-applies the per-op gate on the template, so a gated
# -off op (which 501s) is never treated as signable.
if discovery_for(op_method, template) is not None:
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sign every paid JSON response, not only Bazaar routes

With measurement signing enabled, a successful paid POST /v1/vm/{vm_id}/extend is not signed because this predicate only accepts paths listed in DISCOVERY, which contains /v1/vm/create but not the paid extend route. The same gap applies to other paid routes that are not Bazaar declarations. This contradicts the configured and documented contract of signing each paid 2xx JSON body, leaving buyers unable to verify those delivered results; derive this from the payment gate/route metadata or add all paid routes to the signed-operation set.

Useful? React with 👍 / 👎.

Comment on lines +107 to +111
def quote_hash(*, quote_id: str | None, amount: Decimal | str | None, route: str, method: str) -> str:
"""Deterministic binding of a proof to one quoted purchase: the token is
only valid for the same quote/amount/route/method it was issued for."""
material = f"{quote_id or ''}|{amount if amount is not None else ''}|{route}|{method}"
return hashlib.sha256(material.encode()).hexdigest()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Canonicalize proof amounts before hashing

In enforce mode, a proof submitted with a numerically correct but differently formatted amount (for example "9") hashes differently from the create route's computed Decimal("9.00"), so the retry is rejected as proof_invalid. This is especially problematic for unquoted creates because the PROOF-REQUEST returns only the hash, not the server's exact decimal spelling for clients to reproduce. Hash a normalized decimal representation (or bind to a server-issued quote) so equivalent amounts produce the same token binding.

Useful? React with 👍 / 👎.

Comment on lines +105 to +112
RPC outage can never affect this endpoint (soft-fail invariant). The
same-origin serving location is what proves domain control to registry
consumers; `registrations` appears only once the on-chain ceremony
(scripts/erc8004_register.py, human-controlled) has produced an agentId.

Domain policy (AGENTS.md): this is customer-facing Hyrule Cloud
identity — it lives under hyrule.host, never servify.network or
as215932.net.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not advertise disabled receipt endpoints in the agent card

When TRUST_AGENT_CARD_ENABLED=true but TRUST_RECEIPTS_ENABLED=false, this unconditional services list advertises /v1/receipts/{receipt_id} even though get_receipt returns 404 whenever receipts are disabled. The flags are intentionally independent, so an identity-only rollout publishes a service agents cannot use; add the receipt and JWKS service declarations only when their corresponding trust functionality is enabled.

Useful? React with 👍 / 👎.

Comment on lines +195 to +199
evidence=(
{"artifact_snapshot_id": str(body.artifact_snapshot_id)}
if body.artifact_snapshot_id
else None
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the delivered artifact digest in job evidence

For a completed BGP job this receipt records only the opaque snapshot ID, even though BGPSnapshotRow stores the artifact's sha256 and the receipt profile promises a delivered artifact hash as evidence. A recipient who downloads the gzip cannot verify that it is the artifact being attested, so the receipt is not usable as the advertised delivery evidence; load the snapshot and include its digest (and preferably format) in evidence.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant