Skip to content

INTERFACE_CONTRACT

Jacob S Vickers edited this page Aug 6, 2026 · 6 revisions

Integrity Protocol — Interface Contract (v1)

This document is the single source of truth for how INTEGRITY-LATEST packages and internal services talk to each other. Every package is being rebuilt from scratch in parallel by a different engineer (human or agent) — if something isn't pinned down here, packages will drift and stop interoperating. When in doubt, follow this doc over any assumption from the old codebase.

Repo documentation precedence is:

  1. README.md defines repo-level ownership, current package status, and source-of-truth pointers.
  2. This file defines internal schemas, ports, env vars, service boundaries, and cross-package call conventions inside INTEGRITY-LATEST.
  3. spec/ defines externally-supported design and wire surfaces.
  4. docs/wiki/ is the compiled long-term knowledge layer generated out to the GitHub Wiki and Integrity MVP browser wiki.

Scope: this rewrite covers six protocol core packages: contracts/, integrity-zkp/, integrity-oracle/, integrity-sdk/, integrity-cli/, bcc_middleware/ — plus integrity-dashboard/, the original INTEGRITY-LATEST dashboard/landing app (its demo/ subdirectory is the multi-vertical investor/developer closed-loop scenario engine, see §11 — merged from the former separate integrity-dashboard/ + integrity-demo/ packages on 2026-07-09), integrity-userapi/, a dedicated user-accounts/auth backend kept strictly separate from the oracle (see §13), and integrity-framework/, a reputation-derivatives package reviving the old repo's marketplace/lending concept (see §12, not yet built). Everything else from the old repo (marketing site, unrelated scaffolding, legacy backups, stray installer scripts) is intentionally left out.

External repositories are deliberately outside this interface contract. integrity-mvp is the standalone web presentation layer, and xibalba-shield is the endpoint-security evidence producer. They consume public INTEGRITY-LATEST interfaces; INTEGRITY-LATEST must not import, call, or require either external repo. The cross-repository dependency map is docs/architecture/ecosystem-dependencies.md and docs/wiki/architecture/ecosystem-dependencies.md.

Ground rule for this rewrite: no silent mocks. Every previously-stubbed piece (ZK proving, TEE attestation, OPA policy evaluation, on-chain BAA checks, Merkle anchoring) must be a real, working implementation, tested against the real toolchain below — not a hardcoded return value. If something truly cannot be made real in this environment (e.g. real AWS Nitro hardware attestation, which requires physical/cloud enclave hardware we don't have), implement the real verification logic against the real wire format, use a real published test vector as a fixture, and say so explicitly in a comment and in that package's README — don't fake the check silently.

1. Toolchain (confirmed installed in this dev environment)

Tool Version Used by
forge / anvil (Foundry) 1.7.1 contracts
cargo / rustc 1.96.0 integrity-oracle
nargo (Noir) 1.0.0-beta.22 integrity-zkp, integrity-oracle circuits
bb (Barretenberg) 5.0.0-nightly integrity-zkp, integrity-oracle (proof gen/verify)
opa 1.18.2 bcc_middleware, integrity-sdk
node / npm 22.x / 10.x integrity-dashboard, contracts (npm-based deps)
python / uv 3.12 / 0.11 integrity-sdk, integrity-cli, bcc_middleware

All of these are on PATH (added to ~/.bashrc). Use them for real — compile the circuits, run bb prove/bb verify, run forge test, run opa eval against real policies. Don't write code you haven't run.

2. Ports & local endpoints (dev/docker-compose defaults)

Service Port Package
Postgres 5432 integrity-oracle
Redis 6379 integrity-oracle
Anvil (local EVM chain) 8545 contracts
OPA server 8181 bcc_middleware
BCC Middleware (FastAPI) 8000 bcc_middleware
Integrity Oracle backend (Axum) 8080 integrity-oracle
Integrity Oracle OTLP/gRPC receiver 4317 integrity-oracle
Integrity User API (FastAPI) 8090 integrity-userapi
Postgres (userapi) 5435 integrity-userapi
Integrity Dashboard (Vite dev) 5173 integrity-dashboard

3. Environment variables (shared names — use exactly these)

  • DATABASE_URL — Postgres connection string (oracle). Must point at a timescale/timescaledb instance, not plain postgres — migration 0004 runs CREATE EXTENSION timescaledb (see docker-compose.yml's postgres service and §1's OTLP receiver below).
  • REDIS_URL — Redis connection string (oracle)
  • OTLP_GRPC_ADDR — bind address for the oracle's real OTLP/gRPC receiver (integrity-oracle/backend/src/otlp.rs), defaults to 0.0.0.0:4317 — the standard OTLP/gRPC port integrity-sdk's OTLPSpanExporter/OTLPMetricExporter already target by default. A second listener, separate from the oracle's HTTP BIND_ADDR.
  • KYC_PROVIDER_KEYS — comma-separated trusted KYC receipt issuers in provider=hex-ed25519-public-key form. Empty disables KYC receipt acceptance. Keys belong to independently operated commercial or self-hosted open-source verification stacks; clients never supply trust roots in requests.
  • RPC_URL — EVM RPC endpoint, defaults to http://localhost:8545 (anvil) for local dev
  • CHAIN_ID31337 for local anvil
  • OPA_URLhttp://localhost:8181 (bcc_middleware, sdk)
  • BCC_MIDDLEWARE_URLhttp://localhost:8000 (sdk, cli)
  • ORACLE_URLhttp://localhost:8080 (sdk, cli, bcc_middleware)
  • USERAPI_URLhttp://localhost:8090 (dashboard; integrity-userapi's own outbound calls to ORACLE_URL for agent data, never the reverse — see §6.10 on the backend split)
  • RESOLVER_ADDRESS / RESOLVER_PRIVATE_KEY — the demo/market resolver signer (see §6.9's RESOLVER_ROLE trust boundary). Defaults to the deployer/funder for a single-operator testnet deployment, same posture as ORACLE_SIGNER_ADDRESS etc below.
  • DEPLOYMENTS_FILE — path to deployments.local.json (see §6.6), defaults to repo root
  • BASE_SEPOLIA_RPC_URL — RPC endpoint for the base_sepolia entry in contracts/foundry.toml's [rpc_endpoints] (already configured there, referenced as ${BASE_SEPOLIA_RPC_URL}). Needed by anything deploying or reading from the Base Sepolia testnet deployment rather than local anvil. The corresponding deployments file for that network is deployments.baseSepolia.json (also whitelisted in foundry.toml's fs_permissions, same shape as §6.6).
  • Signing keys are dev-only, read from .env files that are .gitignored in every package. Never commit a populated .env. Only commit .env.example with placeholder values. contracts/.env.example does not exist yet as of this revision — the keys below describe the intended Base Sepolia deploy flow per the self-sovereign model (§6), not a file that's already checked in:
    • DEPLOYER_PRIVATE_KEY — deploys the protocol singletons and cloneTemplates (§6.6), once per network.
    • ORACLE_SIGNER_PRIVATE_KEY — the oracleSigner address wired into every agent's ReputationRegistry/StateAnchor at registration (§6.1, §6.3 step 3).
    • GOVERNANCE_PRIVATE_KEY — the governance address that is every agent's Slasher arbiter (§6.2) — never the agent's own key.
    • DISPUTER_PRIVATE_KEY — the protocol's disputer signer for Slasher.raiseDispute (§6.2). At runtime, this role is actually held by bcc_middleware's REPUTATION_SIGNER_PRIVATE_KEY (falls back to ANCHOR_SIGNER_PRIVATE_KEY) — see §7a — not a separate standalone process; this entry describes the on-chain role granted at deploy time, not a second service.
    • FUNDER_PRIVATE_KEY — funds new agent wallets with enough native gas to self-deploy their SovereignAgent/StateAnchor and call registerPrimitives (§6.3), since under the self-sovereign model no shared factory pays those gas costs on the agent's behalf. Corresponds to protocolAddresses.funderWallet in the deployments file (§6.6).
    • Per-agent controller keys are not protocol env vars at all — they're generated/held client-side by whatever created the agent (integrity-sdk, integrity-cli), since the whole point of the self-sovereign model is that the protocol never custodies them.

4. Core data contracts

4.1 DID Document (produced/consumed by integrity-sdk, integrity-oracle)

{
  "id": "did:integrity:<hex-pubkey-fingerprint>",
  "controller": "did:integrity:<hex-pubkey-fingerprint>",
  "created": "<ISO8601>",
  "verificationMethod": [{
    "id": "did:integrity:<fingerprint>#key-1",
    "type": "Ed25519VerificationKey2020",
    "publicKeyMultibase": "<base58/multibase-encoded pubkey>"
  }]
}

Real Ed25519 only (via the cryptography library) — no HMAC pseudo-signature fallback.

4.2 BCC Commitment (the "Behavioral Commitment Chain" intent-lock object)

{
  "agent_id": "did:integrity:...",
  "intent_type": "string, e.g. 'payment' | 'data_access' | 'contract_call'",
  "intended_state_hash": "0x<32-byte hex, sha256 of the canonical intent payload>",
  "nonce": "monotonic per-agent integer",
  "timestamp": "<unix ms>",
  "agent_public_key": "z<multibase base58btc, multicodec ed25519-pub || raw 32-byte pubkey>",
  "covered_entity_address": "0x<20-byte hex EVM address> | null",
  "intent_rationale": "public-safe intent rationale, signed and preferred by policy",
  "signature": "0x<hex, Ed25519 sig over the above fields except signature itself, canonical JSON>"
}

This exact shape is POSTed by integrity-sdk and integrity-cli to bcc_middleware's POST /v1/bcc/intercept. Field names are load-bearing — don't rename them per-package.

Three fields were added after this doc's original draft, all now ✅ RECONCILED and required by bcc_middleware's real implementation (app/schemas.py, app/canonical.py) — not carried in isolation, but included in the signed payload, so neither can be swapped post-signature:

  • agent_public_keyrequired. integrity-sdk's DID fingerprint is sha256(pubkey), not the raw public key, so a verifier holding only agent_id cannot recover the key needed to check signature. The agent therefore carries its own public key here, same multibase form as the DID document's publicKeyMultibase (§4.1: "z" + base58btc(0xed 0x01 || raw_pubkey)). The receiving service must bind it before trusting it: sha256(decoded_pubkey) == agent_id's fingerprint, or reject — this is what makes trusting a carried key safe (a substituted key can't also produce a sha256 preimage collision on the victim's fingerprint).

  • covered_entity_addressoptional; null/omitted for non-healthcare intent types. contracts' real SmartBAAFactory.isBAAActive takes two addresses, coveredEntity and businessAssociate (the agent), not one — this field names which covered entity (hospital) a healthcare-vertical commitment (EMR_WRITE, DISPENSE_MEDICATION, BILLING_SUBMISSION, SECURE_EMR_WRITE, CLINICAL_DATA_ACCESS) is claiming access against. Any commitment whose intent_type causes OPA to set requires_baa := true MUST carry it, or the on-chain BAA check fails closed with BAA_CANNOT_VERIFY regardless of the agent's actual BAA status. Deliberately an address, not a DID: covered entities are registered directly by EVM address in contracts/src/health/CoveredEntityRegistry.sol and have no DID layer of their own.

  • intent_rationalerequired for agent tool calls. This is the public-safe, signed intent explanation that bcc_middleware's AOS policy gates on. The legacy agent_thought field is still accepted as an alias for compatibility, but new callers should populate intent_rationale and let agent_thought mirror it only if they need backwards compatibility.

Canonicalization, pinned: the signature covers every field above except signature itself, serialized as json.dumps(fields, sort_keys=True, separators=(",", ":"), ensure_ascii=True) (UTF-8 bytes). ensure_ascii=True specifically — not the RFC 8785/JCS default — because it's the byte-for-byte rule integrity-sdk, integrity-cli, and bcc_middleware all independently implement today; a mismatch here silently breaks every signature on non-ASCII content. (integrity-oracle's Rust-side serde_json does not escape non-ASCII by default and does not yet participate in this signature scheme — see PRODUCTION_GAPS.md for that gap if it ever needs to.)

4.3 Agent Integrity Score (AIS)

Formula (from the product spec, keep as-is): AIS = (S_entropy^wE * S_grounding^wG * S_sacrifice^wS * S_compliance^wC) * ZK_boost

Default weights (must sum to 1.0, make them configurable but ship this default): wE = 0.30, wG = 0.30, wS = 0.20, wC = 0.20. ZK_boost is 1.15 when a real Barretenberg proof was verified for the reporting period, else 1.0. This formula lives in integrity-oracle/scoring-core and is the only place it's computed — other packages call the oracle's /v1/agent/{id}/ais endpoint rather than recompute it.

Input-signal trust: the four S_* inputs (performance_variance, hgi_raw, gpu_hours_verified, penalty_ratio) are not taken from a client's self-reported derived_signals in POST /v1/telemetry/ingest. The oracle independently recomputes entropy/grounding/sacrifice server-side from the same request's otel_spans content (integrity-oracle/backend/src/derive.rs, mirroring integrity_sdk/telemetry/derive.py's algorithms), and does the on-chain ComplianceGate "wins" check itself rather than trusting an SDK-side opt-in. A client's signature proves who sent a request; it was never proof the claimed numbers were honest, and this is the layer that closes that gap. derived_signals is still part of the signed envelope (so old clients don't break) and is still stored, but purely as an audit trail (telemetry_events.payload.derived_signals vs. payload.oracle_recomputed_signals) — it does not feed the formula. See docs/wiki/concepts/ais.md for the full data-flow diagram and PRODUCTION_GAPS.md §1a for what's still open (ZK-boost is a period-wide, not per-event, binding). The oracle-to-chain score push is implemented by bcc_middleware §7a.

4.2a Signed telemetry envelope version

The signed object of POST /v1/telemetry/ingest carries schema_version, an integer inside the signature:

schema_version = 1          # integrity_sdk.client.TELEMETRY_SCHEMA_VERSION
                            # backend::handlers::MAX_TELEMETRY_SCHEMA_VERSION

Both constants must move together. Rules, all load-bearing:

  • Inside the signed object, not beside it. A version outside the signature could be rewritten in transit to make the oracle reinterpret a payload under different rules.
  • An absent schema_version is the pre-versioning envelope and stays valid forever. Signed payloads are evidence; old evidence must remain verifiable. The oracle therefore rebuilds the signable bytes without the key when a request omits it — serializing it as null would change the canonical JSON and reject every historical signature.
  • A version above MAX_TELEMETRY_SCHEMA_VERSION is refused (400), not parsed. Misreading a future shape and storing it as signed evidence is worse than rejecting it.
  • Bumping the version is therefore a coordinated change: raise the SDK constant, raise the oracle's maximum, and deploy the oracle first so it can accept the new shape before any agent emits it.

Covered by oracle_e2e_telemetry_schema_version_is_signed_and_backward_compatible, which asserts all four: legacy accepted, v1 accepted, unknown refused, and an injected version failing verification.

4.4 Merkle tree convention (must match between integrity-oracle and contracts)

  • Hash function: keccak256 (not SHA-256) — this tree's root gets verified on-chain in StateAnchor.sol, and keccak256 is native/cheap in the EVM.
  • Leaf hashing: keccak256(abi.encodePacked(leafData)).
  • Parent hashing: sort the pair of child hashes ascending before concatenating (keccak256(a < b ? a,b : b,a)) — the standard OpenZeppelin MerkleProof convention. This avoids second-preimage ordering ambiguity and lets contracts use OZ's MerkleProof.verify directly instead of a custom verifier.

⚠ UNPINNED: odd-width levels. The three rules above are everything this section has ever specified. It says nothing about a level with an odd number of nodes, and the three implementations filled that silence differently: integrity-oracle/backend/src/merkle.rs and bcc_middleware/app/merkle.py duplicate the odd node; integrity_sdk/vault.py promotes it unchanged. All three are compliant with this section as written, and all three claim to match the others "bit-for-bit" — an unfalsifiable claim until conformance vectors exist. Nothing is broken today only because each component builds, anchors and verifies within its own tree; the first cross-system verification breaks. Duplication also makes [A,B,C] and [A,B,C,C] produce the identical root, which contradicts StateAnchor.sol's own NatSpec claim that the root is "a true function of the set of leaves." Recorded as E9 in docs/design/e2e-audit-2026-07-31.md; proposed amendment + migration in docs/design/merkle-standardization.md. Do not add a fourth implementation until this is resolved.

4.4a Genesis memory root (spec v0.3 §4.1/§7.2) — cross-package constant

Registration requires StateAnchor.latestRoot != bytes32(0) (§6), but an empty-but-initialized Trust Vault is valid at birth — and anchorRoot reverts on bytes32(0) (EmptyRoot). An agent registering with nothing yet in its vault therefore needs a defined, non-zero root meaning "initialized, empty":

GENESIS_VAULT_ROOT = keccak256("integrity.trust-vault.genesis.v1")
                   = 0x… (computed identically in every package — never hardcode a literal)

Every package MUST derive it by hashing that exact ASCII string, not by copying a hex literal — the same discipline §4.4's hashing rules exist to enforce, and the reason this constant lives here rather than in registration.py. Defined in: integrity_sdk/chain.py::GENESIS_VAULT_SEED.

It is a sentinel, not a commitment to any content: an agent whose vault already has entries at registration time should anchor its real vault root instead. The oracle's §7.1 gate checks only that the root is non-zero — it deliberately does not require this specific value, so a genuinely non-empty vault at birth is equally valid.

Authorization (§7.2): the genesis root (epoch 0→1) must be anchored by the agent itself — its controller via SovereignAgent.execute, which works because StateAnchor's admin is the SovereignAgent contract and the constructor grants it ANCHOR_ROLE. No Solidity change is needed for this; enforcement that the protocol's ANCHOR_ROLE signer cannot anchor epoch 1 is still [PLANNED] (Appendix A gap 2), since StateAnchor is deployed per-agent and already-deployed anchors keep their current bytecode.

4.4b Memory DAG node schema (spec §7.4 lineage) — [VERIFIED 2026-08-05]

Design: docs/design/memory-dag.md. Implementation: integrity_sdk/memory_dag.py. Written 2026-07-31 with no shell available and left unexecuted; run and confirmed 2026-08-05 — tests/test_memory_dag.py passes 21/21, including the cross-runtime provenance acceptance test (step 7 of the design doc's order-of-work). This section is now binding.

§4.4's tree commits to a flat set — its sorted-pair rule exists precisely to make leaf position meaningless, so it cannot express that one memory derives from another. Lineage needs a DAG, not a bigger tree.

Node preimage (canonical JSON per §4.2's rule — sort_keys=True, separators=(",",":"), ensure_ascii=True; deliberately the same encoding, not a second one):

{ schema, agent_id, kind, content_hash, parents[], edge_type, timestamp, source }
node_id = "0x" + keccak256(canonical(preimage))
schema  = "integrity.memory.node.v1"

Three rules are load-bearing:

  • parents is ordered and positionalparents[0] is the superseded version. It MUST NOT be sorted. Sorting it would erase lineage semantics exactly the way §4.4's sorted pairs erase leaf position; the two layers have deliberately opposite rules, and conflating them is the failure mode to guard against.
  • parents may reference only already-stored nodes, which are therefore always older. This single rule makes the graph acyclic by construction — no cycle detection exists or is needed.
  • Semantic [[links]] are committed inside content_hash, never as parents. Wiki links may be cyclic and may dangle (naming a memory that does not exist yet is explicitly allowed); both are fine as content and both would be fatal as hash edges.

Anchoring. A head node id is a 32-byte value, so root_of_heads() (a §4.4 tree over current ref heads) satisfies the existing §6 / §7.1 gate StateAnchor.latestRoot != bytes32(0) unchanged — no contract change, no oracle change, and §4.4a's GENESIS_VAULT_ROOT sentinel remains valid as the empty head. Because each node commits to its parents, anchoring one head transitively commits its whole reachable history.

4.5 Trace tree view (GET /v1/traces/{trace_id})

LangSmith-style nested run-tree reconstruction over the real spans in otel_spans (integrity-oracle/backend/src/trace_tree.rs) — the flat, start-time-ordered rows db::get_otel_spans_for_trace returns, reassembled into a parent/child tree by parent_span_id. Top-level route (not /v1/agent/{id}/traces/...): a trace_id is a global OTel identifier, not scoped to one agent. Same unauthenticated-data caveat as otel_spans generally (§1a in PRODUCTION_GAPS.md) — a span whose claimed parent isn't present in the queried set is surfaced as a root rather than erroring, and a chain deeper than trace_tree::MAX_TREE_DEPTH (500) is truncated with truncated: true in the response rather than silently cut. 404 on an unknown trace_id means "nothing was ever ingested under that ID," not an access-control decision.

Known tooling gotcha, not an API behavior: integrity-oracle/backend/src/openapi.rs splits its #[derive(OpenApi)] paths(...) list across two structs (ApiDocCore/ApiDocExtra, merged via combined_openapi()) because utoipa 5.5.0 silently drops the last entry once a single paths(...) list exceeds 15 items — confirmed by direct testing (macro expansion is correct; the drop happens in utoipa's runtime aggregation). Doesn't affect the live server at all (only the separate gen_openapi dev binary calls this code), but any future new endpoint must go in whichever of the two structs currently has room, not just be appended to ApiDocCore, or it will silently vanish from the generated spec the same way.

5. Zero-knowledge proving pipeline (must be real, end-to-end)

  1. Circuit lives in integrity-zkp/src/main.nr (Noir). It proves: "I know a private Ed25519-derived secret and an intent payload whose hash equals the public intended_state_hash, without revealing the secret or full payload." Keep the circuit's constraint logic real — no assert(true)-style shortcuts.
  2. Compile with nargo compile (produces the ACIR bytecode).
  3. Generate a proving/verification key and Solidity verifier with bb: bb write_vk, and bb write_solidity_verifier to emit a real contracts/src/oracle/UltraPlonkVerifier.sol (generated file — replace the old hand-written always-true stub entirely).
  4. integrity-sdk's prover.py shells out to nargo execute + bb prove to produce a real proof for a given commitment, and can call bb verify locally before submission.
  5. contracts' verifier contract is the on-chain source of truth; integrity-oracle also verifies proofs off-chain for scoring purposes using the same bb verify flow (or a Rust binding) — no independent/duplicate mock verifier.

Since this requires re-running nargo/bb commands as part of the build, document the exact commands in integrity-zkp/README.md and wire them into that package's Makefile target so CI actually exercises them.

6. On-chain architecture: per-agent primitives, not a singleton registry

This section supersedes the old singleton model. The old contracts/ had one shared ReputationRegistry, one shared Slasher, one shared StateAnchor, and an admin-controlled AgentFactory that registered each new agent into that shared state. That model is gone. It has been replaced end-to-end (127/127 forge test passing) with a self-sovereign per-agent model: every agent deploys and owns its own 7 "primitive" contracts at registration time, and there is no longer a global ReputationRegistry/Slasher/StateAnchor address to hardcode anywhere. AgentFactory.sol has been deleted; its replacement is AgentPrimitivesFactory.sol (§6.3).

6.1 The seven primitives

# Contract Deploy mode Purpose
1 SovereignAgent (core/SovereignAgent.sol) Direct, by the agent's own EVM wallet The agent's on-chain account: identity (DID), controller, execute(), cached AIS. Its address is the agent's canonical identity everywhere downstream.
2 StateAnchor (oracle/StateAnchor.sol) Direct, by the agent's own EVM wallet (constructor admin = the just-deployed SovereignAgent address) Anchors Merkle roots of this agent's off-chain Trust Vault state (§4.4) so individual leaves can be proven on demand.
3 ReputationRegistry (oracle/ReputationRegistry.sol) EIP-1167 clone This agent's AIS ledger: oracle-pushed baseScore plus a self-earned ZK_boost from a verified Barretenberg proof (§4.3).
4 Slasher (oracle/Slasher.sol) EIP-1167 clone Holds this agent's $ITK collateral; dispute-gated, arbiter-resolved slashing.
5 VerifierRegistry (oracle/VerifierRegistry.sol) EIP-1167 clone This agent's versioned pointer to whichever IZkVerifier implementation it currently trusts, so a global circuit upgrade doesn't force every agent onto a new version simultaneously.
6 ComplianceGate (health/ComplianceGate.sol) EIP-1167 clone This agent's regulated-industry (Integrity Health) compliance declaration + a single live-verified isHealthcareCompliant read.
7 AgentProfile (framework/AgentProfile.sol) EIP-1167 clone Domain-membership pointer (primaryDomain) + off-chain metadata URI (profileURI).

Only #1 and #2 are fully, independently deployed (their own bytecode, their own address derivation) — directly by the agent's own wallet, which is itself the cryptographic proof of self-sovereign control (nobody else's transaction created them). #3–#7 are cheap EIP-1167 minimal-proxy clones of 5 shared implementation contracts, deployed by AgentPrimitivesFactory in one registration transaction.

XibalbaAgentRegistry.sol (reshaped from its previous role) is the canonical index of all 7 addresses per agent, via a PrimitiveSet struct:

struct PrimitiveSet {
    address sovereignAgent;
    address stateAnchor;
    address reputationRegistry;
    address slasher;
    address verifierRegistry;
    address complianceGate;
    address agentProfile;
}

It is keyed both by keccak256(bytes(did)) (resolveDID/resolveDIDHash) and by the agent's SovereignAgent address (resolveAgent, isRegisteredAgent) — the latter is what downstream consumers use, since that's the address that arrives as msg.sender on every other call (e.g. EHRGate.checkAccess, §6.4). registerPrimitives is restricted to REGISTRAR_ROLE, granted only to AgentPrimitivesFactory — no other contract should hold it.

6.2 Call-routing convention

Every clone's DEFAULT_ADMIN_ROLE is the agent's own SovereignAgent contract address, never the raw controller EOA. An agent that wants to change its VerifierRegistry pointer, update its ComplianceGate self-declared flags, or update its AgentProfile metadata routes that call through SovereignAgent.execute(target, value, data), which checks onlyController (the EOA) before forwarding — so control ultimately still traces back to the controller key, but every clone only ever sees the SovereignAgent contract as its admin. This is deliberate: it means a compromise of the raw EOA's signing key is recoverable by rotateController, without having to re-point every clone's admin role individually.

Exception — the AgentPrimitivesFactory.registerPrimitives call itself is EOA-signed directly, not routed through execute, because SovereignAgent cannot route a call to register itself (that would be circular: the account doesn't have an admin-recognized execute path until after it exists). Instead, registerPrimitives verifies the caller by checking that msg.sender holds DEFAULT_ADMIN_ROLE on the SovereignAgent it claims to own (sa.hasRole(sa.DEFAULT_ADMIN_ROLE(), msg.sender)) — this is the one bootstrap exception to the "route everything through execute" rule.

Slasher is a partial exception to "admin = SovereignAgent": its DEFAULT_ADMIN_ROLE (arbiter) is governance — the protocol's, not the agent's — passed in at AgentPrimitivesFactory construction time, because an agent must never be able to arbitrate its own slashing dispute (see the NatSpec on Slasher.sol). DISPUTER_ROLE is likewise a protocol-held signer (disputer), separate from governance, so a bridge/oracle compromise and a governance-key compromise are independently revocable. ReputationRegistry's ORACLE_ROLE and Slasher's DISPUTER_ROLE follow the same pattern: protocol-held signers, distinct from both the agent's admin role and each other.

6.3 Registration sequence

AgentPrimitivesFactory (framework/AgentPrimitivesFactory.sol) replaces the deleted AgentFactory.sol. The real end-to-end sequence (mirrored exactly by contracts/test/AgentPrimitivesFactory.t.sol, and the sequence integrity-sdk will implement against real Base Sepolia transactions) is:

  1. The agent's own wallet deploys SovereignAgent directly: new SovereignAgent(did, controller, oracleSigner, factory).
  2. The same wallet deploys StateAnchor directly, passing the just-deployed SovereignAgent address as admin: new StateAnchor(address(sovereignAgent)).
  3. The wallet calls SovereignAgent.execute(stateAnchor, 0, abi.encodeCall(AccessControl.grantRole, (ANCHOR_ROLE, oracleSigner))) — routing the grant through the agent's own account (per §6.2) to give the protocol's oracle signer ANCHOR_ROLE on this agent's StateAnchor, so the oracle can anchor Merkle roots on the agent's behalf.
  4. The wallet calls AgentPrimitivesFactory.registerPrimitives(sovereignAgent, stateAnchor, did, domainId, vertical, profileURI) (EOA-signed directly, per the §6.2 bootstrap exception). This single transaction:
    • verifies the caller controls the claimed SovereignAgent,
    • verifies domainRegistry.canJoin(domainId, msg.sender),
    • clones and initializes all 5 remaining primitives with the SovereignAgent address as their admin (protocol-held roles — oracleSigner, disputer, governance — come from the factory's own immutables, never from the registering agent),
    • calls XibalbaAgentRegistry.registerPrimitives to atomically record the full PrimitiveSet,
    • calls DomainRegistry.recordJoin to mark the SovereignAgent address as a member of domainId,
    • emits PrimitivesRegistered with all 7 addresses.

No consumer can ever observe an agent that only half-exists: registration either completes all 5 clones + both registry writes in one transaction, or reverts entirely.

Step 5 — off-chain: oracle independent re-verification. After the 4 on-chain steps above, integrity-sdk's registration.register_agent() POSTs to integrity-oracle's POST /v1/agent/register (integrity-oracle/backend/src/handlers.rs's RegisterAgentRequest), which independently re-derives the agent's primitives from XibalbaAgentRegistry.resolveDID on-chain and rejects (400) if the client's claim doesn't match byte-for-byte — this is what makes the agent "really" registered from the protocol's point of view, not just on-chain from the SDK's own say-so. This schema was previously undocumented here (silent gap, not a deliberate omission) — that silence is exactly how integrity-sdk's payload drifted from the oracle's real struct undetected until 2026-07-09 (see docs/wiki/WIKI_LOG.md's entry of that date). The real JSON shape, pinned to the actual Rust struct:

// POST /v1/agent/register
{
  "did": "did:integrity:<sha256-hex-fingerprint>",   // required — NOT "agent_id"
  "did_document": { /* §4.1 DID Document, verbatim */ },
  "primitives": {
    // exactly these 7 fields (checksummed hex addresses) — nothing else;
    // extra fields are silently ignored by serde, but don't rely on that
    "sovereign_agent": "0x...",
    "state_anchor": "0x...",
    "reputation_registry": "0x...",
    "slasher": "0x...",
    "verifier_registry": "0x...",
    "compliance_gate": "0x...",
    "agent_profile": "0x..."
  },
  "ed25519_pubkey_hex": "0x...",   // optional, but the handler 400s if this
  "eth_address_hex": "0x...",      // AND eth_address_hex are both absent —
                                    // integrity-sdk always sends both
  "verification_tier": 0           // accepted for wire compatibility but ignored;
                                    // the server assigns the registration floor
                                    // and evidence-backed checks raise effective tier
}

integrity-cli's agent register command hand-builds this same POST body independently (it does not import integrity_sdk.registration — see entities/integrity-cli.md's "no sibling dependency" note) and had the identical agent_id-vs-did drift until it was fixed in lockstep on 2026-07-09, the same day as the SDK fix above — both client implementations now conform to this exact schema.

integrity-sdk's registration.py sends this exact shape as of the 2026-07-09 fix (it previously sent {"agent_id", "did_document", "primitives": <the full AgentRegistration dataclass, extra fields and all>}, which 422'd on the missing did field and would then 400 on the missing pubkey/address fields even if did were fixed alone — never caught because every test up to that point ran with skip_oracle_registration=True).

6.3a Verification ladder and evidence revocation

Registration establishes only the tier-1 floor. The Oracle derives the effective tier from that floor plus active, unexpired, unrevoked evidence:

  • Tier 2: dual-resolver DNS TXT proof or a signed challenge read from a GitHub repository that GitHub reports as owned by the claimed login. Evidence expires after 90 days.
  • Tier 3: a nonce-bound AWS Nitro COSE/CBOR attestation whose certificate chain reaches AWS's Nitro root. Evidence expires after 30 days.
  • KYC: a nonce-bound receipt signed by a key in KYC_PROVIDER_KEYS. The open_source_kyc_v1 profile requires affirmative document-authenticity, biometric- liveness, and sanctions/PEP checks. The Oracle stores only an opaque subject reference, explicit check flags, validity timestamps, provider id, and receipt hash—never raw PII.

The route pairs are POST /v1/agent/{id}/verify/{dns|github|tee|kyc}/challenge and POST /v1/agent/{id}/verify/{dns|github|tee|kyc}. Current evidence and the derived tier are returned by GET /v1/agent/{id}/verify.

KYC uses POST /v1/agent/{id}/verify/kyc/challenge with {"provider":"open-kyc"} and POST /v1/agent/{id}/verify/kyc with a signed receipt. The exact signed bytes are:

integrity-kyc-receipt:v1:<agent_did>:<provider>:<opaque_subject_reference>:<assurance_profile>:<document_authenticity_0_or_1>:<biometric_liveness_0_or_1>:<sanctions_pep_screening_0_or_1>:<verified_at_unix>:<expires_at_unix>:<nonce>

opaque_subject_reference is restricted to 8–200 URL-safe characters. Receipts must be currently valid, cannot begin more than five minutes in the future, and cannot last longer than 365 days. A configured issuer signature proves which verifier asserted the checks; it does not imply that every jurisdiction treats the profile as legally equivalent.

Evidence revocation is agent-authorized and audit-preserving:

  1. POST /v1/agent/{id}/verify/{verification_id}/revoke/challenge returns a fresh 60-minute nonce.
  2. The client signs the UTF-8 bytes of integrity-verification-revoke:v1:<did>:<verification_id>:<nonce>:<hex-utf8-reason> with the Ed25519 key registered for the DID.
  3. POST /v1/agent/{id}/verify/{verification_id}/revoke accepts {"signature":"<hex>","reason":"<1-500 UTF-8 bytes>"}. It sets revoked_at and revoked_reason, consumes the nonce, and returns the newly derived effective_tier; it never deletes the evidence row.

6.4 EHRGate reputation resolution (was: one immutable global registry)

health/EHRGate.sol used to hold one immutable global ReputationRegistry address, read once at construction. Now that every agent owns its own ReputationRegistry clone, there is no single address to point at. EHRGate instead holds the shared XibalbaAgentRegistry and resolves msg.sender's own clone on every call:

if (!registry.isRegisteredAgent(msg.sender)) return false;
address reputationRegistry = registry.resolveAgent(msg.sender).primitives.reputationRegistry;
if (ReputationRegistry(reputationRegistry).effectiveScore(msg.sender) < minAisThreshold) return false;

This resolution is itself a meaningful check, not just plumbing: an address that was never registered through AgentPrimitivesFactory has no entry in XibalbaAgentRegistry, so checkAccess returns false before it can even reach the reputation check — closing off any hand-rolled contract that only pretends to be a Sovereign Agent. All three of EHRGate's gates (patient consent, active BAA, AIS ≥ minAisThreshold) are required simultaneously; consent alone is necessary but not sufficient.

6.5 Known gap: CCIPReputationBridge is unwired

oracle/CCIPReputationBridge.sol predates the per-agent clone model and still assumes one global, immutable ReputationRegistry — its registry.getAgent(agent) / registry.updateScoreByBridge(agent, baseScore) calls no longer resolve to "the" registry for an arbitrary agent now that every agent has its own clone. This is a documented, honest gap, not a silently broken feature: the contract is not deployed by the deploy script and not referenced by AgentPrimitivesFactory. It needs to be reworked to resolve each agent's own ReputationRegistry clone via XibalbaAgentRegistry before reading/writing a score, before it can be wired back in. Don't build cross-chain reputation sync against this contract as it stands today.

6.6 Deployments file shape

Local/dev deployments get written to deployments.local.json at the repo root (gitignored). This shape has changed — it is no longer a flat list of contract addresses, because most of those addresses no longer exist as singletons:

{
  "chainId": 31337,
  "singletons": {
    "IntegrityToken": "0x...",
    "IntegrityGovernance": "0x...",
    "UltraPlonkVerifier": "0x...",
    "XibalbaAgentRegistry": "0x...",
    "XibalbaNameService": "0x...",
    "DomainRegistry": "0x...",
    "AgentPrimitivesFactory": "0x...",
    "CoveredEntityRegistry": "0x...",
    "SmartBAAFactory": "0x...",
    "HIPAAGuardrailRegistry": "0x..."
  },
  "cloneTemplates": {
    "ReputationRegistry": "0x...",
    "Slasher": "0x...",
    "VerifierRegistry": "0x...",
    "ComplianceGate": "0x...",
    "AgentProfile": "0x..."
  },
  "protocolAddresses": {
    "oracleSigner": "0x...",
    "governance": "0x...",
    "funderWallet": "0x...",
    "resolverSigner": "0x..."
  }
}

XibalbaNameService + IntegrityGovernance (optional singletons): both are deployed by genesis Deploy.s.sol but their Base Sepolia broadcast is deferred, so deployments.baseSepolia.json may legitimately omit either key. The oracle parses them as Option<Address>; endpoints that need an absent one (/v1/xns/resolve, /v1/agent/{id}/handle, /v1/governance/proposals) return ChainError::MissingSingletonHTTP 400 (a deployment-shape fact, not a transient failure — same mapping as the market/health singletons) rather than fabricating a result. Dashboard consumers degrade to an honest "not deployed" state on that 400.

Market/application layer additions (§6.9): singletons.MarketFactory and singletons.A2ACapitalPool (protocol-level, deployed once), plus cloneTemplates.IntegrityMarket (the shared implementation MarketFactory clones per-market — the sixth clone template, alongside the five identity ones above). These are written by a SEPARATE, INCREMENTAL script, contracts/script/DeployMarkets.s.sol — not genesis Deploy.s.sol — because by the time the market layer was added, the genesis singletons already had real registered agents on them; re-running Deploy.s.sol would redeploy IntegrityToken/XibalbaAgentRegistry/etc from scratch and orphan every one of them. DeployMarkets.s.sol reads the existing deployments file, deploys only the new contracts against the existing IntegrityToken/ XibalbaAgentRegistry addresses, and merges the new fields into the same file (every pre-existing field is re-serialized unchanged). This is now the general pattern for any future protocol-layer addition after genesis: a new, narrowly-scoped incremental script, never a re-run of Deploy.s.sol against a live network.

  • singletons — protocol-level contracts that exist exactly once, deployed by governance, unchanged from before except for the removal of AgentFactory (deleted) and ReputationRegistry/Slasher/StateAnchor (no longer singletons — see below) and the addition of AgentPrimitivesFactory.
  • cloneTemplates — the 5 shared implementation contracts every agent's EIP-1167 clones delegatecall into (§6.1, #3–#7). These are deployed once with _disableInitializers() already called, so they can never be initialized/hijacked directly — only clones of them can be. Note SovereignAgent and StateAnchor do not appear here: they aren't clone templates, they're fully independent bytecode the agent deploys itself (§6.1).
  • protocolAddresses — signer/governance addresses the deploy flow wires into AgentPrimitivesFactory's constructor (oracleSigner, governance, and a funderWallet intended to gas-fund new agent wallets on Base Sepolia, since agents now sign their own deployment transactions instead of a shared factory paying for them).

Per-agent primitive addresses are deliberately NOT in this static file. There is no fixed set of them — a new set of 7 is created every time an agent registers. integrity-oracle is now built and is exactly this resolution layer: GET /v1/agent/{id} resolves a given agent's primitives live from XibalbaAgentRegistry.resolveAgent/resolveDID on-chain (via alloy, see integrity-oracle/backend/src/chain.rs), independently re-verifying anything a client claims rather than trusting it. Any package needing an agent's primitive addresses should call the oracle's HTTP API (what bcc_middleware's agent_id_to_address/resolve_agent_primitives does) rather than querying XibalbaAgentRegistry directly — the oracle is the one place that owns turning a DID into live on-chain primitive state.

integrity-oracle, integrity-sdk, integrity-cli, and integrity-dashboard read the singleton and template addresses from this file rather than hardcoding them; per-agent addresses are always resolved live (or, once built, via the oracle's cache) rather than read from any static file.

6.7 Integrity Health / regulated-industry compliance wiring

health/ComplianceGate.sol (primitive #6, §6.1) is the concrete wire between the core protocol and the Integrity Health (HIPAA) vertical, and it is worth spelling out because it has two halves that must never be confused with each other:

  • Live-verified complianceisHealthcareCompliant(coveredEntity) returns true only if (a) the agent declared Vertical.Healthcare at registration (ComplianceGate.vertical, set once in initialize from AgentPrimitivesFactory.registerPrimitives's vertical parameter) and (b) a live on-chain read against the real CoveredEntityRegistry (isActiveCoveredEntity) and SmartBAAFactory (isBAAActive) both pass. CoveredEntityRegistry and SmartBAAFactory addresses are baked into ComplianceGate's implementation contract as constructor immutables (shared across every agent's clone, same pattern as AgentProfile's domainRegistry), so every clone reads the same, correct Integrity Health registries without a per-agent storage write. This is a read path only — ComplianceGate does not replace EHRGate as the PHI-access enforcement boundary; EHRGate.checkAccess still performs its own independent live checks (patient consent, BAA, AIS threshold — §6.4) at access time. ComplianceGate is a read-optimized compliance summary for callers like integrity-oracle's S_compliance AIS component or integrity-dashboard's Integrity Health page, not a second enforcement point.
  • Self-declared compliancehipaaEligible, zdrEnabled, externalWebAccessDeclared, dataResidencyRegion, set via setSelfDeclaredCompliance (routed through SovereignAgent.execute, per §6.2 — ComplianceGate's admin is the agent's own SovereignAgent). These mirror integrity_sdk/telemetry/conventions.py's IntegrityAttributes.COMPLIANCE_HIPAA_ELIGIBLE / COMPLIANCE_ZDR_ENABLED / COMPLIANCE_EXTERNAL_WEB_ACCESS / COMPLIANCE_DATA_RESIDENCY_REGION span attributes — i.e. they are an on-chain mirror of an off-chain-attested claim the agent makes about itself. isHealthcareCompliant never reads any of these fields. Treat them as "what the agent says about itself" vs. "what the chain actually verified" — a consumer that needs an enforceable guarantee (e.g. gating PHI access) must use the live-verified boolean or EHRGate, never the self-declared flags.

6.8 Agent Contract Ownership — a formal protocol primitive

This is the protocol's core architectural thesis, stated once, formally, so every future package builds on it consistently rather than reinventing it ad hoc. Integrity Protocol agents do not merely use smart contracts the way a normal dApp user does — they own and deploy them, and that ownership is itself the cryptographic substrate the rest of the protocol (reputation, compliance, markets) is built on. Two deployment modes are both first-class, and any future primitive or application layer must pick one of them explicitly rather than a third, undocumented pattern:

  1. Direct deployment. The agent's own EVM wallet signs and broadcasts the contract-creation transaction itself. The deployment signature is the proof of self-sovereign control — nobody else's key could have produced it. This is how SovereignAgent and StateAnchor work (§6.1, primitives #1–#2), and it is available to any agent for a fully custom, hand-authored contract too, with no protocol factory involved at all — an agent can deploy literally anything from its own wallet.
  2. Factory-mediated clone deployment. The agent calls a shared, protocol-level factory to cheaply clone (EIP-1167) and initialize its own instance of a shared implementation contract. The agent still ends up as that clone's owner/admin (DEFAULT_ADMIN_ROLE = the agent's SovereignAgent address, per §6.2's call-routing convention) — the factory only pays the one-time cost of the shared implementation's bytecode once, not per agent. This is how primitives #3–#7 work (AgentPrimitivesFactory), and — critically — it is not limited to identity primitives. MarketFactory (§6.9) applies the exact same pattern one layer up, at the application level: any registered agent can deploy and own its own customized IntegrityMarket instance the same way it owns its ReputationRegistry clone. This is the concrete proof that "agents own their contracts" is a general protocol property, not a one-off fact about identity.

Why this matters beyond mechanics: it inverts the usual "platform owns the contract, users are just addresses in someone else's system" model. Trust earned by one agent-owned contract (a market it created, a reputation ledger it accrued) is portable and composable, because it's genuinely the agent's own on-chain footprint — not a row in a platform database the agent has no control over. Any future application layer (lending, insurance, whatever comes after integrity-framework, §12) should extend this same two-mode pattern rather than introducing a third, platform-owned model. integrity-dashboard's Contracts/Factory-IDE page (§9) is expected to expose BOTH modes to a human operator/developer: a "deploy from template" path calling a factory, and a "deploy custom" path where the agent's own wallet broadcasts a contract the developer authored directly.

6.9 Market / application layer (contracts/src/markets/)

The first concrete application of §6.8's second mode beyond identity. Global infrastructure, deployed via the incremental DeployMarkets.s.sol script (§6.6):

  • IntegrityMarket.sol — an EIP-1167 clone template (like primitives #3–#7), NOT a singleton. One clone = one market. Backs both prediction markets (N outcomes) and binary options (the 2-outcome case) as the same mechanism. enterPosition gates entry on the caller's LIVE ReputationRegistry.effectiveScore (resolved via XibalbaAgentRegistry, same pattern as EHRGate.checkAccess, §6.4) and records a bccCommitmentHash binding the position to the agent's off-chain BCC commitment (§4.2) — the position is provably the agent's own pre-committed call, not a reaction to information obtained afterward. Payout is pari-mutuel across the full pool on resolve.
    • Trust boundary, documented not hidden: resolve() is gated to RESOLVER_ROLE, set by the market's creator at deploy time (itself, a delegate, or the protocol's demo/oracle signer). For the investor/developer Dashboard this is a clearly-labeled demo resolver, not a live price-feed oracle network (Chainlink/UMA) — staking, AIS-gating, BCC-commitment binding, and payout are all real; only ground-truth outcome resolution is a documented, swappable trust boundary. A production deployment swaps RESOLVER_ROLE's holder; the contract's interface doesn't change.
    • Fraud/misreporting (a BCC-committed intent not matching an agent's actual position) is deliberately NOT handled inside this contract — the oracle is expected to compare telemetry/BCC commitments against on-chain positions and raise a dispute on the offending agent's own Slasher clone (the existing mechanism, §6.1 primitive #4). This keeps IntegrityMarket a small, auditable escrow rather than a second slashing engine.
  • MarketFactory.sol — the factory (singleton) any registered agent calls to deploy+own its own IntegrityMarket clone. Deliberately ungated (no curator role, unlike SmartBAAFactory's entity-registry check) — restricting who may create a market would undercut §6.8's thesis. Discovery/quality (e.g. surfacing markets by creator AIS) is a dashboard/oracle-index concern, not an on-chain gate.
  • A2ACapitalPool.sol — a global singleton (deliberately NOT agent-clonable like IntegrityMarket — a capital pool is a shared many-allocator-to-many-agent venue, not an application one party authors and owns). Real on-chain agent-to-agent capital allocation: allocate escrows ITK from an allocator (a human wallet, or another agent's SovereignAgent) earmarked for a target agent, gated on that target's live AIS; release pays out (re-checking the AIS gate); clawback reclaims still-escrowed (pre-release) funds. Post-release misconduct has no fund-reversal path in this contract by design — the punitive lever is the target agent's own Slasher; flagBreach records a non-fund-moving history marker for dashboard/leaderboard display.
  • ComplianceGate.Vertical (§6.7) extended: { None, Healthcare, PredictionMarket, Trading, CapitalAllocation }. New values are additive-only (existing numeric ids never change). Like Healthcare's self-declared flags, these have no live-verified is*Compliant check of their own yet — they're a self-declared operating-domain badge for dashboard/discovery, and do not gate IntegrityMarket/ A2ACapitalPool participation, which only ever check live AIS.

6.10 Backend responsibility split (two trust domains, not three peer services)

As of the multi-vertical Dashboard, protocol-facing HTTP is split across two trust domains — not three interchangeable peer services, a framing this section used to have and which undersold how tightly the first two pieces below are coupled — with one hard rule: only integrity-oracle ever reads on-chain state. No other backend queries a chain RPC or a contract directly.

  • The Oracle trust domain — one domain, two processes, split by before/after the action:
    • bcc_middleware (§7, Python/FastAPI) — the BEFORE-the-action half: pre-execution BCC/OPA policy gating, real on-chain BAA checks, Merkle anchoring. An agent's own process could simply skip calling an SDK-side check, which is why this can't live in integrity-sdk — it has to be a service the agent cannot bypass or tamper with, and it does real on-chain reads/writes, which is Oracle's territory, not a peer concern.
    • integrity-oracle (§2, Rust/Axum) — the AFTER-the-action half (plus always-on reads): agent registration re-verification, telemetry ingest, AIS computation, and live reads of markets, positions, allocations, wallet balances, and a derived leaderboard (§6.9). No demo-orchestration logic lives here — it is a read/verify layer over real on-chain + telemetry state, nothing else.
    • These two keep separate codebases/processes (no forced rewrite — bcc_middleware stays Python, integrity-oracle stays Rust) but are organizationally one deployment group and one trust boundary: both independently re-verify what a client claims against real on-chain/ policy state rather than trusting it.
  • integrity-userapi (FastAPI + Postgres, §13) — a second, separate trust domain: user-facing data ONLY — accounts/auth, developer API keys, which DIDs a human user has claimed as "theirs," and a record of demo runs a user requested. It never calls a contract or a chain RPC; anything protocol-related it needs, it fetches from integrity-oracle over HTTP (ORACLE_URL). This keeps the oracle's scope narrow and auditable and keeps user-account concerns (passwords, sessions, ownership) out of a service whose whole job is being a trustworthy on-chain-state verifier.

integrity-dashboard (the one dashboard/landing app, §9) is the only client expected to talk to both the Oracle trust domain and integrity-userapi directly.

7. OPA policy integration (must be real, no "assume success" fallback)

  • bcc_middleware/policies/*.rego holds the real Rego policies (carry over the HIPAA guardrail logic from the old repo, cleaned up).
  • Both bcc_middleware and integrity-sdk evaluate policy by calling a real, running OPA server's REST API: POST {OPA_URL}/v1/data/integrity/bcc/allow with the intent as input. No local regex-only fallback path — if OPA is unreachable, the request must fail closed (deny), not silently approve.
  • Ship a bcc_middleware/policies/*_test.rego suite runnable via opa test .

7a. Reputation sync & slashing signer (bcc_middleware/app/reputation.py, scoring_loop.py)

bcc_middleware is also the protocol's oracle-signer/disputer for on-chain reputation — not just the pre-execution BCC policy gate §7 describes. A periodic background loop (started at FastAPI lifespan startup, SCORE_SYNC_INTERVAL_SECONDS, default 300s; also triggerable on-demand via POST /v1/reputation/sync) lists every agent the oracle knows about and, per agent:

  1. Treats GET /v1/agent/{id}/ais's final ais as authoritative and removes only its reported zk_boost (baseScore = round(ais / zk_boost)) before signing and submitting ReputationRegistry.updateScore(agent, baseScore). It MUST NOT recompute from components/weights: doing so duplicates the canonical Rust formula and loses the Oracle's already-applied effective identity-tier ceiling. The contract independently earns and applies its own on-chain ZK boost.
  2. Reads GET /v1/agent/{id}/telemetry/volume's flagged-event ratio over a lookback window (DISPUTE_LOOKBACK_BUCKET); if it crosses DISPUTE_FLAGGED_RATIO_THRESHOLD with at least DISPUTE_MIN_EVENTS samples, signs+submits a real Slasher.raiseDispute(agent, amount, reason) locking DISPUTE_STAKE_BPS of the agent's currently-available stake, subject to a DISPUTE_COOLDOWN_SECONDS per-agent cooldown.

Reuses the existing ANCHOR_SIGNER_PRIVATE_KEY (Merkle-anchoring signer) by default via REPUTATION_SIGNER_PRIVATE_KEY's fallback, rather than a dedicated key — a deliberate, user-made tradeoff documented in PRODUCTION_GAPS.md §1. integrity-oracle itself remains read-only; it never signs or submits a transaction (see chain.rs).

8. TEE attestation (honest real-verification scope)

We do not have real Nitro/SGX hardware in this environment, so we cannot generate genuine attestation documents. But the verification code must be real: parse the actual AWS Nitro Enclave COSE_Sign1/CBOR attestation format, verify the COSE signature against the embedded leaf certificate, and verify the certificate chain up to AWS's published Nitro root CA. Use AWS's publicly documented example attestation document as a test fixture (cite the source in a code comment). Document explicitly in integrity-sdk/security/attestation.py and its README that proof generation needs real enclave hardware we don't have, while verification is fully implemented and tested against the fixture. Do not leave literal placeholder strings like the old "MRENCLAVE_STUB".

9. Directory layout (top level)

INTEGRITY-LATEST/
  docs/INTERFACE_CONTRACT.md   <- this file
  docker-compose.yml
  Makefile
  .gitignore
  README.md
  contracts/
  integrity-zkp/
  integrity-oracle/
  integrity-sdk/
  integrity-cli/
  bcc_middleware/
  integrity-userapi/
  integrity-dashboard/            <- the ONE dashboard/landing app
    src/                     (React/Vite/TS — every product page)
    demo/                    (Python closed-loop scenario engine, §11 — a
                               script this package runs, not a second app)

integrity-dashboard/ and integrity-demo/ (below in §11's prose, and anywhere else in this doc) were two separate packages until 2026-07-09, when they were merged into integrity-dashboard/ — one deployed app, per the "exactly one user-facing product surface" rule. Any reference elsewhere in this document to either old name means the corresponding piece of integrity-dashboard/.

Each package keeps its own README, its own .env.example, its own test suite, and its own CI-runnable make test / make build targets, wired into the root Makefile and docker-compose.yml.

10. Code style

Same languages/frameworks as the old repo per package (Solidity/Foundry, Noir, Rust/Axum, Python SDK+CLI, FastAPI, React/Vite/TypeScript). Code must be commented for a human reader: explain why, not what — skip comments that just restate the code, but do explain non-obvious cryptographic/protocol invariants, since this is exactly the kind of code where a subtle mistake (e.g. hash ordering, signature domain separation) is a real vulnerability.

11. integrity-dashboard/demo/ (multi-vertical investor/developer closed-loop Dashboard)

Lives at integrity-dashboard/demo/ — a Python subdirectory of the one dashboard app package (§9), not a standalone top-level package (it was integrity-demo/ until the 2026-07-09 merge). It has no UI of its own: a runnable script/CLI entrypoint (make demo from the repo root) that drives real on-chain activity for integrity-dashboard's dashboard pages to display — the dashboard reads the results back out via integrity-oracle (live chain reads) and integrity-userapi (GET /demo/runs), it does not embed or launch this script itself.

Purpose: prove the whole stack works end-to-end, across MULTIPLE verticals, by running a small fleet of real agents through a real, repeated loop — not a static mockup, not canned data, not a single-vertical toy. This is the flagship demonstration of §6.8's thesis (agents own their contracts, and that ownership underwrites real financial + regulated actions) — the target audience is investors and developers, so every step must be independently verifiable (real tx hashes, real BaseScan links when run against Base Sepolia), not narrated-but-faked.

Agent fleet (each REALLY self-registers all 7 primitives via integrity-sdk's registration.register_agent, real wallet, real minted ITK — see §6.3): an honest, well-grounded agent whose AIS climbs and wins real market calls; a reckless/overleveraged agent whose AIS decays; an agent exercising the real Integrity Health/healthcare vertical (§6.7, real CoveredEntity + SmartBAA + EHRGate access flow); and a scenario demonstrating what a BCC-commitment/on-chain-action mismatch would surface once oracle-side detection exists (honestly labeled if not yet automated — never a faked slash event).

Loop, generalized across verticals: an agent signs a real BCC Commitment (§4.2) for its intended action → bcc_middleware's /v1/bcc/intercept (real OPA evaluation, real circuit breaker) → the action lands on a real vertical contract (IntegrityMarket/ A2ACapitalPool from §6.9, or EHRGate for the healthcare vertical) → real telemetry reported to integrity-oracle → the oracle recomputes AIS (§4.3) and (for markets/allocation) the demo's resolver settles real ITK payouts → the fleet's next actions are chosen with awareness of each agent's own current score — closing the loop. Capital visibly reallocates toward the trustworthy agent via A2ACapitalPool as the loop progresses.

This package depends on the real, running integrity-sdk and deployed contracts/ (both required); bcc_middleware/integrity-oracle are strongly preferred for full realism but the SDK's lower-level, non-BCC-gated functions (markets.enter_position, etc.) let the core chain mechanics be proven even if those services aren't up in a given environment — any such gap must be stated explicitly in this package's own README.md, never silently skipped. It holds the demo RESOLVER_ROLE (§6.9) via a dedicated signer (RESOLVER_PRIVATE_KEY, §3). It has its own README.md with exact run instructions (which services need to be up, in what order, for local anvil vs. Base Sepolia) and its own real test coverage for the scenario logic (not the sibling services, which are tested in their own packages). integrity-userapi (§13) may record that a user triggered a given demo run (POST /demo/run), but orchestrating the run itself is entirely this package's job, not the user API's.

12. integrity-framework/ (reputation-derivatives — not yet built)

Referenced in §1's scope list but not part of this rewrite's current phases (§6.8–§6.10, §11, §13 cover the multi-vertical Dashboard that now supersedes what this package was originally scoped to explore). Concept carried over from the old repo: a marketplace/lending layer over agent reputation (e.g. AIS-collateralized credit, reputation derivatives). Any future work here should build on §6.8's agent-contract-ownership pattern (agents deploying/owning their own lending-position contracts via a factory, the same way MarketFactory/A2ACapitalPool do today) rather than introducing a new ownership model. Marked here explicitly as not yet built per the "no silent mocks" ground rule — do not assume any integrity-framework/ code exists.

13. integrity-userapi/ (user accounts — strictly non-chain)

Purpose: own user-facing data (accounts, auth, developer API keys, agent-ownership claims, demo-run history) with zero smart-contract or chain-RPC access of its own — see §6.10 for the full three-backend split rationale. FastAPI + a real Postgres (never sqlite/mocked in tests).

Real schema (illustrative, see the package's own migrations for the authoritative shape): users (email, hashed password), api_keys (hashed key, ais_trust_ceiling — mirrors the old dashboard's developer-key convention of capping dev-issued agents at a fixed AIS ceiling), user_agents (a user_id ↔ agent DID ownership POINTER only — never a cache of full agent state, which always comes live from integrity-oracle), demo_runs (status/history of integrity-dashboard/demo/ invocations a user requested).

Core endpoints: POST /auth/register, POST /auth/login, GET /me, POST /api-keys (returns the raw key exactly once, stores only its hash), GET /api-keys, DELETE /api-keys/{id}, GET /me/agents (fans out to integrity-oracle's GET /v1/agent/{id} for live data per owned DID — never duplicates it locally), POST /me/agents, POST /demo/run, GET /demo/runs.

Hard rule, worth repeating from §6.10 because it is the one invariant this whole package exists to preserve: if a change to this package would require importing web3/alloy-equivalent tooling or reading a deployments.*.json file, that change belongs in integrity-oracle instead.

Postgres, wiring, tests (as of 2026-07-09): docker-compose.yml has a dedicated userapi-postgres service (postgres:16-alpine, integrity/integrity_dev_only, db integrity_userapi, host port 5435) — deliberately its own instance/port, never sharing integrity-oracle's postgres service (5432) or its ad hoc e2e-test convention (5434, see integrity-oracle/README.md), since two separate Postgres instances per trust domain is the whole point of the §6.10 split. The userapi app service builds integrity-userapi/Dockerfile (uv-based, same pattern as bcc_middleware/Dockerfile) and exposes 8090. 33 real pytest tests in integrity-userapi/tests/ run against a real Postgres via this same server (a separate integrity_userapi_test database, auto-created by tests/conftest.py on first run) — unlike integrity-oracle's opt-in/env-gated e2e test, this suite has no skip path: it fails hard (connection refused at collection time) if userapi-postgres isn't reachable, since "pytest green against a real Postgres" is this package's stated completion gate, not an optional extra. The GET /me/agents tri-state (live data / not found / oracle unreachable) is tested against a real local HTTP server standing in for integrity-oracle, never a mock of app/oracle_client.py's internals.

CORS (added 2026-07-09, real gap found wiring integrity-dashboard's auth swap): this service had no CORS policy at all before — every request from a browser-hosted integrity-dashboard (served by Vite on its own port, never the same origin as this API) would be blocked outright. Fixed with fastapi.middleware.cors.CORSMiddleware in app/main.py (allow_origins=["*"], allow_credentials=False — every authenticated request here carries a Authorization: Bearer <jwt> header, never a cookie, so a wildcard origin is safe; combining a wildcard origin with allow_credentials=True is invalid per the CORS spec anyway). Verified: integrity-userapi's 33 pytest tests still pass unchanged after the addition (CORS is a browser-enforced concern, invisible to a server-side test client), and a real cross-origin browser call from integrity-dashboard (Playwright, e2e/auth.spec.ts) now succeeds end-to-end.

14. integrity-dashboard's real integration with integrity-userapi (auth swap, 2026-07-09)

Per §13's "one unified app" rule and the previously-pending item this section resolves: integrity-dashboard's dashboard now authenticates against this service for real (src/lib/api/userapi.ts, src/auth/AuthContext.tsx) — the Firebase-based AuthContext/AuthGate/firebase.ts from the original scaffold are gone, not left running alongside a second auth system. A JWT from POST /auth/login/POST /auth/register is stored in localStorage and attached to every userapiClient request via an axios request interceptor (src/lib/api/client.ts). Only account-scoped routes (/account — API keys, owned-agent pointers, demo-run history) sit behind a real session check; Landing, the agent list/detail, markets, leaderboard, and wallet stay public — real protocol data doesn't require an account to read, matching investor/developer intent. integrity-dashboard/e2e/global-setup.ts now boots a real integrity-userapi instance (its own ephemeral Postgres database on the same E2E container, real uvicorn process) alongside the oracle, so e2e/auth.spec.ts exercises real registration, real login, and a real 401 on bad credentials against this actual service — not a mock.

Real oracle wire-shape corrections found while wiring this (integrity-oracle untouched, worked around client-side)

Running the pre-existing Playwright suite for the first time (before this pass's fixes) surfaced that integrity-dashboard's lib/api/types.ts had drifted from what integrity-oracle/backend/src/handlers.rs actually serializes — 4 of 5 specs failed. Documented here because it's a cross-package contract fact, not just an integrity-dashboard-internal detail:

  • GET /v1/agents returns {id, verification_tier, created_at} (AgentSummary) — no ais/alias/zk_proof_verified/registered_at/ last_active fields ever existed in this response; those were an unverified assumption in the original dashboard scaffold.
  • GET /v1/agent/{id} (AgentResponse) never returns a did_documentPOST /v1/agent/register's RegisterAgentRequest.did_document is accepted on the way in but never persisted or returned by any GET. A real, confirmed gap (not fixed here — out of integrity-dashboard's scope to fix the oracle).
  • PrimitiveSetDto's own doc comment in handlers.rs claims its fields match the dashboard "field-for-field (camelCase)" — this is incorrect; the struct has no #[serde(rename_all = "camelCase")], so it actually serializes snake_case (sovereign_agent, state_anchor, ...). Worth fixing the stale comment (or adding the attribute) in a future integrity-oracle pass; integrity-dashboard now assumes the real snake_case shape.
  • ComplianceResponse fields are is_compliant/covered_entity (snake_case), not the isCompliant/coveredEntity the original dashboard scaffold guessed.
  • AisResponse.weights (scoring_core::AisWeights) fields are w_entropy/w_grounding/w_sacrifice/w_compliance, and there is no history array anywhere in the response — the dashboard's old AIS sparkline was reading a field the oracle never sends.

GET /v1/markets, GET /v1/markets/{id}, GET /v1/leaderboard, and GET /v1/agent/{id}/wallet had no prior dashboard assumption to drift from (first-time consumers) and match handlers.rs as documented in §6.9. Note that GET /v1/agent/{id}/wallet now returns a WalletResponse containing not just balances, but also arrays for transaction_history and allowances to power the Finance UI. integrity-oracle exposes no A2ACapitalPool read endpoint at all (confirmed against routes.rs) — integrity-dashboard's Capital Allocation page states this as a real, visible gap rather than fabricating live pool data.

15. Financial action, session integrity & Xibalba Shield wire-format additions (2026-08-01)

Normative source: spec/integrity-protocol-v0.4.md §21–§22 and spec/xibalba-shield-v1.md. Everything in this section is [PLANNED] — no package implements any of it yet. Recorded here because these are exactly the kind of cross-package schema facts this document exists to pin before two packages drift into incompatible guesses about a shared shape, per this file's own purpose.

15.1 intent_type namespace extensions (extends §4.2's BCC Commitment)

BCC Commitment.intent_type already accepts any string — no schema change is required for either table below to become valid today. What's pinned here is the vocabulary, so bcc_middleware's policy packs (docs/ENTERPRISE_ADOPTION.md Lever 3) and any future export tooling can pattern-match a stable enum instead of ad hoc strings per integrator.

Financial (protocol spec §21.2): payment_authorize, payment_capture, payment_transfer, payment_stablecoin, payment_escrow_lock, payment_escrow_release, payment_refund, payment_dispute, fx_convert, limit_reserve, limit_release, wallet_sign.

Xibalba Shield security events (Shield spec §5.6): shadow_agent_detected, agent_contained, connection_blocked, guardrail_denied, phi_access_attempt, device_posture_change.

15.2 Action Receipt (formalized name for an existing pairing, not a new object)

An Action Receipt is the existing join of a bcc_middleware policy decision to its anchor_events row by leaf hash (docs/design/evidence-export.md Phase A). No new field, no new table. The name exists so this pairing can be referenced by external audit-trail schemas (the AAT-shape research this addition is based on) without a package inventing a second, parallel concept for the same evidence.

15.3 session_integrity_v1 (optional, additive Action Receipt extension)

{
  "risk_threshold_profile_id": "session_term_v1",
  "R_session": 0.0,
  "action_ladder": "M|S|H|T|K",
  "drift_score": 0.0,
  "pin_hash": "0x...",
  "hard_override": null
}

Absence on any given commitment means "not yet implemented for this commitment," never "passed a session-integrity check that does not exist." See protocol spec §22.7 for the full definition and §22 for the risk model this field summarizes.

15.4 Xibalba Shield event schemas — where they live

ProcessActivity / FileActivity / NetworkFlow / AgentEvent / PolicyDecision are defined in full in spec/xibalba-shield-v1.md §5 — not duplicated here, to avoid the exact two-copies-drift failure mode this document exists to prevent. A PolicyDecision becomes a BCC commitment via the §15.1 intent_types above; the mapping is Shield spec §4.5, not a new oracle endpoint. integrity-oracle requires no route changes to receive Shield telemetry — it arrives through the existing POST /v1/bcc/intercept and telemetry-ingest paths (§2, §4.2 above) like any other agent's traffic.

Clone this wiki locally