docs: Flow Debugger design reconciled with the implementation (arch-review F12, F13)

The Flow Debugger design doc still described the pre-remediation shape: a
debug-stream subscription for the value-change entry, Deployer everywhere, an
unredacted inline trace copy and "~7 audit rows per call". Reconciled with what
shipped on this branch (Tasks 1-12):

- design D6 / §5.4 / §9 / §10: RequireFlowDebugger page policy with Deployer-only
  execution (F3), the PullTraceEvents polling path with AttributeValueUpdate
  fields 7/8 recorded as RESERVED and unconsumed (F12), the redacting + byte-
  capped shared FlowTraceCollector (F1), route-time site scope (F2), the derived
  client waits (F4), the 7-10 row estimate, the doubled central audit write per
  routed call, and the inbound-row-after-the-run ordering.
- sitestream.proto: fields 7/8 marked reserved-but-populated, numbers not to be
  reused (comment only; no regeneration needed).
- Component-InboundAPI: SkipBodyCapture on the RouteCall row (F11),
  SchemaRefResolver's public visibility, WithSiteScope / SITE_SCOPE_DENIED (F2),
  Replay's wider role set (F3).
- Component-CentralUI: page policy vs Deployer-only controls (F3), route-time
  site scope (F2), client wait derivation (F4).
- "7-10 audit rows per call (3 of them trace-only)" everywhere the estimate is
  quoted: the method form, both CLI --trace-all-calls help strings, the CLI
  README, the ApiMethod entity doc, the manual migration script and the two
  Flow Debugger plans.
- arch-review doc: new §7 Resolution listing F1-F13 with the fixing commit, plus
  the tracking-store test race fixed alongside them (86451382).

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 14:35:10 -04:00
parent ccc08e4151
commit e15aa0e430
13 changed files with 104 additions and 42 deletions
+46 -20
View File
@@ -27,7 +27,7 @@ on the `DelmiaReceiver` child → seven attribute writes → wait for Galaxy to
| D3 | **Value-change on a data-sourced attribute does a REAL tag write** through the instance actor's normal write path; the device echo fires the trigger. Correlation is by a short-lived *trace arm* on the instance actor (§5.3). | A debug-only "inject value" command that synthesises the change without touching the device — leaves the instance's cached value diverged from the device until the next real update. |
| D4 | **Live gets its trace inline; Replay reads audit rows.** Site audit telemetry forwards on a 5 s busy / 30 s idle cadence (`AuditLog:SiteTelemetry`), too slow to drive an interactive run, so the routed reply carries the site steps back and the same steps are *also* written as audit rows for Replay. | Live waiting on the audit pipeline. |
| D5 | **Tracing is gated, not always-on.** Sites run scripts at high rates from timers and value changes; a trace flag rides beside `ParentExecutionId` and only flagged runs emit trace rows. One exception is always-on: a central `RouteCall` row per `Route.To().Call` (inbound-driven volume, joins the two halves of a Replay). | Tracing every run (volume), or a purely time-window join. |
| D6 | **Live run is a Deployer act.** `RequireDeployment` gates the page and the Run button; Replay needs `OperationalAudit`. | A separate `RequireOperator` gate for Live. |
| D6 | **Live run is a Deployer act.** `RequireDeployment` gates the Run controls and the two run commands; the page and Replay are open to `OperationalAudit` roles + Deployer (`RequireFlowDebugger`, arch-review F3). | A separate `RequireOperator` gate for Live. |
## 2. What exists today (reused, not rebuilt)
@@ -159,20 +159,24 @@ the echo also fires inherit the parent id exactly as static cascades do today.
`RouteToCallResponse` gains additive `ChildExecutionId` (`Guid?`) and `TraceEvents`
(`IReadOnlyList<AuditEvent>?`, the very events that became the site's audit rows, in emission
order); on the wire it is `repeated AuditEventDto trace_events` (the DTO `sitestream.proto`
already defines). The site collects events in a per-run `FlowTraceCollector` held by
`ScriptRuntimeContext` and attaches the list to the `ScriptCallResult` → `RouteToCallResponse`
when `Trace` is set. Capped at `MaxTraceEventsPerRun` (500); beyond it the collector keeps the
first 500, sets `Truncated`, and the audit rows remain complete. A site on an older build ignores
already defines). The site collects events in a per-run `FlowTraceCollector` (Commons
`Types/Debugging/`, shared with the central `FlowDebugRunner`) held by `ScriptRuntimeContext` and
attaches the list to the `ScriptCallResult` → `RouteToCallResponse` when `Trace` is set. The
collector applies `IAuditRedactor` inside `Add`, so the inline record is the same filtered event
the writer persists, and it caps at `MaxTraceEventsPerRun` (500) **or** 1 MB of summed
`DetailsJson` (`DefaultMaxBytes`), whichever comes first; beyond either bound it keeps the earlier
events, sets `Truncated`, and the audit rows remain complete (arch-review F1). A site on an older build ignores
`Trace`, returns no `TraceEvents`, and the UI labels its lane `site not traced (older build)`.
`FlowStep` is a **central-side projection** (`Commons/Types/Debugging/FlowStepProjector`)
computed from `AuditEvent` + `AuditDetails`; nothing serializes it.
For the value-change entry there is no routed reply to ride on. The debugger instead subscribes
to the instance's debug stream (`SubscribeDebugView`, the Debug View's existing path) for the run's
duration and takes the spawned run's `ExecutionId` from the stamped `AttributeValueChanged`, then
pulls the site's trace rows for that id via a new `PullTraceEvents` RPC on `SiteStreamService`
(same PSK interceptor as `PullAuditEvents`; reads the site audit SQLite by `ExecutionId` /
`ParentExecutionId`). One additive RPC, no streaming change.
For the value-change entry there is no routed reply to ride on. The runner polls the site's
`PullTraceEvents` RPC by `ParentExecutionId` (1 s cadence, 2 s settle window, `WaitSeconds` bound) —
simpler than the debug-stream subscription originally planned. `AttributeValueUpdate.source_execution_id`/`trace`
(fields 7/8) are populated by the site and **consumed by nothing**; they are reserved for a future
debug-stream path and must not be reused. The RPC sits behind the same PSK interceptor as
`PullAuditEvents` and reads the site audit SQLite by `ExecutionId` / `ParentExecutionId` — one
additive RPC, no streaming change.
## 6. Message and wire changes (all additive)
@@ -257,9 +261,14 @@ surface); the walker is syntax-only and never executes anything.
## 9. Security, authorisation, audit
- **Page visibility and Live run:** `RequireDeployment` (same as Debug View) — decided
2026-09-05: a Live run writes real tags, but the Deployer already deploys and redeploys the
scripts that do exactly that, so no separate Operator gate. **Replay:** `OperationalAudit` (it
- **Page visibility:** `RequireFlowDebugger` — the `OperationalAudit` roles plus Deployer
(`AuthorizationPolicies.FlowDebuggerAccessRoles` = Deployer, Administrator, Viewer), so an
auditor can open the page and Replay a past execution (arch-review F3). **Live run:**
`RequireDeployment` (same as Debug View) — decided 2026-09-05: a Live run writes real tags, but
the Deployer already deploys and redeploys the scripts that do exactly that, so no separate
Operator gate. The two run commands, the Live controls, the parked-node Retry/Discard actions and
the `?methodId=`/`?instanceId=` drill-ins stay Deployer-only, and the page pins Replay mode for a
non-Deployer. **Replay:** `FlowDebugReplayCommand` accepts the same `OperationalAudit` roles (it
reads audit rows). No Verifier step — the debugger is a diagnostic, not a production write
control, and the audit row is the safeguard.
- **Inbound audit row for a Live run:** the shared `AuditEvent.Actor` carries the operator's
@@ -267,21 +276,38 @@ surface); the walker is syntax-only and never executes anything.
`Extra.runNumber`; `Target` is the method as usual. API-key authentication is bypassed (there is
no key), **parameter validation is not** — `ParameterValidator` runs exactly as for HTTP so a
bad input fails the same way. The method's `TimeoutSeconds` applies. Site-scope (`SiteScopeService`)
restricts which instances a value-change entry may target.
- Trace payloads pass through the existing redaction policy (auth headers redacted, 8 KB cap,
64 KB on error rows). Console output is capped the same way.
restricts which instances a value-change entry may target. The method entry enforces the
operator's site scope at route time (`RouteHelper.WithSiteScope`, `SITE_SCOPE_DENIED` →
`ManagementUnauthorized`, arch-review F2): the runner resolves the operator's permitted numeric
site ids to site identifiers and hands them to `RouteHelper`, which refuses an out-of-scope target
**before any site traffic**; the denial is rethrown as `SiteScopeViolationException` *after* the
inbound audit row is written, so the attempt is recorded under the operator's name.
Administrators and unscoped users are exempt, exactly as `EnforceSiteScope` treats them.
- Trace payloads pass through the existing redaction policy on BOTH copies: the stored row (writer)
and the inline Live copy (`FlowTraceCollector` applies `IAuditRedactor` and a 1 MB byte budget
beside the 500-event cap — arch-review F1). The row-level caps are unchanged (auth headers
redacted, 8 KB cap, 64 KB on error rows). Console output is capped the same way.
- Nothing here touches the PSK boundary; `PullTraceEvents` sits behind `ControlPlaneAuthInterceptor`.
## 10. Volume and failure behaviour
- Untraced production is unchanged except one `RouteCall` row per routed call (inbound-driven).
Every routed call adds one central `RouteCall` insert — at inbound-driven rates that doubles the
per-call central audit write (accepted, D5).
- A traced Delmia call emits ≈ 1 `InboundRequest` + 1 `RouteCall` + 1 `ScriptRun` + 2
`AttributeWrite` + 1 `AttributeWait` + 1 value-change `ScriptRun` ≈ 7 rows. `TraceAllCalls` on a
100-call/day method is ~700 rows/day.
`AttributeWrite` + 1 `AttributeWait` + 1 value-change `ScriptRun` ≈ **7–10 rows** (a cached DB
write and a notification add 3 trace-only rows). `TraceAllCalls` on a 100-call/day method is
~700–1 000 rows/day.
- Trace emission is best-effort like all audit (a failed trace write never fails the run).
- Live run timeout = method timeout + `FlowDebugRunGrace` (10 s); a timeout renders whatever steps
arrived and marks the run `Timed out`. The site's trace rows still land later, so Replay of the
same ExecutionId is complete.
same ExecutionId is complete. The client waits longer than the server on purpose: the UI derives
its Ask wait from the same bound (`RunAskTimeoutFor` = method timeout + grace + 15 s slack,
floored at 30 s; `ValueChangeAskTimeoutFor` = the wait window + 15 s) and the CLI's
`run-method --timeout-seconds` defaults to 120 while `run-value-change` waits
`--wait-seconds + 25` (arch-review F4).
- **The Live run's inbound row is written AFTER the run completes**, so a Replay taken during a
long Live run shows only the `RouteCall`.
- Older site build: no `TraceEvents`, lane labelled; older central build: ignores new fields; proto3
defaults negotiate as for stream batching.
- **Mixed-version caveat:** an OLD central ingesting `ScriptTrace` rows from a NEW site parses