fix(site-runtime): Flow Debugger inline trace copy is redacted and byte-capped at the collector (arch-review F1, site half)

Every emit site hands FlowTraceCollector the same AuditEvent it hands the
writer, but redaction and payload capping lived inside the writers, which
return a NEW filtered record — so the inline copy that rides
ScriptCallResult.TraceEvents / RouteToCallResponse.TraceEvents was the RAW
event: no per-target SQL-parameter redaction, no header scrubbing, no 8 KB /
64 KB truncation. Live and Replay of the same execution could therefore show
different payloads, and the 500-event-only cap left the reply free to breach
the 4 MB gRPC receive limit — which would fail a real call, since the same
reply carries the script's return value.

The collector now applies the host IAuditRedactor itself (outside the lock;
the writers keep their own idempotent Apply) and enforces a byte budget
(DefaultMaxBytes = 1 MB over the redacted DetailsJson) alongside the event
cap, flagging Truncated when either trips. It moves to Commons
(Types/Debugging) so the central FlowDebugRunner can share it.

The redactor is a singleton, so ScriptRunLauncher resolves it from the ROOT
provider and keeps constructing the collector before the try — the traced
ScriptRun row is emitted from the catch blocks too, including the
shed-at-dequeue path that throws before the DI scope exists.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 14:15:38 -04:00
parent cccef7b2f4
commit e79452d2de
4 changed files with 138 additions and 11 deletions
@@ -247,7 +247,15 @@ internal static class ScriptRunLauncher
// caller to reply to — a trace-armed trigger run (replyTo == Nobody) writes every row to the
// audit writer for PullTraceEvents and would only keep a never-read list alive. The
// ScriptRun row itself is emitted below on every exit path of a traced run.
var collector = trace && !replyTo.IsNobody() ? new FlowTraceCollector() : null;
// Arch-review F1: the inline copy is redacted at the collector, so the collector carries
// the host's IAuditRedactor. That is a SINGLETON (AddAuditLog), resolved here from the
// ROOT provider for the same reason siteEventLogger below is — the traced ScriptRun row
// is emitted from the catch blocks too, including the shed-at-dequeue path that throws
// before the DI scope exists. Null in tests / hosts without AddAuditLog: raw, still capped.
var collector = trace && !replyTo.IsNobody()
? new FlowTraceCollector(
redactor: serviceProvider?.GetService<ZB.MOM.WW.Audit.IAuditRedactor>())
: null;
var startedUtc = DateTime.UtcNow;
ScriptRuntimeContext? context = null;
IAuditWriter? auditWriter = null;