diff --git a/README.md b/README.md index aed988796..823ecad18 100644 --- a/README.md +++ b/README.md @@ -229,13 +229,19 @@ Successful non-dry-run audits require capture metadata, canonical denials JSON, > **Warning:** `--audit` injects `permissiveLearningMode` — AppContainer restrictions are **not** enforced for the duration of the run. Use only for policy authoring. It cannot be combined with `processContainer.captureDenials`; use `captureDenials.mode: "allow"` for permissive application-driven capture. `learningModeLogging` and `permissiveLearningMode` are reserved internal capability names and are rejected in `processContainer.capabilities`. See [docs/learning-mode/capabilities.md](docs/learning-mode/capabilities.md) for the three learning-mode flows. -## Telemetry (Experimental) +## Telemetry MXC supports optional TraceLogging ETW telemetry for execution observability. When enabled, structured events (`MXC.Execution` and `MXC.Error`) are emitted to the local ETW subsystem via the Rust [`tracelogging`](https://crates.io/crates/tracelogging) crate. Every event includes common fields (Version, Channel, IsDebugging, `UTCReplace_AppSessionGuid`) as Part C custom event data. -Telemetry is **experimental** and requires: -1. The `--experimental` CLI flag -2. `"experimental": { "telemetry": { "enabled": true } }` in the JSON config +Telemetry requires: +1. Top-level `"telemetry": { "enabled": true }` in the JSON config +2. Explicit per-user telemetry consent on Windows +3. An administrative policy that permits collection, when a policy is configured + +The configuration flag is an additional per-run opt-in; it cannot grant consent +or bypass an administrative block. Telemetry remains off unless every applicable +gate is open. MXC does not use the Windows Diagnostics & feedback setting as a +substitute for application consent. On non-Windows platforms, all telemetry functions are no-ops. @@ -245,12 +251,11 @@ The software may collect information about you and your use of the software and #### How to turn telemetry off -Telemetry is **off by default**. MXC emits telemetry only when **both** of the following are set, so no action is required to keep it disabled: - -1. The `--experimental` CLI flag is passed, **and** -2. `"experimental": { "telemetry": { "enabled": true } }` is present in the JSON config. +Telemetry is **off by default**. To keep it off, do not set +`"telemetry": { "enabled": true }` for the run. -Omitting either (the default) turns telemetry off entirely. On non-Windows platforms all telemetry functions are no-ops. +If telemetry is enabled in config, collection still does not occur unless +Windows user consent is granted and administrative policy allows collection. #### What official builds send @@ -277,6 +282,8 @@ Privacy information can be found at https://privacy.microsoft.com and in the Mic | [docs/windows-sandbox/windows-sandbox.md](docs/windows-sandbox/windows-sandbox.md) | Windows Sandbox backend | | [docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md](docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md) | State-aware sandbox lifecycle API | | [docs/telemetry/telemetry.md](docs/telemetry/telemetry.md) | TraceLogging telemetry architecture | +| [docs/telemetry/telemetry-consent-design.md](docs/telemetry/telemetry-consent-design.md) | Telemetry consent contract | +| [docs/telemetry/telemetry-administrative-policy.md](docs/telemetry/telemetry-administrative-policy.md) | Administrative telemetry controls | ## Contributing diff --git a/docs/schema.md b/docs/schema.md index cdbcd3128..68ff4135c 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -124,7 +124,7 @@ production configs and the dev schema when working on experimental features: "nestedPty": true, // Allow inner process to allocate its own pty (posix_openpt) "keychainAccess": false // Allow Keychain via securityd / trustd / cfprefsd / lsd.* }, - "telemetry": { // Telemetry (experimental, Windows only) + "telemetry": { // Telemetry (Windows only) "enabled": true // Emit TraceLogging ETW events via pure Rust tracelogging crate } } @@ -134,17 +134,12 @@ production configs and the dev schema when working on experimental features: > **State-aware fields.** The `phase` top-level field is the **state-aware > discriminator**: a request that includes it is parsed as a state-aware > lifecycle request (see below), *not* the one-shot config above. The `sandboxId` -> and `correlationVector` top-level fields are state-aware-only — a one-shot -> request carrying either is rejected with a parse error. `correlationVector` is -> the Microsoft Correlation Vector (MS-CV) seeded at `provision` and relayed by -> the client onto later phases (emitted under the TraceLogging `__TlgCV__` field -> when experimental telemetry is enabled). The client relays the value verbatim; -> the executor validates it on each non-`provision` phase and *spins* a fresh -> child element off a mutable base, passes an already-frozen vector through -> unchanged, and reseeds a new base if the relayed value is missing or malformed. -> See +> top-level field is state-aware-only — a one-shot request carrying `sandboxId` +> is rejected with a parse error. Callers cannot supply `correlationVector`; +> it is rejected as an unknown field because lifecycle correlation is internal +> to MXC and is not part of the request or response contract. See > [`docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md`](state-aware-lifecycle/mxc-state-aware-sandbox-api.md) -> and [`docs/telemetry/telemetry.md`](telemetry/telemetry.md#correlating-a-lifecycle). +> and [`docs/telemetry/telemetry.md`](telemetry/telemetry.md). ### Working Directory diff --git a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md index 541ace5ab..b10190646 100644 --- a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md +++ b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md @@ -395,7 +395,6 @@ type DeprovisionMetadataFor = interface ProvisionResult { sandboxId: SandboxId; metadata?: ProvisionMetadataFor; - correlationVector?: string; // MS-CV to relay onto later phases (telemetry) } interface StartResult { @@ -664,7 +663,6 @@ State-aware-only fields: | Field | Type | Required | Description | |---|---|---|---| | `phase` | `Phase` member | Yes | Discriminator. Absence means a one-shot request. | -| `correlationVector` | string | No. Relayed by the client onto non-`provision` phases; absent on `provision` (seeded by the executor). Rejected as a parse error on one-shot requests. | Microsoft Correlation Vector (MS-CV) seeded at `provision` and returned in its result; the client relays it verbatim into later phases so the lifecycle shares a telemetry base prefix (emitted under `__TlgCV__`). Each non-`provision` phase validates the relayed value and *spins* a fresh child element off a mutable base (keeping repeat invocations distinct), passes an already-frozen vector through unchanged, and reseeds a new base if it is absent or malformed. Ignored unless experimental telemetry is enabled. See [telemetry docs](../telemetry/telemetry.md#correlating-a-lifecycle). | | `process` | `ProcessConfig` | Required for `exec`; absent otherwise. | Cross-backend execution fields. | Cross-cutting fields available to state-aware (state-aware-only at top level — backends @@ -773,7 +771,7 @@ type NonExecResponseEnvelope = { result: TResult } | { error: ErrorEnve | Phase | `TResult` shape | |---|---| -| `provision` | `{ sandboxId: SandboxId; metadata?: object; correlationVector?: string }` | +| `provision` | `{ sandboxId: SandboxId; metadata?: object }` | | `start` | `{ metadata?: object }` | | `stop` | `{ metadata?: object }` | | `deprovision` | `{ metadata?: object }` | diff --git a/docs/telemetry/telemetry-administrative-policy.md b/docs/telemetry/telemetry-administrative-policy.md new file mode 100644 index 000000000..00400aaaf --- /dev/null +++ b/docs/telemetry/telemetry-administrative-policy.md @@ -0,0 +1,161 @@ +# MXC administrative telemetry policy + +Audience: developers embedding MXC in a product that ships to Windows devices. + +MXC supports a single administrative policy that lets an organization prevent +MXC from collecting diagnostic data on a device, regardless of what the user +chooses. It is a **ceiling, never a grant** — see +[Why policy cannot substitute for consent](#why-policy-cannot-substitute-for-consent). + +The policy is **Windows-only**, because [MXC only ever collects telemetry on +Windows](telemetry.md). On Linux and macOS there is nothing to restrict. + +## The setting + +| | | +|---|---| +| Key | `HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Mxc` | +| Value name | `AllowTelemetry` | +| Type | `REG_DWORD` | +| Scope | Machine (all users on the device) | + +`HKLM\SOFTWARE\Policies\...` is an administrator-controlled machine-level registry location. MXC deliberately does not honor a per-user (`HKCU`) equivalent. + +### Values + +The values mirror the Windows diagnostic-data scale so administrators do not +have to learn a second one: + +| Value | Windows name | Effect on MXC | +|-------|--------------|---------------| +| *(value absent)* | — | **Unrestricted.** The user's own choice decides. | +| `0` | Security / Off | **Blocked.** MXC collects nothing. | +| `1` | Required (Basic) | **Blocked.** MXC collects nothing. | +| `3` | Optional (Full) | **Allowed.** MXC may collect *if the user has also consented.* | +| anything else | — | **Blocked** (fail closed). | + +`1` blocks MXC because everything MXC emits is classified as +*product-and-service-usage* data — optional diagnostic data in Windows' +taxonomy. MXC emits no required diagnostic data, so there is nothing left to +send at level `1`. Value `2` is not a defined level on modern Windows and is +therefore treated as unrecognized. + +Any value MXC cannot read or cannot parse — a wrong type, a corrupt value, a +registry error — is treated as `Blocked`. Telemetry collection is never the +outcome of a failure. + +## Deploying it + +### Registry deployment + +Use any deployment mechanism that writes +HKLM\SOFTWARE\Policies\Mxc\AllowTelemetry directly. For example: + +- A startup/admin script: + + ```powershell + $key = 'HKLM:\SOFTWARE\Policies\Mxc' + New-Item -Path $key -Force | Out-Null + Set-ItemProperty -Path $key -Name 'AllowTelemetry' -Value 0 -Type DWord + ``` + +- Any installer or configuration package that writes the same value. + +### Verifying + +Whatever consent-management/status surface ships must report the user's +recorded choice separately from the administrative ceiling. + +For example, if a user previously granted consent and an administrator later +sets `AllowTelemetry=0`, the status must still make both facts visible: + +- the stored user choice remains **granted** +- the administrative policy is **blocked** +- effective collection is therefore **off** +- no prompt is needed while the policy blocks + +Keeping those states separate lets hosts explain why telemetry is currently +disabled without losing the user's prior choice. + +## How the policy interacts with user consent + +MXC collects diagnostic data only when **every** gate is open: + +``` +collect = policy_permits AND user_consented AND build_has_telemetry_enabled +``` + +Concretely: + +| Policy | User consent | Result | +|--------|--------------|--------| +| unrestricted | granted | collects | +| unrestricted | denied / undetermined | **no collection** | +| allowed (`3`) | granted | collects | +| allowed (`3`) | denied / undetermined | **no collection** | +| blocked (`0`/`1`/other) | *anything* | **no collection** | + +Two consequences worth calling out: + +1. **A blocking policy suppresses the first-run consent prompt.** Asking a user + to permit something the administrator has already refused is a question with + no meaning. `needsPrompt` reports `false` while the policy blocks. +2. **A blocking policy does not erase the user's recorded choice.** If a user + had already consented and an administrator later blocks telemetry, + collection stops immediately but the recorded consent is preserved. If the + policy is later relaxed, the user's own prior decision takes effect again + rather than the user being re-prompted. + +### Why policy cannot substitute for consent + +An administrator setting `AllowTelemetry=3` does **not** cause MXC to start +collecting. It only removes MXC's administrative restriction; the user is still +asked, and still decides. + +In particular, **if the user has opted out and the policy permits collection, +the result is opt-out.** A policy can only ever subtract. There is no policy +value, and no combination of policy and configuration, that causes MXC to +collect from a user who has not explicitly granted consent — including a user +who has never been asked. + +This is a product rule MXC holds itself to, not merely a reading of external +guidance. It is also consistent with that guidance: Microsoft's privacy +direction for components classified as *apps* (rather than as parts of the +operating system) requires them to build their own notice-and-consent +experience and not to rely on the Windows diagnostic-data consent. An +administrative policy is an availability control, not an expression of a +user's informed choice, and no Microsoft guidance treats an admin "allow" as +consent on the user's behalf. + +The implementation should enforce that rule in one place rather than +re-deriving it in each host surface. + +## Relationship to the Windows `AllowTelemetry` policy + +MXC reads **only** its own key. It does not read, and is not affected by: + +- `HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection\AllowTelemetry` +- `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection\AllowTelemetry` +- the Windows Settings → Diagnostics & feedback choice +- the `System/AllowTelemetry` Policy CSP node + +Two reasons: + +1. **Microsoft documents that it doesn't apply.** The Policy CSP documentation + for `System/AllowTelemetry` states that it "impacts the operating system and + apps that are considered part of Windows and doesn't apply to any additional + apps installed by your organization." MXC is such an app. +2. **Reading it would leak the user's Windows choice into MXC.** The supported + OS APIs for evaluating that policy deliberately combine the administrative + setting with the user's own Settings-app selection. Consuming them would + make MXC's behaviour depend on Windows consent state, which MXC's design + forbids. + +The dominant pattern among Microsoft first-party applications — Office, +Visual Studio, Visual Studio Code, WinGet, PowerToys — is likewise an +application-specific policy under `SOFTWARE\Policies\Microsoft\`. + +## See also + +- [Telemetry overview](telemetry.md) +- [Telemetry consent design](telemetry-consent-design.md) diff --git a/docs/telemetry/telemetry-consent-design.md b/docs/telemetry/telemetry-consent-design.md new file mode 100644 index 000000000..e6e7a0041 --- /dev/null +++ b/docs/telemetry/telemetry-consent-design.md @@ -0,0 +1,203 @@ +# Telemetry consent design + +This document defines the Windows telemetry-consent contract. + +MXC telemetry is Windows-only and fails closed. Collection requires all four +conditions at the time each event is written: + +```text +per-run telemetry requested +&& current-version user consent is Granted +&& administrative policy does not block +&& the platform/provider is available +``` + +No prompt, no response, the literal string "No", dismissal, withdrawal, +malformed state, an unknown prompt version, or any read/write failure means no +telemetry. Administrative policy is a deny-only ceiling: it may block +collection but can never opt a user in. + +## Canonical consent resource + +MXC owns one immutable, versioned consent resource. Every EXE and SDK presenter +must show every supplied field verbatim. Hosts control layout, accessibility, +and native UI, but may not substitute wording. + +Current resource: + +- Resource version: `1` +- Locale: `en-US` +- Mandatory fallback: `en-US` +- Privacy link: + +### Title + +> Help improve Microsoft eXecution Container (MXC) + +### Body + +> Help improve MXC by sharing optional diagnostic data with Microsoft. +> If enabled, MXC sends diagnostic information about product usage, +> performance, and reliability. MXC does not send your commands, file paths, +> credentials, or other customer content. +> You can change your choice at any time. + +### Actions + +- Affirmative: **Yes** +- Negative: **No** +- Learn more: **Privacy Statement** + +Each field has a stable message ID. Locale lookup normalizes BCP 47 tags and +falls back to `en-US`; complete messages are translated as units rather than +assembled from fragments. Adding a translation does not change the public API. +A material wording or data-inventory change requires a new resource version and +explicit re-consent. + +## SDK presenter requirements + +An SDK host owns the native presentation experience, while MXC owns the +resource, decision validation, and persistence. Implementers must follow this +contract: + +1. Route consent through the MXC-owned request flow. Do not write the consent + store directly or create a separate grant path. +2. Render every supplied field verbatim: title, body, affirmative label, + negative label, learn-more label, and learn-more URL. Hosts may control + layout, accessibility, and platform-native styling, but must not hardcode, + shorten, reorder within a message, or replace the supplied wording. +3. Map the affirmative control to `Yes` and the negative control to `No`. +4. Map window close, cancel, timeout, or no response to `Dismissed`. If the UI + cannot be presented or otherwise fails, return a presenter error instead. + Never infer `Yes` from a default button, policy, prior application + preference, or absence of a response. +5. Make the learn-more control open the supplied URL, normally in the user's + default browser. Do not substitute a different privacy destination. +6. Return only the typed decision to MXC. MXC persists the decision together + with the resource version and locale that were actually presented. +7. Use the typed status API to explain the current stored/effective state and + administrative ceiling. Provide an explicit withdrawal control in the + host's settings or other appropriate telemetry-controls surface. + +MXC does not invoke the presenter when the result is already determined: + +- `AlreadyGranted`: the current resource version is already granted. +- `PolicyBlocked`: administrative policy prevents collection. +- `NotApplicable`: telemetry is unavailable on this platform. + +If the host never calls the request API, telemetry remains off. A presenter +failure must be surfaced as an error to the host and must not create or alter a +grant. + +## State and persistence + +MXC stores consent per user at: + +```text +%LOCALAPPDATA%\mxc\telemetry-consent.json +``` + +The persisted consent record is a versioned JSON document. The current schema +version is 2 and records: + +- `consent`: `granted` or `denied` +- `promptResourceVersion` +- `promptLocale` +- MXC version, source, and update timestamp for local audit/support provenance + +Stored and effective state are reported separately. A legacy grant without the +current prompt version remains visible as `storedState: "granted"` but is +`effectiveState: "undetermined"` with reason `prompt-version-missing` or +`prompt-version-unsupported`. It never authorizes collection. Legacy denial +remains denied. + +Only two operations mutate the store: + +1. A presenter invoked by MXC returns an explicit `yes` or `no` for the prompt + supplied in that same invocation. +2. The explicit withdrawal operation persists `denied`. + +Dismissal, EOF, presenter failure, localization fallback failure, or never +requesting consent does not fabricate or rewrite a decision. + +### Atomicity, locking, and recovery + +Every consent-store mutation must be atomic across processes. Synchronization +must use a separate primitive keyed by the consent-store path, such as a named +mutex or a sibling lock file. It must never keep the live JSON file open +because that can prevent atomic replacement on Windows. + +1. Acquire the cross-process writer lock before reading, validating, or + replacing the file. +2. Write the full replacement document to a sibling temporary file in the same + directory. +3. Flush the temporary file, then replace the live file with a single + same-volume atomic rename/replace operation. +4. Release the lock only after the replacement is durable. + +Readers must coordinate through the same separate primitive or use an +equivalent close-and-retry strategy; they must not hold the live JSON handle +while a writer replaces it. They must never observe or accept a +partially-written document. Any malformed file, missing required field, +version mismatch, unreadable path, or lock/replace failure is treated as +`undetermined`/`denied` as appropriate and must not authorize collection. +Recovery is fail-closed: MXC may surface a typed error or status reason to the +host, but it must not silently recreate a grant from a corrupted file. + +## Administrative ceiling + +Policy is read from: + +```text +HKLM\SOFTWARE\Policies\Mxc\AllowTelemetry (REG_DWORD) +``` + +Only value `3` permits MXC optional diagnostic data. Other values, wrong value +types, unreadable state, and ambiguity fail closed to `blocked`. An absent key +is `unrestricted`. `allowed` and `unrestricted` merely leave the user's choice +in control; neither creates consent. + +A consent request made while policy is blocked does not invoke the presenter or +persist dormant consent. Withdrawal remains available while blocked. + +## Per-run enablement + +Consent is not an execution switch. Each run must also request telemetry with +the stable top-level `telemetry.enabled: true` setting. The switch never +prompts, never persists consent, and never bypasses consent or administrative +policy. + +Consent-management surfaces should still support the three behavior classes +defined here: + +- request consent through a presenter when a decision is needed +- withdraw consent by persisting `denied` +- report stored consent, effective consent, blocking policy, and whether a + prompt is still required + +## Live enforcement and event compatibility + +Consent and policy are checked immediately before each logical telemetry +emission. A terminal failure may emit paired `MXC.Error` and `MXC.Execution` +events under the same authorization decision so collectors never receive half +of the pair. A withdrawal or newly blocking policy stops the next logical +emission from an already-running process even if the provider was registered +earlier. + +The public ETW event identities remain: + +- `MXC.Execution` +- `MXC.Error` + +Collectors may continue filtering those names; consent stabilization does not +rename the provider or events. + +## Validation and release gate + +Implementations should include deterministic coverage for policy evaluation, +prompt gating, withdrawal, corruption recovery, and concurrent consent-store +mutation. The checked-in ETW smoke test only validates event emission; it is +not a substitute for consent-flow coverage. + +The versioned wording and data/control inventory still require Microsoft +privacy/legal approval before release. diff --git a/docs/telemetry/telemetry.md b/docs/telemetry/telemetry.md index 6de0c1117..a734d7195 100644 --- a/docs/telemetry/telemetry.md +++ b/docs/telemetry/telemetry.md @@ -61,6 +61,11 @@ shared Part C custom event fields: ## Events +The provider-qualified uploaded event identities are +`Microsoft.MXC/MXC.Execution` and `Microsoft.MXC/MXC.Error`. `Microsoft.MXC` +is the TraceLogging provider name; `MXC.Execution` and `MXC.Error` are the +event names. + ### MXC.Execution Emitted when a one-shot execution completes (success or failure). It is also @@ -77,111 +82,31 @@ the one-shot path, a clean non-zero sandbox exit is not treated as an MXC error. | Field | Type | Description | |-------|------|-------------| -| `mxc.backend` | string | Containment backend name | +| `mxc.sandbox_kind` | string | Containment kind requested by the caller (`process`, `vm`, or a concrete backend name) | +| `mxc.backend` | string | Concrete containment backend selected on the host | | `mxc.exit_code` | int32 | Process exit code | | `mxc.outcome` | string | `"success"` or `"failure"` | | `mxc.duration_ms` | uint64 | Total execution time | | `mxc.failure_reason` | string | Failure category (if applicable) | | `mxc.phase` | string | State-aware lifecycle phase (`provision`\|`start`\|`exec`\|`stop`\|`deprovision`); empty for one-shot executions | -| `__TlgCV__` | string | Microsoft Correlation Vector (MS-CV) — the lifecycle correlation key (see [Correlating a lifecycle](#correlating-a-lifecycle)); empty for one-shot executions | + +### MXC.Error Emitted on execution errors. | Field | Type | Description | |-------|------|-------------| -| `mxc.backend` | string | Containment backend name | +| `mxc.sandbox_kind` | string | Containment kind requested by the caller (`process`, `vm`, or a concrete backend name) | +| `mxc.backend` | string | Concrete containment backend selected on the host | | `mxc.error_type` | string | Error category (`config_error`, `policy_error`, `process_error`, `timeout`, `init_error`, `internal_error`, `cancelled`, `unknown`) | | `mxc.exit_code` | int32 | Process exit code | | `mxc.phase` | string | State-aware lifecycle phase; empty for one-shot executions | -| `__TlgCV__` | string | Microsoft Correlation Vector (MS-CV) — the lifecycle correlation key (see [Correlating a lifecycle](#correlating-a-lifecycle)); empty for one-shot executions | > **No free-form error text is emitted.** Error messages can contain paths, > usernames, or credentials, so `MXC.Error` deliberately carries only the > bounded `error_type` category and the numeric `exit_code` — never the > message string itself. -### Correlating a lifecycle - -The state-aware lifecycle runs each phase (`provision` → `start` → `exec` → -`stop` → `deprovision`) as a **separate `wxc-exec` process**. The -`UTCReplace_AppSessionGuid` common field is therefore per-process and cannot join -events from different phases of the same sandbox. The stable join key is the -**Microsoft Correlation Vector (MS-CV)**, emitted under TraceLogging's reserved -`__TlgCV__` field. - -MS-CV is a hierarchical, propagatable identifier of the form -`..…` — a random base (128 bits, 22 base64 chars) plus a -dotted chain of decimal elements. MXC uses it as follows: - -- **`provision`** seeds a fresh random base (`correlation_vector::seed()`) and - returns it to the client in its result envelope as `result.correlationVector`. - This is the lifecycle's root vector. -- The client **relays** that vector verbatim into every later phase (the SDK - surfaces it as `ProvisionResult.correlationVector` and accepts it back as - `SandboxSpawnOptions.correlationVector`, sent on the wire as the top-level - `correlationVector` field). -- Each non-`provision` phase **spins** the relayed base - (`correlation_vector::spin()`) to derive a distinct child vector that still - shares the lifecycle's base prefix. Spin (rather than a plain extend) is used - because `exec` is multi-invocation and the client is a dumb relay — a plain - extend would collapse every phase to the same `base.0`, whereas spin folds in a - coarse timestamp + entropy so sibling phases get distinct, ordered vectors. - -An analyst groups all phases of one lifecycle by the shared **base prefix** of -`__TlgCV__`, and orders/​distinguishes phases within it by the spun elements. - -An MS-CV is capped at 127 characters. In the unlikely event a long-lived -lifecycle grows the vector to that cap, the next operator **freezes** it: it -appends the `!` terminator (dropping the trailing element **whole**, at an -element boundary, if needed to stay within the cap — never truncating a -multi-digit element to a falsified value like `.42` → `.4`) and the vector is -never mutated again. `increment` likewise freezes when its trailing element is -already at `u32::MAX` and so cannot advance (the spec's value-overflow -behaviour). Every later phase then relays and emits that same frozen vector -verbatim, so correlation by base prefix still holds — only the per-phase -ordering elements stop advancing. - -**Canonical validation.** A relayed vector is only built on (spun) when it is -*relayable* — a well-formed mutable vector (canonical 22-char base64 base whose -final char encodes a zero low nibble, followed by one or more canonical decimal -`u32` elements) or a valid frozen (`!`-terminated) vector. Elements must be -canonical: no leading zeros (`0` is allowed, `01` is not), no sign, no -whitespace — a non-canonical element such as `01` would silently reshape the -vector (`01` → `2` on increment) and break lexical sortability, so it is -rejected. Anything not relayable — missing, empty, malformed, or a hostile -`!`-terminated value like `user@contoso.com!` — is reseeded to a fresh random -base rather than emitted verbatim. - -The correlation vector is **not** derived from the `sandbox_id`, so no -caller-supplied identity (e.g. a UPN embedded in an IsolationSession -`iso:` id) is ever involved: the base is pure randomness. Spin is defensive -— a missing, empty, or malformed relayed value falls back to a fresh seed rather -than panicking telemetry. `__TlgCV__` is empty for one-shot executions (which have -no lifecycle to correlate) and for a crash during `provision` before the vector is -stashed. It is only computed and emitted when experimental telemetry is active, so -provision output is unchanged when telemetry is off. - -**Why a relayed random vector rather than a hashed `sandbox_id`.** An alternative -design would derive the correlation key deterministically from the `sandbox_id` -(e.g. a hash), avoiding the wire/SDK relay entirely. We deliberately do **not** do -this, for two reasons: - -1. **PII-safety.** A `sandbox_id` is caller-influenced and can embed identity — an - IsolationSession id is literally `iso:`. Hashing narrows but does not - eliminate the exposure (a hash is a stable pseudonym and, for a low-entropy - input like a known UPN, is reversible by dictionary). A base seeded from pure - OS randomness carries no caller identity at all, which is the stronger and - simpler guarantee. -2. **WIL TraceLogging fidelity.** This implementation intentionally mirrors the WIL - TraceLogging correlation-vector design: a random 128-bit base extended/spun with - MS-CV v2 operators and emitted under the reserved `__TlgCV__` field. A bespoke - `sandbox_id`-hash scheme would diverge from that well-understood format and lose - the hierarchical parent/child structure (base prefix + spun elements) that lets - an analyst reconstruct phase ordering, not just group-by a flat key. - -The client relay is therefore a required part of the design, not incidental -plumbing: it is how the random root vector minted at `provision` reaches the later -per-phase executor processes, which otherwise share no state. ### Crash telemetry (panic hook) @@ -207,7 +132,7 @@ hook, so the default stderr backtrace still prints. > code, then claims the exactly-once terminal-emit slot. The recovered > `MXC.Execution` completion event is therefore suppressed, so telemetry reports > `mxc.exit_code` = 101 even though the recovered process ultimately exits with a -> different code (`-1`). The `101` here is a "a panic occurred" sentinel, not a +> different code (`-1`). the `101` here is a "a panic occurred" sentinel, not a > claim about the observed process exit code; `outcome` and `error_type` remain > accurate. Backends that do not catch panics (the Windows one-shot executor) > abort with `101`, so the recorded code matches the real exit. @@ -231,95 +156,41 @@ free-form text. | Linux | No-op — all telemetry functions return immediately | | macOS | No-op — all telemetry functions return immediately | -## Private GUID Substitution (Internal Builds) - -MXC supports an optional Microsoft telemetry group GUID for internal builds. -The mechanism is public; only the GUID value is private. - -### How it works - -``` -build.rs execution flow -======================== +## Consent -1. Check MXC_TELEMETRY_PROVIDER_GROUP_GUID env var - ├── NOT set → generate: define_provider!(MXC_PROVIDER, "Microsoft.MXC"); - └── SET → generate: define_provider!(MXC_PROVIDER, "Microsoft.MXC", - group_id("{guid}")); +Telemetry emission is gated by the per-run request, MXC-owned consent, +administrative policy, and provider availability. See +[`docs/telemetry/telemetry-consent-design.md`](telemetry-consent-design.md). -2. lib.rs includes the generated provider_def.rs via include!() -``` +## Privacy review status -The provider GUID is **not** specified in either branch. The `tracelogging` -crate's `define_provider!` macro derives it deterministically from the provider -name using the standard ETW name-hash algorithm (the same algorithm used by -``, WIL's `IMPLEMENT_TRACELOGGING_CLASS`, and .NET's -`EventSource`). For `"Microsoft.MXC"` the derived GUID is -`{7f10def4-a258-5fea-510e-2c3bb976687f}`. Keeping the name and GUID in lockstep -this way prevents drift and avoids hard-coding a literal that could collide -with another team's GUID. - -### CI pipeline steps - -Internal Microsoft builds set `MXC_TELEMETRY_PROVIDER_GROUP_GUID` to the real -Microsoft telemetry group GUID before `cargo build` on Windows, so events route -through the telemetry pipeline. Community forks that lack access to the private -GUID do not set this variable — the provider is registered without a group GUID -(plain ETW only). - -> **Follow-up:** The provider group GUID is now provided by a secret variable -> on the official Windows build pipeline, so official builds can route events -> through the telemetry pipeline. The build has always honored the variable -> (see *Local developer testing* below); public builds and community forks, -> which do not have access to the variable, continue to register the provider -> without a group GUID (plain ETW only). - -### Local developer testing - -```powershell -# Test with a dummy group GUID (not the real one) -$env:MXC_TELEMETRY_PROVIDER_GROUP_GUID = '00000000-1111-2222-3333-444444444444' -cargo build -p mxc_telemetry - -# Test without (public build) -Remove-Item Env:\MXC_TELEMETRY_PROVIDER_GROUP_GUID -cargo build -p mxc_telemetry -``` +The version 1 `en-US` consent wording is approved for release review. The +canonical title, body, action labels, and privacy link are documented in +[Telemetry consent design](telemetry-consent-design.md#canonical-consent-resource) +and must be rendered verbatim by every EXE and SDK presenter. -### What's public vs. private +### Data sent -| Item | Public? | Why | -|------|---------|-----| -| Provider name `"Microsoft.MXC"` | ✅ | Standard ETW naming | -| Provider GUID `{7f10def4-a258-5fea-510e-2c3bb976687f}` | ✅ | Derived from the name; identifies the provider, harmless | -| `build.rs` env var mechanism | ✅ | Mechanism is public | -| `MXC_TELEMETRY_PROVIDER_GROUP_GUID` env var name | ✅ | Key is public; value is private | -| Actual Microsoft telemetry group GUID | ❌ | Private — set in CI only | +MXC's optional diagnostic events contain: -## SDK License Override (EULA for npm Package) +- MXC version and channel +- Whether the build has debug assertions enabled (`IsDebugging`) +- Caller-requested sandbox kind and the concrete backend selected on the host +- Run outcome and exit code +- Run duration +- Bounded failure category +- State-aware lifecycle phase +- `UTCReplace_AppSessionGuid`, which asks the telemetry pipeline to supply a + random per-session app identifier -The public GitHub repo ships `sdk/node/LICENSE.md` as a plain MIT license. For -internal npm publishes, a separate EULA containing a **Section 2 — DATA** -clause (covering telemetry disclosure, opt-out, and GDPR) will be updated at -pack/publish time. +MXC does not emit commands, file paths, credentials, customer content, or +free-form error text. The consent notice's phrase “other customer content” +covers any such values that a host or sandbox may process but MXC does not +include in these events. -### How it works +### Review status -``` -1. CI pipeline (or local script) sets MXC_LICENSE_OVERRIDE env var - pointing to the markdown file of the EULA including additional telemetry language. - Note that the new EULA will include language outlining what data can be collected but - will otherwise remain MIT licensed. - -2. A license-override script (added in a follow-up build-integration PR) runs: - ├── MXC_LICENSE_OVERRIDE is set: - │ ├── Back up sdk/node/LICENSE.md → sdk/node/LICENSE.md.public - │ └── Copy new EULA over sdk/node/LICENSE.md - └── MXC_LICENSE_OVERRIDE is NOT set: - └── Restore sdk/node/LICENSE.md from .public backup (if exists) - -3. npm pack / npm publish picks up the new EULA as the LICENSE.md - in the published package (sdk/node/package.json "files" includes LICENSE.md). - -4. After publish, the revert path restores the original EULA document. -``` +The consent wording and canonical resource are approved for release review. +The broader data inventory, retention, access, regional processing, deletion, +and localization/accessibility decisions remain pending explicit privacy +review. diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md index d9c3b55d2..3101dd2cc 100644 --- a/sdk/dotnet/README.md +++ b/sdk/dotnet/README.md @@ -1,6 +1,6 @@ # Microsoft.Mxc.Sdk (C# SDK) -A .NET binding for [MXC](../README.md) (Microsoft eXecution Container), +A .NET binding for [MXC](../../README.md) (Microsoft eXecution Container), implemented in C#. It runs a command inside a sandbox described by a `SandboxPolicy`, capturing the output — by P/Invoking the native `mxc_ffi` library, which wraps the Rust engine. @@ -155,6 +155,30 @@ failure, `MxcSandboxProcess.OutputMetadata` exposes the same structured feature outputs as `RunResult.OutputMetadata`. Disposing without waiting deletes an internal ETL even when retention was requested. +## Telemetry consent + +MXC telemetry is Windows-only and remains off until both of these are true: +1. the user has explicitly granted MXC-owned consent, and +2. the caller opts this invocation in via telemetry settings. + +Telemetry remains off by default unless the caller opts in with `SandboxPolicy.TelemetryEnabled = true` (or the equivalent phase-level `TelemetryEnabled` setting for state-aware requests) and applicable Windows consent/policy gates permit collection. + +Any .NET consent surface should stay UI-agnostic, present the canonical +resource verbatim through a host callback, persist only explicit yes/no +decisions, treat dismissal and failures as non-grants, and follow the rules in +[`docs/telemetry/telemetry-consent-design.md`](../../docs/telemetry/telemetry-consent-design.md) +and its +[SDK presenter requirements](../../docs/telemetry/telemetry-consent-design.md#sdk-presenter-requirements). + +### Administrative policy + +An IT administrator can still block MXC telemetry device-wide via MXC's own +registry policy setting. See +[`docs/telemetry/telemetry-administrative-policy.md`](../../docs/telemetry/telemetry-administrative-policy.md) +for the stable registry contract and interaction rules. Any eventual .NET +policy/consent query must fail closed rather than upgrading an unreadable +device state into collection. + ## Projects - **`Microsoft.Mxc.Sdk`** — the class library (public API + generated P/Invoke). @@ -163,6 +187,8 @@ internal ETL even when retention was requested. Build/test everything: `dotnet test sdk/dotnet/Microsoft.Mxc.Sdk.slnx`. +Run native telemetry-consent integration tests in the **Debug** configuration. + ## Native library loading `NativeLibraryResolver` locates `mxc_ffi` in this order: diff --git a/sdk/node/README.md b/sdk/node/README.md index 0a7b421a9..f5b6bbb99 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -241,16 +241,15 @@ import { } from '@microsoft/mxc-sdk'; // Every call takes a single options object (3rd arg). Experimental backends -// must pass `experimental: true`; relay `correlationVector` on every phase -// after provision so telemetry shares one base prefix. +// must pass `experimental: true`. // isolation_session provision requires the unrestricted-network acknowledgment: // the container's network cannot be filtered or denied, so you must opt in. -const { sandboxId, correlationVector } = await provisionSandbox( +const { sandboxId } = await provisionSandbox( 'isolation_session', { network: { defaultPolicy: 'allow', allowLocalNetwork: true } }, { experimental: true }, ); -const opts = { experimental: true, correlationVector }; +const opts = { experimental: true }; await startSandbox(sandboxId, undefined, opts); @@ -261,8 +260,6 @@ await stopSandbox(sandboxId, undefined, opts); await deprovisionSandbox(sandboxId, undefined, opts); ``` -> **Correlating telemetry across phases:** when experimental telemetry is enabled, `provisionSandbox` returns a `correlationVector` (a Microsoft Correlation Vector). Relay it verbatim as `options.correlationVector` on every later phase so all phases of one lifecycle share a telemetry base prefix (emitted under `__TlgCV__`). The client relays the value unchanged; the executor validates it on each phase and derives that phase's own vector from it (spinning a fresh child element off a mutable base, or reseeding if it is missing or malformed). It is `undefined` when telemetry is off, and safe to omit otherwise. - `windows_sandbox` follows the same shape (substitute the containment string and provide `filesystem.readwritePaths` / `readonlyPaths` at provision if needed). See [`docs/windows-sandbox/windows-sandbox.md`](https://github.com/microsoft/mxc/blob/main/docs/windows-sandbox/windows-sandbox.md) for the per-phase config matrix. `wslc` follows the same shape and needs no provision config at all (it defaults to an `alpine:latest` container with no network). Provide `filesystem.readwritePaths` / `readonlyPaths` (mounted for the sandbox's lifetime), `network.defaultPolicy: 'allow'` (a bridged container; the default `'block'` gives no network), and/or a backend-specific `image` / `imageTarPath` at provision; inject a cooperative `network.proxy: { url }` per-exec. WSLc state-aware requests default to schema `0.8.0-alpha`. See [`docs/wsl/wslc-state-aware.md`](https://github.com/microsoft/mxc/blob/main/docs/wsl/wslc-state-aware.md) for the per-phase config matrix. @@ -430,6 +427,26 @@ Full TypeScript definitions ship with the package (`dist/index.d.ts`). All expor --- +## Telemetry Consent + +Telemetry is off-by-default unless the caller opts in with top-level `telemetry.enabled: true` and the applicable Windows consent/policy gates permit collection. + +Telemetry consent behavior follows +[`docs/telemetry/telemetry-consent-design.md`](https://github.com/microsoft/mxc/blob/main/docs/telemetry/telemetry-consent-design.md): +the SDK stays UI-agnostic, renders the canonical resource verbatim through a +host presenter, persists only explicit yes/no decisions, treats dismissal and +failures as non-grants, and never lets policy or transport failures opt a user +in. + +### Administrative policy + +An IT administrator can still block MXC telemetry device-wide via MXC's own +registry policy setting. See +[`docs/telemetry/telemetry-administrative-policy.md`](https://github.com/microsoft/mxc/blob/main/docs/telemetry/telemetry-administrative-policy.md) +for the stable registry contract and interaction rules. + +--- + ## Further Reading - [`docs/schema.md`](https://github.com/microsoft/mxc/blob/main/docs/schema.md) — full configuration schema reference diff --git a/tests/scripts/run_telemetry_etw_smoke_test.ps1 b/tests/scripts/run_telemetry_etw_smoke_test.ps1 index 7c0243948..b27603fa4 100644 --- a/tests/scripts/run_telemetry_etw_smoke_test.ps1 +++ b/tests/scripts/run_telemetry_etw_smoke_test.ps1 @@ -8,7 +8,7 @@ that at least one event was captured. This test uses the PUBLIC provider GUID (already in the open-source - code) — it does NOT depend on or reveal the private telemetry group GUID. + code) - it does NOT depend on or reveal the private telemetry group GUID. Requires: Administrator privileges (for ETW session creation), wxc-exec.exe built, logman.exe (ships with Windows). @@ -111,6 +111,26 @@ if (Test-Path $etlDir) { Remove-Item -Recurse -Force $etlDir } New-Item -ItemType Directory -Path $etlDir -Force | Out-Null $etlFile = Join-Path $etlDir 'mxc_trace.etl' +# --------------------------------------------------------------------------- +# Setup: isolated consent seed for deterministic telemetry gating +# --------------------------------------------------------------------------- +$localAppDataOverride = Join-Path $etlDir 'localappdata' +$consentDir = Join-Path $localAppDataOverride 'mxc' +New-Item -ItemType Directory -Path $consentDir -Force | Out-Null +$consentFile = Join-Path $consentDir 'telemetry-consent.json' + +# Seed an explicit granted consent record for this test run so event capture +# does not depend on host-global consent state. +$consentRecord = @{ + version = 2 + consent = 'granted' + promptResourceVersion = '1.0' + promptLocale = 'en-US' + source = 'run_telemetry_etw_smoke_test.ps1' + updatedAtUtc = [DateTime]::UtcNow.ToString('o') +} +$consentRecord | ConvertTo-Json -Depth 4 | Set-Content -Path $consentFile -Encoding utf8 + # --------------------------------------------------------------------------- # Step 1: Start ETW trace session # --------------------------------------------------------------------------- @@ -132,18 +152,40 @@ Write-Host "ETW session started, writing to $etlFile" Write-Host "`n--- Running wxc-exec with telemetry ---" -ForegroundColor Yellow try { - # Run with --experimental to enable the telemetry section. The provider is - # registered during init (before execution); the MXC.Execution / MXC.Error - # events are emitted on completion, after the runner returns. The sandbox - # itself may fail (e.g. AppContainer prerequisites), but completion - # telemetry still fires for the failure, so events should be captured. - $proc = Start-Process -FilePath $wxcExe ` - -ArgumentList "--debug", "--experimental", $configFile ` - -PassThru -NoNewWindow -Wait - Write-Host "wxc-exec exited with code $($proc.ExitCode)" + $previousLocalAppData = $env:LOCALAPPDATA + $previousTestOverride = $env:MXC_TEST_LOCALAPPDATA_OVERRIDE + + $env:LOCALAPPDATA = $localAppDataOverride + $env:MXC_TEST_LOCALAPPDATA_OVERRIDE = $localAppDataOverride + try { + # Run with --experimental to enable the telemetry section in this branch. + # The provider is registered during init (before execution); completion + # telemetry events are emitted after dispatch returns. + $executionTimeoutSeconds = 60 + $proc = Start-Process -FilePath $wxcExe ` + -ArgumentList "--debug", "--experimental", $configFile ` + -PassThru -NoNewWindow + if (-not $proc.WaitForExit($executionTimeoutSeconds * 1000)) { + Stop-Process -Id $proc.Id -Force + $proc.WaitForExit() + throw "wxc-exec timed out after $executionTimeoutSeconds seconds" + } + Write-Host "wxc-exec exited with code $($proc.ExitCode)" + } finally { + if ($null -eq $previousLocalAppData) { + Remove-Item Env:LOCALAPPDATA -ErrorAction SilentlyContinue + } else { + $env:LOCALAPPDATA = $previousLocalAppData + } + if ($null -eq $previousTestOverride) { + Remove-Item Env:MXC_TEST_LOCALAPPDATA_OVERRIDE -ErrorAction SilentlyContinue + } else { + $env:MXC_TEST_LOCALAPPDATA_OVERRIDE = $previousTestOverride + } + } } catch { Write-Host "wxc-exec failed to run: $_" -ForegroundColor Yellow - # Continue — even a crash after init may have emitted events. + # Continue - even a crash after init may have emitted events. } # Brief pause for ETW buffers to flush. @@ -168,8 +210,8 @@ $etlSize = (Get-Item $etlFile).Length Write-Host "ETL file size: $etlSize bytes" if ($etlSize -eq 0) { - Write-Host "FAILED: ETL file is empty — no events captured." -ForegroundColor Red - Write-Host "All prerequisites were met (admin, wxc-exec present, ETW session created)," -ForegroundColor Red + Write-Host "FAILED: ETL file is empty - no events captured." -ForegroundColor Red + Write-Host 'All prerequisites were met (admin, wxc-exec present, ETW session created),' -ForegroundColor Red Write-Host "so the provider should have emitted at least one completion event." -ForegroundColor Red exit 1 } @@ -187,21 +229,42 @@ $eventCount = ([regex]::Matches($xmlContent, '') + $matchingEvent = $null + + foreach ($eventBlock in $eventBlocks) { + $eventXml = $eventBlock.Value + $eventMissingFields = @( + $expectedFields | Where-Object { + $eventXml -notmatch ([regex]::Escape($_)) + } + ) + if ($eventMissingFields.Count -eq 0) { + $matchingEvent = $eventXml + break + } + } - # Check for expected field names (public, not private). - $expectedFields = @('mxc.backend', 'mxc.exit_code', 'mxc.outcome', 'mxc.duration_ms') + if (-not $matchingEvent) { + $missingFieldsMessage = 'No single captured event contained all required public fields: ' + ($expectedFields -join ', ') + '.' + Write-Host '' + Write-Host '=== ETW CAPTURE SMOKE TEST FAILED ===' -ForegroundColor Red + Write-Host $missingFieldsMessage -ForegroundColor Red + exit 1 + } + + Write-Host '' + Write-Host '=== ETW CAPTURE SMOKE TEST PASSED ===' -ForegroundColor Green + Write-Host ($eventCount.ToString() + ' event(s) captured from the MXC provider.') foreach ($field in $expectedFields) { - if ($xmlContent -match $field) { - Write-Host " [OK] Found field: $field" -ForegroundColor Green - } else { - Write-Host " [--] Field not found: $field (may not be in this event type)" -ForegroundColor Yellow - } + Write-Host (' [OK] Found field in one event: ' + $field) -ForegroundColor Green } } else { - Write-Host "`n=== ETW CAPTURE SMOKE TEST FAILED ===" -ForegroundColor Red - Write-Host "ETL file had content ($etlSize bytes) but no parseable events were found." -ForegroundColor Red + $noEventsMessage = 'ETL file had content (' + $etlSize + ' bytes) but no parseable events were found.' + Write-Host '' + Write-Host '=== ETW CAPTURE SMOKE TEST FAILED ===' -ForegroundColor Red + Write-Host $noEventsMessage -ForegroundColor Red exit 1 }