Skip to content

Compass v0.5 — whole-codebase ultrareview (158 commits)#7

Open
StephenSook wants to merge 184 commits into
baseline-initialfrom
main
Open

Compass v0.5 — whole-codebase ultrareview (158 commits)#7
StephenSook wants to merge 184 commits into
baseline-initialfrom
main

Conversation

@StephenSook
Copy link
Copy Markdown
Owner

Synthetic PR for /ultrareview to scan the entire Compass codebase against the empty initial commit.

Scope

All 158 commits between the initial commit (8654981) and HEAD. Diff covers:

  • contracts/AgentRegistry.sol, CompassHub.sol, IAgentRegistry.sol, Hardhat config, ~40 unit tests, property-based invariants.
  • enclave/ — TypeScript receipt-signer, dstack TDX integration, off-chain verifier, status list, trust list, ~103 vitest cases.
  • app/ — Next.js 16 + Turbopack frontend: 21 routes (incl /, /about, /verify, /onboard, /faq, /roadmap, /demo, /audit, /clinic/*, /kiosk, /policies/*, /receipt/*, /analytics, /vault, /api/consume, /api/issue, /api/tee-status, /api/csp-report, /sitemap.xml, /robots.txt, dynamic OG images, manifest), 4 API routes, 25 vitest cases.
  • docs/ — whitepaper, architecture, threat-model, honest-limits (25 sections), audit reports, distribution playbooks, press kit.
  • skills/compass-eligibility-check Claude Code / OpenClaw skill.
  • .github/ — CI workflow (Hardhat + Slither + App + Enclave + Vitest), issue templates, PR template.
  • Demo/ — F.1 storyboard + script, F.4 HackQuest submission cheat sheet, F.5 X post template.
  • Repo health: README.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, CHANGELOG.md, LICENSE.

Prior review passes already completed

  • Codex GPT-5.5 adversarial: 3 rounds (pre-mainnet caught 1 BLOCKER agentIdCommitment encoding; mid-cycle caught 2 BLOCKERs + 2 HIGH + 1 MEDIUM; full-codebase caught 2 BLOCKERs + 2 HIGH + 1 MEDIUM)
  • Claude code-reviewer: 3 rounds (post-Tier-S+A+B; post-/verify; post-eIDAS)
  • Claude silent-failure-hunter: 1 round (12 findings closed)
  • Claude type-design-analyzer: 1 round (4 discriminated-union upgrades shipped)
  • Claude pr-test-analyzer: 1 round (16 test cases now in vitest, 25/25 passing)
  • Claude OWASP API security: 1 round (3 ship-list items closed)
  • Architecture audit + Lighthouse-equivalent perf audit
  • Slither 0.11.5 with 101 detectors: 0 security findings (2 INFO documented)

All actionable findings landed. Five remaining items documented as v0.6/v0.7 (docs/honest-limits.md §18, §19, §20, §21, §22, §23, §24, §25 — public, enumerated).

What I want from /ultrareview

Fresh adversarial eyes on:

  • Cryptographic correctness across the on-chain + enclave + browser-side verifier chain
  • Any reviewer-blind-spot I haven't been told about
  • Production readiness vs hackathon-stage tradeoffs
  • Dependency surface (npm audit summary in docs/honest-limits.md §25)
  • Anything that would embarrass me on submission day

If /ultrareview finds nothing new across this surface, the project is genuinely done.

StephenSook and others added 30 commits May 6, 2026 01:09
Added additional entries to .gitignore for Hardhat, Vercel, IDE, OS, and extra secrets.
Phase 0 + Phase 1.1-1.14 from the locked Compass plan.

contracts/
- Hardhat 2.x + @nomicfoundation/hardhat-toolbox@hh2
- Solidity 0.8.24 + Cancun + viaIR + optimizer 200 runs
- 0G Galileo (16602) + Aristotle (16661) networks wired
- @openzeppelin/contracts for EIP-712 + ECDSA primitives

app/
- Next.js 14 App Router + TypeScript + Tailwind v4 + ESLint
- Turbopack dev server, src/ layout, @/* import alias

enclave/
- @0glabs/0g-serving-broker + @0glabs/0g-ts-sdk + ethers v6
- src/generate-wallet.ts — Phase 1.10 prereq, generates fresh testnet wallet
- src/smoke-test.ts — Phase 1.10 broker smoke test (canonical Day-1 gate)

Day-1 deployer wallet generated:
0x05b5Bb550eb8401fC4b8a33bf566C03f49ef5d34 (testnet only)

Pending operator step: fund wallet via https://faucet.0g.ai/, then re-run
smoke test. Smoke infra confirms RPC reachable, wallet detected, balance
correctly reports 0 before funding.

Other Day-1 setup:
- .env.example with all variables documented
- .renovaterc.json pinning 0G/sd-jwt/Privy/Spline deps (Phase 1.13)
- .github/workflows/ci.yml — contracts + app + enclave on push/PR
  (Phase 1.12)
- README rewritten with project overview, honest-limits preview,
  judge-replicable verify-receipt CLI invocation pattern

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review of Day-1 scaffolding (background task) caught 6 real bugs:

smoke test (enclave/src/smoke-test.ts):
- FATAL exit if zero TeeML providers with non-zero pricing (was silent pass)
- BigInt-safe JSON.stringify via replacer fn (raw provider list crashed)
- Validate inputPrice + outputPrice > 0n (free/dead provider could pass)
- addLedger failure now FATAL unless error confidently says "already exists"
- Dump unique serviceType + verifiability values for operator inspection
- Refactored notes-writing into helper used on both pass + fail paths

contracts:
- TypeScript 5.6 (downgrade from 6.0 — moduleResolution=node10 deprecated)
- tsconfig now includes ./hardhat.config.ts, ./scripts, ./test
- evmVersion stays cancun (OZ v5 needs mcopy); Galileo deploy in Phase 2.11
  is the verification step before mainnet — comment documents the fallback
  to OZ v4.x if 0G Aristotle lacks Cancun

CI:
- Node 22 → 20 LTS (Hardhat 2.x toolbox stack stability per Codex finding)

Skeleton tests: 2/2 green locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lock the Aztec-Authwit-style Grant struct exactly per the plan:
- policyId    bytes32  matches PolicyMeta key
- provider    address  caller binding
- nonce       uint256  one-time use
- expiry      uint64   unix timestamp
- nullifier   bytes32  = keccak256(abi.encode(nonce, provider, policyId))

CompassHub inherits OpenZeppelin EIP712("Compass", "1") so the typed-data
hash is computed with the canonical EIP-712 domain on every chain. consumeGrant
reverts NotImplemented in this skeleton — Phase 2.3-2.9 fill in EIP-712 hash,
ECDSA recover, nullifier replay protection, expiry, provider binding, and
the GrantConsumed event one TDD cycle at a time.

Skeleton tests passing: deploys + reverts NotImplemented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the locked Aztec-Authwit pattern in CompassHub:

Phase 2.3 — EIP-712 typed hash via OZ EIP712 domain ("Compass", "1")
Phase 2.4 — ECDSA recover via OZ ECDSA.recover
Phase 2.5 — nullifier replay protection via usedNullifiers mapping
Phase 2.6 — expiry check (block.timestamp > g.expiry → revert)
Phase 2.7 — provider binding (msg.sender != g.provider → revert)
Phase 2.8 — malformed signature handling (sig.length != 65 → revert)
Phase 2.9 — GrantConsumed event with (nullifier, policyId, provider, ts)

Order of checks intentional: provider → expiry → replay → sig shape →
recover → mark used → emit. Each check has its own custom error so
reverts are diagnosable on-chain.

Test suite (9/9 passing):
- deploys with Compass EIP-712 domain
- hashGrant deterministic typed-data hash
- consumes valid signed grant + emits event
- rejects replayed nullifier (GrantAlreadyUsed)
- rejects expired grant (GrantExpired)
- rejects wrong provider (WrongProvider)
- rejects malformed sig (MalformedSignature) — wrong length
- rejects unsigned 0-byte sig (MalformedSignature)
- handles valid-length-wrong-content sig — InvalidSigner / MalformedSignature

Phase 2 exit local: 9/9 green. Galileo deploy (2.11) + hand-test (2.12)
gated on Phase 1.10 broker smoke test passing → which gates on operator
funding the deployer wallet via https://faucet.0g.ai/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ERC-7857-stripped Agent INFT per the locked plan:

struct Agent {
  bytes32 metadataHash;   hash of encrypted credential vault on 0G Storage
  string  encryptedURI;   0G Storage root-hash URI
  address attestor;       genesis credential issuer (mocked v1)
  bytes32 trustListRoot;  Merkle root of accepted issuers
}

Functions (locked from plan):
- mintAgent(metadataHash, encryptedURI, attestor, trustListRoot) → tokenId
- updateMetadata(tokenId, newHash, newURI, teeAttestation)  owner-gated
- authorizeUsage(tokenId, user, permissions)                ERC-7857 delegation
- attestEligibility(tokenId, policyId, receiptHash, quote)  oracle-only
- getEncryptedURI(tokenId) → string
- verifyAttestation(tokenId, quote) → bool                  v1 STUB
- setOracle(oracle, active)                                 owner-only

verifyAttestation is a v1 STUB returning true — the real TDX RA quote
verification happens off-chain in the enclave service. Documented in
the contract NatSpec, in the upcoming docs/honest-limits.md, and in
the README "What's Real / What's Mocked" table per the locked plan.

Test suite (9/9 passing):
- mints agent + emits AgentMinted
- only owner updates metadata
- authorizeUsage stores permission blob
- non-owner rejected from authorizeUsage
- attestEligibility stores receipt + emits (registered oracle only)
- non-oracle rejected from attestEligibility
- verifyAttestation stub returns true
- getEncryptedURI reads correctly
- setOracle is owner-only + emits OracleUpdated

Cumulative contract tests: 18/18 green (CompassHub Authwit 9 + AgentRegistry 9).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ases 3b+3c)

Phase 3b — Policy Registry
==========================
- struct PolicyMeta { policyHash, admin, minAnonymitySet, active, uri }
- registerPolicy(policyId, policyHash, uri, minAnonymitySet) — caller becomes admin
- deactivatePolicy(policyId) — admin-only
- minAnonymitySet field carries the threat-model 1h k-anonymity floor

Phase 3c — Receipt Log
======================
- ReceiptIssued event: (receiptId, policyId, resultHash, expiry, attestationDigest, timestampBucket)
- issueReceipt() — registered-oracle-only; gates on policy.active
- timestampBucket = (block.timestamp / 900) * 900 — 15-min granularity
  per threat model section 1b verifier-collusion mitigation
- setOracle() — oracleAdmin-only registration of authorized oracles

Custom errors added: PolicyAlreadyRegistered, PolicyNotFound,
NotPolicyAdmin, NotAuthorizedOracle, PolicyInactive.

Test suite (9/9 new tests passing):
- registers policy + emits PolicyRegistered
- rejects duplicate registration
- only admin deactivates
- rejects deactivate on unknown policy
- registered oracle issues receipt + bucketed timestamp event
- non-registered oracle rejected
- inactive policy rejected
- unknown policy rejected
- setOracle is oracleAdmin-only

Cumulative contract tests: 27/27 green
  CompassHub Authwit       9
  CompassHub Policy+Receipt 9
  AgentRegistry            9

Phase 3 exit (local) ready. Next: 3d hardening (slither + aderyn +
security-audit skill) + 3e docs (architecture, threat-model, honest-limits,
3 demo-policy JSONs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-way review (Codex GPT-5.5 + Sonnet code-reviewer + silent-failure-hunter)
converged on critical issues breaking locked plan invariants. All fixed.

BLOCKERS (security-load-bearing) — fixed:

1. consumeGrant single-principal binding
   - Grant struct now includes agentTokenId
   - CompassHub constructor takes IAgentRegistry address (immutable)
   - consumeGrant requires recovered == agentRegistry.ownerOf(g.agentTokenId)
   - New error UnauthorizedSigner(recovered, expected)
   - Single-principal model now enforced on-chain, not just in narrative

2. "wrong content" test that blessed the bug — DELETED
   - Replaced with two strict tests:
     * REJECTS a grant signed by a non-owner
     * REJECTS a grant where provider self-signs as random key

3. verifyAttestation no longer returns true unconditionally
   - Now: returns false on empty quote, true on non-empty
   - NatSpec aligned with code; real RA verification documented as off-chain

4. issueReceipt hardening
   - usedReceiptIds mapping prevents duplicate emission
   - expiry > block.timestamp check (rejects already-expired receipts)
   - ReceiptAlreadyIssued + ReceiptExpired custom errors
   - attestationDigest semantics documented in NatSpec (digest = H(policyHash ||
     providerChallenge || agentIdCommitment || verifierPubKey || result ||
     expiry || credentialBundleHash)) — the receipt's load-bearing field

HIGHs — fixed:

5. AgentRegistry soulbound (_update override) — Maria's agent identity cannot be
   transferred. SoulboundTransferDenied error on transferFrom + safeTransferFrom

6. mintAgent rejects zero metadataHash (the existence-check sentinel)
7. updateMetadata rejects zero metadataHash
8. attestEligibility uses NotAuthorizedOracle (not the misleading NotAgentOwner)
9. attestEligibility rejects empty quote (EmptyAttestationQuote)
10. registerPolicy rejects zero policyHash (InvalidPolicyHash)
11. consumeGrant checks policies[g.policyId].active before consuming
12. CompassHub setOracle emits OracleUpdated event
13. CompassHub transferOracleAdmin (no longer immutable; emits event)

smoke-test.ts hardening (silent-failure-hunter findings):

14. provider.getBalance wrapped — distinct exit 5 (RPC unreachable)
15. createZGComputeNetworkBroker wrapped — distinct exit 6 (broker construction)
16. broker.inference.listService wrapped — distinct exit 7 (inference subsystem)
17. addLedger "already exists" matcher tightened to /ledger.*(already|exist|registered)/i
    (was loose substring; could swallow unrelated errors like
    "endpoint exists but unreachable")
18. Bigint coercion via toBig() helper — prevents TypeError on SDK shape drift
    (price field as number/string instead of bigint)

README correction:
19. verify-receipt CLI section reframed as roadmap (Phase 10.5.5), not a
    current capability — Codex flagged the original framing as overclaim

Tests: 38/38 green (up from 27)
  AgentRegistry          11 (was 9; +2 zero-hash + soulbound transfer block)
  CompassHub Authwit     14 (was 9; +5 single-principal + policy gate + agent existence)
  CompassHub Policy+Receipt 13 (was 9; +4 dedup + expiry + transferOracleAdmin + zero-hash)

Enclave TypeScript downgraded to 5.6 to silence node10 moduleResolution deprecation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Source: user-provided Telegram bug-report channel screenshots +
0G-Compute-SDK-Fine-tune-Bug-Report.docx download.

Captures:
- Chain ID drift flag: Telegram says mainnet is 16600, our plan + .env
  have 16661 (Aristotle). Verify via eth_chainId before Phase 8 deploy.
- Faucet status: hub.0g.ai/faucet broken per multiple Telegram reports
  ("none of the buttons work"). faucet.0g.ai 301-redirects to same.
  Alternate paths: Discord #faucet bot, Telegram channel direct ask,
  build.0g.ai may have hackathon faucet.
- Canonical TeeML provider candidate for Phase 6a.1 pin:
  0xA02b95Aa6886b1116C4f334eDe00381511E31A09 (Galileo, confirmed
  working per the bug-report doc — verify post-funding via smoke test)
- Known SDK bug: @0glabs/0g-serving-broker@0.7.5 fineTuning path
  traversal off-by-one. We use 2.0.0 + don't touch fine-tuning, so
  doesn't affect Day-1 smoke. Banked for future-phase awareness.
- Daily Q&A window: 14:00-15:00 UTC+8 in the bug-report Telegram.
  Use for Phase 6 TEE questions if needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…variants

Phase 3d coverage of the locked plan:

3d.1 slither static analysis
  Initial run found 4 issues (1 medium + 3 low):
  - missing-zero-check on transferOracleAdmin(next) → FIXED with new
    InvalidOracleAdmin error + revert when next == address(0)
  - divide-before-multiply on bucket = (block.timestamp / 900) * 900 →
    annotated as intentional; bucket-flooring is the desired behavior
    (15-min granularity for verifier-collusion mitigation per threat
    model section 1b). slither-disable-next-line + comment.
  - timestamp comparisons in consumeGrant + issueReceipt → annotated
    as accepted (block.timestamp manipulation by miners ±15s is
    inconsequential for our 15-min bucket / 1-hour expiry windows).
    slither-disable-next-line.
  Re-run after fixes: 0 results at medium+high severity.

3d.3 solidity-coverage baseline
  100% statements, 100% lines, 100% functions, 96.55% branches
  on both AgentRegistry.sol and CompassHub.sol. Easily clears the
  >85% target.

3d.7 hand-written invariants (Phase 3d.7 of locked plan + post-Codex
     additions). New file: test/invariants.t.ts. Property-based via
     fast-check (npm i -D fast-check) — five invariants, randomized
     inputs, all green:

  - inv-1: usedNullifiers monotonic across 30 random grants
  - inv-2: agent metadataHash only changes via updateMetadata from owner
  - inv-3: issueReceipt requires PolicyRegistered + active
  - inv-4: usedReceiptIds monotonic across 20 random receipts
  - inv-5: AgentRegistry soulbound — no transferFrom or safeTransferFrom
           ever moves agent between non-zero addresses

3d.2 aderyn — skipped (no cargo installed; would need brew tap +
     cargo install). Slither + invariants + Codex/Sonnet review cover
     same surface.
3d.6 mythril — skipped for now (slow concolic execution). Re-run
     pre-Phase-8 mainnet deploy if time permits.

Cumulative tests: 44/44 green
  AgentRegistry             11
  CompassHub Authwit        14
  CompassHub Policy+Receipt 14 (was 13; +1 transferOracleAdmin zero-addr)
  Invariants (fuzz)          5

Phase 3d local-state DONE. Phase 3e docs (architecture + threat-model +
honest-limits + policy JSONs + receipt-v1 schema) is the next batch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ASCII layer diagram of the full Compass stack: frontend (Privy + Next.js)
→ off-chain enclave (SD-JWT VC + 0G Storage + Sealed Inference broker)
→ on-chain (AgentRegistry + CompassHub on 0G Chain).

Documents:
- Trust boundaries table (who trusts what for what reason)
- Single-principal model (Privy key plays 4 roles: EVM owner, Authwit
  signer, cnf claim, grant signer)
- Receipt lifecycle (13 steps from provider challenge to on-chain emission)
- Deployment status per network (Galileo testnet vs Aristotle mainnet)
- Generalization beyond Track 5 — same primitive serves DV survivors,
  foster youth, undocumented immigrants, journalists' sources

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the locked plan's Section 1 unified threat model. Each surface gets
(a) realistic mitigation, (b) what cannot be solved in 31 days,
(c) honest README framing.

Surfaces:
1a. Attestor coercion / compromise — multi-issuer trust list ≥2 attestors
1b. Verifier collusion — bucketed timestamps + per-policy nullifiers
1c. Metadata leakage — Privy social login + 0G gas sponsorship
1d. Revocation privacy — RFC 7644 status-list batching
1e. Device compromise — duress PIN + NGO kiosk recommendation
1f. Coercive disclosure — explicit "blast radius bounded" framing
1g. TEE side-channels — TDXdown, StumbleStepping, TEE.Fail (2025) cited;
    Plan B enclave-key-bound-into-attestation requirement documented
1h. Small-population correlation — minAnonymitySet declared per policy;
    k<100 flagged as narrow
1i. GDPR vs blockchain immutability — crypto-shredding as the
    deletion mechanism; do NOT claim Article 17 compliance

Unifying principle: Compass minimizes the BLAST RADIUS of disclosure.
We do not claim Maria is invisible. We claim a clinic / employer / state
under subpoena pressure can disclose only a non-identifying receipt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ase 3e.3)

13 honest acknowledgements of v1 gaps + a clear "what we DO protect" closer.
This is the README's competitive moat — judges reading carefully see the
gaps named, not glossed.

Highlights:
- Cannot defeat physical coercion (rubber-hose threat)
- No network-layer anonymity (Tor/mixnet out of scope)
- NGO issuers MOCKED — we have not partnered with HELP / Bethune /
  Mission for Migrant Workers; their names appear for narrative only
- Trust list governance OWNER-MANAGED v1
- verifyAttestation v1 STUB — real TDX RA verify off-chain
- SD-JWT VC pinned to draft-ietf-oauth-sd-jwt-vc-15 (in flux)
- No cross-issuer threshold trust (BBS+ / threshold sigs out of scope)
- No full unlinkable cross-policy presentations
- No differential privacy budget across receipts
- No Article 17 full GDPR compliance
- No mainnet ecosystem-credit grant (Phase 8 needs bridge from ETH)
- oid4vc-ts intentionally NOT integrated (2-4 days for zero demo gain)
- evmVersion cancun unverified for 0G mainnet (Phase 2.11 testnet
  deploy is the verification gate — fallback to OZ v4.x if needed)

Closes with explicit "what Compass v1 DOES protect" — the receipt is
non-identifying, the clinic never holds raw credentials, single-principal
is on-chain-enforced, receipts are replay-protected (inv-1 + inv-4 invariants),
bounded disclosure is the load-bearing claim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 3e.4)

Three real-world eligibility checks modeled from documented HK NGO services
(per the locked plan + Claude brief Section 6 ground truth):

1. help-legal-aid.json — Helpers for Domestic Helpers (St. John's Cathedral,
   Hong Kong; founded 1989). Predicate: is_FDH_in_HK ∧ has_pending_case.
   minAnonymitySet: 100.

2. bethune-shelter.json — Bethune House Migrant Women's Refuge (under
   Mission for Migrant Workers, founded 1986; ~25-30 women per shelter,
   ~680 women per year served). Predicate: is_female ∧ is_FDH_in_HK ∧
   in_distress_marker_from_NGO. minAnonymitySet: 50 (smaller, narrower
   eligibility population — flagged for additional review).

3. hk-fdh-hospital.json — HK Hospital Authority FDH free-care eligibility.
   Predicate: has_active_FDH_visa ∧ HKID_valid. minAnonymitySet: 1000
   (HK has 368,000 FDHs — large population, cleanest demo of the binary
   eligibility check). Rate disparity in humanReadable: HK$180 A&E
   eligible vs HK$1,230 non-eligible vs HK$39k-90k obstetric non-eligible.

All three:
- RFC 8785 JSON Canonicalization Scheme for policyHash computation
- sha256 hash algorithm
- did:key trustedIssuers placeholders (replaced at Phase 4a.1 with
  real Ed25519 fixture keys)
- credentialBundleSchema declares required claims for the predicate

These three feed into CompassHub.registerPolicy (Phase 3b.5) and the
demo flow on Day 19 onwards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Canonical JSON Schema for the off-chain receipt payload that gets hashed
into CompassHub.issueReceipt's attestationDigest field. Reproducible
byte-for-byte by the verify-receipt CLI (Phase 10.5.5) for judge
replication.

Fields (all required, additionalProperties: false):
- version (const "compass-receipt-1.0.0")
- challenge — provider 32-byte freshness primitive
- policyHash — sha256 of canonical policy JSON
- agentIdCommitment — keccak256(agentTokenId || ownerAddress)
- verifierPubKey — enclave-born signing key (Plan B requirement —
  MUST be bound into TEE REPORTDATA per Phase 6b.0)
- credentialBundleHash — sha256 of the SD-JWT VC bundle
- result {eligible: bool, reason: enum}
- expiry, issuedAt unix timestamps

Canonicalization: RFC 8785 JCS (JSON Canonicalization Scheme).
attestationDigest = sha256(canonicalize(this-document)).

The SAME canonicalizer must run in:
- Issuer (enclave when constructing the receipt)
- Verifier (off-chain consumer when verifying the receipt)
- Judge (verify-receipt CLI when reproducing the digest)

Any whitespace / key-order divergence breaks the digest match. This is
the load-bearing reproducibility primitive for the judge-replicable
verification claim in the README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…view

3-way review (Codex GPT-5.5 + Sonnet code-reviewer + comment-analyzer)
caught 4 real BLOCKERs in the prior "review fixes" pass that this commit
addresses comprehensively.

CONTRACT REFACTOR — atomic flow

CompassHub.consumeGrantAndIssueReceipt(grant, sig, receiptInputs):
  Replaces the prior split consumeGrant + issueReceipt.
  Eliminates the griefing window where a provider could consume Maria's
  single-use grant without producing a receipt. Both effects (nullifier
  + receiptId) write in the same tx; both events emit in the same tx.

ReceiptIssued event now contains:
  receiptId, policyId, nullifier, agentIdCommitment, resultHash, expiry,
  attestationDigest, timestampBucket
Adds nullifier + agentIdCommitment that the docs always claimed were there.
Removes the oracle-only issueReceipt path that let an oracle assert receipts
without any user grant.

Zero-value rejection on receipt fields:
  InvalidReceiptId / InvalidResultHash / InvalidAttestationDigest

Policy registration zero-rejection:
  InvalidMinAnonymitySet (was accepting 0)

AgentRegistry.updateMetadata now requires verifyAttestation(tokenId, quote).
The teeAttestation parameter is no longer ignored. Owner cannot repoint the
encrypted vault without passing the v1 stub (non-empty bytes + tokenId
exists). Forward-compatible with v2 which will read trustListRoot.

AgentRegistry.verifyAttestation switched from pure to view + extracted
_verifyAttestation internal helper. updateMetadata now calls the internal
helper directly (saves the STATICCALL gas overhead slither flagged).

AgentRegistry.authorizeUsage now emits UsageRevoked when permissions.length == 0
distinct from UsageAuthorized — indexer can disambiguate intent.

Dropped from AgentRegistry: attestEligibility, registeredOracles, lastReceipt
mapping, setOracle, Ownable. Receipts live ONLY on CompassHub. Cleaner.

DOC HONESTY PASS — eliminate AI slop / overclaim / unverifiable cites

honest-limits.md:
  - RFC 9901 -> RFC 9601 (December 2024)
  - "$1.26 / $18 gas" -> "low-single-digit / $10-20 (verify at deploy time)"
  - Drop "competitive moat" meta-commentary intro
  - Phase references -> status-based language
  - Update receipt fields to match actual on-chain event

threat-model.md:
  - "95% UK DV cases" stat -> hedged Refuge UK framing without unverified
    percentage
  - TDXdown CCS 2024 -> USENIX Security 2024 (correct venue)
  - StumbleStepping -> USENIX Security 2025 (correct venue)
  - Botto/Iovino/Visconti 2023 cite (unverifiable) -> dropped, kept
    EDPB Guidelines 02/2025 (verified)
  - Receipt fields aligned to actual on-chain ReceiptIssued event
  - Duress PIN marked as "planned, not yet implemented" (was implied built)

architecture.md:
  - "AES-256-GCM correct nonce reuse" -> "never nonce-reuses" (was inverted)
  - "the architecture is the moat" filler -> dropped

README.md:
  - "autonomous agent" overclaim -> "soulbound agent identity" (accurate)
  - Phase reference stuffing (2.11 / 3a.8 / 6a.1 / 8 etc.) replaced with
    status table + concrete state-of-today
  - chainId hedge added to align with architecture.md (16661 vs 16600)
  - "Sookra Methodology framework" undefined name -> dropped
  - Status section restructured as concrete what's-built / what's-not table

receipt-v1.json:
  - Added receiptId + resultHash to required fields (was missing — judge
    couldn't reproduce the on-chain event)
  - Added _alignmentToOnChain section explaining schema-event mapping
  - Dropped _planRefs phase-reference block

TEST OVERHAUL

All test files rewritten for the combined function:
- CompassHub.authwit.t.ts: 16 tests for consumeGrantAndIssueReceipt
- CompassHub.policy.t.ts: 8 tests for policy registry + admin
- AgentRegistry.t.ts: 11 tests including 4 soulbound paths
  (transferFrom, safeTransferFrom 3-arg + 4-arg, approve+pull)
- invariants.t.ts: 5 invariants, EXPLICIT REPLAY in inv-1 (not vacuous
  random sampling), real Hardhat signers in inv-4 (not invalid hex
  checksums)

40 tests passing (was 44; net -4 from removing attestEligibility surface).
Slither: 2 informational findings (cyclomatic-complexity 13 — atomic flow
intentional; missing-inheritance — cosmetic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
coverage/ + coverage.json + .coverage_* are regenerated on every
hardhat coverage run. Belongs out of version control.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end credential pipeline matching the locked plan:

enclave/src/types.ts        shared ClaimSet, EligibilityResult, CompassPolicy types
enclave/src/keys.ts         Ed25519 (issuer) + secp256k1 (holder) keypair
                             helpers; deterministic seed-derived fixtures;
                             did:key:z6Mk... encoding via @scure/base
enclave/src/policy.ts       PURE predicate evaluator. AND/OR/leaf nodes,
                             missing-claim short-circuit, deterministic output.
enclave/src/trust.ts        Set<DID> trust list (v1; Merkle proof in v2).
enclave/src/fixtures.ts     Maria persona + 3 mock issuers (HELP, Bethune,
                             Hospital). Deterministic seeds = same did:key
                             across test runs.
enclave/src/issuer.ts       CompassIssuer wraps SDJwtVcInstance with custom
                             Ed25519 signer. Per-claim selective disclosure.
                             holderJwk -> cnf claim (single-principal binding).
enclave/src/holder.ts       CompassHolder signs presentations with secp256k1
                             ES256K. v1 skips KB JWT (documented in
                             honest-limits.md gap #13 — equivalent integrity
                             from on-chain Authwit signer recovery).
enclave/src/verifier.ts     CompassVerifier: trust-list check (per Ultraplan
                             A4), revocation check (per Ultraplan A6 fail-
                             closed), Ed25519 sig verification, expiry,
                             policy evaluation. Returns EligibilityResult
                             with deterministic policyHash.
enclave/src/receipt.ts      RFC 8785 JCS-style canonicalize + sha256
                             attestationDigest. Reproduces docs/schemas/receipt-v1.json
                             byte-for-byte (Phase 4c.8 deterministic fixture).
enclave/test/policy.spec.ts 5 unit tests: AND, OR, false-on-mismatch,
                             missing-required-claim, hash echo.
enclave/test/e2e.spec.ts    6 round-trip tests:
                             - happy path: issue -> present -> verify -> eligible
                             - issuer-not-trusted rejection (Ultraplan A4)
                             - revoked credential rejection
                             - status-list unreachable fail-closed (Ultraplan A6)
                             - partial-claim disclosure rejection
                             - Phase 4c.8 deterministic receipt digest

Vitest as the test runner (lighter than mocha for this workspace).

Cumulative tests: 51 green
  contracts (40 — Authwit + Policy + soulbound + invariants)
  enclave   (11 — policy + issuer/holder/verifier + receipt)

Phase 4 exit (per locked plan): SD-JWT VC issuer→holder→verifier round-trip
works end-to-end with the demo persona. EligibilityResult shape ready to
feed into Phase 6 TEE wrapper. Receipt digest deterministic for judge
replication.

Honest-limits.md gap #13 added — KB on holder side skipped in v1.

Dependencies added:
  @sd-jwt/sd-jwt-vc    SD-JWT VC implementation
  @sd-jwt/core         underlying SD-JWT primitive
  @sd-jwt/crypto-nodejs digest + saltGenerator
  @sd-jwt/jwt-status-list (RFC 7644 — wired but mocked v1)
  @noble/curves        Ed25519 + secp256k1
  @noble/hashes        sha256
  @scure/base          base58 multibase encoding
  vitest               test runner

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex + Sonnet reviews flagged the deterministic seed-derived keys as
needing harsher labels — anyone reading the seed string instantly
reconstructs the private key. New names make this impossible to miss
in import sites (issuer.ts, holder.ts, fixtures.ts, e2e.spec.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sonnet review flagged: empty and:[] vacuously returned true (no-op
predicate granting eligibility), empty or:[] silently denied, no
recursion depth limit (DoS via deeply-nested predicate JSON). All three
now surface as predicate-false. MAX_PREDICATE_DEPTH = 16. Three new
tests cover each path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…KER)

Codex review surfaced the central security gap: presentations without
KB-JWT were bearer tokens, cnf claim was never verified, vct was never
checked. Holder + verifier now enforce the full single-principal model:

  - Holder attaches KB-JWT covering {aud, nonce, iat, sd_hash} signed by
    the secp256k1 key declared in cnf.jwk.
  - Verifier pre-checks KB-JWT sig against cnf.jwk before sdjwt.verify
    (the lib bundles KB sig failures and issuer sig failures behind one
    error string, so we differentiate by checking KB first).
  - Verifier also enforces vct === policy.expectedVct, distinct
    error reasons for status-list-unreachable / wrong-vct /
    kb-binding-failed / malformed-presentation.
  - Disclosed claim allowlist now derives from
    policy.credentialBundleSchema.required (Codex finding: schema is
    authoritative, blacklist of JWT claims was wrong).
  - secp256k1 sigs forced lowS on both sign + verify (malleability fix).
  - Receipt doc now includes policyId + sorted disclosedClaims.
  - canonicalize() rejects non-finite numbers, lone surrogates, and
    non-plain objects (RFC 8785-aligned subset).

8 new e2e tests cover wrong-vct, replay-via-stale-nonce, audience
mismatch, forged KB-JWT (attacker key), malformed presentation, and
distinct status-list-unreachable reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Honest-limits gap #13 was overclaiming KB-JWT as "belt-and-suspenders" while
in fact KB was the load-bearing security claim and v1 had it disabled.
Rewritten to disclose v2 enforces it and v1 did not. Threat-model 1d
incorrectly cited RFC 7644 (SCIM bulk ops) for the JWT status list — actual
spec is the IETF OAuth Token Status List draft.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…5.5)

The encryption layer for 0G Storage encrypted credential vaults. Maria's
SD-JWT VCs encrypt client-side BEFORE upload — the storage layer holds
ciphertext only.

  - AES-256-GCM authenticated encryption (96-bit IV, 128-bit auth tag)
  - PBKDF2-SHA-256, 600k iterations (OWASP 2023 baseline), 256-bit key
  - Random salt + IV per encrypt — passphrase reuse cannot leak via
    key/IV collision
  - AAD binds ciphertext to context (typically agentIdCommitment), so
    a credential blob replayed into a different agent's slot fails
    auth-tag verification
  - Wire format [1B version][16B salt][12B iv][N B ct||tag] matches
    WebCrypto subtle.encrypt output convention so the same blob is
    cross-decryptable in-browser when Phase 7 lands

13 tests cover round-trip, IV uniqueness, tamper detection, wrong
passphrase, wrong AAD, serialize/deserialize round-trip, version-byte
rejection, truncated buffer rejection, salt-length validation, random
binary plaintext, and empty AAD path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CompassStorage wraps @0glabs/0g-ts-sdk Indexer for opaque-blob upload +
download by Merkle root. Inputs are byte buffers (typically a serialized
EncryptedVault from crypto.ts) — the storage layer never sees plaintext.

  - upload(bytes) -> {rootHash, txHash} via MemData + Indexer.upload
  - download(rootHash) -> bytes via tmpfile + Indexer.download
  - compassStorageFromEnv() factory reads ZG_RPC_URL, ZG_INDEXER_URL,
    ZG_STORAGE_PRIVATE_KEY (or DEPLOYER_PRIVATE_KEY); returns null when
    any var missing so integration tests skip cleanly without funded env

Live round-trip test gated on ZG_* env presence — currently skips while
testnet OG funding lands. Constructor + env-gate behavior tests always
run (3 unit tests passing).

SDK module-resolution glue:
  - SDK ships only an "exports" map; TS5.6 + moduleResolution:node10
    can't resolve the bare specifier
  - Ambient declaration in src/types/0g-ts-sdk.d.ts re-exports from the
    explicit CommonJS subpath
  - vitest.config.ts forces resolve.conditions: [require, node] so Vite
    picks the CJS entry the same way TypeScript does

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hecks)

5-way review (Codex + 4 Sonnet specialists) surfaced one real bug + two
boundary gaps + comment bloat:

  - HIGH: deserializeVault used `bytes.slice()` which on a Node Buffer
    returns a VIEW sharing the parent ArrayBuffer. Caller mutation or
    pooled-buffer reuse silently corrupted the deserialized vault and
    surfaced as opaque "GCM auth-tag mismatch" downstream — impossible
    to diagnose. Fixed by `Uint8Array.from(bytes.subarray(...))` which
    forces a copy. New test mutates input post-deserialize and asserts
    the vault still decrypts.
  - HIGH: encryptVault accepted empty plaintext + empty passphrase.
    Empty plaintext encrypted to a 45-byte "vault containing no
    credential" that would silently round-trip and surface as a JSON
    parse error on retrieval. Empty passphrase derived a key from
    sha256(""+ salt) — the salt is in the public blob, so anyone could
    recompute. Both now reject at the boundary.
  - MEDIUM: GCM auth-tag failure threw raw "Unsupported state or unable
    to authenticate data" — opaque. Wrapped with cause and explicit
    candidate-cause list (without leaking which check failed, to avoid
    a padding-oracle channel).
  - LOW: comment bloat — 23-line file header had threat-model prose,
    Maria-narrative, OWASP citation, and restated GCM 101. Trimmed to
    5 lines focused on the WHY of the wire format choice.

3 new tests: salt-sensitivity (deriveKey), empty-passphrase rejection,
empty-plaintext rejection, buffer-aliasing trap, null-byte binary
plaintext (replaces tautological random-bytes test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tics)

5-way review surfaced CLAUDE.md violations + diagnostics gaps:

  - Dropped dead `if (!result)` check after Indexer.upload — SDK source
    inspection (Uploader.js:22-86) confirms result is always present
    when err is null. CLAUDE.md says don't validate scenarios that
    can't happen.
  - Dropped 3 constructor field-presence throws — internal boundary,
    factory already gates, ethers/Indexer fail clearly on bad input.
  - Tightened rootHash regex from /^0x[0-9a-fA-F]+$/ (any length) to
    /^0x[0-9a-fA-F]{64}$/ (exact 32-byte Merkle root).
  - Use `||` not `??` for SDK error message extraction — `??` only
    triggers on null/undefined, leaving empty-string messages as
    "0G upload failed: " with no signal. Now falls back to constructor
    name.
  - Add `{ cause: err }` on rethrows — preserves SDK stack trace.
  - Add zero-byte-download guard — SDK can return success with an
    empty file under partial-write / TOCTOU race; surface explicitly
    instead of letting the empty buffer trip "vault buffer too short"
    in the crypto layer.
  - Add `[inspect.custom]` redacting `privateKeyHex` — defense in
    depth against accidental log/serialization leaks.
  - Drop `private readonly config` field; pull only `rpcUrl` to
    instance — shrinks the temporal exposure window of the private
    key to constructor scope only.
  - WARN when DEPLOYER_PRIVATE_KEY fallback fires — co-mingles deploy
    and storage signers, which is a real audit-trail concern in prod.
  - Pass `bytes` (Uint8Array) directly to MemData — its constructor
    accepts ArrayLike<number>; `Array.from(bytes)` was a wasteful
    materialization step.
  - Trimmed redundant method docstrings + file header.

Test suite reshaped:
  - Dropped tests for the removed constructor throws (testing dead
    defensive code).
  - Added redaction test for inspect.custom.
  - Added tighter rootHash rejection cases.
  - Wrapped env mutation in try/finally so a failed assertion doesn't
    leak deleted env state into other vitest workers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deployed contracts to Galileo testnet (chainId 16602):
  AgentRegistry: 0xf1FAaBef1d00Db1a15b7637Dc0d8526449D06Bf9
  CompassHub:    0xe42fd4F0a3197126fEeF5e6AAfC5Fb8848bBC58b

Funded via Google Cloud Web3 faucet for 0G Galileo (0.1 OG daily limit).
Tx 0xe7ba6e364cd4a2c72f110f50ba7a0ca66a9316c3...

Phase 5.6 integration script (`enclave/scripts/mint-with-storage.ts`):
  1. Encrypt fixture credential bundle via crypto.ts
  2. Upload to 0G Storage via storage.ts (gated COMPASS_LIVE_STORAGE=1)
  3. Mint AgentRegistry INFT with rootHash as encryptedURI

End-to-end smoke run with placeholder rootHash (sha256 of encrypted blob):
  metadataHash: 0xedb4bdc36fcefc986f554ce191f6df01997db4aaca17b171f1f22e44e87ff100
  mint tx:      0x9d64e3953f12a9d2ca679d369c6fcf535512901c426551fbc5de761deeeed0ef
  tokenId:      1

Live storage upload deferred — 0G V3 testnet Flow contract reverts on
submit() with require(false) regardless of fee size (tested 30 gwei calc
and 0.001 OG explicit override). Pre-checked: market() resolves,
paused() returns false, submission struct is well-formed. Cause likely a
permission / pre-deposit / market-reward shape change in V3. Filed for
0G dev TG follow-up. The wire-up itself is proven; flipping
COMPASS_LIVE_STORAGE=1 once the SDK quirk is resolved upgrades the demo
to live storage with no code changes.

Storage SDK API: extended `CompassStorage.upload` to accept `{ fee }`
override so callers can experiment with fee values when debugging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3-way review (Codex + 2 Sonnet) on Phase 5.6 surfaced:
  - opts.fee !== undefined branch aliased the SDK's defaultUploadOption
    singleton instead of always spreading. Future SDK mutations of that
    singleton could leak across uploads. Now always spread.
  - No negative-fee guard — caller passing fee=-1n would silently coerce
    inside the SDK or throw an opaque error. Now validates fee >= 0n.
  - sdkErrorMessage fell back to "Error" for empty err.message — adds
    constructor name + colon to give callers something to grep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Phase 5.6 review)

3-way review surfaced:
  - BLOCKER: writeFileSync silently obliterated docs/deployments/<network>.json
    on re-run. One accidental --network og_galileo against a populated
    aristotle.json would have lost the mainnet deploy record. Now refuses
    to overwrite without OVERWRITE=1; archives the prior file with a
    timestamp suffix when overwrite is allowed.
  - HIGH: Non-atomic deploy — if AgentRegistry succeeded but CompassHub
    reverted on insufficient balance, the script threw before the JSON
    write, leaving an unrecorded contract on chain. Now writes the
    manifest after each successful deploy so a partial-failure run still
    captures the AgentRegistry address for recovery.
  - LOW: console.error flattened the cause chain. util.inspect with
    depth:null shows nested ethers RPC errors.
  - LOW: dropped unused `join` import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3-way review surfaced 1 BLOCKER + 5 HIGH + 4 MEDIUM:

  - BLOCKER: COMPASS_VAULT_PASSPHRASE fallback to a fixed dev string
    silently downgraded encryption to a key derivable from the public
    repo. Now hard-required; throws if unset.
  - BLOCKER: tx.wait() in ethers v6 does NOT throw on revert. Reverted
    mint produced "Phase 5.6 complete" + a chainscan link to the failed
    tx. Now checks receipt.status !== 1 and throws.
  - BLOCKER: placeholder rootHash = sha256(wire) was identical to
    metadataHash, which would silently pass tests in dev mode and
    diverge the day COMPASS_LIVE_STORAGE flips on. Now uses a distinct
    placeholder constant (0x70…70, "p" repeated) and an explicit
    `compass-skipped://placeholder` URI scheme that cannot be confused
    with a real 0G Storage root.
  - HIGH: hand-written ABI fragments + double-cast through `unknown`
    bypassed type checks. Now imports the canonical ABI from
    contracts/artifacts/contracts/AgentRegistry.sol/AgentRegistry.json
    and uses `interface.parseLog` with proper LogDescription typing.
    Throws if the AgentMinted event is missing (ABI drift detection).
  - HIGH: docstring claimed env was sourced from .env, but the script
    never called dotenv. Now auto-loads from repo-root .env via
    dotenv.config.
  - HIGH: COMPASS_LIVE_STORAGE === "1" silently treated "true"/"on"
    as off. Now strictly accepts "0" or "1" and throws on anything
    else.
  - HIGH: JSON.parse(readFileSync) threw uncaught with no context if
    the deployment manifest was missing or malformed. Now validates
    existence + JSON validity + required fields with named errors and
    a hint to run deploy.ts first.
  - HIGH: chainId from RPC was never asserted against the deployment
    manifest's chainId — could mint to a stale address on a wrong
    network. Now validates.
  - MEDIUM: ZeroHash inlined; replaced with ethers ZeroHash export.
  - MEDIUM: Top-level error handler dropped cause chains; now uses
    util.inspect with depth:null.
  - MEDIUM: Indexer URL was only echoed on the gated-on path; now
    always echoed with "(default)" when env unset.
  - MEDIUM: Fake "0xskipped..." tx hash string replaced with explicit
    null when storage is gated off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StephenSook and others added 30 commits May 11, 2026 02:46
The new vitest suite (commit 1c66a9d) covers verifyReceipt.ts +
ratelimit.ts — the two crown-jewel modules. CI now runs them on every
push so regressions can't sneak through (e.g. the canonicalize EOS
bug that the v0.5 polish push test surfaced).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build regression from commit 5f5b36b (Wave 3 type design). The
`as const satisfies Record<string, GlossaryEntry>` annotation on
GLOSSARY narrowed each entry to its literal shape — entries without
a `link` field (e.g. "pdpo-57") lost the optional property from
their literal type, so `entry.link` access in Term.tsx tripped:

  Property 'link' does not exist on type '{ readonly term: ...;
  readonly definition: ...; }'

Fix at the access site: widen via `GLOSSARY[k] as GlossaryEntry |
undefined`. Keeps the compile-time key check (`k: keyof typeof
GLOSSARY`) intact AND restores well-typed access to optional fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
silent-failure-hunter 2026-05-11 LOW findings:

- app/src/app/error.tsx: in production, log only error.digest + name
  rather than the full Error object. Production console no longer
  exposes potentially-sensitive Error.message contents (e.g.
  unrecovered viem RPC payloads, stack paths, env-derived hints) to
  visitor DevTools. Local dev still gets the full object.

- app/src/app/verify/page.tsx: FileReader.onerror now surfaces
  reader.error.name + .message instead of a generic "could not read
  file". Disk error vs permission denied vs file-deleted-during-read
  now distinguishable in the UI.

- app/src/lib/verifyReceipt.ts: recoverEthAddress now pre-checks
  sigBytes.length === 64 (compact r || s — @noble/curves
  Signature.fromBytes shape; recovery byte is added separately via
  addRecoveryBit). Malformed signature length surfaces as
  "signature must be 64 bytes (compact r || s), got N" instead of
  the less-helpful "candidates []" recovery-loop fall-through.

Tests updated:
- One existing test ("fails when signature bytes are random garbage")
  had used 65 bytes; corrected to 64 bytes (the actual signature is
  also 64 bytes in the sample). One new test asserts the explicit
  64-byte error message surfaces for a clearly-malformed length.
- vitest: 25/25 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c §25

Whole-codebase /review surfaced one real fix + four tracked items.

Real fix:
- app/src/app/onboard/page.tsx: liveMint.txHash footer hardcoded
  https://chainscan-galileo.0g.ai/tx/... Under
  NEXT_PUBLIC_COMPASS_USE_MAINNET=1 the MintAgentButton (Wave 1)
  now routes through activeChain(), so the resulting tx hash is on
  Aristotle — but this footer link still sent users to the testnet
  scanner. Replaced with `${activeChain().blockExplorers.default.url}/tx/${liveMint.txHash}`.
  The fixture AGENT_MINT_TX_HASH below it stays on chainscan-galileo
  because that specific hash exists only on Galileo (comment added).

Tracked-not-fixed (documented):
- §25 added to docs/honest-limits.md inventorying npm audit findings:
  HIGH axios CVE is dead-code transitive via Privy → wagmi →
  coinbase/cdp-sdk; Compass uses Privy embedded EOA only and never
  invokes the wagmi connector graph. Build-time postcss/next/spline
  vulns don't reach runtime. Contracts + enclave dev-stack vulns
  don't reach production bytecode/CVM. v0.6 plan: roll deps as
  upstream patches land.
- Other Galileo-scanner references at /receipt/[id]/page.tsx,
  /clinic/subpoena/page.tsx, /onboard fixture-fallback all link
  Galileo-only fixture tx hashes — CORRECTLY hardcoded chainscan-
  galileo.0g.ai. No change.
- tsconfig stricter flags (noUncheckedIndexedAccess,
  exactOptionalPropertyTypes) deferred to v0.6 polish.
- Enclave-side canonicalize EOS test (paired with Wave 4 CLI fix)
  deferred to v0.6 alongside the CVM rebuild (honest-limits §21).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Repository-health pass aligned with current OSS best practices, plus
three pre-rendered visualizations of the codebase shipped under
docs/visualizations/.

README + CHANGELOG
- Add 22-entry table of contents near the top for fast judge skim.
- Add "Why this matters" elevator pitch above Maria's story.
- Add "Three pre-rendered views" subsection inside Architecture
  pointing at the new gource MP4 + 3D arch graph + graphify graph.

docs/visualizations/
- compass-architecture-3d.html — hand-curated 21-node / 27-edge 3D
  force graph of the 6 Compass layers (holder, credential, 0G
  Storage, 0G Chain, TEE, verifier). three.js + 3d-force-graph +
  bloom post-processing; hover-to-trace, click-for-detail.
- compass-knowledge-graph.html + .json — auto-generated full-codebase
  graph via `graphify update .`: 484 nodes, 491 edges, 132
  communities. RAG-ready node-link JSON.
- compass-graph-report.md — plain-language audit of the graphify
  output.
- compass-gource.mp4 — animated git-history time-lapse, 1280x720
  H.264, 3.2 MB.
- README.md — index + regeneration commands for all three.

Community health files
- .github/dependabot.yml — weekly npm + github-actions updates Mon
  06:00 HKT for /app, /enclave, /contracts, workflows.
- .github/CODEOWNERS — pre-wired review routing (currently all
  paths -> @StephenSook; adding a teammate is a one-line change).
- SUPPORT.md — channel-by-channel help index with response-time
  expectations.

Issue templates — convert .md -> .yml forms
- bug.yml + feature.yml — modern GitHub issue forms (dropdowns,
  validation, required fields).
- config.yml — contact-link routing: security disclosures go to
  SECURITY.md privately, whitepaper / verifier / honest-limits
  surfaced before users open a "bug".
- Removes legacy bug.md + feature.md (GitHub API was returning
  issue_template: null on the community profile because the .md
  templates lack form schema).

Repo metadata (applied via `gh repo edit`)
- Homepage URL set to https://app-psi-pied.vercel.app (previously
  empty).
- 20 discovery topics added (0g, web3, privacy, zero-knowledge,
  sd-jwt, tee, intel-tdx, phala-network, dstack, ...).
- Wiki + Projects disabled (both unused).
…scussions

docs/adr/ (3 MADR-format records)
- ADR-0001: 0G Chain (Aristotle) + Phala dstack TDX as the privacy
  platform — over Aztec, Aleph, SGX, SEV-SNP, 0G TeeML.
- ADR-0002: SD-JWT VC for selective disclosure — over PCD, zkSNARK
  Groth16, BBS+, plain JWT.
- ADR-0003: Per-receipt RA quote with `report_data` binding to
  (ethAddress, composeHash, receiptId) — over boot-quote binding.
- README.md — ADR index + "when to write an ADR" guidance.

README
- Add Mermaid C4Context diagram inside Architecture section. 3
  person-actors (worker, clinician, judge), 4 systems (Compass,
  NGO Issuer, 0G Chain+Storage, Phala dstack TDX), 7 relationships.
  Renders natively on GitHub.
- Add docs/adr/ link to the documentation map.

GitHub repo
- docs/social-preview.png — 1280x640 custom social preview rendered
  from the live /opengraph-image endpoint (Next.js next/og), padded
  to GitHub's 2:1 spec on a dark canvas matching the Cinematic
  Privacy aesthetic. Awaits 1-click upload at Settings -> Social
  preview.
- .github/FUNDING.yml — single `custom:` entry pointing at the
  maintainer's Telegram for collaboration / NGO partnership.
- Discussions enabled via `gh api -X PATCH`. Six default categories
  auto-created.
Independent vercel:performance-optimizer agent audit returned two
SHIP-tier wins. Both applied.

app/src/components/providers/PrivyClientProvider.tsx
- Replace static `import { PrivyProvider } from "@privy-io/react-auth"`
  with `next/dynamic({ ssr: false })`. The ~1.22 MB Privy vendor
  chunk was being hoisted into the root layout shared chunk and
  shipped on every route — including /verify, which uses no wallet.
  Now it loads only when PRIVY_APP_ID is set AND this provider
  mounts (i.e., on /onboard).
- Expected impact: /verify loses ~1.0 MB JS; / LCP drops ~150 ms
  on slower devices.

app/src/components/primitives/BlurText.tsx
- First word (i === 0) now skips the blur/opacity stagger.
  Reason: Chrome's LCP picker waits for opacity > 0 and filter:
  blur(0) before counting a glyph as paint-complete. Animating
  the LCP word delayed real LCP by ~400 ms (4 words x 100 ms
  stagger). Other words still animate; only the first is instant.

Two CONSIDER-tier fixes (Floating-UI tooltip dynamic-load,
Instrument_Serif italic drop) intentionally not applied yet —
left for v0.6 if perf budget justifies it.
Round-4 code-reviewer + silent-failure-hunter caught: the perf fix in
62dbac1 wrote dynamic(() => import("@privy-io/react-auth"), { ssr: false })
at root layout. With ssr:false the wrapper renders null server-side, so
{children} are not emitted in server HTML — every SSR route (/about, /faq,
/roadmap, /audit, /clinic/*, /vault, /analytics, /demo, /policies/*,
/receipt/*) regressed. The PRIVY_APP_ID===null dev branch masked it locally
via the early-return short-circuit.

Combined fix — mounted-flag pattern:
- SSR + dev (no APP_ID) + post-hydration-before-Privy-chunk-loaded: emit
  <>{children}</> directly so server-rendered HTML is correct.
- Post-hydration with APP_ID: mounted flips → Privy lazy-loads → wraps
  children.
- Chunk-load failure (offline / CDN 4xx / stale _next/static / ad-blocker):
  PrivyProvider stays null, /onboard step 1 falls back to fixture timer,
  NOT a white-screen.

Verified by Codex round-4 INFO: "children withheld until provider loads,
wallet hooks stay under provider, activeChain() evaluated at use sites.
No chain-switching invariant break, no hydration race found."

File: app/src/components/providers/PrivyClientProvider.tsx
…viewer)

README
- Aristotle mainnet row: draft -> real with both 0xf1FA... and 0xe42f...
  addresses. Was contradicting README's own deployment table, /about page,
  threat-model, and the green Aristotle badge in the header. (Gemini)
- /verify-receipt CLI: --bundle <path> was marked "v2 roadmap" but is
  already shipped and demonstrated 2 sections above. Drop the line.
  (Gemini)
- C4Context diagram: split "0G Chain + 0G Storage" into two systems and
  mark Storage as v2/draft since browser-side ciphertext upload is still
  draft per the reality table. Overstated privacy boundary otherwise.
  (Codex round 4)

docs/architecture.md
- ASCII diagram had `authorizeUsage (ERC-7857)` listed under
  AgentRegistry, but contracts are ERC-7857 stripped (matches
  /about + README + on-chain ABI). Replace with explicit "stripped --
  no authorizeUsage by design" annotation. (Gemini)

docs/adr/ADR-0001 (platform)
- "Phala dstack-0.5.9 production-channel CVMs" was unclear vs.
  enclave/package.json pinning @phala/dstack-sdk@0.5.7. Spell out:
  CVM image is dstack-0.5.9, SDK is 0.5.7 -- the one-patch lag is the
  normal Phala release cadence. (Gemini)

docs/adr/ADR-0002 (credential)
- Rejected-alternative justification for BBS+ mixed up two properties.
  Replay protection runs through buildNullifier(nonce, provider, policy)
  against CompassHub.usedNullifiers and does NOT require a stable
  agentIdCommitment. Rewrote to make nullifier + commitment orthogonal,
  with file refs. (Codex round 4)

docs/adr/ADR-0003 (per-receipt quote binding)
- "Canonicalization function shared between Node and browser ports" was
  literally wrong -- it's byte-parity-duplicated, with paired vitest
  cases enforcing parity. Clarified. Also clarified that the Claude
  Code skill wraps the CLI rather than re-implementing it -- so the
  "three places" framing is correct but the "same code" framing wasn't.
  (Gemini)

docs/adr/README.md
- Template link pointed at .claude/skills/adr-writer/references/adr-template.md
  which doesn't exist in this repo (the skill lives in the user's
  global Claude config). Replace with copy-an-existing-ADR guidance.
  (code-reviewer round 4)

SUPPORT.md
- "Open a Bug issue" and "Open a Feature issue" links used ?template=bug.md
  and feature.md, but the repo ships .yml issue forms (legacy .md files
  were deleted in commit de52863). Update to bug.yml / feature.yml.
  (Codex round 4)
- Response-time table referenced "security@..." but SECURITY.md doesn't
  expose that address -- the real disclosure channel is
  stephensookra@gmail.com or Telegram @stephensookra. Align.
  (Codex round 4)
…in()

Codex round 4 caught: mainnet mode still hardcoded "Galileo" in user-
facing copy + linked to the Galileo faucet, even though activeChain()
routes writes to Aristotle when NEXT_PUBLIC_COMPASS_USE_MAINNET=1.
A live mainnet demo sent users to the wrong faucet (faucet.0g.ai is
testnet-only) and stalled minting.

app/src/app/onboard/page.tsx
- Status line "Minting agent on Galileo" -> "Minting agent on
  ${activeChain().name}".
- Step-2 detail text branched on useMainnet(): testnet still points
  at "fund the wallet via the Galileo faucet"; mainnet says "acquire
  OG via the funding-options doc linked below" (which is wired to
  docs/notes/0g-mainnet-funding-options.md via the button affordance).
- Tokenized network name ("0G Galileo" -> activeChain().name) so the
  detail panel matches the actual write target.

app/src/components/onboard/MintAgentButton.tsx
- needs-fund branch: switches href + label based on useMainnet().
  Testnet -> Galileo faucet button. Mainnet -> "Acquire OG for
  Aristotle ->" linking to docs/notes/0g-mainnet-funding-options.md
  on GitHub.

The historical-tx footer link (AGENT_MINT_TX_HASH) stays on
chainscan-galileo because that specific hash exists only there
(per the inline comment that survived through the whole-codebase
review on 2026-05-11).
H4 — silent-failure-hunter round 2: ADR-0003 §"Negative" framed DCAP
signature-chain verification as a scope boundary in prose only.
Programmatic consumers couldn't branch on "incomplete verification."

app/src/lib/verifyReceipt.ts
- VerifyResult discriminated union now carries `dcapVerified: false`
  as a typed literal on both ok:true and ok:false variants.
- verifyBundle returns `dcapVerified: false` in both code paths.
- v1 always emits false because the in-repo verifier runs the 4
  cryptographic checks but does NOT run Intel DCAP cert-chain
  verification (left to the DStack Verifier or Intel QVL externally).
  The literal false widens to true only when a verifier wraps the 4
  checks with an external DCAP run.

docs/adr/ADR-0003 §"Negative"
- Replaced the "out of scope" prose with a reference to the new typed
  field, so downstream agents branch on dcapVerified rather than
  parsing free-text caveats.

Cx4 — Codex round 4: dependabot config covered npm + github-actions
but not Docker base images. enclave/Dockerfile and
enclave/phala/Dockerfile both pin node:22-alpine; base-image CVEs got
no Dependabot PRs.

.github/dependabot.yml
- Two new package-ecosystem: docker entries for /enclave and
  /enclave/phala. Same weekly Monday 06:00 HKT cadence as the npm
  ecosystems. Labels include "enclave" + "docker" so dependabot PRs
  route to the same review surface as the rest of the enclave work.

CHANGELOG.md
- Round-4 wave entry covering this commit + the 3 prior commits in
  this audit cycle (Privy SSR fix, 9 doc drifts, mainnet network
  copy). Audit attribution names every reviewer. Deferred-to-v0.6
  section explicit about what was knowingly skipped.
OG team's "Final Push — 5 DAYS TO GO" tweet (Dragon, May 11) + the
"0G APAC Hackathon · Final Week Sprint" Discord post locked the
HackQuest submission deadline at May 16 2026 23:59 GMT+8. Several
Demo/* drafts had June 5 inherited from an earlier internal
timeline. Sync.

SUPPORT.md
- Response-time table footnote: "June 5 2026" -> "May 16 2026
  23:59 GMT+8" with attribution to the OG team announcement.

Demo/x-post-final.md
- Post timing: primary post target moved from "June 3 evening US
  time / June 4 morning APAC" to "May 15 evening US time / May 16
  morning APAC, within 24hr of HackQuest submission."

Demo/hackquest-submission-answers.md (+ regenerated .pdf)
- Replace "Revised timeline (BEYOND Expo Macau = Yes)" section.
  Old reached June 5; new "Final-week timeline" maps May 11 ->
  May 16 -> May 17+ community award push.
- Remove Macau travel prep (per user 2026-05-10 "leave out Macau,
  not confirmed I've won").
- Add "Dragon's final-sprint checklist" sub-section mapping the
  4 points from the May 11 tweet against Compass status:
  (1) going-to-market via NGO outreach drafts
  (2) one-liner locked "Prove eligibility, not identity."
  (3) Pitch Video covers user-journey arc at 2:55; separate
      "How It Works" video deferred to v0.6
  (4) Community Award eligibility via 4 mandatory tags in F.5
- Add "What's NOT in this submission (intentional)" sub-section.

Demo/otc-discord-ping.md + Demo/discord-support-ticket.md
- Three "June 5 submission" references -> "May 16 2026 23:59
  GMT+8 HackQuest submission deadline." (Historical Discord
  ticket drafts kept for reference; Aristotle deploy itself
  shipped 2026-05-11.)

CHANGELOG.md
- Sixth-wave entry per the doc-sync rule.
When NEXT_PUBLIC_COMPASS_USE_MAINNET was added to Vercel prod env, the
deploy failed with:

  useWallets was called outside the PrivyProvider component
  TypeError: Cannot read properties of undefined (reading 'current')
  Error occurred prerendering page "/onboard"
  ⨯ Next.js build worker exited with code: 1

Root cause: the mounted-flag pattern in PrivyClientProvider (commit
a332736) keeps SSR working for non-Privy pages but means PrivyProvider
is NOT mounted at prerender time. /onboard's wallet step calls
useWallets() during prerender → throws.

This wasn't surfaced before today because the prior Privy fix swallowed
all children on SSR, so /onboard prerendered as empty (also broken,
but silently).

Fix: opt /onboard out of static prerender via a route-segment layout
that exports dynamic = "force-dynamic". At request time, the client
hydrates → mounted-flag flips → Privy mounts → useWallets works.

File: app/src/app/onboard/layout.tsx (new, 8 lines)
…oard

After commit e796f59 (force-dynamic on /onboard layout), the build
succeeded but runtime SSR still failed because useWallets() / usePrivy()
fire during the request-time SSR pass — force-dynamic only defers
the prerender, it does NOT skip SSR.

The mounted-flag pattern in PrivyClientProvider (commit a332736) keeps
SSR working for non-Privy pages but means PrivyProvider is never
mounted server-side, ever. Any component that calls a Privy hook
during the SSR pass throws "called outside the PrivyProvider component."

Fix: wrap the three wallet-using buttons in next/dynamic with ssr:false.
At SSR time the dynamic shims render null. At hydration time the
Privy chunk loads, PrivyProvider mounts, the buttons swap in, and
useWallets resolves cleanly.

- PrivyConnectButton
- MintAgentButton
- RequestEligibilityButton

IssueCredentialButton is NOT wrapped because it doesn't use Privy
hooks (server-side issuance via /api/issue; no wallet call).

File: app/src/app/onboard/page.tsx
The recording script referenced state that drifted across today's
round-4 audit + mainnet-flag flip. Six surgical updates:

Beat 1 — Hero
- Note the BlurText first-word LCP fix (commit 62dbac1): first word
  ("Prove") renders instantly without the stagger; only words 2+
  animate in. Visual is still a stagger, but the LCP-eligible glyph
  is paint-complete on frame one.
- Document the actual CTA row visible on prod (7 CTAs + ROADMAP)
  including GUIDED DEMO, VERIFY A RECEIPT, FAQ which weren't in
  scope when the script was written.

Beat 3 — Compass moment
- Replace "set NEXT_PUBLIC_COMPASS_USE_MAINNET=1 before recording"
  with "verified set on Vercel prod as of 2026-05-11; no flag-flip
  needed." Production alias defaults to mainnet now.
- Add note that the network name + faucet affordance derive from
  activeChain() at runtime (commit 3e7038d), not hardcoded Galileo.
  Mainnet now shows "Acquire OG for 0G Aristotle ->" linking to
  the funding-options doc; testnet still shows the faucet button.

Beat 4 — Architecture
- Note the /about reality table flipped the Aristotle row from
  "draft / scaffolded" to "real" with both deployed addresses
  visible (commit 743f8d0). The table is now a credible visual
  pair-up with the Beat 3 mint.

Recording checklist
- Mainnet-flag check: rewritten to reflect "verified set" state +
  cold-incognito sanity check that /about shows the Aristotle row
  as "real" not "draft."
- Add /verify to the route smoke-check list.
- Add the __next_error__-in-HTML check (today's wave-7 fix for the
  dynamic wallet-button SSR break, commit 0689fa9). If /onboard's
  HTML has __next_error__ id, the SSR pass is failing — abort and
  diagnose.

Cuts to make in editing
- Add "the Privy chunk-load spinner during initial /onboard mount"
  to the cuts list. With the new mounted-flag pattern, the first
  frame may show fallback state for ~100ms; jump-cut past it.

Open uncertainties
- Decide: /verify does NOT get its own beat — overflows 3:00.
  Positioned in the F.5 X post (Beat 4 of optional thread
  continuation) and in the README. Lets engineering-credibility
  load sit on written copy.

The 6-beat structure + timing + voiceover lines are unchanged. The
script remains locked at 2:55 runtime under the 3:00 HackQuest cap.
Live /onboard renders STEP_ORDER = [connect, mint, issue] (3
numbered steps) plus a separate Request-HELP-eligibility CTA below
the 3-step walkthrough at page.tsx:373. The script previously
called it 'four-step walkthrough' which mismatches the page's
progress label '1 OF 3'.

Rewrote Beat 3 to spell out the actual flow:
- 3 numbered steps (connect / mint / issue)
- 1 final CTA below (Request HELP eligibility) — that's the mint
- Semantic flow unchanged, layout naming corrected

Saves the recording from a confused 'find the 4th step that
doesn't exist' beat.
…ns 64

Every /api/consume POST since commit f3dc918 (Wave-2 OWASP hardening)
returned 503 with "enclave signature missing or not 65-byte 0x-hex"
because my regex required 130 hex chars (Ethereum-style r||s||v) but
the enclave's signReceipt uses @noble/curves v2 secp256k1.sign() which
returns 64-byte compact (r||s, no recovery byte) = 128 hex chars.

The system-wide invariant is 64-byte compact: verifyReceipt.ts already
has `sigBytes.length === 64` pre-check + reconstructs recovery by
trying both v=0 and v=1 during ECDSA-recover (browser + CLI). The
server-side regex was the only place demanding 65 bytes, and it never
should have been — defense-in-depth on the wrong invariant rejects
every legitimate response.

Tonight's recording was blocked on this. Vercel function logs showed
the identical error message on every Request-HELP-eligibility click:
  "[/api/consume] enclave call failed — will fail closed (503
  tee_required) Error: enclave signature missing or not 65-byte 0x-hex"

Fix: regex `^0x[0-9a-fA-F]{130}$` -> `^0x[0-9a-fA-F]{128}$`. Updated
the surrounding comment to spell out the noble/curves v2 compact
signature shape so this doesn't get re-tightened by a future
audit pass.

The Phala CVM, dstack getQuote(), and TDX attestation were ALL working
the whole time — the failure was purely server-side validation.

File: app/src/lib/compassEnclave.ts:188
…k omits it

After commit 0cb941e fixed the 65-byte signature regex, /api/consume
hit the NEXT downstream validation:

  Error: enclave perReceiptQuoteHex present but malformed:
  040002008100000000000000939a7233f79c4ca9...

The value IS a valid TDX v4 quote (byte 0 = 0x04 = TDX v4 version
field), just emitted as raw hex without the `0x` prefix.
`@phala/dstack-sdk` `client.getQuote()` returns the raw quote bytes
as a hex string; the enclave's `requestPerReceiptQuote` passes that
through unchanged via `fresh.quoteHex`. My regex demanded `^0x[0-9a-fA-F]+$`,
rejecting every legitimate quote.

Same root cause as the signature regex bug (commit 0cb941e) — Wave-2
OWASP hardening (f3dc918) wrote validation against assumed Ethereum
formatting conventions instead of the actual shape the enclave/SDK
return. Defense-in-depth on the wrong invariant rejects valid data.

Fix: normalize the prefix client-side before regex validation. Accept
the raw hex from the SDK, prefix with `0x` if missing, then the
downstream `0x${string}` typed value stays uniform.

This is the second of two regex bugs introduced in f3dc918. Should
unblock /api/consume end-to-end — Phala CVM was healthy the whole
time; the breakage was 100% server-side validation against the wrong
invariant.

File: app/src/lib/compassEnclave.ts
…nter

User feedback (2026-05-11 21:55, post-recording-readiness): the
Step 01 Connect-wallet pill shows the truncated address (e.g.,
0xa086…3078) but provides no way to copy or reveal the full 42-char
address. No hover affordance, no click action, no cursor change.

Add:
- Click → copy full address to clipboard via navigator.clipboard.writeText
- Brief "✓ copied" feedback for 1.5s after copy
- Hover → native title tooltip shows the full address
- Hover → border + text brighten (subtle, matches Cinematic Privacy)
- Cursor: pointer to signal interactivity

Extracted into a `WalletPill` sub-component so the connected-state
branch in PrivyConnectButton stays a one-liner. Visual frame
composition is unchanged — same pill shape, same colors, same font;
only the interaction layer is added. Recording captured in this
state will render identically to the prior <span>.
User found ~2-3s of dead air at the end of Beat 4 between the meta-
joke 'Three sentences.' and the visual hard-cut to /clinic/subpoena
(Beat 5). Adds a 4-word Socratic bridge:

  'Now imagine the subpoena.'

~2.5s spoken length at the script's quiet flat cadence, drops on
'subpoena' for cinematic emphasis. The word 'imagine' verbally cues
the audience's expectation right before the visual reveal of the
PDPO §57 disclosure log on /clinic/subpoena.

Matches the script's fragment-pattern at beat transitions:
  Beat 2 ends 'Fourteen days. Deportation.'
  Beat 4 ends '...Three sentences. Now imagine the subpoena.'
  Beat 5 ends 'That's all that exists.'

Updated total Beat 4 voiceover length ~17s (still well under the
48s beat envelope; visual scroll continues to do the work for the
remaining ~31s of silent-camera time).
…s, faster viem poll, return 200 on tx-pending

User got 8 consecutive 504s during demo recording, each retry submitting
a NEW tx with a new nonce (8 different tx hashes in 90s, all returning
"Transaction receipt with hash 0x... could not be found"). Same code
worked earlier today when Aristotle confirmed within Vercel's 10s
function ceiling; tonight's congestion pushes confirmation past the
window every time.

Three fixes, one commit:

1. `export const maxDuration = 60` — Vercel Hobby allows up to 60s via
   this directive (default is 10s). Buys 50 extra seconds of
   confirmation budget. Aristotle blocks are ~2-3s so 55s = ~20+
   block confirmations of head-room.

2. viem `waitForTransactionReceipt` now passes `timeout: 55_000,
   pollingInterval: 1_500`. Default polling is 4s which adds latency
   for no reason; 1.5s catches confirmation within a single Aristotle
   block. 55s upper bound stays under our 60s maxDuration so the
   function returns cleanly before Vercel kills it.

3. On the (now-rare) timeout, return HTTP 200 with `pending: true,
   txHash, ...full canonical fields` instead of HTTP 504. Client treats
   the response as success (existing branch only checks `res.ok`); UI
   shows the green ✓ receipt minted pill with the tx hash. timestampBucket
   is computed deterministically from issuedAt (`floor(issuedAt / 900) *
   900`) so the 15-min bucket pill renders even pre-confirmation. The
   tx IS on Aristotle; the client just gets the hash before the chain
   has finalized it. User can verify on chainscan a few seconds later.

The downstream effect for retries: previous behavior was "504 -> retry
button -> new tx with next nonce." Now first call returns 200 with the
tx hash; no retry button is offered; no nonce bump. Even if the tx
takes 30s to confirm on chain, the user sees their hash within Vercel's
function ceiling.

This is the proper fix for serverless + on-chain mismatch. v0.6 should
decouple submit from confirmation entirely (fire-and-forget + client-
side polling), but that's a bigger refactor.

Files:
- app/src/app/api/consume/route.ts (maxDuration export, timeout
  + pollingInterval on waitForTransactionReceipt, 200 fallback path)
…/etc), not undefined names

Build failed on previous push because the pending-tx fallback path
referenced 'issuedAt', 'expiry', 'POLICY_ID', and 'enclaveResp' — none
of which exist in /api/consume's handler scope.

The actual local names in scope at that point are:
- nowSec (Math.floor(Date.now()/1000)) instead of issuedAt
- receiptExpiry instead of expiry
- body.grant.policyId instead of POLICY_ID
- attestationDigest (already in scope) instead of enclaveResp.attestationDigest
- body.grant.nullifier for nullifier
- teeSource / teeSignerAddress / perReceiptQuoteHex / teeReceiptVersion
  / teeError for the tee object

Response shape now mirrors the success path exactly, plus pending:true.
Local tsc + Vercel build both pass.
…aces

YouTube unlisted: https://www.youtube.com/watch?v=vg5WZHmlzZI
Runtime: 2:52, 1080p H.264, -14 LUFS YouTube spec.

Updates:
- README.md hero block: placeholder -> live URL + one-liner
- Demo/x-post-final.md: both [DEMO_VIDEO_URL] placeholders filled
- docs/distribution/dorahacks-listing.md: demo video filled
- docs/distribution/devpost-listing.md: demo video filled
- CHANGELOG.md: seventh-wave entry with attribution
- Vercel prod env NEXT_PUBLIC_COMPASS_DEMO_VIDEO_URL set
  (separate deploy app-m4py9g0pg activates the sticky DemoCta primitive)

Gated on user action:
- F.5 X post (template ready in Demo/x-post-final.md)
- HackQuest submission with the demo URL
README hero block links Demo/edit-recipe.md but the file didn't
exist. Adds the 7-stage ffmpeg pipeline that produced
compass-demo-final.mp4 (vg5WZHmlzZI). Includes:

- Stage 1: blackbox cleanup + lead/trail trim
- Stage 2: hands-sink Ken Burns (13.4s, zoom 1.00 -> 1.08)
- Stage 3: composite Beat 2 over cleaned video
- Stage 4: Beat 6 title card via PIL + ffmpeg loop (since this
  Homebrew ffmpeg build lacks drawtext)
- Stage 5: concat composed + Beat 6 = 2:52 master
- Stage 6: loudnorm to -14 LUFS YouTube spec
- Stage 7: upload + propagation surfaces

Useful for v0.6 re-cut once the onboard UX migrates from 3 steps
to a different shape.
Adds Form 3 section to Demo/hackquest-submission-answers.md covering the
full Project Archive form (Overview / Checkpoints / Team tabs) the user
hit after the Google form submission. Maps every field across the 9
screens to a copy-pasteable value: intro 195/200, 3 sector chips, 8 tech
tags (5 stock + 3 Add-New), MVP/GitHub/X links, 4 image sources,
YouTube demo+pitch URLs, full Description + Progress rich-text blocks,
Fundraising statement, Deployment Details with all mainnet + Galileo
addresses + 3 registered policy IDs, 6 checkpoint phases A-F, and a
solo Team Intro. One-pass fill order avoids back-tracking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compass formally filed via HackQuest Submit Project form on 2026-05-12.
All 4 submission legs now checked off (Google Form + Project Archive
+ Submit Project + X post). Eighth-wave CHANGELOG entry captures the
load-bearing references (contract address, X post URL with all 6
mandatory tags/hashtags, GitHub link) so a future re-submission or
audit can reproduce the answers verbatim without re-deriving them
from chat history. Gated next-actions (confirmation screenshot, X
cadence 7-10/10, 48hr reply window, Phala restart 24hr before judge
demo) listed explicitly to prevent post-submission drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…IRL pitch

Closes the Pillar 3 (opening stat hook) and Pillar 5 (closing business
numbers) + Pitch Formula step 6 (lead-with-business) gaps surfaced by
the post-submission methodology audit.

Whitepaper §Business Impact added: TAM (368K HK FDHs + 27.2M APAC
migrant workers + US$1.087B HK remittance outflow + 17%/60% exploitation
rates), cost per incident (US$22.2K to worker + US$14.1K to HK gov),
sustainability (open-core + managed-hosted tier + 12-month grant ladder
Phala/0G/EF PSE → OSF/Luminate/HKJC). All 9 primary-source URLs (HK
LegCo, ILO, World Bank, HK Labour Dept, HK LAD, Justice Centre, Amnesty,
0G, Phala) footnoted.

FDH population corrected from rough "~340,000" to LegCo-sourced 368,000
(end-2024). README §Why this matters rewritten to lead with hard
numbers. Demo/script-irl.md added as the BEYOND Expo Macau IRL pitch
that opens on the three business numbers before introducing Maria as
proof (mentor input from Aaron + Henry per Sookra methodology distilled
notes). HackQuest Description paste block updated with stat-opener +
Business Impact subsection for re-paste into the Project Archive
rich-text field (still editable post-submission). Whitepaper PDF
regenerated via md-to-pdf skill (Letter, 0.5in margins, 260KB).

Demo Beat 6 title card stays at "15M migrant workers across APAC" —
rendered video locked at vg5WZHmlzZI; 27.2M ILO figure cited in every
textual surface instead. v0.6 demo re-cut will align the title card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ethodology numbers

Closes the doc-sync gap from the ninth wave. Locks all repo surfaces
to the same defensible primary-source numbers (368K HK FDHs from
LegCo ISSH02/2025; 27.2M APAC migrant workers from ILO 2024; cost-per-
incident US$22.2K worker + US$14.1K HK gov; sustainability grant
ladder).

### Changed

- **`docs/threat-model.md` §1h** — small-population correlation
  threat: "~370k FDH population" → "368,000 FDH population (HK LegCo
  Research Office ISSH02/2025)".
- **`docs/press-kit.md` §Long description** — opens with the hard
  stats (368K HK FDHs · 17% forced labour · 60% deportation-fear
  deterrence · 27.2M APAC migrant workers) instead of the rough
  "~5% of the population" framing.
- **`docs/press-kit.md`** — new §Business Impact section between
  What's built and Founder quote, mirroring the whitepaper section
  (TAM + cost per incident + sustainability + grant ladder).
- **`docs/press-kit.pdf`** — regenerated (155 KB).
- **GitHub repo description (via gh repo edit)** — "Compass, a
  private eligibility firewall on 0G. Prove eligibility, not
  identity." → "Compass — private eligibility firewall on 0G. Built
  for 368K HK migrant workers + 27M across APAC. Prove eligibility,
  not identity. Live on Aristotle mainnet."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the hard primary-source numbers (368K HK FDHs · 27.2M APAC migrant
workers · US$22K worker cost · US$14K HK gov cost) to every website
surface judges + crawlers see first. Closes Pillar 3 (opening stat
hook) and Pillar 5 (closing business numbers) at the web level after
the post-submission methodology audit.

### Added

- **`/` (homepage hero)** — mono tracking-wider stats strip between
  the Maria paragraph and the CTAs: "368,000 AT RISK · $22K LOST PER
  WORKER · $14K LOST PER GOV CASE". Matches existing design system
  (mono uppercase, muted-foreground with foreground-highlighted
  numerals). No layout regression.
- **`/about` → new "By the numbers" Section** — first content block
  after the lead paragraph and before the Architecture diagram. Three
  Stat cards (368K · ≈US$22,200 · ≈US$14,100) with per-stat footnote
  citations (LegCo 2025 · HK Labour Dept 2025 + Amnesty 2013 · HK LAD
  FY24/25 + Budget Head 70). Trailing paragraph adds APAC 27.2M + 17%
  forced-labour + 60% deterrence + LAD HK$679.6M total spend with a
  link to the whitepaper Business Impact section.
- **`/faq` → new entry in FAQ_HUMAN** — "What's the business case —
  TAM, cost per incident, sustainability?" inserted as the third
  question, between "Is this real, or a demo?" and the coercion
  question. Four-paragraph answer covering TAM, cost per incident
  (worker + HK gov), sustainability model, and whitepaper link.
- **`/demo` (guided tour)** — inline mono stats strip below the lead
  paragraph: "368,000 HK FDHs at risk · 27.2M APAC migrant workers ·
  $22K lost per deportation".
- **`Demo/assets/hackquest/`** — 5 PNGs (4 captured screenshots +
  1024×1024 brand-mark avatar) that the HackQuest Project Archive
  Images and avatar fields were populated with during F.4.
- **`.gitignore`** — added `Demo/build/` (ffmpeg intermediates, 70 MB,
  regenerable via `Demo/edit-recipe.md`), `Demo/compass-demo-final.mp4`
  (final rendered video, 15 MB, lives on YouTube), and
  `docs/deployments/og_*.json` (deployment artifacts).

### Changed

- **`app/src/app/opengraph-image.tsx` (root)** — subtitle expanded
  with 368K HK + 27M APAC framing; footer changed from "Aristotle
  mainnet · live" to "368K at risk · Aristotle mainnet · live".
  Affects social-share preview for every page that falls back to
  root OG (Twitter/X, LinkedIn, Slack, Discord embeds).
- **`app/src/app/about/opengraph-image.tsx`** — subtitle now includes
  "Built for 368K HK migrant workers + 27M across APAC. $22K/$14K
  cost per incident."; footer changed to "/about · TAM + cost-per-
  incident inside".
- **`app/src/app/layout.tsx` → root metadata.description** — leads
  with "Built for 368,000 HK migrant workers and 27M across APAC."
  Lands in Google SERP + every Twitter/X / LinkedIn / Slack /
  Discord link unfurl that doesn't override per-route.
- **`app/src/app/manifest.ts` → PWA description** — same business-
  led copy as layout.tsx. Renders in the install banner on iOS /
  Android Chrome and in browser bookmark dialogs.
- **`docs/distribution/devpost-listing.md` → §The asymmetry** —
  "~5% of the population" replaced with 368K + 9.6% workforce-share
  + 27.2M APAC + 17%/60% rates. New §Why this matters in dollars
  subsection added between the asymmetry and the Compass-changes
  sections, locking in the cost-per-incident framing for the
  post-June-5 cross-listing window.
- **`docs/distribution/dorahacks-listing.md` → short description** —
  140-char hook now leads with "368K HK migrant workers prove
  eligibility…" instead of the generic "vulnerable worker" framing.

### Skipped (intentional)

- Per-route opengraph-image.tsx for /demo, /verify, /faq, /onboard,
  /audit, /clinic/subpoena, /roadmap — each already has a tight
  route-specific hook that would be diluted by stuffing in stats.
  Root + /about cover the discoverability surface.
- /clinic/subpoena page body — intentionally minimal ("That's all
  that exists"). Adding stats would dilute the wow moment.
- /onboard + /kiosk hero copy — working-flow pages; copy changes
  carry regression risk (multilang in /kiosk; long flow in
  /onboard). Judges interact, not read.
- /audit, /verify, /receipt — narrative-light technical surfaces.
  No methodology-gap to close there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final stat-sync — `<Term k="fdh">` tooltip definition in glossary.ts
was the last remaining "~340,000 / roughly 5%" reference in the
codebase. Drives every FDH term tooltip on /clinic/subpoena, /faq,
/about, /onboard. Now reads "368,000 visa holders (HK LegCo Research
Office, end-2024), 9.6% of the local workforce" — matching the
whitepaper, README, press kit, distribution drafts, and HackQuest
Description.

CHANGELOG tenth-wave entry catalogs everything in the website
methodology lockstep (homepage stats strip, /about By-the-numbers
Section, /faq new TAM Q, /demo stats strip, root + /about OG cards,
layout.tsx meta, manifest.ts PWA, distribution drafts, glossary
fdh) plus the intentional skip list with reasons (per-route OGs,
subpoena scene, working flows, technical surfaces, video re-render,
ecosystem skill).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant