Sensitive content stays on the instance

What a red-log ticket publishes, and the switch that publishes nothing. The platform-wide rule — what a log line may carry at all — is core's Sensitive Content in Logs (get Doc/Architecture/SensitiveContentInLogs). This page is the ticketing half: what the incident filer copies from the logs into GitHub.

What was wrong

An enterprise client's instance holds health records in node content. A call site that formats a value into its message — Update failed for record {name} — puts that value into the formatted line, and the console formatter Loki ships has no message template to fall back to: the TEXT is all there is. The filer then copied that text into three public places:

Where What it carried
the issue body the samples verbatim (Recent log lines), the per-shape ledger's normalized excerpt
every "still happening" comment, and the retirement note the last three samples, the shape ledger
the site-fold note a Detail row — the normalized exception message

Plugins#4444 masked mesh PATHS in all of them, and that is the limit of masking: a path has a shape, a person's name or a diagnosis is just a word. LogLineParser.Normalize keeps such a word too — it masks guids, numbers, quoted literals and paths, nothing that reads as prose — so the normalized text is not a template either.

What is published now

Every text the filer posts — LogIncidentFiler.Body, the recurrence comment, RetirementComment, SiteFoldNote — carries:

The incident node in the Admin partition keeps the samples verbatim — that is on the instance that recorded them, under its own access control.

The triage draft is not published either — by default

The triage agent reads the incident node, raw samples included: that is how it finds the concrete case, and it cannot do its job on the template-level fields alone. So whatever it writes can echo a logged value, and telling it not to (its instructions do) is a request, not a guarantee. The input cannot be cut without blinding it, so the cut is at the OUTPUT:

Key Default Effect
LogWatch:PublishTriageDraft false false ⇒ none of the agent's TEXT is published: the issue title is BUILT from the exception's short type and the fault site (InvalidOperationException in MeshDataSource.Apply — the top frame's Type.Method, else the category), the body opens with a line saying the write-up stays on the incident node, the routing row says an override's reason is on the node, and the fold note carries neither the agent's title nor its write-up. The draft's labels and severity still apply, and the write-up stays on the incident node in the Admin partition. true ⇒ the previous behaviour, path-masked: for an instance whose logs carry only its own platform's text.

Pinned by PublishedIncidentTextCarriesNoLogTextTest.ATriageDraftThatEchoesASample_IsNotPublished_ByDefault: a marker in a sampled line, echoed into the draft's title, body and routing reason, reaches neither the issue nor the fold note.

The switch: LogWatch:FileIssues

Key Default Effect
LogWatch:FileIssues true false ⇒ incidents are still ingested and recorded on this instance, and nothing reaches GitHub: no issue, no recurrence comment, no reopen, no retirement note, whatever GitHub App credentials the instance holds. No triage is asked for either — a draft for a ticket that is never opened is a model round spent on nothing, and a Failed incident would otherwise be re-triaged on every report. A Triage, File or Comment request already PERSISTED on a node (asked before the switch, or by an admin) is consumed at the claim and granted nothing, with the reason on the incident; the comment-debt clock and the stranded-triage reconcile ask for nothing. An admin's Suppress is still honoured.

The other ways the egress is off, for completeness: LogWatch:IngestToken unset (the ingest endpoint is not mapped at all), no GitHub App (GitHub:App), or a watcher with no LogWatcher:PortalUrl. Each is an absence; FileIssues=false is the one that says so on purpose.

The in-cluster watcher sends raw samples to its portal

🚨 The watcher posts its samples to LogWatcher:PortalUrl. Raw lines (up to LogWatcher:MaxSamplesPerReport × LogWatcher:MaxSampleLength, default 5 × 2000 characters) travel to whichever portal that names, and are stored there on the incident node. An instance whose logs can carry sensitive content points its watcher at ITS OWN portal — never at a shared control instance — and the watcher's LogWatcher:Namespaces lists only that instance's namespace, so no other deployment's lines are read into it either. None of LogWatch:* above governs this hop: it happens before the portal sees anything.

[SensitiveData] in this repository's own paths

Core's [SensitiveData] (MeshWeaver.Messaging.SensitiveDataAttribute) marks a content property, and core's ONE redaction point (SensitiveDataRedaction) replaces its value on every log and egress rendering. Where this repository's code renders content or writes about it:

Path What it does
The MCP SDK's own logs (ModelContextProtocol.*) the SDK renders whole JSON-RPC messages — tool arguments and RESULTS — at Trace. McpPayloadLogCeiling holds those categories at Debug after every other logging configuration, for every provider, on every reload: no configuration can open the payload events. It is applied by the HOST — Memex.Portal.ServiceDefaults' AddServiceDefaults, which both portal hosts call — never by the MeshWeaver.Mcp module: logging levels belong to the process that owns the pipeline, and a module's logging registration is forwarded into the root through the module registry, which made the root logger factory depend on itself (Plugins#1785). The mesh's own MCP tool logs name tools, paths and lengths, never arguments.
AI thread execution ([ThreadExec] TOOL_START) the tool call's argument LENGTH, never the first 100 characters of the arguments (a create's first characters are the node). The thread's own status, which its authorised reader sees, still shows them.
Icon generation a reply without an SVG is logged by length, never by text.
Feedback hand-over the viewed node's marked values are scrubbed from the agent-written context before the event leaves; whatever cannot be established fails closed (no node context, no path). The user's own message is sent verbatim. See the Feedback guide.
Incident filing, the triage draft template-only (above); kept.

What it does NOT cover is core's list (authorised reads; free text; content without a $type), plus: content an agent sends to a language model on an authorised user's behalf is an authorised read, and a description a model writes INTO a node is free text on that node.

Pinned by

McpPayloadLogCeilingTest (Memex.Portal.Shared.Test): configuration that opens Trace in every way (Default, the SDK's own categories, a provider-specific Default) leaves the SDK's Trace closed, its Debug open, and every other category at Trace; an Information default is not lowered; and the portals' own AddServiceDefaults carries the ceiling on a real generic host. McpModuleLoggerFactoryTest (Memex.Hosts.Test): the MeshWeaver.Mcp module, loaded off disk and held in its own load context the way the publish-bake gate and a portal load it, leaves the root logger factory buildable. ToolCallArgumentsStayOutOfTheLogTest (MeshWeaver.AI.Test): a real round whose model calls a tool with a marker at the front of its arguments; the TOOL_START line is logged and no line at any level carries the marker (it did before this change).

PublishedIncidentTextCarriesNoLogTextTest (MeshWeaver.Observability.Test): the issue body, the recurrence comment, the site-fold note and the retirement note carry none of a logged value while carrying the category, exception type, top frame, count and pod; FileIssues=false refuses every GitHub write with credentials present, asks for no triage or comment, consumes a persisted Triage/File/Comment request at the claim, keeps the comment-debt clock silent, and still grants a Suppress; a marker in a sampled line echoed into the triage draft reaches neither the issue nor the fold note.