fix(esp): bind the session export to a redacting crate boundary (#549) - #558
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe PR adds a redacted ESP session capture envelope and moves export construction, serialization, and file writing to a feature-gated Tauri command. The frontend now invokes that command, while the parser retains read-only capture parsing. ChangesESP session export
Estimated code review effort: 4 (Complex) | ~45 minutes Mergeability Score: 🔵 Low · up to Concurrent session exports can collide on the temporary file used during writing, potentially causing one export to overwrite or fail another. The PR is otherwise mergeable with explicit owner awareness or follow-up for this bounded export-path risk. Sequence Diagram(s)sequenceDiagram
participant Workspace as EspDiagnosticsWorkspace
participant Commands as exportEspSession
participant Backend as export_esp_session
participant Capture as EspSessionCapture
participant File as Destination file
Workspace->>Commands: submit destination, snapshot, and metadata
Commands->>Backend: invoke export_esp_session
Backend->>Capture: construct redacted capture and serialize JSON
Backend->>File: write serialized capture
Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/cmtraceopen-parser/tests/esp_export_boundary.rs`:
- Around line 17-21: Update the narrative-text export assertions in the test
around bare device serials and tenant domains so both identifiers are absent
from the exported output. Remove the known-residual-gap documentation and rename
or revise bare_identifiers_in_free_text_are_a_known_residual_gap to enforce the
boundary contract, ensuring narrative fields are redacted or omitted rather than
preserving cleartext identifiers.
In `@src-tauri/src/commands/esp_diagnostics.rs`:
- Around line 138-145: Update the export-writing flow around the blocking
std::fs::write call to write contents to a uniquely named temporary file in the
destination directory, then replace the destination only after the temporary
write succeeds. Preserve EspExportError::Write reporting for task, write, and
replacement failures, and use Windows-compatible replacement semantics while
cleaning up any temporary file when appropriate.
- Around line 138-145: Update the ESP export write flow to use
tokio::fs::write(...).await directly instead of
tauri::async_runtime::spawn_blocking with std::fs::write. Preserve both error
mappings in the surrounding command, including distinct messages for the async
task/write failures, and run the required Cargo checks from src-tauri.
In `@src/workspaces/esp-diagnostics/EspDiagnosticsWorkspace.test.tsx`:
- Around line 2488-2503: Update the test around the invoke call assertions so it
explicitly verifies that export_esp_session occurred and write_text_output_file
was not called, rather than iterating over an empty writtenText collection.
Remove the ineffective writtenText redaction loop and leave serialized-file
redaction coverage to the Rust export-boundary tests.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: dd13439f-57dc-47d8-bf25-50a60b4f9a17
📒 Files selected for processing (11)
crates/cmtraceopen-parser/src/esp/export.rscrates/cmtraceopen-parser/src/esp/mod.rscrates/cmtraceopen-parser/src/esp/redaction.rscrates/cmtraceopen-parser/tests/esp_export_boundary.rssrc-tauri/src/commands/esp_diagnostics.rssrc-tauri/src/lib.rssrc/lib/commands.tssrc/workspaces/esp-diagnostics/EspDiagnosticsWorkspace.test.tsxsrc/workspaces/esp-diagnostics/EspDiagnosticsWorkspace.tsxsrc/workspaces/esp-diagnostics/esp-session-capture.test.tssrc/workspaces/esp-diagnostics/esp-session-capture.ts
There was a problem hiding this comment.
Pull request overview
This PR fixes ESP session export so exported captures are always produced through a redacting parser-crate boundary, preventing cleartext identifiers from being written to user-chosen files (issue #549).
Changes:
- Add a parser-crate export boundary type (
EspSessionCapture) that appliesredacted_export_projectionon construction and recordsredacted: truein the export envelope. - Move ESP session export to a new Tauri command (
export_esp_session) and update the React workspace to call it (removing frontend-side capture building/serialization). - Add end-to-end Rust tests guarding against identifier leakage in exported JSON and update frontend tests around the export boundary.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/workspaces/esp-diagnostics/EspDiagnosticsWorkspace.tsx | Switch export to backend exportEspSession command to ensure export goes through the redacting boundary. |
| src/workspaces/esp-diagnostics/EspDiagnosticsWorkspace.test.tsx | Add UI-level tests asserting the export path uses export_esp_session (one assertion currently becomes vacuous; see PR comment). |
| src/workspaces/esp-diagnostics/esp-session-capture.ts | Remove write capabilities from the frontend capture module; keep parsing/reading only and add redacted? envelope support. |
| src/workspaces/esp-diagnostics/esp-session-capture.test.ts | Adjust tests to build an envelope inline (since frontend can’t/shouldn’t serialize captures anymore). |
| src/lib/commands.ts | Add exportEspSession() wrapper invoking the new backend command. |
| src-tauri/src/lib.rs | Register export_esp_session in the Tauri invoke handler. |
| src-tauri/src/commands/esp_diagnostics.rs | Implement export_esp_session command that constructs EspSessionCapture and writes its JSON to disk. |
| crates/cmtraceopen-parser/tests/esp_export_boundary.rs | Add end-to-end Rust tests ensuring exported bytes contain no planted identifiers/identifier-shaped strings and include redacted: true. |
| crates/cmtraceopen-parser/src/esp/redaction.rs | Make export projection reassemble EspDiagnosticsSnapshot field-by-field to force explicit handling of future fields. |
| crates/cmtraceopen-parser/src/esp/mod.rs | Export the new export module publicly from the ESP lane. |
| crates/cmtraceopen-parser/src/esp/export.rs | Add EspSessionCapture / EspSessionCaptureMeta as the enforced exportable capture envelope and serialization API. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Hermes charter review — gate-state reportReviewed exact remote head Findings
Gate states observed
This review covered the exact PR head, the crate/API boundary, recursive projection/exhaustiveness, named egress inventory, residual characterization, replayability, scope, and live PR gate metadata. It did not treat the acknowledged non-export IPC/clipboard/temp-file paths as fixed by this PR. |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/cmtraceopen-parser/src/esp/redaction.rs (1)
652-656: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
node_cache.expected_valueis masked outside the shared walker, so its literal is never scrubbed from free text.Lines 652-656 mask
expected_valueunconditionally. That value is a classified value the export refuses to emit, but it is not visited byfor_each_masked_classified_mut, socollect_classified_literalsnever sees it. If anexpected_valueholds a value that no identity, profile, enrollment, hardware, or Graph classified field also holds, that exact string still survives in every narrative and evidence field.The fixture hides this:
crates/cmtraceopen-parser/tests/esp_export_boundary.rsline 383 setsexpected_valuetoENTDM_ID, which is already collected fromidentity.entdm_id. Change the fixture to a node-cache-only value to expose the gap.This also weakens the invariant stated at lines 627-629 and 749-752.
expected_valueis aString, not anEspClassifiedString, so it cannot join the walker directly. Collect it separately before masking.🛡️ Proposed fix: collect node-cache literals alongside classified literals
fn collect_classified_literals(snapshot: &mut EspDiagnosticsSnapshot) -> ClassifiedLiterals { let mut values = BTreeSet::new(); for_each_masked_classified_mut(snapshot, |classified| { values.insert(classified.value.clone()); }); + // Masked at the same boundary but not an `EspClassifiedString`, so it + // cannot ride the walker above. + for node in &snapshot.node_cache { + if let Some(expected) = &node.expected_value { + values.insert(expected.clone()); + } + } ClassifiedLiterals::new(values) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/cmtraceopen-parser/src/esp/redaction.rs` around lines 652 - 656, Update the ESP redaction flow around the node-cache masking loop and collect_classified_literals so each non-empty node.expected_value is added to the classified-literal set before it is replaced with REDACTED. Preserve the existing unconditional masking behavior, and update the esp_export_boundary fixture to use a value unique to node_cache rather than ENTDM_ID so the free-text scrubbing path is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/cmtraceopen-parser/src/esp/redaction.rs`:
- Around line 756-803: Make for_each_masked_classified_mut destructure each
visited EspIdentityEvidence, EspProfileEvidence, EspEnrollmentEvidence,
EspHardwareEvidence, and Graph managed-device value with explicit field bindings
and no `..`. Route the existing classified fields through visit_optional while
binding non-classified fields explicitly, so adding any nested field causes a
compile-time update requirement and all classified fields remain masked.
- Around line 1961-1972: Update redact_graph_overlay to redact all remaining
sensitive fields in graph.device_match.data and graph.autopilot_identity.data,
including the listed device, identity, name, user, and profile identifiers,
using the existing redaction helpers. Also redact GraphSectionError.request_id
and blocked_by alongside the existing error redaction calls, preserving
optional-field handling.
In `@crates/cmtraceopen-parser/tests/esp_export_boundary.rs`:
- Around line 212-234: Add an assertion in
a_degenerate_firmware_serial_does_not_scrub_unrelated_narrative that the
exported identity.serial_number remains masked rather than equal to "0". Keep
the existing narrative assertion unchanged, and use the exported typed-field
path to verify short serials are still scrubbed.
---
Outside diff comments:
In `@crates/cmtraceopen-parser/src/esp/redaction.rs`:
- Around line 652-656: Update the ESP redaction flow around the node-cache
masking loop and collect_classified_literals so each non-empty
node.expected_value is added to the classified-literal set before it is replaced
with REDACTED. Preserve the existing unconditional masking behavior, and update
the esp_export_boundary fixture to use a value unique to node_cache rather than
ENTDM_ID so the free-text scrubbing path is exercised.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: db0184a6-e41b-46d3-b8a6-1021a06667e4
📒 Files selected for processing (2)
crates/cmtraceopen-parser/src/esp/redaction.rscrates/cmtraceopen-parser/tests/esp_export_boundary.rs
RED. Two end-to-end guards on the path the "Export session" button actually
takes, both failing on main:
- crates/cmtraceopen-parser/tests/esp_export_boundary.rs serializes a session
through the same envelope esp-session-capture.ts writes and asserts no
planted identifier survives. A canary asserts each marker IS present in the
raw snapshot so the assertions can never pass vacuously, and a shape scan
walks every string in the export for mail/SID/MAC/IPv4 shapes so a newly
added identifier-bearing field fails even when no marker was updated.
- EspDiagnosticsWorkspace.test.tsx clicks Export with a UPN in the store and
asserts nothing cleartext reaches a file write, and that the export is
produced by a redacting backend command rather than by the frontend.
Failing output:
the exported session leaks the user principal name
the exported session leaks a MAC address at $.snapshot.activity[0].detail
the export envelope must record that it is a redacted projection
expected '{"kind": "esp-session-capture"...}' not to contain
'adele.vance@contoso.onmicrosoft.com'
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GREEN for the RED in the previous commit. The export projection existed, was tested, and was called from nowhere. The Export button read the store snapshot, embedded it in an envelope the frontend built, and handed the text to write_text_output_file verbatim, while the UI masked the same values on screen. The fix binds at the crate edge rather than at each egress point: - crates/cmtraceopen-parser/src/esp/export.rs adds EspSessionCapture, the only exportable shape for an ESP session. Its snapshot field is private and its only constructor runs redacted_export_projection, so no caller outside the crate can assemble a capture around local values. There is deliberately no Deserialize impl and no field-wise constructor, either of which would hand that ability back. The envelope records redacted: true, so an export can say what it is. Same arrangement as SccmRawEvidenceSnapshot::export. - redacted_export_projection now reassembles the snapshot field by field instead of returning the mutated clone. A field added to EspDiagnosticsSnapshot stops compiling there until someone decides how it is exported, rather than riding out unexamined. - export_esp_session (src-tauri) builds the capture and writes it. The frontend hands over the session it is displaying and no longer serializes one: buildEspSessionCapture and serializeEspSessionCapture are gone, and esp-session-capture.ts is now read-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Export is also the replay format, so "Open session" has to be able to read what "Export session" wrote after redaction drops raw evidence records and masks typed fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bare device serial has no distinctive shape and a bare DNS tenant
domain has no label, so the free-text rules could not find either one.
Both were masked where they are typed fields and survived verbatim in
narrative: raw evidence text, timeline detail, registration named data.
The projection already knows those values -- it reads them off the typed
fields a moment before masking them. Collect them there and scrub the
exact strings from every free-text field, so the gap closes by
construction instead of by another pattern.
One walker now carries the field list. collect_classified_literals reads
the values through it and the projection masks through it, so a
classified field cannot be masked without its literal also being
scrubbed. The scrub runs last in each pipeline: the shaped rules must
see the original text, or a scrubbed tenant domain would break the
mail-address match on a UPN that contains it and leak the local part.
Values shorter than six bytes are left alone. Firmware reports junk
serials ("0", "N/A"), and a token that short cannot be told apart from
an ordinary word once it sits unlabelled in narrative; the floor stays
below a seven-character Dell service tag.
Deletes bare_identifiers_in_free_text_are_a_known_residual_gap and the
module note that pinned the gap. The fixture now plants the serial and
the tenant domain unlabelled in narrative, so the file-wide marker guard
covers them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
237ad45 to
e7f1d95
Compare
std::fs::write truncated the destination before finishing, so a failed write damaged the prior capture. The contents are now written to a sibling temporary file and renamed into place, and tokio::fs replaces the spawn_blocking round trip. A failed rename cleans up the temporary file.
The classified-field walker accessed nested structs by field name, so a new classified field on EspIdentityEvidence, EspProfileEvidence, EspEnrollmentEvidence, EspHardwareEvidence, or a Graph managed device compiled without being masked. Each struct is now destructured without `..`, so adding a field stops the build until it is routed. The Graph managed device, Autopilot identity, and section error carried device, Entra, user, and profile identifiers as plain strings that the export left unchanged. Those identifiers are now masked, and GraphSectionError request_id and blocked_by are masked alongside the message.
The frontend test iterated an empty writtenText list, so it passed vacuously once the export moved to export_esp_session. It now asserts the generic file writer is never the path a session takes to disk, leaving serialized-file redaction to the Rust boundary tests. The degenerate-serial test asserted only the narrative side, so a serial too short to scrub could still leak as a typed field. It now also asserts the typed serial_number stays masked.
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/cmtraceopen-parser/src/esp/redaction.rs`:
- Around line 1204-1217: Extend redact_reference after the SID replacement and
before literals.scrub to apply azure_storage_credential_pattern,
ipv4_address_pattern, mac_address_pattern, and redact_ipv6_addresses, matching
the pass order used by redact_text_for_context. Prefer reusing a shared
redaction-prefix helper if one exists or can be introduced without changing
behavior, so both pipelines remain consistent.
- Around line 2030-2052: Update redact_graph_overlay to apply the same redaction
rule to EspGraphOverlay.request_id and GraphSectionError.request_id, ensuring
the overlay-level request ID cannot escape unmasked. If request IDs are
intentionally not sensitive because they are client-generated, document that in
the function’s existing doc comment and preserve the current masking behavior
for error.request_id.
- Around line 1059-1077: Update collect_export_redaction to populate literals by
calling collect_classified_literals(snapshot) during ExportRedaction
construction instead of using ClassifiedLiterals::default(). Remove the separate
caller-side assignment so every constructed ExportRedaction includes the
complete literal set and cannot be left in a fail-open state.
- Around line 681-745: Update redacted_export_projection in
crates/cmtraceopen-parser/src/esp/redaction.rs:681-745 to redact
delivery_optimization transfers and the applicable Graph sections using the
existing narrative/evidence helpers, or document explicitly why the subtree has
no exportable identifiers. Update the export-boundary fixture in
crates/cmtraceopen-parser/tests/esp_export_boundary.rs:412-456 to populate
delivery_optimization with an identified transfer, autopilot_identity, one of
apps/policies/scripts, and a GraphSectionError; add all new markers to
PLANTED_IDENTIFIERS so the guards exercise these paths.
In `@src-tauri/src/commands/esp_diagnostics.rs`:
- Around line 148-156: In the rename error branch, replace the blocking
std::fs::remove_file cleanup with awaited tokio::fs::remove_file(&tmp), matching
the existing async file I/O in the surrounding export flow while preserving the
existing error return.
- Around line 142-147: Update the temporary path construction in
export_esp_session to include a per-invocation unique component in addition to
the process ID, ensuring concurrent exports to the same destination never share
the same .tmp file. Keep the existing atomic write and rename flow unchanged.
In `@src/workspaces/esp-diagnostics/EspDiagnosticsWorkspace.test.tsx`:
- Around line 2506-2519: Replace the duplicate assertion in the test named
“produces the export through the redacting backend boundary” with checks on the
mocked invoke payload. Verify the `export_esp_session` command receives the path
returned by `save()` as `destination` and the currently displayed snapshot and
metadata through the wrapper’s argument shape in `commands.ts`, while leaving
command-name coverage to the first test.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 76d45280-ddb9-49e9-8f4e-08ed4a5c4d35
📒 Files selected for processing (4)
crates/cmtraceopen-parser/src/esp/redaction.rscrates/cmtraceopen-parser/tests/esp_export_boundary.rssrc-tauri/src/commands/esp_diagnostics.rssrc/workspaces/esp-diagnostics/EspDiagnosticsWorkspace.test.tsx
The temporary path used only the process id, so two concurrent exports to one destination shared a file. A nanosecond timestamp is now part of the name, and the failed-rename cleanup uses tokio::fs rather than blocking the executor.
ExportRedaction now collects the classified literals itself, so it cannot be built with an empty set that makes the free-text scrub a silent no-op. redact_reference runs the network-identifier passes (Azure SAS, IPv4, MAC, IPv6) that redact_text_for_context already applies, so a host or address in a reference id no longer escapes. delivery_optimization transfers and profile_assignments assignments now mask their device, content, app, target, and filter identifiers, and the overlay's own request_id is masked like the section errors' request_id. The boundary fixture populates delivery_optimization and autopilot_identity with planted identifiers so the canary reaches them.
The second boundary test only checked that export_esp_session was called, which the first already proved. It now asserts the destination is the save() path and the snapshot is the one the operator was shown, which is the only forwarding the frontend owns.
Closes #549.
The ESP "Export session" button wrote the raw store snapshot to a user-chosen file.
redacted_export_projectionexisted for exactly this, was tested, and was called from nowhere. The UI masked the same values on screen, so a user saw masked values and exported cleartext.RED first
38c71a32adds the tests only. Real failure output on that commit:TypeScript, same commit:
6fb225fbis the fix.622137bfadds one more test (a redacted export must still reload as a session, since export is also the replay format).Where the boundary is, and why there
At the crate edge, in
crates/cmtraceopen-parser/src/esp/export.rs.EspSessionCaptureis the only exportable shape for an ESP session:snapshotfield is private, and its only constructorfrom_snapshotrunsredacted_export_projection, so no caller outside the crate can assemble a capture holding local values;Deserializeimpl and no field-wise constructor. Either would hand back the ability to build a capture around an unprojected snapshot;redacted: true. An export that cannot say whether it was redacted is not auditable.This is the
SccmRawEvidenceSnapshot::exportarrangement (sccm/evidence.rs:329) rather than the "possible but discouraged" one: the point is that the unprojected value is unreachable from outside, not that the safe call is documented. Per-egress hygiene is what produced this bug, so the fix is not at the file-save call site.Second,
redacted_export_projectionnow reassembles the snapshot field by field, never with... A field added toEspDiagnosticsSnapshotstops compiling there until someone decides how it is exported, instead of riding out through theclone()unexamined. That is the propertyconfiguration/redaction.rsalready had and every other lane lacked.App surface touched (kept minimal)
export_esp_session(src-tauri/src/commands/esp_diagnostics.rs) builds the capture and writes it. Added because the projection lives in Rust and the writer must be on the same side of the boundary as the projection.exportEspSessioninsrc/lib/commands.ts;EspDiagnosticsWorkspace.exportSessionnow calls it instead of serializing locally.buildEspSessionCaptureandserializeEspSessionCaptureare deleted fromesp-session-capture.ts. Leaving them would leave a working way to write an unredacted capture. That module is now read-only (parse for "Open session"); its tests build the envelope inline.No reducer semantics changed. No token or scoped-key mechanics introduced (ADR-004 / #550 untouched). No DsRegCmd changes (#556). No
expected.jsonedited; none needed changing.What the end-to-end test covers
crates/cmtraceopen-parser/tests/esp_export_boundary.rs. It does not call the projection directly: everything goes throughexported_session_json, which is the export path (build anEspSessionCapture, serialize it). Two independent guards on the same bytes:Plus: the export declares
redacted: true; exporting does not mutate the local session; a redacted export still deserializes as a snapshot.The frontend test (
EspDiagnosticsWorkspace.test.tsx, "ESP session export boundary") clicks Export with a UPN in the store and asserts nothing cleartext reaches a file write, and that the export goes throughexport_esp_session. That is what catches a future frontend bypass; a Rust test cannot see the frontend.What it does NOT cover, honestly
EspDiagnosticsSnapshotfield a compile error, which forces a decision. The shape scan only catches a new field if a test populates it with an identifier shape. A new field carrying an identifier the fixture never sets, in a shape the scan does not know, passes both.bare_identifiers_in_free_text_are_a_known_residual_gap, which asserts the gap so it stays visible rather than assumed closed. Not fixed here: closing it is a redaction-policy change, not a boundary change.export_esp_sessionagainst a real file.Egress paths still unprojected after this change
Named explicitly, none of them fixed here:
analyze_esp_evidence(commands/esp_diagnostics.rs:96) returnsEspDiagnosticsSnapshotover IPC unprojected.emitstream (commands/esp_diagnostics.rs:67-73,ESP_SESSION_UPDATE_EVENT) sends every full snapshot to the webview unprojected, as doesget_esp_diagnostics_session/start_esp_diagnostics_sessionviaEspSessionEnvelope.intune.rs:64-71returnsIntuneAnalysisResultwith no redaction module at all;intune.rs:350-354writes%TEMP%/cmtrace-guid-diag.logon every analysis run.DsregcmdWorkspace.tsx:162copies the full analysis JSON to the clipboard;intune/ScriptCodeViewer.tsx:213copies recovered script bodies.Whether the IPC return and the emit stream should also be projected is the ADR-004 question and is deliberately out of scope.
Gates
Recommend running
/code-reviewon this one: it changes a public crate type and a command surface.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests