Integrate stable telemetry into MXC runtime - #820
Conversation
Promote telemetry to the stable configuration, add executor consent maintenance and live authorization, update generated schemas, and cover Windows consent and ETW behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
| @@ -0,0 +1,79 @@ | |||
| { | |||
| "$schema": "http://json-schema.org/draft-07/schema#", | |||
There was a problem hiding this comment.
This is a detail I think I'm unclear about. Do we expect that we'll ever have to support external callers trying to invoke telemetry config via the JSON config file with the EXE? If we don't then this is probably not needed.
| return Err(WxcError::ConfigParse( | ||
| "'experimental.telemetry' has moved to the stable section; \ | ||
| use top-level 'telemetry' instead." | ||
| .to_string(), |
There was a problem hiding this comment.
should we only reject legacy telemetry if the passed config's version is < 0.8.0?
| ], | ||
| "description": "macOS Seatbelt backend configuration. Used when containment is `seatbelt`." | ||
| }, | ||
| "telemetry": { |
There was a problem hiding this comment.
In this context telemetry is a gate that the invocation is requesting telemetry. We're trying to keep execution policy "enable telemetry gate" separate from state mutation ("I consent to telemetry, I withdraw consent, I would like status.") But as noted earlier exposing the surface of telemetry in the JSON schema might be overkill.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2da373b9-0a51-4aa4-a968-14f05f27b5c7
There was a problem hiding this comment.
Pull request overview
Promotes telemetry into MXC’s stable runtime configuration and integrates consent-aware emission across executors and the engine.
Changes:
- Adds stable telemetry parsing, authorization gates, and state-aware correlation.
- Introduces typed consent-maintenance requests and generated schemas.
- Adds Windows consent/ETW smoke tests and updates documentation.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/Build.Windows.Job.yml |
Adds telemetry tests and smoke coverage. |
docs/schema.md |
Documents stable telemetry configuration. |
docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md |
Updates telemetry correlation guidance. |
docs/telemetry/telemetry-consent-design.md |
Marks consent design as implemented. |
docs/telemetry/telemetry-consent-privacy-review.md |
Updates implementation status. |
docs/telemetry/telemetry-policy.md |
Marks policy integration as implemented. |
docs/telemetry/telemetry.md |
Updates telemetry implementation status. |
schemas/dev/mxc-config.schema.0.8.0-dev.json |
Promotes telemetry to the schema root. |
schemas/dev/mxc-telemetry-consent.schema.1.json |
Adds the maintenance-request schema. |
src/core/lxc/src/main.rs |
Adds consent maintenance and stable telemetry wiring. |
src/core/mxc_darwin/src/main.rs |
Adds equivalent macOS maintenance handling. |
src/core/mxc_engine/src/lib.rs |
Adds telemetry-aware streaming wrappers. |
src/core/mxc_engine/src/policy.rs |
Adds per-request telemetry enablement. |
src/core/mxc_engine/src/state_aware.rs |
Adds state-aware telemetry and correlation. |
src/core/wxc/src/main.rs |
Integrates maintenance commands and runtime emission. |
src/core/wxc_common/Cargo.toml |
Clarifies telemetry test-support behavior. |
src/core/wxc_common/src/config_parser.rs |
Parses stable telemetry and migration cases. |
src/core/wxc_common/src/models.rs |
Moves telemetry into ExecutionRequest. |
src/core/wxc_common/src/telemetry/consent_cli.rs |
Implements shared consent maintenance handling. |
src/core/wxc_common/src/telemetry/events.rs |
Updates ETW event documentation. |
src/core/wxc_common/src/telemetry/mod.rs |
Adds live authorization and SDK emission paths. |
src/core/wxc_common/src/wire.rs |
Defines telemetry maintenance wire contracts. |
src/testing/wxc_e2e_tests/tests/e2e_windows.rs |
Updates telemetry E2E invocation. |
src/tools/mxc_schema_gen/src/main.rs |
Adds consent schema/type generation modes. |
tests/examples/28_telemetry_enabled.json |
Uses stable top-level telemetry. |
tests/scripts/run_telemetry_consent_smoke_test.ps1 |
Adds isolated consent smoke coverage. |
tests/scripts/run_telemetry_etw_smoke_test.ps1 |
Strengthens isolated ETW smoke coverage. |
Suppressed comments (1)
src/core/wxc_common/src/telemetry/events.rs:106
- The provider emits
MXC.Error, notError; preserving this exact identity is part of the ETW smoke assertion. Keep the public function documentation aligned with the actual event contract.
/// Log an Error ETW event.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if config | ||
| .get("experimental") | ||
| .and_then(serde_json::Value::as_object) | ||
| .is_some_and(|object| object.contains_key("telemetry")) | ||
| { |
| "title": "MXC Telemetry Consent Maintenance Contract", | ||
| "description": "Public JSON contract for telemetry-consent maintenance. It is separate from the execution configuration and uses an explicit command discriminator.", |
| "telemetry": { // Telemetry (experimental, Windows only) | ||
| "enabled": true // Emit TraceLogging ETW events via pure Rust tracelogging crate | ||
| } | ||
| }, |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2da373b9-0a51-4aa4-a968-14f05f27b5c7
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.
Suppressed comments (3)
docs/telemetry/telemetry-consent-design.md:2
- Removing the implementation-status notice makes this document claim that the Rust/C ABI and Node/.NET consent APIs described later are available now, but this PR's own stack assigns those surfaces to follow-up PRs #821 and #822. Keep a partial-implementation notice until those PRs land so readers are not directed to APIs absent from this branch.
# Telemetry consent design
src/core/mxc_engine/src/lib.rs:195
- A timed-out state-aware streaming exec is converted to a generic
BackendError, soclassify_mxc_errorreportsprocess_errorinstead of the existingtimeouttelemetry category. PreserveErrorKind::TimedOutwhen constructing the state-aware telemetry outcome (or add a state-aware emit path that acceptsFailureReason::Timeout) so one-shot and state-aware timeout events agree.
let outcome = match result {
Ok(exit_code) => Ok(
wxc_common::state_aware_dispatch::DispatchOutcome::ExecCompleted {
exit_code: *exit_code,
},
),
Err(error) => Err(wxc_common::mxc_error::MxcError::backend_error(
error.to_string(),
)),
src/core/mxc_engine/src/policy.rs:787
SandboxRequestis re-exported bymxc-sdk, so this method immediately adds a public Rust SDK API. The repository's SDK-change convention requires updating the Rust SDK crate docs andsrc/core/mxc-sdk/README.mdin the same PR; neither currently documents this method. Please add those updates, or keep the method non-public until the SDK-exposure PR.
/// Enable or disable telemetry for this invocation.
///
/// Enabling this per-request switch is necessary but not sufficient:
/// telemetry still requires persisted user consent and an administrative
/// policy that permits collection. It is independent of experimental mode.
pub fn set_telemetry_enabled(&mut self, enabled: bool) -> &mut Self {
| .inner | ||
| .telemetry | ||
| .as_ref() | ||
| .map(|config| telemetry::init(config, &mut logger)) |
There was a problem hiding this comment.
High (security) — Registering ETW from mxc_engine invalidates the provider's executable-only unsafe precondition.
This newly moves provider registration into a library used by mxc-sdk and the existing mxc_ffi cdylib. mxc_telemetry still justifies the unsafe registration by stating MXC is an executable, not a DLL, so unload ordering is irrelevant. A leaked streaming handle or explicit native-library unload can therefore leave ETW holding callbacks into an unmapped module.
Fix: Keep registration in process-lifetime executables, or add an explicit FFI/global teardown contract that prevents unloading until all provider references are released, and update the unsafe preconditions accordingly.
|
|
||
| /// Handle a typed consent-maintenance envelope, if present. | ||
| pub fn handle_maintenance_input(input: &str, is_base64: bool) -> Option<ConsentCliOutcome> { | ||
| let json = match crate::config_parser::decode_request_input(input, is_base64) { |
There was a problem hiding this comment.
High (reliability/cross-platform) — The maintenance probe consumes ordinary config streams before normal parsing reads them again.
Every executor calls this probe first. For a non-maintenance request, decode_request_input reads the supplied path here, then the normal loader reads the same path again. Regular files pay duplicate I/O/parsing; named pipes, /dev/stdin, and process-substitution paths are drained by the first read and fail or block on the second.
Fix: Decode the input once in the executor and pass the loaded JSON to both maintenance discrimination and ordinary request parsing.
| } | ||
|
|
||
| impl Drop for TelemetryProcess { | ||
| fn drop(&mut self) { |
There was a problem hiding this comment.
Medium (reliability) — Dropping a live telemetry wrapper releases the provider without emitting the promised terminal event.
wait() emits, but Drop only shuts down. kill() likewise only forwards, and try_wait() ignores its error branch. Kill-then-free, drop-without-wait, or a failed poll can therefore lose the invocation's Execution/Error record while releasing its provider reference, contradicting the wrapper's “exactly one terminal event” contract.
Fix: Use an explicit terminal-state machine and emit a bounded killed/cancelled/error outcome on kill, drop, and try_wait failure before releasing the provider exactly once.
| ); | ||
| let json = serde_json::to_string(&presentation) | ||
| .map_err(|error| format!("failed to serialize consent presentation: {error}"))?; | ||
| println!("{json}"); |
There was a problem hiding this comment.
Medium (reliability) — A closed presenter pipe causes a Rust panic.
println! panics on stdout write failure. If the SDK host times out, crashes, or closes its pipe before the child emits the presentation envelope, this turns a routine IPC disconnect into an uncontrolled panic before normal executor setup.
Fix: Write through stdout().lock()/writeln! and map BrokenPipe or other write failures to a clean transport error and nonzero outcome.
| // cross-phase lifecycle to join. | ||
| correlation_vector: "", | ||
| }); | ||
| if invocation_can_emit(active) { |
There was a problem hiding this comment.
Medium (performance/reliability) — Reuse one authorization result per logical emission.
The outer completion path checks authorization, then this check rereads the consent file and policy registry before Execution, and line 223 does it again before the paired Error. Similar repetition exists in early-exit, crash/cancellation, and state-aware paths. This adds blocking retry-capable I/O and can split an intended event pair if authorization changes between reads.
Fix: Perform one live check immediately before each logical emission and pass the result through to both event writes.
| telemetry::emit_sdk_early_exit( | ||
| telemetry_active, | ||
| &containment, | ||
| telemetry::FailureReason::InitError, |
There was a problem hiding this comment.
Medium (correctness) — Preserve the actual error category instead of reporting every spawn failure as initialization.
dispatch::spawn_runner returns typed MxcError codes, but this path emits every failure as InitError. Malformed requests, unsupported containment, and policy validation failures are therefore recorded under the wrong bounded telemetry category.
Fix: Share the exhaustive MxcErrorCode → FailureReason mapping used by the state-aware path and emit the mapped reason here.
| let phase = parsed.phase; | ||
| let phase_name = phase.as_str(); | ||
| let incoming_correlation = parsed.correlation_vector.clone(); | ||
| let mut logger = Logger::new(Mode::Buffer); |
There was a problem hiding this comment.
Medium (correctness/observability) — State-aware telemetry initialization warnings are discarded.
Both state-aware entry points initialize telemetry with a buffered Logger, but never surface logger.take_warnings(). Ordinary SDK spawn attaches those warnings to its process wrapper, while state-aware envelope and streaming calls silently lose provider-registration diagnostics.
Fix: Include warnings in envelope results and wrap streaming state-aware processes with the same warning propagation used by spawn.
| if phase == Phase::Provision && telemetry_active { | ||
| inject_correlation_vector(&mut outcome, &correlation); | ||
| } | ||
| telemetry::emit_sdk_state_aware( |
There was a problem hiding this comment.
Medium (test coverage) — Add an engine-level state-aware telemetry execution test.
The shared crate now tests event planning/emission, and engine tests cover correlation helpers, but no test drives this integration end to end. Phase attribution, provision-vector injection, engine error mapping, and provider release can still regress independently while both component suites stay green.
Fix: Add an engine/integration test with a capture sink that executes representative provision and exec outcomes and asserts phase, shared correlation base, error mapping, and final provider release.
.github/copilot-instructions.md.Summary
Promotes telemetry to the stable top-level configuration and integrates the reviewed consent and policy foundation with the engine and executors. Adds typed maintenance requests, live authorization checks, generated schemas, migration handling, and Windows consent/ETW smoke coverage.
This is PR 3 of 5 and depends on the consent/policy foundation in PR 2.
Stack
Review only this PR's diff; prerequisite behavior is in the PR above.