You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Wayfinder design ticket: #193. Map: #182. Immune-system lane. Promise: Immune system ("external content fills parameters, never gives instructions"). Users: Bell, Auditor. Design, not implementation.
Sibling specs this binds to: typed owner identity #197 (PrincipalId), disclosure gating on external egress #204 (which named the two extension points below). Research ruling: #190 / research/immune-system-design-inputs — provenance is HYBRID (compile-time untrusted-content typing + runtime label/lineage in the ledger); nothing raw crosses the quarantine boundary; the branch-steering residual is bounded by grants, not eliminated.
Problem Statement
A briefcase section today carries a disclosure_class sensitivity (Public/Internal/Private/Sensitive) and its payload is referenced by digest, but it does not record whose data it is. ClassifiedBriefcaseItem is { item_ref, disclosure_class }; check_egress walks the set of sensitivity classes × relationship × egress class against relationship-scoped policy. So the deterministic egress gate can tell that an item is "Private" but cannot tell a Client's Private datum from a Vendor's Private datum. It has no typed-identity dimension.
That missing dimension is exactly the Immune system gap for Bell: a stranger (an unauthenticated conversational principal) must never receive another counterparty's — or the owner's internal — information in a reply, even when both are the same sensitivity class. Today nothing in the deterministic gate stops "internal/other-counterparty data in a reply to a stranger" or cross-counterparty leakage, because the gate cannot see origin identity. For the Auditor, a disclosure decision can be reconstructed by (sensitivity class, relationship, egress class) but not by which identity's data was evaluated — origin is not an audit dimension.
The disclosure-gating design (#204) deliberately left two extension points for this ticket, and the codebase already contains the proven identity-bound-provenance pattern (learned_artifacts::Provenance::ProducedBy { source_event_id, source_exchange, source_scope: Ulid }, consulted deterministically via SQL predicates and DB triggers, fail-closed, with the counterparty_erasure table). Typed owner identity (#197) is introducing PrincipalId(Ulid). The pieces exist; what is missing is a label that binds provenance to typed identity and is consulted deterministically at context assembly and egress.
Solution
Introduce a provenance label: an immutable, kernel-minted value carried by each classified briefcase item that records the item's typed-identity origin alongside its existing disclosure_class sensitivity and its source lineage (producing event + producing exchange digest). Generalize the proven Provenance::ProducedBy lineage shape rather than inventing a new abstraction: widen its identity binding from a bare counterparty-scope Ulid to a closed, typed ProvenanceOrigin.
The label is then consulted deterministically at two points, with no LLM or agent judgment ever involved:
Context assembly (kernel briefcase packing) derives each classified item's origin from the resolved typed identity of its source, instead of leaving origin unrecorded. A worker-visible section that cannot be given an origin fails closed for rated egress.
External egress gains a second ordered check, inserted at the seam Spec: disclosure gating on external egress #204 named, after the existing disclosure-coverage stage: an origin-vs-recipient closure. An outbound item whose origin is a counterparty identity other than the bound recipient counterparty (and is not the owner, system, or public) is blocked unless a covering policy authorizes it. This is the Bell "internal data to a stranger" / cross-counterparty failure mode closed deterministically.
Authority to widen that closure extends through the grant, mirroring the existing egress-class caveat exactly: a new Caveat::ProvenanceLabelAllowlist + SupportedCaveatKind::ProvenanceLabelAllowlist + an effectively_allows_provenance_label narrowing helper, compiler-enforced and fail-closed on unknown caveats, with worker sub-grants provably narrowed. The dead gate()EgressClassifier seam stays deleted (#183) and is not resurrected.
The label binds to the typed identity vocabulary, not to strings: the owner arm reuses #197's PrincipalId; the counterparty arm uses a sibling IdentityRef(Ulid) newtype under the same discipline. Identity is not authority (D-006): the label records origin only and carries zero authority fields.
The hybrid split (per #190): the compile-time half is that the label is a kernel-minted, unforgeable typed value with no worker-reachable constructor (mirroring the kernel-only Briefcase mutators), and its origins are typed newtypes that cannot be a raw string a worker sets; the runtime half is the label/lineage carried in the ledger and consulted at the gate. The broader inbound UntrustedContext<T> quarantine-decode wrapper is a distinct adjacent lane concern and is out of scope here.
User Stories
As Bell, I want every classified datum the kernel packs to carry the typed identity it originated from, so the runtime can later tell one counterparty's data from another's rather than seeing only a sensitivity class.
As Bell, I want an outbound reply to an unauthenticated stranger to be blocked deterministically when it would carry another counterparty's data, so a crafted conversation can never talk a worker into leaking a third party's information.
As Bell, I want the owner's internal data (owner-origin, non-public) to be blocked from a stranger-facing reply unless a policy authorizes it, so "internal data in a reply to a stranger" cannot happen by omission.
As Bell, I want the origin decision made from a kernel-derived typed-identity binding, never from an LLM's judgment about the text, so a compromised worker cannot assert "this is fine to send."
As the Auditor, I want each disclosure decision reconstructible by exactly which typed-identity origin, sensitivity class, relationship, and egress class it was evaluated against, so I can verify no cross-identity data crossed the boundary without a matching authorization.
As the Auditor, I want the origin binding to be immutable and append-only, so a later authorization or reconfirmation never rewrites what a datum's origin was at production time.
As the Auditor, I want a datum whose origin cannot be resolved to be treated as most-restrictive (fail closed), so the absence of an origin is never mistaken for "safe to send."
As Lyra's owner (trusted owner's assistant), I want owner-origin data labelled with my typed PrincipalId, so my own information is distinguishable from any counterparty's and is never conflated with a stranger's.
As Lyra's owner, I want authority to permit an origin to egress to a given scope to travel through the grant chain as a caveat that can only narrow, never widen, so answering "yes" once cannot silently broaden what a later sub-grant may disclose.
As a developer minting a worker sub-grant for one counterparty's task, I want the sub-grant provably closed to that counterparty's (plus owner/public/system) origins by an empty-list-narrows caveat, so a worker handling counterparty X's conversation is structurally unable to egress counterparty Y's data.
As a developer, I want the label's owner origin to reuse Spec: Typed Owner Identity #197's PrincipalId and the counterparty origin to be a sibling IdentityRef(Ulid) newtype, so origins are type-safe and carry no authority (D-006), and non-Ulid shapes are impossible by construction.
As a developer extending the model later with tenancy, I want the typed-identity origin binding to be the mechanism a future principal-facing visibility class (customer-visible/staff/owner) rides on, so that axis can be added without redesigning provenance.
As the Auditor, I want a single authoritative sensitivity vocabulary (DisclosureClass) with a deterministic mapping from the duplicate DataClassification (Unknown -> most-restrictive), so two enums can never disagree about what an item's sensitivity is.
As the Unattended workhorse (kernel-origin/proactive sends, no principal present), I want kernel-origin outbound content to pass the identical origin-closure check as worker-requested content, so an autonomous loop cannot become a second ungated path for cross-identity leakage.
Implementation Decisions
Generalize the proven lineage pattern (not a new abstraction). A provenance label reuses the Provenance::ProducedBy shape — source_event_id, source_exchange (digest), and the producing-identity binding — widening that binding from source_scope: Ulid to a closed typed ProvenanceOrigin. The label additionally carries the item's disclosure_class. It supersedes ClassifiedBriefcaseItem's role of "sensitivity + digest" by adding the identity dimension. (Considered and rejected: a new provenance edge table keyed by a typed id, and a bolt-on Option<origin> field on ClassifiedBriefcaseItem — both fragment the already-proven, trigger-enforced pattern.)
ProvenanceOrigin is a closed enum:Owner(PrincipalId), Counterparty(IdentityRef), System. IdentityRef(Ulid) is a new newtype mirroring Spec: Typed Owner Identity #197's PrincipalId discipline (serializes as Ulid.to_string(), zero authority, D-006). RelationshipKind/RelationshipTier/ChannelTrust are derived attributes of an identity that the egress check reads; they are never the origin key. A CounterpartyRef::Unresolved (no identity_id) yields no origin and fails closed for rated egress.
Kernel-minted and immutable. The label has no worker-reachable constructor (mirrors the kernel-only Briefcase mutators / AD-032 confused-deputy defense). A caller-supplied value can only widen scrutiny (be treated as more restrictive), never narrow it. This is the compile-time half of the hybrid.
Context assembly derives origin from resolved typed identity. Kernel briefcase packing (provenance_from_sections and the pack path) records each classified worker-visible item's origin from the identity resolved for its source, rather than leaving origin unrecorded. A worker-visible section with no resolvable origin returns the existing fail-closed unclassified error for rated egress.
Grant-side extension point, exact mirror of egress-class. Add Caveat::ProvenanceLabelAllowlist { origins }, SupportedCaveatKind::ProvenanceLabelAllowlist, and effectively_allows_provenance_label(grant, origin) — the same append-only-narrowing, empty-list-narrows, fail-closed-on-unknown-caveat pattern as EgressClassAllowlist/effectively_allows_egress_class. The caveat is designed and enforced in v1, but the v1 single-owner owner grant is minted without it (no narrowing today); worker sub-grants adopt it. MAC discipline preserved (AD-148): the caveat is chain-appended, never a root-field mutation.
gate() stays pure; the dead EgressClassifier stays dead. The origin check lives at the same kernel-side dispatch step as the existing disclosure hook (after gate() returns Allow), never inside openspine-gate. The dead _egress: &dyn EgressClassifier parameter (Fold the settlement and reservation shape into the Effect Truth issues #183) MUST NOT be resurrected.
Uniform across dispatch origin. Worker-requested and kernel-origin/proactive dispatches (Unattended workhorse) pass through the identical origin-closure check — no second ungated path.
One authoritative sensitivity vocabulary.DisclosureClass is authoritative; DataClassification maps deterministically into it with Unknown -> Sensitive (most-restrictive, fail-closed). The prose-level mapping is fixed here; the code migration that removes the duplicate is its own implementation ticket.
No LLM/agent judgment anywhere. Every consult compares typed fields or digests only, following the counterparty_erasure SQL-predicate / DB-trigger precedent.
Testing Decisions
Good tests here assert only external behaviour — given a label set (origins × sensitivity), a relationship, a recipient, an egress class, and a policy/grant set, does the deterministic core return Allow or Block with the right escalation — never internal call sequencing.
Pure origin-closure check beside check_egress in disclosure_policy.rs: table-driven over origin × recipient × sensitivity × egress class × policy coverage × caveat. Prior art: the inline check_egress#[cfg(test)] mod tests and disclosure_tests.
effectively_allows_provenance_label grant helper: unit, exactly mirroring the effectively_allows_egress_class tests, including empty-list-narrows and fail-closed-on-unknown-caveat.
Kernel origin derivation (provenance_from_sections extended): unit — a resolved counterparty section carries Counterparty(IdentityRef); an owner section carries Owner(PrincipalId); an unresolved section fails closed.
Dispatch-integration (disclosure_regression_tests): production-entering cases — an item whose origin is counterparty X blocked from egress to counterparty Y; a kernel-origin (non-worker) dispatch hitting the same origin check.
Invariants: immutability/append-only of the origin binding; D-006 (label carries no authority); AD-148 MAC preservation when the caveat is appended. Prior art: Spec: Typed Owner Identity #197's MAC and D-006 invariant tests; learned_artifacts erasure SQL-predicate tests.
Out of Scope
Implementation. This is a design spec; landing code is the to-tickets tickets that follow.
The inbound UntrustedContext<T> quarantine-decode wrapper and schema-bounded extraction — a distinct adjacent Immune-system concern (the quarantine boundary), not this label.
Capability-derived tool catalogs — the third Immune-system lane, pre-inference, no overlap.
The principal-facing visibility class values (customer-visible/staff/owner from Bell invariant I4) — a tenancy concern, fenced post-fit-review (DIRECTION.md). This spec only names the typed-identity origin binding as the mechanism that axis will ride on.
Multi-tenant policy storage/isolation — Bell v1 is instance-per-tenant.
Resurrecting or re-purposing gate()'s dead EgressClassifier.
Canon cited: D-006 (identity != authority), AD-032/AD-121 (kernel-owned briefcase, no worker mutators), AD-140 (producing scope recorded in the provenance edge, not inferred), AD-146 (single owner principal v1), AD-148 (kernel-owned MAC-covered grant fields), AD-060 (typed egress classes; caveat mirror), D-107 (budget-scoped standing rules). The new load-bearing decision — "provenance and visibility labels bound to typed identity, consulted deterministically at context assembly and egress; kernel-minted, immutable, fail-closed, no LLM judgment" — should be recorded as a new D-0XX by the map owner / implementing change.
Glossary proposals (for the map owner to fold into CONTEXT.md, not applied by this worker): provenance label — an immutable, kernel-minted value carried by a classified datum, binding its typed-identity origin to its sensitivity class and source lineage; provenance origin — the typed identity a datum originated from (owner principal, counterparty identity, or system); consulted deterministically, never by LLM judgment.
Wayfinder design ticket: #193. Map: #182. Immune-system lane. Promise: Immune system ("external content fills parameters, never gives instructions"). Users: Bell, Auditor. Design, not implementation.
Sibling specs this binds to: typed owner identity #197 (PrincipalId), disclosure gating on external egress #204 (which named the two extension points below). Research ruling: #190 /
research/immune-system-design-inputs— provenance is HYBRID (compile-time untrusted-content typing + runtime label/lineage in the ledger); nothing raw crosses the quarantine boundary; the branch-steering residual is bounded by grants, not eliminated.Problem Statement
A briefcase section today carries a
disclosure_classsensitivity (Public/Internal/Private/Sensitive) and its payload is referenced by digest, but it does not record whose data it is.ClassifiedBriefcaseItemis{ item_ref, disclosure_class };check_egresswalks the set of sensitivity classes × relationship × egress class against relationship-scoped policy. So the deterministic egress gate can tell that an item is "Private" but cannot tell a Client's Private datum from a Vendor's Private datum. It has no typed-identity dimension.That missing dimension is exactly the Immune system gap for Bell: a stranger (an unauthenticated conversational principal) must never receive another counterparty's — or the owner's internal — information in a reply, even when both are the same sensitivity class. Today nothing in the deterministic gate stops "internal/other-counterparty data in a reply to a stranger" or cross-counterparty leakage, because the gate cannot see origin identity. For the Auditor, a disclosure decision can be reconstructed by (sensitivity class, relationship, egress class) but not by which identity's data was evaluated — origin is not an audit dimension.
The disclosure-gating design (#204) deliberately left two extension points for this ticket, and the codebase already contains the proven identity-bound-provenance pattern (
learned_artifacts::Provenance::ProducedBy { source_event_id, source_exchange, source_scope: Ulid }, consulted deterministically via SQL predicates and DB triggers, fail-closed, with thecounterparty_erasuretable). Typed owner identity (#197) is introducingPrincipalId(Ulid). The pieces exist; what is missing is a label that binds provenance to typed identity and is consulted deterministically at context assembly and egress.Solution
Introduce a provenance label: an immutable, kernel-minted value carried by each classified briefcase item that records the item's typed-identity origin alongside its existing
disclosure_classsensitivity and its source lineage (producing event + producing exchange digest). Generalize the provenProvenance::ProducedBylineage shape rather than inventing a new abstraction: widen its identity binding from a bare counterparty-scopeUlidto a closed, typedProvenanceOrigin.The label is then consulted deterministically at two points, with no LLM or agent judgment ever involved:
Authority to widen that closure extends through the grant, mirroring the existing egress-class caveat exactly: a new
Caveat::ProvenanceLabelAllowlist+SupportedCaveatKind::ProvenanceLabelAllowlist+ aneffectively_allows_provenance_labelnarrowing helper, compiler-enforced and fail-closed on unknown caveats, with worker sub-grants provably narrowed. The deadgate()EgressClassifierseam stays deleted (#183) and is not resurrected.The label binds to the typed identity vocabulary, not to strings: the owner arm reuses #197's
PrincipalId; the counterparty arm uses a siblingIdentityRef(Ulid)newtype under the same discipline. Identity is not authority (D-006): the label records origin only and carries zero authority fields.The hybrid split (per #190): the compile-time half is that the label is a kernel-minted, unforgeable typed value with no worker-reachable constructor (mirroring the kernel-only
Briefcasemutators), and its origins are typed newtypes that cannot be a raw string a worker sets; the runtime half is the label/lineage carried in the ledger and consulted at the gate. The broader inboundUntrustedContext<T>quarantine-decode wrapper is a distinct adjacent lane concern and is out of scope here.User Stories
PrincipalId, so my own information is distinguishable from any counterparty's and is never conflated with a stranger's.DisclosurePolicyKey/DisclosurePolicyshape, so I extend the sequence rather than redesign it.PrincipalIdand the counterparty origin to be a siblingIdentityRef(Ulid)newtype, so origins are type-safe and carry no authority (D-006), and non-Ulidshapes are impossible by construction.DisclosureClass) with a deterministic mapping from the duplicateDataClassification(Unknown-> most-restrictive), so two enums can never disagree about what an item's sensitivity is.Implementation Decisions
Provenance::ProducedByshape —source_event_id,source_exchange(digest), and the producing-identity binding — widening that binding fromsource_scope: Ulidto a closed typedProvenanceOrigin. The label additionally carries the item'sdisclosure_class. It supersedesClassifiedBriefcaseItem's role of "sensitivity + digest" by adding the identity dimension. (Considered and rejected: a new provenance edge table keyed by a typed id, and a bolt-onOption<origin>field onClassifiedBriefcaseItem— both fragment the already-proven, trigger-enforced pattern.)ProvenanceOriginis a closed enum:Owner(PrincipalId),Counterparty(IdentityRef),System.IdentityRef(Ulid)is a new newtype mirroring Spec: Typed Owner Identity #197'sPrincipalIddiscipline (serializes asUlid.to_string(), zero authority, D-006).RelationshipKind/RelationshipTier/ChannelTrustare derived attributes of an identity that the egress check reads; they are never the origin key. ACounterpartyRef::Unresolved(noidentity_id) yields no origin and fails closed for rated egress.Ownerarm reusesPrincipalId; the implementation ticket that introduces the origin type is blocked by Introduce PrincipalId newtype and OwnerPrincipal aggregate #199 (PrincipalId introduction). Owner-origin data is never labelled with a rawUlidor an"owner"string.Briefcasemutators / AD-032 confused-deputy defense). A caller-supplied value can only widen scrutiny (be treated as more restrictive), never narrow it. This is the compile-time half of the hybrid.provenance_from_sectionsand the pack path) records each classified worker-visible item's origin from the identity resolved for its source, rather than leaving origin unrecorded. A worker-visible section with no resolvable origin returns the existing fail-closed unclassified error for rated egress.check_egress) gains a second stage after disclosure-coverage: origin-vs-recipient closure. An item whoseProvenanceOriginis aCounterpartyother than the bound recipient (and notOwner/System/Public) blocks unless covered. No change toDisclosurePolicyKey/DisclosurePolicyshape. The stage is inserted at the core-vs-preparation seam Spec: disclosure gating on external egress #204/Prefactor: extend EgressClass and split the disclosure core from web-search preparation #205 established.Caveat::ProvenanceLabelAllowlist { origins },SupportedCaveatKind::ProvenanceLabelAllowlist, andeffectively_allows_provenance_label(grant, origin)— the same append-only-narrowing, empty-list-narrows, fail-closed-on-unknown-caveat pattern asEgressClassAllowlist/effectively_allows_egress_class. The caveat is designed and enforced in v1, but the v1 single-owner owner grant is minted without it (no narrowing today); worker sub-grants adopt it. MAC discipline preserved (AD-148): the caveat is chain-appended, never a root-field mutation.gate()stays pure; the deadEgressClassifierstays dead. The origin check lives at the same kernel-side dispatch step as the existing disclosure hook (aftergate()returns Allow), never insideopenspine-gate. The dead_egress: &dyn EgressClassifierparameter (Fold the settlement and reservation shape into the Effect Truth issues #183) MUST NOT be resurrected.DisclosureClassis authoritative;DataClassificationmaps deterministically into it withUnknown -> Sensitive(most-restrictive, fail-closed). The prose-level mapping is fixed here; the code migration that removes the duplicate is its own implementation ticket.counterparty_erasureSQL-predicate / DB-trigger precedent.Testing Decisions
Good tests here assert only external behaviour — given a label set (origins × sensitivity), a relationship, a recipient, an egress class, and a policy/grant set, does the deterministic core return Allow or Block with the right escalation — never internal call sequencing.
check_egressindisclosure_policy.rs: table-driven over origin × recipient × sensitivity × egress class × policy coverage × caveat. Prior art: the inlinecheck_egress#[cfg(test)] mod testsanddisclosure_tests.effectively_allows_provenance_labelgrant helper: unit, exactly mirroring theeffectively_allows_egress_classtests, including empty-list-narrows and fail-closed-on-unknown-caveat.provenance_from_sectionsextended): unit — a resolved counterparty section carriesCounterparty(IdentityRef); an owner section carriesOwner(PrincipalId); an unresolved section fails closed.disclosure_regression_tests): production-entering cases — an item whose origin is counterparty X blocked from egress to counterparty Y; a kernel-origin (non-worker) dispatch hitting the same origin check.learned_artifactserasure SQL-predicate tests.Out of Scope
UntrustedContext<T>quarantine-decode wrapper and schema-bounded extraction — a distinct adjacent Immune-system concern (the quarantine boundary), not this label.gate()'s deadEgressClassifier.Further Notes