Sensitive Content Stays Out of Logs

Node content is whatever an installation stores, and an enterprise client may store health or personal data in it. The store guards that content with access control. A log line has none: it is shipped to Loki, read by whoever reads the instance's logs, and — through Red-Log Watching & Ticketing — pasted into GitHub issues. A body that reaches a log has left every access rule the store applied to it.

So the rule is:

A log line names a message; it never carries one. It carries the type, the id, sender → target and the delivery state — enough to find the message and the code that handled it — and not the body. Rendering a body is a deployment's explicit, documented opt-in, off by default.

And beside it: every path that moves node content OFF the instance has a deployment key that turns it off, so an instance holding sensitive content can close the egress without a code change.

Marking a property: [SensitiveData]

using MeshWeaver.Messaging;   // a standard using of every in-mesh compile

public record PatientRecord([SensitiveData] string? Diagnosis = null)   // a positional parameter
{
    public string? Ward { get; init; }
    [SensitiveData] public DateOnly? BirthDate { get; init; }           // a property, any type
    public ImmutableList<Visit> Visits { get; init; } = [];             // nested: marked members inside are found
}

SensitiveDataAttribute (namespace MeshWeaver.Messaging, assembly MeshWeaver.Messaging.Contract) is the ONE attribute for sensitive content. It goes on a property, a field, or a positional record parameter (no property: target needed). It changes nothing for an authorised reader: the value is stored, read, rendered in an area and returned by an MCP get exactly as before. It changes every rendering that leaves the access rules behind — there the value becomes ‹redacted› (SensitiveDataAttribute.Placeholder; null stays null) and the member stays visible, so a log still shows that the field was there.

It is found by reflection on the CLR type, so it works on a content type compiled LIVE from a NodeType's node source exactly as on a platform type: the in-mesh compile references MeshWeaver.Messaging.Contract, and the mesh-wide content-type registry resolves a compiled type's $type for untyped JSON.

[PreventLogging] is the older, different control: it OMITS a member from log renderings (built for large payloads). [SensitiveData] replaces the value. Both are honoured by the same logging serializer.

The one redaction point

MeshWeaver.Messaging.Serialization.SensitiveDataRedaction is the only place that decides what a rendering may show. Every log/egress path goes through it:

API For
hub.GetLoggingSerializerOptions() the hub's write-only LOGGING options, built once per hub. [PreventLogging] members stripped, [SensitiveData] values redacted.
hub.RenderForLog(value) any object into a log line or an exception text. Never throws.
hub.RedactJson(json) JSON TEXT (a payload, a tool argument); text that does not parse is never echoed — only its length.
hub.SensitiveValues(content) + SensitiveDataRedaction.ScrubText(text, values) FREE TEXT written about content (a gist an agent wrote): the content's marked values are removed from it. Values under 3 characters and booleans are not collected.

How the redaction reaches every shape:

Covered paths

Path What it does now
Message logging, bodies opted in (Messaging:LogMessageBodies=true) the body is rendered through the logging options: marked values redacted. Off (the default), only the summary is logged.
A packaged payload in a log or a failure report (RawJson, MessageSizeGuard refusals, the oversized-NACK omission) identified by its byte count and $type only. The head of the payload used to be quoted, and the first fields of a payload are content.
Changed-data Debug logging (WorkspaceStreams) rendered through the logging options (it used the hub's own options).
MeshNode.ToString() (any {node} in a log line or exception text) prints path, NodeType, version, state and the content's TYPE — never the content, which printed every member through its own ToString().
Data-binding conversion failures (GetDataBoundValue, ConvertString, integer range checks) name the types and the length, never the bound value — and do not log the exception, whose message quotes its input (The input string '…' was not in a correct format).
Storage diagnostics (FileSystemStorageAdapter tiny-write trace) the length only.
The error-signature digest, incident filing template-only already (MeshWeaver.Plugins); kept and tested there.
MCP server logs, AI thread execution logs, feedback hand-over MeshWeaver.Plugins — see its Observability and Feedback pages.

What is NOT covered

What a log line carries

MessageService writes one line about a delivery on each of these paths, and every one of them renders the delivery through LogSummary — SensitiveRequest (ID: …, client/1 -> host/1, Failed):

Path Level Carries
A handler threw Error the summary and the exception
A handler outlived the disposal timeout Warning the summary and the duration
No handler matched the message Warning (the delivery failure) the message's type name and id
Start-processing / deserializing Debug the summary (unchanged — this was already the rule on the hot path)

Two shapes that used to carry the body, and no longer do:

The exception is still logged in full: an exception's message is the handler's own text, not the payload. What this page does not establish is that no handler ever puts content into its exception message; a handler that does is a defect in that handler.

The opt-in: Messaging:LogMessageBodies

Key Default Effect
Messaging:LogMessageBodies false true renders the delivery body (through the logging serializer: [PreventLogging] members still stripped, [SensitiveData] values still redacted) on the handler-fault Error and the disposal-timeout Warning. Anything other than a literal true keeps bodies out.

It is meant for a development mesh or a short, deliberate diagnosis on an instance that holds no sensitive content. Never set it on an instance that stores personal or health data: the log lines it produces reach Loki and, from there, any issue a red line is filed into.

Egress switches

Egress Key Default Off means
GitSync export (mesh → repository) GitSync:Export:Enabled on (absent = true) every export is refused with gitsync.policy.exportDisabled; imports are unaffected. Any value but true is OFF — a typo in an off switch never reads as on.
GitSync partitions GitSync:AllowedPartitions absent = every partition set (an array, or one comma-separated value — which, when configured, is the whole list and replaces an array from an earlier configuration layer; an entry ending in * matches by prefix), a sync source ({partition}/_GitSync, {partition}/_GitSync/{id}) outside the list can be neither created nor updated, and no export, import or re-import runs for it (gitsync.policy.partitionNotAllowed).
Incident filing (log → GitHub issue), feedback hand-over to a control instance MeshWeaver.Plugins keys on documented on the Observability and Feedback pages in MeshWeaver.Plugins, which own those paths.

🚨 The GitSync allow-list binds the platform as well. A system-synced module Space provisioned by module discovery is a partition like any other: an instance that sets GitSync:AllowedPartitions lists those partitions too, or their sync sources are refused at creation and every update of their last-sync state is refused with them. Deleting a sync source is always allowed.

Both GitSync keys are read once, when the service is created; a change reaches a running instance with its next restart.

Where it is pinned

See also