Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/Versioning.Checks.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ jobs:
- name: Check exact contract generated artifacts and fixtures
run: node scripts/versioning/check-contract-codegen.js

- name: Check telemetry policy binding parity
run: node scripts/check-telemetry-policy-parity.js

- name: Check C# bindings codegen (regenerates and asserts entry points)
run: node scripts/check-dotnet-bindings-codegen.js

Expand Down
4 changes: 2 additions & 2 deletions docs/telemetry/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ the one-shot path, a clean non-zero sandbox exit is not treated as an MXC error.
| Field | Type | Description |
|-------|------|-------------|
| `mxc.sandbox_kind` | string | Containment kind requested by the caller (`process`, `vm`, or a concrete backend name) |
| `mxc.backend` | string | Containment backend name |
| `mxc.backend` | string | Backend identifier; ProcessContainer reports `processcontainer`, not its runtime-selected tier |
| `mxc.exit_code` | int32 | Process exit code |
| `mxc.outcome` | string | `"success"` or `"failure"` |
| `mxc.duration_ms` | uint64 | Total execution time |
Expand All @@ -182,7 +182,7 @@ Emitted on execution errors.
| Field | Type | Description |
|-------|------|-------------|
| `mxc.sandbox_kind` | string | Containment kind requested by the caller (`process`, `vm`, or a concrete backend name) |
| `mxc.backend` | string | Containment backend name |
| `mxc.backend` | string | Backend identifier; ProcessContainer reports `processcontainer`, not its runtime-selected tier |
| `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 |
Expand Down
240 changes: 240 additions & 0 deletions scripts/check-telemetry-policy-parity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#!/usr/bin/env node
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const { readFileSync } = require("fs");
const { join } = require("path");

const root = join(__dirname, "..");
const protocol = readFileSync(
join(root, "src", "core", "wxc_common", "src", "telemetry", "consent_protocol.rs"),
"utf8"
);
const policy = readFileSync(
join(root, "src", "core", "wxc_common", "src", "telemetry", "policy.rs"),
"utf8"
Comment thread
RamonArjona4 marked this conversation as resolved.
);
const csharp = readFileSync(
join(root, "sdk", "dotnet", "Microsoft.Mxc.Sdk", "MxcTelemetry.cs"),
"utf8"
);
const rustSdk = readFileSync(
join(root, "src", "core", "mxc-sdk", "src", "telemetry.rs"),
"utf8"
);
const typescript = readFileSync(
join(root, "sdk", "node", "src", "telemetry.ts"),
"utf8"
);
const errors = [];

function requiredMatch(source, expression, label) {
const match = source.match(expression);
if (!match) {
console.error(`ERROR: could not parse ${label}`);
process.exit(1);
}
return match;
}

function rustWireName(variant, rename) {
const kebab = variant.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
return rename === "camelCase"
? kebab.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
: kebab;
}

function rustEnum(name) {
const match = requiredMatch(
protocol,
new RegExp(
`#\\[serde\\(rename_all = "([^"]+)"(?:,[^\\]]*)?\\)\\]\\s*` +
`pub\\(super\\)\\s+enum\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`
),
`Rust enum ${name}`
);
if (!["camelCase", "kebab-case"].includes(match[1])) {
console.error(`ERROR: unsupported serde rename '${match[1]}' on ${name}`);
process.exit(1);
}
return new Set(
[...match[2].matchAll(/^\s*([A-Z][A-Za-z0-9_]*)\s*,/gm)]
.map((item) => rustWireName(item[1], match[1]))
);
}

// The C# binding consumes the FFI, whose results come from the Rust SDK enum,
// not from the executor's private CLI protocol.
function rustSdkConsentResults() {
const body = requiredMatch(
rustSdk,
/impl ConsentActionResult \{[\s\S]*?match self \{([\s\S]*?)\n\s*\}/,
"Rust SDK ConsentActionResult::as_str"
)[1];
return new Set([...body.matchAll(/Self::\w+\s*=>\s*"([^"]+)"/g)].map((item) => item[1]));
}

function policyStates() {
const body = requiredMatch(
policy,
/fn as_str\(&self\)\s*->\s*&'static str\s*\{[\s\S]*?match self \{([\s\S]*?)\n\s*\}/,
"PolicyState::as_str"
)[1];
return new Map(
[...body.matchAll(/PolicyState::(\w+)\s*=>\s*"([^"]+)"/g)]
.map((item) => [item[2], item[1]])
);
}

function typescriptValues(name) {
const body = requiredMatch(
typescript,
new RegExp(`const ${name}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*as const`),
`TypeScript literal set ${name}`
)[1];
return new Set([...body.matchAll(/["']([^"']+)["']/g)].map((item) => item[1]));
}

function csharpSwitch(functionName, enumName) {
const body = requiredMatch(
csharp,
new RegExp(
`private\\s+static\\s+[\\w?<>]+\\s+${functionName}\\s*\\([^)]*\\)[\\s\\S]*?` +
`(?:=>\\s*value|return\\s+value\\.GetString\\(\\))\\s+switch\\s*\\{([\\s\\S]*?)\\};`
),
`C# ${functionName}`
)[1];
return new Map(
[...body.matchAll(new RegExp(`"([^"]+)"\\s*=>\\s*${enumName}\\.(\\w+)`, "g"))]
.map((item) => [item[1], item[2]])
);
}

function csharpStatusReasons() {
const body = requiredMatch(
csharp,
/private\s+static\s+bool\s+IsKnownConsentStatusReason\s*\([^)]*\)[\s\S]*?return\s+value\.GetString\(\)\s+switch\s*\{([\s\S]*?)\};/,
"C# IsKnownConsentStatusReason"
)[1];
return new Set([...body.matchAll(/"([^"]+)"/g)].map((item) => item[1]));
}

function functionBody(name) {
const start = requiredMatch(
csharp,
new RegExp(`private\\s+static\\s+[\\w?<>]+\\s+${name}\\s*\\(`),
`C# ${name}`
).index;
const end = csharp.indexOf("\n private static ", start + 1);
return csharp.slice(start, end < 0 ? undefined : end);
}

function compareSets(label, expected, actual) {
for (const value of expected) {
if (!actual.has(value)) errors.push(`${label} is missing '${value}'`);
}
for (const value of actual) {
if (!expected.has(value)) errors.push(`${label} has unexpected '${value}'`);
}
}

function expectedVariant(wire) {
return wire
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
.replace(/^./, (letter) => letter.toUpperCase());
}

function compareMappings(label, expected, actual) {
for (const wire of expected) {
const variant = actual.get(wire);
if (variant === undefined) {
errors.push(`${label} does not handle '${wire}'`);
} else if (variant !== expectedVariant(wire)) {
errors.push(`${label} maps '${wire}' to ${variant}`);
}
}
for (const wire of actual.keys()) {
if (!expected.has(wire)) errors.push(`${label} handles unexpected '${wire}'`);
}
}

const runtimePolicy = policyStates();
const protocolPolicy = rustEnum("PolicyState");
compareMappings("Rust PolicyState::as_str", protocolPolicy, runtimePolicy);
compareSets("TypeScript policy", protocolPolicy, typescriptValues("TELEMETRY_POLICY_STATES"));
compareMappings(
"C# ParsePolicyState",
protocolPolicy,
csharpSwitch("ParseKnownPolicyState", "TelemetryPolicyState")
);

const consentStates = rustEnum("ConsentState");
compareSets("TypeScript consent state", consentStates, typescriptValues("TELEMETRY_CONSENT_STATES"));
compareMappings(
"C# ParseConsentState",
consentStates,
csharpSwitch("ParseKnownConsentState", "TelemetryConsentState")
);

const bindingResults = rustSdkConsentResults();
const protocolResults = rustEnum("ConsentResult");
for (const value of bindingResults) {
if (!protocolResults.has(value)) {
errors.push(`Rust SDK consent result '${value}' is missing from the CLI protocol`);
}
}
const protocolOnlyResults = new Set(
[...protocolResults].filter((value) =>
value === "status" || value === "presentationRequired"
)
);
const terminalResults = new Set(
[...protocolResults].filter((value) => !protocolOnlyResults.has(value))
);
compareSets(
"TypeScript private protocol result",
protocolOnlyResults,
typescriptValues("CONSENT_PROTOCOL_ONLY_RESULTS")
);
compareSets(
"TypeScript consent result",
terminalResults,
typescriptValues("TELEMETRY_CONSENT_RESULTS")
);
compareMappings(
"C# ParseConsentActionResult",
bindingResults,
csharpSwitch("ParseConsentActionResult", "TelemetryConsentActionResult")
);

const statusReasons = rustEnum("StatusReason");
compareSets(
"TypeScript private status reason",
statusReasons,
typescriptValues("CONSENT_STATUS_REASONS")
);
compareSets("C# private status reason", statusReasons, csharpStatusReasons());

for (const [name, sentinel] of [
["UnrecognizedConsentState", "return TelemetryConsentState.Undetermined;"],
["UnrecognizedConsentActionResult", "return TelemetryConsentActionResult.Unknown;"],
["ReportUnrecognizedConsentStatusReason", "return false;"],
["UnrecognizedPolicyState", "return TelemetryPolicyState.Blocked;"],
]) {
if (!functionBody(name).includes(sentinel)) {
errors.push(`C# ${name} must contain '${sentinel}'`);
}
}

if (errors.length > 0) {
console.error("ERROR: telemetry wire parity check failed:");
for (const error of errors) console.error(` - ${error}`);
process.exit(1);
}

console.log(
`Telemetry wire parity OK: ${protocolPolicy.size} policy states, ` +
`${consentStates.size} consent states, ${terminalResults.size} terminal results, ` +
`${protocolOnlyResults.size} private protocol results, ` +
`and ${statusReasons.size} private status reasons match`
);
7 changes: 7 additions & 0 deletions scripts/versioning/check-schema-versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ expectConst(
/const WSLC_STATE_AWARE_VERSION\s*=\s*'([^']+)'/,
stateAwareWslc
);
expectConst(
"state-aware-helper.ts",
stateAwareTs,
"TELEMETRY_STATE_AWARE_VERSION",
/const TELEMETRY_STATE_AWARE_VERSION\s*=\s*'([^']+)'/,
maxSupported
);

// -- C# SDK (sdk/dotnet/Microsoft.Mxc.Sdk/SchemaVersions.cs) --
const schemaVersionsCs = read(
Expand Down
2 changes: 2 additions & 0 deletions sdk/dotnet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ a 0.8.x patch**. Package versions are bumped in a dedicated release PR (see
call sites keep compiling. Because adding a parameter changes the emitted
method signature, assemblies compiled against 0.8.0 must be recompiled
rather than dropped in place.
- Non-exec phases reject `StateAwareExecOptions` instead of ignoring exec-only
fields. Use `StateAwarePhaseOptions`.

### Deprecated

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<DefineConstants Condition="'$(MxcWithIsolationSession)' == 'true'">$(DefineConstants);MXC_WITH_ISOLATION_SESSION</DefineConstants>
<MxcSrcDir>$(MSBuildProjectDirectory)/../../../src</MxcSrcDir>
<MxcTestCargoTargetDir>$(MxcSrcDir)/target/dotnet-test-support</MxcTestCargoTargetDir>
<MxcTestCargoProfileArg Condition="'$(Configuration)' == 'Release'">--release</MxcTestCargoProfileArg>
<MxcTestCargoProfileDir Condition="'$(Configuration)' == 'Release'">release</MxcTestCargoProfileDir>
<MxcTestCargoProfileDir Condition="'$(Configuration)' != 'Release'">debug</MxcTestCargoProfileDir>
<!-- Test isolation overrides are compiled only with Rust debug assertions,
including when the managed tests themselves run in Release. -->
<MxcTestCargoProfileDir>debug</MxcTestCargoProfileDir>
<MxcTestCargoFeatures>test-support</MxcTestCargoFeatures>
<MxcTestCargoFeatures Condition="'$(MxcWithIsolationSession)' == 'true' and $([MSBuild]::IsOSPlatform('Windows'))">$(MxcTestCargoFeatures) isolation_session</MxcTestCargoFeatures>
<MxcTestCargoFeatures Condition="'$(MxcWithWslc)' == 'true' and $([MSBuild]::IsOSPlatform('Windows'))">$(MxcTestCargoFeatures) wslc</MxcTestCargoFeatures>
Expand Down Expand Up @@ -52,7 +52,7 @@
<MxcTestNativeOutput>$(MxcTestCargoTargetDir)/$(MxcTestRustHostTriple)/$(MxcTestCargoProfileDir)/$(MxcTestNativeFileName)</MxcTestNativeOutput>
</PropertyGroup>
<Message Importance="high" Text="mxc: building isolated test-support native library for $(MxcTestRustHostTriple)" />
<Exec Command="cargo build -p mxc_ffi --features &quot;$(MxcTestCargoFeatures)&quot; --target $(MxcTestRustHostTriple) $(MxcTestCargoProfileArg) --target-dir &quot;$(MxcTestCargoTargetDir)&quot;" WorkingDirectory="$(MxcSrcDir)" />
<Exec Command="cargo build -p mxc_ffi --features &quot;$(MxcTestCargoFeatures)&quot; --target $(MxcTestRustHostTriple) --target-dir &quot;$(MxcTestCargoTargetDir)&quot;" WorkingDirectory="$(MxcSrcDir)" />
<Error Condition="!Exists('$(MxcTestNativeOutput)')"
Text="The test-support native build did not produce $(MxcTestNativeOutput)." />
<Copy SourceFiles="$(MxcTestNativeOutput)"
Expand Down
Loading
Loading