Privacy & Data-Classification — cross-artefact reference¶
Type: Reference Normative status: Supporting companion. Defers to 009-Instrumentation, 010-Schemas, 014-HTTPRequests, 015-Data-Classification, Tool-Pair, Conventions, and Security for every contract surface named here. This doc is the discoverability index — one place to land for "where do privacy primitives live across re-frame2's artefacts, what is the composition order, and what do I declare to keep a value out of off-box egress?"
re-frame2's privacy surface is the leak-prevention overlay on observability. Real data flows through events / cofx / handlers / fx / app-db / subs / views unchanged; sentinel substitution happens only at the observation/egress boundary. The contract spans four artefacts (re-frame.core, whose re-frame.epoch namespace carries the epoch surface; re-frame.http; re-frame.schemas; tools/mcp-base) and the owner-classification declaration sources of the EP-0015 model — frame config (durable frame-wide facts), per-slot schema props (owner-local schema'd data), and registration metadata (transient payloads) — this doc gathers them into one inventory and pins the composition order.
The model. This doc indexes the EP-0015 owner-classification +
project-egress+:rf.egress/*-profile model that Spec 015 specifies, and the EP-0017 recordable-coeffect secrets-exclusion rule (see §Recordable coeffects must exclude secrets). There are no "marking sites", no publicadd-marks/set-marksapp-db path-mark API, no schema-attached app-db classification, no positionalredact-interceptor, and nore-frame.marksnamespace — projection runs in there-frame.classification/re-frame.elisionengine (see §Removed surfaces). The normative contract lives in Spec 015; this doc is the cross-artefact index.Posture (per Spec 015 §Posture). Privacy here is observability hygiene, not authorisation. Apps still own auth, authorisation, encryption-at-rest, and transport security. The classification machinery exists so that the framework's own dev-time observability surfaces (and their downstream consumers — log sinks, AI agents, dashboards) cannot accidentally exfiltrate user secrets or stuff log lines with multi-megabyte blobs. See Security.md §Privacy / secret handling for the pattern-level threat model.
Table of contents¶
- The six observation boundaries
- What the production bundle itself discloses — the build artefact is not one of the six, and projection never runs over it
- Inventory by artefact — every imperative + declarative entry point, grouped by owning namespace
- Inventory by declaration source — same surfaces, grouped by where the author declares the classification
- The composition order (data-flow) — what runs when, from handler exit to off-box wire
- Recordable coeffects must exclude secrets — the EP-0017 secrets-exclusion rule
- Display sentinels — what observation surfaces render
- Config knobs — the two verb families and the configure-keys
- Indicator slots — what observers expose so callers know the payload was filtered
- Worked example — the canonical case
- Author guidance — the exception-path residual
- Removed surfaces
- Cross-references
The six observation boundaries¶
Privacy declarations exist to stop leaks at every framework-mediated observation/egress boundary. Per 015 §Scope the complete set, each with the egress profile Spec 015 projects it under:
| # | Boundary | What sees it | Profile / production posture |
|---|---|---|---|
| 1 | Trace-bus emit — every :rf/trace-event built by emit! / emit-error! |
Trace listeners, Xray panel, error monitors, log sinks | Dev stream gated on re-frame.interop/debug-enabled? (the CLJS mirror of goog.DEBUG); the always-on error-emit substrate (009 §Error-emit substrate) survives production |
| 2 | Xray / Story panels — Event Detail, App-DB Diff, Subscriptions, Trace, Causality Graph, Machine Inspector, Flow Panel, Story scenarios | The on-box dev tool; CLJS-only | :rf.egress/local-redacted by default; dev-only (production-elided) |
| 3 | MCP / tool wire transport — tools/re-frame2-pair-mcp, tools/story-mcp, any future MCP server |
Off-box LLM / tool consumers | :rf.egress/off-box-tool; N/A (tooling, not in the production bundle) |
| 4 | AI / LLM context lifted by tools — any code path that lifts trace events / app-db / sub outputs / machine :data into an LLM prompt |
The hosted LLM endpoint | :rf.egress/off-box-tool; N/A |
| 5 | Hosted log sinks — Datadog, Sentry, LogRocket, Honeybadger, custom fan-outs; routed by frame :observability |
Off-box ops/monitoring | :rf.egress/off-box-observability; the always-on error-emit substrate is the live path here — it survives goog.DEBUG=false, so sensitive-projection MUST work in production builds |
| 6 | Epoch export, SSR / hydration, public error responses, HTTP diagnostics, schema-validation failure records | Off-box recorders, the browser, hosted dashboards | each its own profile — :rf.egress/ssr-hydration (after the §SSR allowlist), :rf.egress/public-error, :rf.egress/off-box-observability |
The contract for the off-box boundaries (3 / 4 / 5 / 6) is project before egress: the runtime projects every record under the owning frame's classification and the boundary's :rf.egress/* profile via project-egress before any sink, tool, or wire sees it; a sink never receives a raw record. On the always-on trace/error substrate, a record carrying :sensitive? true at the top level is dropped by the off-box forwarder. Apps opt back in by passing the off-box-wire opt — the MCP tool argument is the unqualified :include-sensitive (the Anthropic tool-input-schema regex rejects a trailing ?; see tools/mcp-base/spec/sensitive.md §Cross-server arg-vocabulary), which resolves to the :rf.egress/local-raw profile — or, for an on-box panel, the {:show-sensitive? true} UI toggle. The off-box-wire verb family is named include-sensitive? (the ? rides the config-knob verb, not the MCP wire key). See Conventions §Privacy config-knob naming.
What the production bundle itself discloses¶
Every boundary above is runtime egress — a record leaving the process, projected on its way out. A production JavaScript bundle is a different kind of surface. It is a build artefact, served to and readable by anyone who can fetch the app's JS, and no classification declaration reaches it: project-egress projects records under a frame's classification, and a bundle is neither. One thing rides into it that an author would not necessarily predict. It is deliberate and it is not user data — what it discloses is the code's own structure.
Source-tree paths, one per macro-driven registration. Per 001 §Production elision contract, source-coord capture has two sinks and only the first is stripped in production. Sink 2 — the always-on error coordinate on each registration's descriptor — is retained under :advanced + goog.DEBUG=false on purpose, so that Sentry / Honeybadger / Rollbar reports keep source-line information in the builds that actually break. Its :file is the classpath-relative path the reader supplied (acme/payments/events.cljs), the same in every checkout, so a production bundle carries the source tree's layout and each registration's line — code structure that :ns already discloses — as ordinary string literals, and never the building machine's environment. When a build supplies only an absolute path (the native ClojureScript compiler and a JVM load-file do), the coordinate omits :file and ships {:ns :line}. The dev coordinate keeps the absolute on-disk path for editor tooling, and it is eliminated from a release bundle.
A production error record shipped off-box carries one only when the implementation that ran was registered through a reg-* macro: the record reads the coordinate off the descriptor the failing frame actually ran. Two shapes carry none and are the common ones: a programmatic registration that bypassed the macro path (a programmatic re-registration that replaced a macro one included), and an id that was never registered. In each of those the emitter conditionally omits the slot, so the record arrives with no :source-coord key at all rather than one holding nil (the same partial-shape caveat Spec-Schemas §:rf/source-coord-meta records for programmatic registrations). The bundle-level disclosure is unconditional regardless: the paths are string literals in the shipped artefact whether or not any record ever quotes one.
A :rf.error/frame-destroyed record always has an addressed id to name: the category's :op realm enum (009 §Error event catalogue) has exactly three values (:dispatch / :dispatch-sync / :subscribe), so every emit in the category is a :dispatch, :dispatch-sync or :subscribe against an addressed id — but the enum bounds the value of :op, not its presence. Per the same 009 row the slot is present wherever the emit site knows the realm (the subs recover-and-emit path is subscribe-realm by construction and stamps it unconditionally), and absent on the ordinary address-directed :dispatch recover-and-emit path, which keeps its tight keyset. An off-box consumer may read :op as one of exactly three values where it appears, and must not require a named realm on every such record. The category reaches the wire coordinate-less only by the two shapes above — a :subscribe-realm record whose sub-id is unregistered omits the slot rather than resolving a same-keyword event's coord.
There is no knob and no projection profile for this; the retention is the contract.
None of this is a leak of anything a classification declaration could have covered, so it has no remedy in this document's vocabulary. It is recorded here because "what does a shipped bundle disclose" is a privacy question an author will land on this page to ask.
Inventory by artefact¶
The complete imperative + declarative surface, grouped by owning namespace. Every entry's normative owner lives in the cited Spec section; this table is the index, not the contract.
re-frame.core (production-survivable subset re-exported from artefacts below)¶
| Surface | Kind | Purpose | Owner |
|---|---|---|---|
:sensitive |
reg-meta key | Vector of paths into the registration's primary data shape (event arg-map, fx-input map, cofx value, sub output, flow output, machine transition payload) | 015 §Registration-owned transient classification |
:large |
reg-meta key | Symmetric to :sensitive — paths to slots elided with :rf.size/large-elided |
015 §Registration-owned transient classification |
reg-classification |
registration ((rf/reg-classification id {:sensitive [paths] :large [paths]})) |
The owner of durable app-db classification — a closed declaration of app-db-absolute :rf/path vectors, registered beside the feature that writes them and resolved through each frame's own view into its classification policy. There is no frame-config :sensitive {:app-db …} annotation and no add-marks / set-marks / declare-sensitive-*! surface. |
015 §Static app-db classification |
subsystem :sensitive / :large |
subsystem registration (reg-machine, reg-resource, reg-mutation, reg-route) |
Projection-relative durable classification declared on a subsystem definition (e.g. a machine's :data-rooted :rf/path vectors), applied at egress to each instance present, re-rooted to the instance's absolute runtime path, and unioned into the frame's classification policy. |
015 §Subsystem projection-relative classification |
:rf.http/managed :carriers |
reg-fx :rf.http/managed meta |
App-specific HTTP carrier names (a union onto the immutable built-in defaults) — the EP-0025 transient-payload case. (rf/reg-fx :rf.http/managed {:carriers {:headers […] :query-params […]}} h). |
015 §HTTP carriers |
frame :observability |
frame config (make-frame) |
Observability sink policy. There is no frame :sensitive key — durable app-db classification is reg-classification above, and HTTP carriers ride :rf.http/managed; a frame config :sensitive is rejected fail-loud. |
015 §Frame-owned observability sink policy |
project-egress |
record-level boundary primitive | (rf/project-egress record opts) → projected record. The public record-level projection primitive (knows app-db-/event-/exception-/HTTP-/summary-shaped slots); the required step before any off-box sink. Delegates per tree-shaped slot to elide-wire-value. A façade export (subject to the facade-export classification rule). |
015 §project-egress |
register-observability-sink! / unregister-observability-sink! |
façade fns | Register the concrete sink fn against the id a frame's :observability policy names; the sink consumes the already-projected record. Façade exports (subject to the facade-export classification rule). |
015 §Frame-owned observability sink policy |
sensitive? |
predicate | (rf/sensitive? trace-event) → bool. True iff the event carries a truthy :sensitive? stamp at the top level. The framework-published predicate every forwarder composes against; there are no tool-side aliases over it. Fail-closed: true is sensitive, false / nil / absent are not, and any OTHER truthy value (a string "true", a keyword :yes, a number) counts as SENSITIVE — the schema types the stamp as a boolean, so a non-boolean is a contract violation, and a (true? …) reading would forward the event in exactly the case where the producer has already proved unreliable. Matches the posture re-frame.mcp-base.sensitive/sensitive-stamp? applies on the MCP wire. |
009 §Privacy |
re-frame.elision/elide-wire-value |
walker (framework-internal — no façade export, no manifest row) | (re-frame.elision/elide-wire-value v opts) → walked v. The low-level value walker for tree-shaped values; project-egress delegates to it per tree-shaped slot. Sinks and tools never call it directly — project-egress with a named :rf.egress/* profile is the one door. |
API.md §wire-elision walker, 009 §Size elision |
(configure! {:elision ...}) |
runtime config | {:rf.egress/threshold-bytes N} — wire-elision size cap. Default 16384. |
API.md §Configure keys |
Not present (listed so a reader looking for one of these names lands here):
:rf.egress/output-sensitivityreg-meta key — the derived-output declassification claim + its:rf.egress/inherit/:sensitive/:publicvalue set; classification does not propagate, so there is nothing to declassify (per 015 §No propagation, no taint).- the frame-config
:sensitive/:large {:app-db …}durable annotation — durable app-db classification isreg-classificationabove; a frame config:sensitive {:app-db …}is rejected fail-loud. :sensitive/:large/:clear-sensitive/:clear-largeasreg-eventeffect-map keys — a handler returning one is refused pre-commit with:rf.error/effect-map-shape(per 002 §Classification is a registration, not an effect).redact-derived-slots— a value-match / taint dual ofelide-wire-value; absent from the façade and fromre-frame.elision(value-match is propagation by another name). The only derived-tree egress boundary isproject-egressover a:rf.observe/derived-treerecord, path-walked against the frame's classification (per 015 §project-egress).populate-elision-from-schemas!/populate-sensitive-from-schemas!— there is no schema→classification bridge (schemas describe shape, not durable app-db egress policy — per 015 §Schemas describe shape).
re-frame.http¶
HTTP carrier policy. The HTTP fx maps headers + query-strings + the decoded response body into :rf.http/* trace events; their classification surfaces:
| Surface | Kind | Purpose | Owner |
|---|---|---|---|
| Built-in header / query-param denylists | framework default (immutable) | Closed sets of always-sensitive header / query-param names redacted in every :rf.http/* trace event regardless of the request :sensitive? flag — the name is the signal. No frame can remove a built-in name. |
014 §1–2 |
| Managed-HTTP carriers | declarative (reg-fx :rf.http/managed meta) |
(rf/reg-fx :rf.http/managed {:carriers {:headers [..] :query-params [..]}} h) — app-specific carrier names that union onto the immutable built-in defaults (the transient-payload case). There is no frame :sensitive {:http …} block and no process-global declare-sensitive-*! mutator. |
014 §HTTP carriers |
| Response-body classification | declarative (:decode schema) |
Per-slot :sensitive? / :large? props on the request's :decode Malli schema classify the decoded body. Whole-body root prop redacts everything; an unschematized body fails closed off-box. |
014 §Response-body classification |
:sensitive? (per-call) |
request arg | {:rf.http/managed {:sensitive? true}} — opts a specific request in. When true, the request body is redacted to the sentinel and all query params are scrubbed (broader than the denylist). Sugar form: {:request {:sensitive? true}}. |
014 §Privacy |
Built-in denylists ship populated with the obvious cross-app names (authorization, cookie, x-api-key, set-cookie, ...; api_key, access_token, auth, token, ...). App-specific carrier names (X-MyApp-Auth, shop_token) are declared on the :rf.http/managed reg-fx registration via the :carriers block — there is no frame :sensitive {:http …} block and no process-global declare-sensitive-header! / declare-sensitive-query-param! mutator.
re-frame.schemas (declarative — no imperative surface)¶
Schema-attached slot props. On an HTTP request's :decode schema they classify the decoded response body; elsewhere they drive only schema-validation error-trace redaction, and only where the runtime validates against that schema. They are not a route for durable classification — app-db, or machine / resource / mutation state — per 015 §Schemas describe shape; there are no schema→classification hydrators (populate-elision-from-schemas! / populate-sensitive-from-schemas!; see the note above).
| Surface | Kind | Purpose | Owner |
|---|---|---|---|
:sensitive? true |
schema slot prop | Per-slot Malli property {:sensitive? true} on a schema slot. On an HTTP :decode schema, which validates the response, it classifies the decoded response body. On a schema the runtime validates against — an event, coeffect, fx or sub schema, a machine [:schemas :data], a resource or mutation :params-schema — it drives validation-failure-trace redaction only. On an app-schema slot it still redacts that schema's failures, but an app-db path's failure traces follow the path's reg-classification, so the path is declared once. A resource :data-schema is shape metadata reflected to tooling and nothing validates against it, so there the prop drives nothing. Durable machine / resource state is classified by the owner's projection-relative :sensitive paths, per 015 §Schemas describe shape. Schema-validation error traces consult the prop (:value / :received / :explain / :rf.fx/args / :rf.sub/query-v redaction). |
010 §:sensitive?, 015 §Machine-owned |
:large? true |
schema slot prop | Symmetric to :sensitive? for the size axis. On an HTTP :decode schema it elides the matching slot of the decoded response body to :rf.size/large-elided. On a schema the runtime validates against — a machine [:schemas :data], a resource or mutation :params-schema, an app-schema slot — validation-failure traces consult it and substitute :rf.size/large-elided for the matching slot value. A resource :data-schema is shape metadata that nothing validates against, so there the prop drives nothing. (Not a durable app-db, machine or resource route — those ride reg-classification and the owner's projection-relative :large paths.) |
010 §:large? |
re-frame.epoch¶
Per EP-0015 issue 6, epoch records are causal replay material (per EP-0010) and there is no storage-side mutation — the raw record stays in the ring and every register-epoch-listener! listener receives it unmutated; off-box egress MUST project through project-egress under an off-box profile, mapped over epoch-history when the whole ring egresses. There is no :redact-fn hook: a forwarder needing a further scrub for slots the frame's classification cannot prove composes one at the sink, (-> r (rf/project-egress opts) scrub).
| Surface | Kind | Purpose | Owner |
|---|---|---|---|
:rf.epoch/sensitive? |
record-level rollup | Top-level boolean on the assembled :rf/epoch-record — true iff any captured trace event / declared-sensitive leaf in the record was sensitive. Computed at build-time from the stamped trace events and the non-nil leaves at the sensitive paths the frame's classification declares in the raw record's db, so it stays an accurate off-box-branch signal on the raw ring record. It means the record contained classified material, not that the run touched a secret. The re-frame2-pair MCP surfaces it as an annotation on the projected record it forwards; an external forwarder may drop on it. |
Tool-Pair §Time-travel |
The :kind :rf/epoch-record arm of project-egress |
per-kind projector (late-bound, never public) | (rf/project-egress record) — off-box-safe projection of a :rf/epoch-record. The one public door dispatches the record's stamped :kind to epoch's private projector (late-bound as :epoch/project-record); there is no second public name, because the door names the BOUNDARY, not a record kind. Routes each tree slot through elide-wire-value under the named profile, strips raw :db-before / :db-after, keeps the structured fields (:trigger-event, :fx, :halt-reason, :schema-digest, :rf.epoch/sensitive?, :rf.epoch/redacted-modified-paths-count). The single projection site when shipping epoch data off-box. Idempotent. The whole ring is (mapv #(rf/project-egress % opts) (rf.epoch/epoch-history frame-id)). |
Tool-Pair §Direct-read privacy |
:rf.egress/include-fx-args? / :rf.egress/include-runtime-db? / :rf.egress/include-event-args? |
trusted-local egress opts | The three epoch-only axes on project-egress — effect :args, the :rf.db/runtime frame-state partition, and trigger / trace event args. They are NOT app-db axes: each defaults false and lifts only its own boundary, so :rf.egress/include-sensitive? true never implies any of them. All six axes share the :rf.egress/* namespace, and these three ride the one project-egress door. |
Tool-Pair §Time-travel |
tools/mcp-base (cross-MCP wire egress)¶
The framework-published privacy filter every MCP forwarder composes. Apps don't author against this directly — MCP server implementations do, conforming to the cross-server vocabulary.
| Surface | Kind | Purpose | Owner |
|---|---|---|---|
sensitive-event? |
predicate | Fail-closed predicate over a trace-event map — true for a literal true AND for any other truthy stamp (which additionally logs and bumps a malformed counter). The zero-framework-dependency Node-bundle mirror of re-frame.privacy/sensitive?, which cannot load re-frame.privacy; the two agree on the fail-closed reading. |
tools/mcp-base/spec/sensitive.md |
strip-sensitive |
walker | (strip-sensitive coll) → [kept dropped-count]. The dropped-count becomes the :dropped-sensitive envelope counter on the MCP response. |
tools/mcp-base/spec/sensitive.md |
:include-sensitive |
cross-MCP wire arg | Per-call opt-in on every MCP tool surfacing trace-like data. Defaults to false. The wire-key spelling is uniform across every server — story-mcp and re-frame2-pair-mcp both ship the unqualified :include-sensitive (no trailing ? — the Anthropic tool-input-schema regex ^[a-zA-Z0-9_.-]{1,64}$ rejects ?). The ? appears only on the internal walker option (:rf.egress/include-sensitive?) and the config-knob verb (include-sensitive?), never the MCP wire key. |
tools/mcp-base/spec/sensitive.md §Cross-server arg-vocabulary, Conventions §Privacy config-knob |
:rf.size/large-elided (elision marker) + :rf.egress/include-large? (walker opt) |
cross-MCP wire vocabulary | Size-elision peer of :sensitive?. The walker substitutes :rf.size/large-elided {:bytes N :head "..." :handle ...} at over-threshold or :large?-declared slots; off-box callers opt in with {:rf.egress/include-large? true}. |
tools/mcp-base/spec/elision.md, 009 §Size elision |
Inventory by declaration source¶
Same surfaces, regrouped by the owner that declares the classification (015 §The ownership split): reg-classification declarations beside the feature classify durable app-db state; machine / resource / mutation definitions classify owner-local schema'd data (projection-relative); registration metadata classifies transient payloads (including the :rf.http/managed :carriers block — HTTP carrier names); frame config carries frame-local egress facts (observability sink policy, SSR allowlist).
reg-classification (durable app-db)¶
Durable app-db classification is declared once, beside the feature that writes the paths; every frame's egress resolves it through the frame's own view, value-independently:
(rf/reg-classification :auth {:sensitive [[:auth :token]]})— classify durable app-db sensitive paths (:rf/pathvalues)(rf/reg-classification :docs {:large [[:docs :csv-upload]]})— classify durable app-db large paths(rf/clear :classification :auth)— remove a declaration; re-registering the id replaces it
There is no frame-config :sensitive / :large {:app-db …} annotation and no add-marks / set-marks app-db path-mark API (see §Removed surfaces). Per 015 §Static app-db classification.
Frame config (frame-local egress facts)¶
The frame carries observability sink policy (there is no :sensitive key — durable app-db classification is reg-classification and HTTP carriers ride :rf.http/managed; a frame config :sensitive is rejected fail-loud):
(rf/make-frame {:id :app :observability {:handled-events […] :errors […]}})— production sink policy (§Frame-owned observability sink policy)- The SSR hydration allowlist is NOT frame config: it is the SSR host adapter's
:payloadopt (top-level app-db keys and paths, a path naming a classified leaf being the permit for the user's own browser to receive it raw) — the allowlist-first SSR/hydration boundary, per 011 §:rf/app-dbprojection
Frame config installs atomically at frame creation (before the :initial-events setup runs); re-registering replaces it wholesale. HTTP carrier names are not frame config — they ride the :rf.http/managed reg-fx registration's :carriers block (see the next section); there is no frame :sensitive {:http …} block and no process-global declare-sensitive-header! / declare-sensitive-query-param! mutator (see §Removed surfaces).
Per-slot schema props (owner-local schema'd data)¶
{:sensitive? true} / {:large? true} Malli props on an HTTP request's :decode schema classify the decoded response body — the transient product that schema owns. Whole-shape claims are the degenerate root-prop case; an unschematized HTTP body is whole-sensitive (fail-closed). On a schema the runtime validates against — a machine [:schemas :data], a resource or mutation :params-schema, an app-db schema — the props redact only that schema's own validation-failure traces. A resource :data-schema is shape metadata that nothing validates against, so its props drive nothing. None of these props classify durable state or feed the frame's classification policy. Durable machine / resource / mutation state is classified by projection-relative :sensitive / :large paths on the machine, resource or mutation spec, applied at egress to each instance present. Per 015 §Machine-owned, §Resource and mutation, §HTTP response bodies. (Schema props on an app-db schema are not a route to durable app-db classification — that rides reg-classification; there are no schema→classification hydrators, per 015 §Schemas describe shape.)
Registration metadata (transient payloads)¶
reg-event / reg-sub / reg-fx / reg-cofx / reg-flow accept :sensitive / :large (vectors of paths) into the registration's primary data shape. Empty path [[]] marks the whole shape. (There is no :rf.egress/output-sensitivity declassification key — classification does not propagate, so there is nothing to declassify; to expose a derived secret safely, classify only the paths you mean to redact.)
| Reg kind | Path root | Owner |
|---|---|---|
reg-event |
the event arg-map (second element of [:event-id {arg-map}]) |
015 §Registration-owned transient classification |
reg-sub |
the sub's output value (classify the output paths directly — no propagation, no declassification claim) | 015 §Registration-owned transient classification |
reg-fx |
the fx-input map | 015 §Registration-owned transient classification |
reg-cofx |
the coeffect value ([[]] = the whole value) — see also §Recordable coeffects must exclude secrets |
015 §Registration-owned transient classification |
reg-flow |
the flow's :output value (classify the output paths directly — no propagation, no declassification claim) |
015 §Registration-owned transient classification |
Machine transition payloads are transient payloads classified by the transition/registration metadata that introduces them — not by the machine's durable :data policy. Durable runtime-subsystem state (resource/mutation) is owned by its definition (projection-relative :sensitive / :large paths on the spec), not classified as a transient payload merely because it is declared by reg-resource / reg-mutation.
HTTP carriers (:rf.http/managed registration) — quick reference¶
(rf/reg-fx :rf.http/managed {:carriers {:headers ["X-MyApp-Auth"]}} h)— header carrier; unions onto the immutable built-in header denylist(rf/reg-fx :rf.http/managed {:carriers {:query-params ["my_token"]}} h)— query-param carrier; unions onto the built-in query-param denylist{:rf.http/managed {:decode <malli-schema-with-:sensitive?-props>}}— per-slot response-body classification{:rf.http/managed {:sensitive? true ...}}— per-call opt-in (body redaction + ALL params scrubbed)
The composition order (data-flow)¶
The single most-asked question this doc answers: what runs when, in what order, between handler exit and off-box wire? The order is fixed and documented in pieces across 009 / 014 / 015 / Tool-Pair — this section pins it in one place.
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. HANDLER BODY runs with REAL VALUES │
│ - Event handler sees the raw event arg-map (via :event coeffect) │
│ - Cofx values, app-db reads, fx args — all unredacted │
│ - This is by design — the handler MUST see real values to do its job │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. SPEC 015 CLASSIFICATION PROJECTION (classification/project-trace-event) │
│ - The trace bus chokepoint walks :tags for per-registration marks │
│ declared at reg-time (`:sensitive [paths]` on the registration meta). │
│ - Substitutes :rf/redacted at sensitive paths, :rf.size/large-elided │
│ at large paths inside the per-tag shape (events under :event, fxs │
│ under :fx-args, cofx under :coeffects, subs under :value, machines │
│ under :before / :after / :snapshot). │
│ - No propagation: a sub reading a sensitive app-db path does not │
│ inherit it — classify the sub's own output paths. │
│ - Stamps :sensitive? true at the top level of the trace event. │
│ - Gated on `re-frame.interop/debug-enabled?` — production CLJS bundles │
│ DCE this away. │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. HTTP-SPECIFIC REDACTION (re-frame.http.privacy/prepare-emit-tags) │
│ - For :rf.http/* trace events only. │
│ - `redact-headers` walks the :headers map, replaces values whose name │
│ is in the header denylist with :rf/redacted (unconditional — denyy- │
│ listed names are the signal). │
│ - `redact-url-query-string` walks the :url string, replaces query- │
│ param values whose name is in the query-param denylist (unconditional).│
│ - When `:sensitive? true` is the per-call flag: also scrubs :body and │
│ ALL params (broader than the denylist). │
│ - `:sensitive? true` stamped on the trace event when ANY scrub fired │
│ (denylist hit OR per-call opt-in OR upstream from path-mark). │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. TRACE-BUS EMIT — every listener receives the redacted, stamped event │
│ - Dev-only listeners (Xray, story recorder, dev panels): consult │
│ :sensitive? at top level; on-box dev panels render an opaque indicator│
│ and require `:trace/show-sensitive? true` to reveal. │
│ - Always-on error-emit substrate listeners (production-survivable): │
│ consult :sensitive? and drop the whole event by default at off-box │
│ egress (Sentry/Honeybadger/Datadog forwarders). │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. EPOCH ASSEMBLY (re-frame.epoch/build-record) │
│ - Per-frame, once per dequeued event. │
│ - sensitive-rollup computes :rf.epoch/sensitive? from the raw record's │
│ schema-declared sensitive leaves at build-time. │
│ - The RAW record is appended to the ring and delivered to every │
│ register-epoch-listener! listener UNMUTATED — there is NO │
│ storage-side mutation (EP-0015 issue 6): epoch records are EP-0010 │
│ causal replay material, and mutating them at rest would corrupt │
│ the replay contract. │
│ - Redaction runs at step 6 (off-box egress), not here. │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 6. OFF-BOX PROJECTION (rf/project-egress → │
│ re-frame.elision/elide-wire-value) │
│ - `project-egress` is the record-level boundary primitive; it │
│ delegates per tree-shaped slot to `elide-wire-value` (the value │
│ walker, single emission site for :rf/redacted + :rf.size/large- │
│ elided) under the frame's classification + the boundary's │
│ :rf.egress/* profile. │
│ - The :kind :rf/epoch-record arm strips raw :db-before / │
│ :db-after under the frame/profile projection, never at │
│ storage time. A forwarder may compose a further scrub over │
│ its RESULT, at the sink. │
│ - The structured :effects rows' :args (raw fx-handler payload, not │
│ app-db-rooted so the walker cannot prove it safe) FAIL CLOSED to │
│ :rf/redacted off-box, lifted only by :rf.egress/include-fx-args? true. │
│ - `elide-wire-value` walks tree-typed payloads against the frame's │
│ classification policy (reg-classification, flow-output and │
│ subsystem declarations — union at lookup time). │
│ - Composition rule: sensitive drop WINS over large elision when both │
│ apply at the same path (the size marker would otherwise leak :path / │
│ :bytes / :digest). │
│ - Default `{:rf.egress/include-sensitive? false :rf.egress/include-large? false}` — │
│ maximum elision unless the caller explicitly opts in. │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 7. MCP TOOL EGRESS (tools/mcp-base/sensitive + elision) │
│ - Cross-MCP filter a dropping server (story-mcp) runs on its tool │
│ response payloads. re-frame2-pair-mcp drops nothing: it forwards │
│ the step-6 projection, :rf.epoch/sensitive? riding as a label. │
│ - `strip-sensitive` returns [kept dropped-count]; populates the │
│ `:dropped-sensitive` envelope counter (omitted when zero). │
│ - `:elided-large` envelope counter sums the `:rf.size/large-elided` │
│ substitutions. │
│ - The counters ride alongside the payload as unqualified keys so the │
│ calling agent recognises filtering without re-inferring from absence. │
└─────────────────────────────────────────────────────────────────────────────┘
Rule summary¶
- Composition is additive at every site. A
reg-classificationpath, a flow output declaration and a subsystem's projection-relative declaration all join the frame's one classification policy, and each redacts at every observation surface — they union. - Sensitive wins over large at the same path. 015 §
:rf/redacted {:bytes N}and 009 §Size elision in traces. The sensitive drop suppresses the size marker because the marker carries:path/:bytes/:digestwhich would themselves leak. - HTTP denylists are upstream of the trace stream. They run inside
prepare-emit-tags/prepare-emit-failurebeforetrace/emit!fires — they shape the trace event itself, not its downstream consumers. Per Spec 014 §Privacy. - Real values are never redacted mid-handler. The handler body reads the unredacted
:eventcoeffect. Each observation is projected at its own emit / egress boundary, before delivery — whether it is raised before, during or after the handler runs. - Production has one live path: the always-on error-emit substrate. Everything else (dev trace bus, epoch ring, schema-validation traces, Xray) elides via
goog.DEBUG. The error substrate honours:sensitive?in production — that's the load-bearing case for substrate-level enforcement. - runtime-db is redacted/omitted off-box by default (EP-0001). Off-box egress of frame-state — the epoch arm of the
project-egressboundary, Xray-MCP, and pair recorders — redacts or omits the runtime-db side of frame-state by default; the off-box default fails closed. Only the app-db partition (subject to its own:sensitive?/:large?elision) and explicitly allowlisted serializable runtime-db facts cross the wire. The SSR hydration payload likewise ships only the serializable runtime-db facts the client needs to reconstitute (machine snapshots, route slice, SSR metadata, per 011 §The:rf/hydrateevent), never transient runtime side-channel state. A trusted-local tool may request richer runtime-db diagnostics explicitly (the same opt-in shape as:rf.egress/include-sensitive?for app-db); off-box / AI / log egress fails closed. This is the runtime-db peer of the app-db:sensitive?default: app-db redacts at marked paths, runtime-db redacts/omits wholesale unless explicitly opted in. The normative statement is 009 §Privacy / sensitive data in traces.
Recordable coeffects must exclude secrets¶
Recordable coeffects fold host facts into durable frame-state — values written into the :rf.cofx envelope map and re-presented verbatim by replay (the discipline: durable state folds facts, never reads; see 002 §The recordable-coeffect rule, EP-0017). A recordable coeffect is durable — it lands in every epoch record, replay fixture, and exported trace. That durability is the threat model for credentials:
Crypto-grade randomness, tokens, nonces, session ids, and key material MUST NOT be minted or carried as recordable coeffects. Recording a secret does not make it safe; it makes it durable — copied into every recording, fixture, and exported trace. Secrets are generated at the edge and handled by guarded runtime mechanisms, off the ledger.
The rule is a normative review discipline, not a structural guarantee: app-owned reg-cofx suppliers mint whatever value they return, so the framework cannot prove a value is not a secret. The enforcement surface is this guide's secrets material plus a recommended lint (a handler that writes durable state declaring an ambient-grade id — "durable state folds facts, never reads", mechanically checkable). See EP-0017 §Security Considerations.
Projection composes the same way for cofx values as for event payloads. Each :rf.cofx leaf follows the same projection / redaction rules as event-arg values — classify the cofx's value shape per-slot (reg-cofx :sensitive / :large, or the cofx's registered :schema props), and the off-box projection redacts it like any other transient payload. The framework's one built-in recordable fact, :rf/time-ms, is classified always-safe and never redacted. So the cofx surface has two complementary obligations:
- Off-box egress redaction — a recordable coeffect that is legitimately sensitive (e.g. a non-secret-but-private fact a fold needs) is classified and projected like any transient payload; it is never shipped raw off-box.
- The secrets exclusion (above) — a secret must not be a recordable coeffect at all, because redaction does not undo durability: the raw value still lives in the on-box ring, the local epoch, and any trusted-local raw read. The two rules are not substitutes — exclusion is the load-bearing rule for credentials; projection is the safety net for the merely-private.
The analogous obligation already holds for resource scopes / params (classified, projected — but not a place to put a secret) and for HTTP response bodies (fail-closed when unschematized). Recordable coeffects extend that posture to the input-fold side.
Display sentinels¶
Per 015 §Display contract and API.md §wire-elision walker:
| Sentinel | When | Drillable? |
|---|---|---|
:rf/redacted (opaque keyword) |
Sensitive content. Carries no information about the underlying value — not its type, not its size, not a hash, not a prefix. | No. A tool that offers a "show original" affordance against :rf/redacted is non-conformant. |
:rf.size/large-elided {:path [...] :bytes N :type :map :hint "..." :handle [:rf.elision/at path] :digest "sha256:..." (when:include-digests?true)} |
Large content; size diagnostic without leaking content. The :hint rides from the schema's :hint prop. |
Yes for on-box panels with size-confirmation modal; no for off-box egress by default. |
:rf/redacted at a path also marked large |
Sensitive + large composed — sensitive wins. The size marker is suppressed entirely (the marker payload would leak :path / :bytes). |
No. (Per 015 §:rf/redacted {:bytes N} — the Spec contemplates a :rf/redacted {:bytes N} composed form preserving the size diagnostic; the CLJS reference suppresses the marker entirely. Both are conformant — readers should not depend on :bytes being present alongside :rf/redacted.) |
All three sentinel keywords are framework-reserved per Conventions §Reserved namespaces — apps MUST NOT use them as legitimate payload values.
Config knobs¶
The two verb families that decide whether a sensitive value passes through a consumer. The verb encodes the trust boundary. Per Conventions §Privacy config-knob naming:
| Verb | Where | Default | Trust boundary |
|---|---|---|---|
show-sensitive? (the verb) / per-tool :rf.<tool>/egress-profile (the knob) |
On-box devtools panels (Xray, Story trace panel) — each tool owns its own knob and resolves it per (tool, frame) pair: (xray-config/configure! {:rf.xray/egress-profile :rf.egress/local-raw}) or xray-config/set-egress-profile!, and Story's :rf.story/egress-profile via config/set-session-egress-profile! (session pin) / config/set-frame-egress-profile! (one frame). There is no process-global on/off toggle and no cross-tool shared atom; see tools/xray 015-Configuration.md §Egress profile. |
:rf.egress/local-redacted (suppress; fail-closed) |
The panel is for the operator running this process; the profile controls UI visibility, not egress. Revealing a frame is an explicit, trace-visible operator act. |
include-sensitive? (the verb) / :rf.egress/include-sensitive? (the opt) |
Off-box wire egress (MCP servers, hosted-LLM preload, error monitors, Datadog/Sentry forwarders) | false (suppress) |
The toggle controls whether sensitive values cross the process trust boundary. |
Both default to suppress per Spec 009's default-private posture. A sixth consumer adding a knob picks the verb by trust-boundary class — on-box panel → show-sensitive?; off-box wire → include-sensitive?.
Configure-keys that touch privacy¶
Per API.md §Configure keys and 015:
(rf/configure! {<key> {...}}) |
Privacy-relevant opt | Default | Purpose |
|---|---|---|---|
:elision |
:rf.egress/threshold-bytes N |
16384 |
Wire-elision size threshold — advisory, not a cap. An over-threshold value at an undeclared path fires :rf.warning/large-value-unschema'd but ships unchanged; only declared / schema-marked entries elide. Non-negative integer; 0 disables the auto-detect warning. |
:epoch-history |
:depth N / :trace-events-keep N |
depth 50, trace-events-keep 50 |
Bounds the ring (doesn't redact; bounds the surface). |
Indicator slots¶
Counters that ride alongside MCP tool responses so the calling agent knows the payload was filtered, without re-inferring from absence. Per Conventions §Reserved indicator slots:
| Slot | Meaning | Where | Owner |
|---|---|---|---|
:dropped-sensitive |
Integer count of leaves the walker dropped because they matched :sensitive? true. Omit when zero. |
MCP response envelope (unqualified key) | Cross-MCP convention |
:elided-large |
Integer count of leaves replaced with the :rf.size/large-elided marker. Omit when zero. |
MCP response envelope (unqualified key) | Cross-MCP convention |
[● REDACTED N] / [● ELIDED N] |
Panel-chrome mirror of the MCP slots for on-box surfaces (Xray, story panel) | Panel chrome (not JSON) | Conventions §Reserved panel-chrome surface |
The walker also emits a top-level :rf.epoch/redacted-modified-paths-count on :rf/epoch-record values — apps can detect "this many classified sensitive slots changed under the sentinel" without re-walking.
Worked example — password in app-db + token header on HTTP¶
The canonical question: "I have a :password field in app-db and a :token header on an HTTP request — what do I declare where to keep both out of off-box egress?"
;; 1. Declare the durable app-db classification with reg-classification,
;; beside the feature that writes the paths. It is value-independent and
;; holds before any event runs, so a declared path is redacted from its
;; first egress. (App-specific HTTP carriers ride the :rf.http/managed
;; registration — see step 3; the observability sink policy lives on the
;; frame map.)
(rf/reg-classification :auth
{:sensitive [[:auth :password]
[:auth :token]
[:auth :refresh-token]
[:user :ssn]]})
;; App-specific HTTP carrier names ride the :rf.http/managed registration's
;; :carriers block (EP-0025 §HTTP carriers). The frame map carries frame
;; concerns such as observability sink policy, not carriers.
(rf/reg-fx :rf.http/managed
{:carriers {:headers ["X-MyApp-Session"]}}
re-frame.http.managed/managed-handler)
;; 2. Declare the event-arg-side mark on the login handler — the password
;; arrives in the event arg-map before it lands in app-db.
(rf/reg-event :auth/log-in
{:sensitive [[:password] [:totp-code]]}
(fn [{:keys [db]} [_ {:keys [email password totp-code]}]]
;; The handler sees real password / totp-code values.
;; The trace event sees [:auth/log-in {:email "..."
;; :password :rf/redacted
;; :totp-code :rf/redacted}].
{:fx [[:rf.http/managed
{:method :post
:url "/api/login"
:body {:email email :password password}
:sensitive? true ;; per-call opt-in — body + ALL params scrubbed
:on-success [:auth/log-in-success]
:on-failure [:auth/log-in-failure]}]]}))
;; 3. The app-specific auth-token header carrier `X-MyApp-Session` was
;; declared on the :rf.http/managed registration's :carriers block above.
;; The built-in defaults already cover `authorization` / `x-api-key` /
;; `cookie` / `set-cookie`; the carrier unions onto those immutable
;; defaults (EP-0025 §HTTP carriers).
;; 4. The on-success event receives the JWT in the response payload. Mark
;; its event arg so the trace surface sees :rf/redacted there too.
(rf/reg-event :auth/log-in-success
{:sensitive [[:jwt] [:refresh-token]]}
(fn [{:keys [db]} [_ {:keys [jwt refresh-token user]}]]
;; Writing the JWT into app-db [:auth :token] — the reg-classification
;; from step 1 means downstream Xray renders the path as :rf/redacted.
;; Classification does NOT propagate: a sub/flow reading this path does
;; not auto-inherit — classify the derived output path too (see step 5).
{:db (-> db
(assoc-in [:auth :token] jwt)
(assoc-in [:auth :refresh-token] refresh-token)
(assoc-in [:user :id] (:id user)))}))
;; 5. (Optional) — a subscription reading from a sensitive path does NOT
;; inherit its input's classification (EP-0025: no propagation, no taint).
;; A derived prefix is a NEW value at a new output path — it ships raw
;; unless YOU classify the output. There is nothing to "declassify": if a
;; sanitised derived value is safe, simply do not classify it; if a derived
;; value is itself sensitive, declare the sub's own output path `:sensitive`:
(rf/reg-sub :auth/token-prefix
{:inputs [[:db/auth]]}
(fn [[auth] _] (str (subs (:token auth) 0 8) "..."))) ;; sanitised — ships raw
;; 6. (Optional) — compose a defence-in-depth scrub AT THE FORWARDER for
;; slots no classification covered (raw exception messages, custom
;; :trace-events slots). It runs over the value project-egress
;; returned, so the in-process ring stays raw (causal replay material).
(defn- scrub-exception-messages [record]
(update record :trace-events
#(mapv (fn [ev]
(cond-> ev
(= :error (:op-type ev))
(update :tags dissoc :exception-message)))
%)))
(rf/register-listener! :epoch ::ship
(fn [record]
(-> record rf/project-egress scrub-exception-messages ship!)))
What every observation surface sees after the drain settles:
| Surface | Observation |
|---|---|
Handler body (:auth/log-in) |
Real password value in :event coeffect (via the regular handler arg) |
Trace bus :rf.event/dispatched |
[:auth/log-in {:email "..." :password :rf/redacted :totp-code :rf/redacted}], top-level :sensitive? true |
Trace bus :rf.fx/handled for :rf.http/managed |
:rf.fx/args body and params scrubbed (per-call :sensitive? true); :headers X-MyApp-Session value :rf/redacted (denylist hit) |
Trace bus :rf.event/db-changed |
[:auth :token] slot renders :rf/redacted (the step-1 reg-classification that declares [:auth :token]) |
| Xray App-DB Diff panel | Same as above (Xray projects via project-egress under :rf.egress/local-redacted, consulting the same frame classification) |
MCP get-app-db tool response |
:rf/redacted at the marked slots (projected under :rf.egress/off-box-tool); :dropped-sensitive N envelope counter set to the count of dropped leaves |
| Off-box log shipper (Datadog/Sentry) | Routed by frame :observability under :rf.egress/off-box-observability; drops the whole :rf.event/dispatched and :rf.fx/handled events (top-level :sensitive? true); ships the structural skeleton only |
| Always-on error-emit substrate (production survives) | The error record carries :sensitive? true and the listener-side projection honours it before egress to Sentry |
Epoch record through rf/project-egress |
All of the above redactions, applied at egress and never at storage; the structured :effects rows' :args fail closed to :rf/redacted off-box (lifted only by :rf.egress/include-fx-args? true); the in-process ring + listener fan-out see the RAW record. A forwarder may compose a further scrub over the projected value |
What's NOT covered by this declaration set:
- An
ex-infomessage that interpolates the password into the string ((throw (ex-info (str "User " email " failed login") {...}))) — the path walker can't resolve into a string substring. See §Author guidance — the exception-path residual below. - An
ex-datamap whose author-chosen key name ({:user/email "..."}) has no relationship to the path-marked declarations. Substitute:rf/redactedat the assembly site, or omit the key.
Author guidance — the exception-path residual¶
Classification declarations are projected at the six observation boundaries named above. Projection walks known data shapes; it does NOT walk exception messages or ex-data map keys. The residual surface — the handler read a sensitive value AND threw with that value in ex-message or ex-data — is author responsibility. Per 015 §Author guidance for the exception-path residual and Security §Author guidance for exceptions under path-level :sensitive?:
| Anti-pattern | Preferred |
|---|---|
(throw (ex-info (str "User " email " failed login") {:user/email email :reason :invalid-credentials})) — leaks email into :exception-message and :exception-data |
(throw (ex-info "Invalid credentials" {:reason :invalid-credentials})) — name the category in the message; correlate via :dispatch-id against the (correctly redacted) :db-before snapshot |
Author-named ex-data keys carrying the sensitive value |
Substitute :rf/redacted at the assembly site, or omit the key entirely |
The framework does NOT ship a safe-throw helper — which ex-data keys correspond to sensitive paths in this specific app is app-level knowledge. A twelve-line per-app safe-throw helper is the recommended shape; worked example at docs/core §24.08 — Exceptions under :sensitive?.
Removed surfaces¶
Surfaces that do not exist, listed so readers don't search for them.
| Surface | Use instead |
|---|---|
add-marks / set-marks (public app-db path-mark API) |
Durable app-db egress policy is declared by a reg-classification beside the feature, not a post-creation imperative mutation. There is no marks API or marks.cljc namespace; the projection substrate is the elision engine. |
declare-sensitive-header! / declare-sensitive-query-param! (and clear-*!) |
App-specific HTTP carrier names belong on the :rf.http/managed reg-fx registration (:carriers {:headers […] :query-params […]}), union onto the immutable built-in defaults — not process-global mutation and not a frame annotation. |
frame :sensitive {:http …} carrier block |
A frame config carries no :sensitive {:http …} carrier block (there is no :sensitive frame key at all). App-specific HTTP carrier names ride the :rf.http/managed reg-fx registration's :carriers block (the transient-payload case). A frame config :sensitive is rejected fail-loud. |
redact-interceptor (public positional interceptor) |
Registration-owned :sensitive classifies event payload paths; centralized project-egress projects at egress. |
Schema-attached :sensitive? / :large? as the public app-db classification route |
Schemas describe shape; durable app-db egress policy rides reg-classification. Per-slot props classify only an HTTP :decode body and redact a schema's own validation-failure traces; they are not a route for durable app-db, machine or resource state. There are no schema→classification hydrators (populate-elision-from-schemas! / populate-sensitive-from-schemas!). |
inject-cofx (public cofx-injection interceptor) |
Coeffect dependencies are declared with :rf.cofx/requires registration metadata; reg-cofx is value-returning + graded. There is no inject-cofx (calling it is the hard error :rf.error/inject-cofx-removed). Named here because cofx values are a classification surface. |
Handler-meta :sensitive? registration flag |
Use Spec 015 per-path declarations. A handler that is the unit of sensitivity (the rare "this whole run is sensitive" case) declares the path-marks that the handler reads / writes. |
:rf.fx/sensitive-mode configure key |
Use per-call {:sensitive? true} on :rf.http/managed args. The name set-trace-redaction-policy is not in re-frame.core. |
rf/safe-throw framework helper |
Author-level concern; per-app helpers fit the local convention better than a framework default. Worked-example shape lives in the docs/core. |
Cross-references¶
Primary contract owners¶
- 015-Data-Classification — the normative spec for the EP-0015 owner-classification +
project-egress+:rf.egress/*model. The Spec; this doc is the cross-artefact index. - EP-0015 (Frame-Owned Egress Policy) — the proposal Spec 015 specifies; its twelve resolved open issues carry the rationale.
- EP-0017 (Recordable Coeffects) — the recordable-coeffect surface and its secrets-exclusion §Security Considerations (see §Recordable coeffects must exclude secrets).
- 009-Instrumentation §Privacy / sensitive data in traces — the canonical trace-surface privacy posture:
:sensitive?top-level stamp, consumer-side default-drop, the always-on error-emit substrate's posture. - 009-Instrumentation §Size elision in traces — the size-elision peer of sensitive marking.
- 010-Schemas §
:sensitive?and 010-Schemas §:large?— per-slot schema props and schema-validation error-trace redaction. - 014-HTTPRequests §Privacy — HTTP-specific denylists, the
:rf.http/managed:carriersblock, and the per-call:sensitive?request arg. - Tool-Pair §Time-travel — the
project-egressoff-box egress boundary for epoch records, and post-projection scrubbing as composition at the forwarder. - Tool-Pair §Direct-read privacy posture — the MCP wire-egress contract for direct-read tools.
Cross-cutting conventions¶
- Conventions §Reserved namespaces (framework-owned) — the
:rf/,:rf.size/,:rf.elision/namespaces this surface reserves. - Conventions §Reserved indicator slots (MCP-shaped returns) —
:dropped-sensitive,:elided-largeenvelope counters. - Conventions §Privacy config-knob naming —
show-sensitive?(on-box) vsinclude-sensitive?(off-box) verb split. - Security §Privacy / secret handling — the pattern-level threat model and the behavioural MUSTs.
Implementation cross-references¶
tools/mcp-base/spec/sensitive.md— cross-MCPsensitive-event?predicate,strip-sensitivewalker and the:include-sensitivearg vocabulary (the unqualified MCP wire key; the?appears only on the internal:rf.egress/include-sensitive?walker option and the config-knob verb).tools/mcp-base/spec/elision.md— cross-MCP elision walker; the size opt-in it resolves to is the framework walker opt:rf.egress/include-large?(the:rf.egress/*egress vocabulary, not a wire arg of its own).
API.md projection¶
- API.md §wire-elision walker —
elide-wire-value,project-egress. - API.md §Privacy —
sensitive?. - API.md §Configure keys — the four
(rf/configure! ...)keys, including:elisionand:epoch-history.
Author-side guide¶
- docs/core §23a — Privacy: keeping secrets out of traces — guide-side worked-example tour for declaring
:sensitive?on schema slots. - docs/core §23b — Large blobs — guide-side companion for
:large?declarations. - docs/core §24.07 — Privacy and elision in practice — operational config walkthrough.
- docs/core §24.08 — Exceptions under
:sensitive?— the per-appsafe-throwconvention and the three patterns for the exception-path residual.