Status: implemented and merged to main (2026-07-03). This is the
architecture reference — how the shipped system works. The design rationale is
docs/roadmap/TIER_5.2_TRUST_MEMBRANE.md; the
per-artefact validator model is TIER_5.2_VALIDATORS.md;
marketplace portability is TIER_5.2_PORTABILITY.md.
1. The idea in one paragraph
Trust is a visible, cryptographically-verifiable badge on an artefact, and the agent is the primary unit: a signed agent is trusted exactly when its whole composition is — its active ontology, its active skills, and the knowledge/planner repositories it consumes must each be signed 🟢. Trust is computed, never re-derived; sharing stays fully open (badge, not gate). Four states: ⚪ unsigned, 🟢 trusted (signed & intact), 🟠 changed (signed, but content drifted since), and ◒ deployment_unattested (publisher-signed and intact, but this instance's model, tool catalog and connector opt-ins have not been attested here — see §3b). Default (no attestations) = a byte-identical baseline — every artefact is ⚪.
2. Module map (src/trusted_data_agent/core/trust/)
| Module | Responsibility |
|---|---|
attestation.py |
Pure core: TrustState, content_hash, id- & order-independent composition_hash, resolve_state, resolve_agent_trust, agent_signable (bottom-up rule), the validator registry, ValidationVerdict + SignBand (clean/partial/blocked). Zero I/O. |
scanners.py |
Deterministic 0-token safety scanners: content_safety_scan (injection), code_safety_scan (AST), sql_safety_scan, markup_safety_scan (XSS), aggregate_verdict. ScanResult splits findings into blocking vs caveats. |
validators.py |
The 11 per-kind validators (ontology · skill · agent [posture, Jul 2026] · extension · knowledge · planner · flow · dashboard · canvas · assurance_module [PROP-003] · data_product [OBDA, the earned signature]), each returning a ValidationVerdict, registered by kind. agent_pack remains the aggregate/composition path with no per-kind register_validator. 11 per-kind + 1 aggregate = the 12 signable categories in §3. |
content.py |
Canonical per-kind content + version_hash resolver (skill/ontology/extension/knowledge/planner/flow/dashboard/canvas). One definition, shared by sign-off, badge state, and portability. |
composition.py |
A real profile → its ingredients (active KG + active skills + the knowledge/planner repositories it consumes) → live AgentTrust. Owns agent_version_hash, the single definition of an agent's portable signable content (ingredients + posture + capability), and folds in the deployment half via compose_agent_state. |
deployment.py |
The deployment half (§3b) — resolved LLM identity, reachable MCP tool/prompt catalog, per-profile connector opt-in. Never travels with a pack; describe_drift names what changed so re-attesting is informed. |
pack.py |
Agent-pack composition resolver — a pack is "an agent whose ingredients are its members". |
store.py |
The trust_attestations table (self-applying), Ed25519 sign/verify reusing the platform provenance key, content-bound signatures + the signer public key carried for cross-instance verification. |
portability.py |
Marketplace capture / reinstate / claim_state + the honesty gate. |
validators registry |
register_validator(kind, fn) — new kind = one function, no forks. |
REST seam: api/trust_routes.py (GET /v1/trust/agent/<id>, GET/POST /v1/trust/artifact(s),
POST /v1/trust/<kind>/<id>/sign-off, POST /v1/trust/agent/<id>/attest-deployment, POST /v1/trust/verify). UI: static/js/handlers/trustBadge.js.
3. The validator model (all 12 categories)
Every signable kind maps onto one closed archetype set — the two shipped poles (conformance, content-safety) plus code-safety, query-safety, provenance-integrity, and aggregate. Each validator is pure, deterministic, 0-token, and maps its result onto the harmonised band clean · partial · blocked.
| Artefact | kind |
Signable content | Archetype |
|---|---|---|---|
| Ontology (KG) | ontology |
entities + relationships | conformance (Tier 4.3 gate) |
| Skill | skill |
instruction text | content-safety |
| Agent (profile) | agent |
composition over ingredients + safety posture | aggregate + posture |
| Extension | extension |
handler source + manifest | code + content-safety |
| Knowledge repo | knowledge |
sorted(per-doc hash) + embedding |
integrity + content-safety |
| Planner repo | planner |
sorted(per-case hash) |
integrity + query-safety |
| Agent pack | agent_pack |
composition over members | aggregate |
| Flow | flow |
node/edge logic (positions excluded) | code + query + content |
| Dashboard | dashboard |
tile structure + data source | query-safety |
| Canvas | canvas |
markup | markup/XSS + content |
| Assurance module | assurance_module |
pack manifest + module content + capability honesty (a claimed tier must be provable) | code-safety + tier proof (ASSURANCE_MODULE_PLANE_ARCHITECTURE.md) |
| Data product | data_product |
{manifest_hash, validation_report_hash} (+ member_hashes lineage for composed) — the proof is inside the signature |
the earned signature: five machine-readable refusals, the V1–V7 gate incl. live probes (OBDA_ARCHITECTURE.md) |
The ontology-hash product exclusion (load-bearing): kg_content_hash excludes
data_product nodes and member edges — the ontology's signable content is the
knowledge model; a product is a contract stacked on it, protected by its own
attestation. Without this rule, authoring a product flipped its own ontology CHANGED
and forced a rubber-stamp re-sign loop (found live, fixed at the root — OBDA Phase A).
Full detail: TIER_5.2_VALIDATORS.md §6. Config artefacts
(MCP servers, LLM configs, vector/graph backends) are not-attestable by design — their
content is credentials, and trust ≠ access.
3a. What counts as an ingredient
An agent's ingredients are its active ontology, its active skills, and the knowledge/planner repositories it consumes.
Repositories earn their place because the compositional promise — a signed agent contains
only signed ingredients — is otherwise false for two whole agent classes. A Focus agent
is its corpus and an Optimize agent is steered by its champion cases; while repositories
sat outside the composition, an agent with a bound corpus and no KG resolved with
ingredients: [], so the bottom-up gate had nothing to gate on and signed instantly over an
unscreened corpus, a corpus edit could not flip the badge, and re-pointing the agent at a
different corpus disturbed nothing.
Applicability mirrors the runtime rather than adding a rule — attesting an artefact the agent never consults would make the badge claim more than it verifies:
| Ingredient | Counts when |
|---|---|
ontology |
it is the profile's active KG (kg_profile_assignments) |
skill |
the skillsConfig entry is active |
knowledge |
the collection is bound and the runtime would retrieve it — per class: rag_focused always (Focus retrieval is unconditional; enabled is ignored by the engine), every other class only when knowledgeConfig.enabled is truthy (runtime default false, mirroring get("enabled", False) in the engines) |
planner |
the collection is bound and the profile is a planning class (tool_enabled/genie), matching planner.py, agent_pack_manager and the ontology projection |
Kind is decided by each collection's own repository_type, never by which field named it —
knowledgeConfig.collections and ragCollections overlap.
Empty and unreadable are deliberately different outcomes. A repository with no content
has nothing to attest and is omitted, so an empty default planner repo cannot make every
agent unsignable. A repository that demonstrably holds content whose hash could not be
computed is emitted unresolved and blocks: silently omitting it would reopen exactly
the gap repositories close, this time under a transient read error. This holds for both
repo kinds: planner_cases returns a None hash when any case file is unparseable (a
corrupt case must not silently shrink the signed set) and planner_has_files is the
independent existence probe, so a populated-but-unreadable planner repo blocks rather than
vanishing. A per-collection failure emits that one repo as unresolved and continues —
it can neither silently drop the repo nor abort the loop and drop every repo after it.
IngredientRef carries the flag explicitly and one shared ingredient_state() resolves it
for both the badge and the sign gate, so what you see and what blocks can never
disagree. A binding to a deleted collection is warned and skipped — a configuration fault,
not an ingredient.
3b. Two signable halves — portable and deployment
An agent is signed in two halves, because two different questions are being asked and one hash cannot answer both:
| Half | Question | Contents | Travels? | Authority |
|---|---|---|---|---|
| portable | "is this agent the artefact that was validated?" | composition (ingredients) + posture (what it will say) + capability (what it can do) | ✅ reinstates on install | approver tier (_authorize_signer) |
| deployment | "is its resolved capability surface still what was attested here?" | resolved LLM identity · reachable MCP tool/prompt catalog · per-profile connector opt-in | ❌ never travels | ownership (_authorize_deployment_attester) |
The split is forced by the marketplace honesty gate (§8): reinstate only re-applies a
signature when the copy's live hash equals what was signed, so anything in the portable hash
must reproduce byte-identically after an id remap. A subscriber uses their LLM, their MCP
server and has opted into nothing — yet a model swap, a server quietly gaining a write
tool, and a connector opt-in are exactly the changes most worth catching. One hash cannot
both survive installation and cover them; two hashes answer both honestly.
Two halves, two authorities (the provenance-vs-policy split every trust system makes — a build system signs the artefact, the deploying org attests its own admission policy). The portable half can be vouched for by a third party, so it keeps the approver-tier gate. The deployment half is a statement of intent about a surface only its owner controls, so its authority is ownership — the profile is resolved under the caller's own uuid, and a non-owner gets 404 (never 403, which would confirm another user's profile id exists).
Routing attestation through the approver-tier gate was wrong for three reasons, all pinned by
test_deployment_attest_authority.py so it is not reinstated: (1) it could never enable a
reviewer, because that same user-scoped lookup means an approver reaches only their OWN
profiles — the gate could only lock the owner out; (2) it disabled the drift detector it
appeared to protect, since with no one able to take the baseline a model swap or a grown tool
catalog never flips the badge — a control that cannot be exercised is not a control; (3) under
require_approval the separation-of-duties rule refuses ALL self-signing, and attesting is
always self, so deployment attestation became impossible platform-wide for every tier
including admin. Ownership is the authority model for a per-user artifact — the same
conclusion TC-30 reached for leaf sign-offs. The record still carries signer_uuid +
signer_fingerprint, so a surface can distinguish an owner-attestation from an admin one
rather than flattening both to 🟢.
Enablement is content. A disabled→enabled flip changes what the agent can do while changing no artefact, so opt-in state is hashed alongside the catalog itself. Conversely absent must equal inert: a disabled session primer, an opted-out connector and a component's intensity dial hash as though they were not there, because a badge that flips on tuning is a badge people learn to ignore.
Portability normalisation. Coordinator routing stores raw profile ids that an install
remaps, so genieConfig is hashed in tag space via an injected resolver; hashing ids
would make every coordinator pack permanently 🟠. Instance-local pointers
(llmConfigurationId, mcpServerId) never enter the portable hash at all — what matters is
the resolved identity, which lives in the deployment half.
The catalog is read from the persisted classification snapshot, not a live MCP connection, so badge resolution works offline. Honest limit: an upstream server change is caught when the profile next re-classifies, not instantaneously. The timestamp is excluded, so re-classifying an unchanged server does not flip the badge.
Drift is described, not merely detected (deployment.describe_drift) — "tools appeared:
base_writeQuery(sql)", "connectors enabled: uderia-shell". An unexplained amber trains
operators to click through it.
skillsConfig is deliberately not signed: active skills are already ingredients, so
signing the block would double-count and flip on inert edits.
4. The bottom-up sign gate
agent_signable(ingredients) refuses to sign an agent unless every ingredient is already
🟢; the blocking list drives the guided "sign these first" flow. The same rule composes for
agent packs (pack.member_refs → agent_signable). Verdicts are Ed25519-signed and
bound to the exact content version (store.sign).
For a Focus agent the chain therefore runs two levels deep: the agent is blocked by its corpus, and the corpus is blocked by its own earned-signature rule (unscreened documents, or injection findings neither quarantined nor acknowledged). That is the intended shape — the badge cannot outrun the screening that justifies it.
5. Badges + sign-off UX
trustBadge.js provides the reusable primitives:
- mountArtifactBadges(root) — fills [data-trust-artifact-*] slots with a signer-aware
seal (green intact · amber changed · faint "sign off" for signers on unsigned; nothing for
non-signers). Wired across Skills, Extensions, KGs, Repos, Agent Packs, and Workspace
instances (flow/dashboard/canvas).
- mountAgentBadges(root) — the compositional agent shield on the profiles list.
- openArtifactTrust(kind, id) — the trust popover: state · offline Verify · signer-only
Sign off / Re-sign (surfaces an agent-pack's blocking members).
- mountMarketplaceTrust(root) — read-only "publisher signed" seal on marketplace cards.
6. Marketplace portability
Trust travels with content, not ids: signatures are content-bound (kind|version_hash),
so an attestation survives an id remap. Every marketplace surface uses the same two calls —
capture(kind, id) at publish, reinstate(kind, new_id, claim) at materialize — and the
honesty gate re-establishes a signature only if the copy's live hash equals what was
signed. The claim carries the signer's public key, so a cross-instance consumer verifies with
the publisher's key. Wired into KG · agent-pack (+ per-member) · skill · extension ·
component-instance; collections inherit trust free via the reference model. Full detail:
TIER_5.2_PORTABILITY.md.
7. Packaging + isolation (the Ontology Suite)
The Trust Membrane ships bundled with the Platform Ontology UX as one physically-isolatable
enterprise feature — the Ontology Suite — governed by a single switch
(core/enterprise.py, ON for all tiers today; a one-line flip makes it Enterprise-only).
Removal manifest + absence-tolerance contract: src/trusted_data_agent/core/trust/README.md.
Canonical suite description: PLATFORM_ONTOLOGY_ARCHITECTURE.md §11.
7a. What an agent's signature covers (Jul 2026)
An agent's signable content is its composition + its safety posture — the grounding, guardrail and honesty modes that decide what it will and will not say. Before this, two agents with identical ontology and skills but opposite enforcement hashed identically and both resolved 🟢: the seal attested to something other than the thing protecting the user.
authorityConfig is deliberately excluded. It governs what an agent may do on whose
behalf, is a strict subset of the deploying owner's permissions, and legitimately differs per
deployment — hashing it would make 🟠 fire as often for a correct adaptation as for
tampering. Trust ≠ access (§8) is the boundary. Authority is still portable,
downgrade-guarded, and enforced fail-closed at every tool chokepoint.
An agent whose posture sits entirely at the runtime defaults hashes exactly as before, so
existing attestations stay 🟢 and nothing re-signs; the posture dimension engages only once
there is real posture to protect. The agent kind also gained its first validator: signing
bands clean (at least one gate acts) or partial (every gate observe-only, with a caveat
naming them) — it never blocks. Full detail: TIER_5.2_POSTURE.md.
7b. Assessment hardening (Aug 2026) — the seal must not lie
A four-lens adversarial audit (TRUST_CIRCLE_DEEP_ASSESSMENT_2026-08.md)
closed a family of fail-open defects. The standing invariants it established:
- One hash space, everywhere. The agent hash is computed in tag space at ALL four
surfaces —
sign_agent, live resolve,pack.agent_live_hash, the REST artifact badge. Signing previously hashed a coordinator'sslaveProfilesin raw-id space while the badge resolved in tag space, so every signed coordinator read 🟠 instantly and no coordinator pack member could ever reinstate. - A row is not an attestation.
store.attested_hashsurfaces a recorded hash only for a genuinely-signed row (signed=1). A row written while the provenance key was absent is a row in a writable table, not a cryptographic attestation — it turns nothing green (mirroringportability.capture, which already refused to travel one). - Badge resolution fails closed. A live-hash resolver that RAN and could not confirm
the content yields 🟠
changed, never 🟢 (_NO_LIVE_HASHdistinguishes "this kind has no resolver" — the only case that legitimately falls back to signed/unsigned). - Sign-offs are ownership-gated. Every owned kind resolves its owner per kind and
fails closed on a lookup error or NULL owner (
trust_routes._assert_owner);skill/extensionare platform-level and stay tier-gated. Every successful sign/attest broadcasts a livetrust_event_notice. - An unreadable surface is UNRESOLVED, never absent. LLM identity, MCP catalog AND
connector opt-in all hash a failure as
UNRESOLVED— a persistent read failure can no longer sign a signature that silently omits an enabled shell/browser connector. - The downgrade guard covers absence. A mode/flag dropped from a present incoming
block (the shallow-merge wholesale replace) is compared against the runtime default —
holding
enforcesteady while quietly droppingpiiis a caught downgrade. verify()names its key.key_source ∈ carried|local— verification proves internal consistency with that key, not publisher identity (trusted-publisher pinning remains a deliberately deferred policy).
Deliberate, documented limits kept as-is: content_hash's type-domain collision and the
catalog entry encoding (any change invalidates every existing attestation — a fix belongs
to an explicit hash-format version bump); the sign-time TOCTOU (fail-safe direction); the
license gate's fail-open (search key TRUST_LICENSE_FAIL_OPEN — must flip with the
Enterprise gate).
8. Guarantees & scope
- Byte-identical baseline — with no attestations, every artefact is ⚪; nothing changes.
- Tamper-evident — any content edit flips 🟢 → 🟠 (hash mismatch); for an agent this now includes weakening its enforcement posture.
- Offline-verifiable — Ed25519 signatures verify with only the signer's public key.
- Honest portability — a shared copy is 🟢 only if byte-identical to the signed original.
- Trust ≠ access — a signed artefact is validated, not authorized; secrets never travel.
Downstream consumers (Jul 2026): the membrane's integrity state now feeds two live
surfaces beyond the badge — (1) Behavioral Trust Score
(AGENT_OPS_EVAL_ARCHITECTURE.md §6c): the composite
per-agent trust state = live conduct capped by this integrity reading (unsigned and
deployment_unattested both cap at "proven", changed caps at "watch" — a signature never
promotes bad conduct, and a deployment never attested here can never out-score a
portable-signed one), rendered as a
seal+ring glyph on the Agents roster; (2) trust-aware routing
(coordinate_engine.py: genieConfig.requireTrustedExperts) — a coordinator can require its
experts to resolve 🟢 at routing time, making compositional trust operative, not just visible.
Both resolve via composition.resolve_profile_trust defensively (suite absent → unknown /
loud fail-open warning), preserving the removability contract.
Remaining (minor): a cross-instance trusted-publisher policy (verification works; the allow-list is a config choice); Model B grounding (opt-in runtime enforcement that a turn only executes signed ingredients).
Tests: test/test_trust_membrane.py (132 assertions — three-state truth, hashing,
compositional truth table, all 10 validators, scanners, bands, portability incl. cross-instance
pubkey round-trip).