Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ The workspace is organized into six top-level directories under `src/`:
WSLC SDK exposes neither); other backends return
`ErrorCode::UnsupportedContainment`.
- The lower-level execution surface lives in `wxc_common::sandbox_process`: the `SandboxBackend` trait (`validate` + `spawn(request, logger, StdioMode) -> Box<dyn SandboxProcess>` + a `diagnose_exit` hook) and the generic `Runner<B>` adapter that bridges any `SandboxBackend` to the run-to-completion `ScriptRunner` (via `spawn(StdioMode::Inherit)` then `wait()`). `SandboxProcess::output_metadata()` carries backend-produced structured outputs after terminal teardown without writing to process-global stdio. `StdioMode::Pipes` hands the caller live stdin/stdout/stderr (what the `mxc-sdk` streaming path uses); `StdioMode::Inherit` lets the child inherit the host's stdio (what the executor binaries use, preserving the TTY under a pty). `SandboxBackend` is implemented for Seatbelt, Bubblewrap, Windows ProcessContainer, and WSLC (on `wslc_common::WSLContainerRunner` itself, which shares one container lifecycle — `start_container` — between its streaming `SandboxBackend` and run-to-completion `ScriptRunner` impls, differing only in where the WSLC SDK's output callbacks write).
- `mxc_ffi` (`ffi/mxc_ffi`, `crate-type = ["cdylib", "staticlib", "lib"]`) is a flat, panic-safe **C ABI over `mxc-sdk`** for language bindings. `mxc_run(policyJson, command, out)` runs a sandbox to completion, filling a `#[repr(C)] MxcRunResult` (status + exit_code + timed_out, owned stdout/stderr/output-metadata C strings, and an `MxcErrorDetail` carrying the failure message plus the failing API call and its platform status); every entry point is `catch_unwind`-wrapped so a panic becomes a status code, never an unwind. Its `build.rs` runs **csbindgen** to generate the C# P/Invoke (`sdk/dotnet/Microsoft.Mxc.Sdk/Native/NativeMethods.g.cs`), gated behind the crate's **`dotnetsdk`** feature (off by default, so the whole-workspace backend build matrix doesn't compile csbindgen). The generated file is **not committed** (gitignored); the C# csproj regenerates it at build time and `scripts/check-dotnet-bindings-codegen.js` runs the codegen in CI and asserts the expected entry points are produced. The C ABI is **not a stable external contract** (native + binding are co-versioned and generated together; see the crate docs). It exposes three surfaces: **run-to-completion** (`mxc_run`), **streaming** (`mxc_spawn` → opaque `MxcSandbox` handle; `mxc_stream_read`/`write`/`flush`, `mxc_sandbox_take_stdin`/`stdout`/`stderr`, `mxc_sandbox_id`/`try_wait`/`wait`/`kill`/`output_metadata_json`/`free`, in `src/streaming.rs`), and the **state-aware lifecycle** (`mxc_state_aware` for the envelope phases, `mxc_state_aware_exec` returning a live streaming handle, and `mxc_state_aware_exec_attached` relaying onto this process's stdio and returning an outcome, in `src/state_aware.rs`). All four `.rs` files are csbindgen inputs in `build.rs` (the shared `MxcErrorDetail` lives in `src/error_detail.rs`); the `MXC_STATUS_*` space already reserves the state-aware phase codes.
- `mxc_ffi` (`ffi/mxc_ffi`, `crate-type = ["cdylib", "staticlib", "lib"]`) is a flat, panic-safe **C ABI over `mxc-sdk`** for language bindings. The one-shot entry points accept the binding-oriented `RequestSpec` envelope; `src/request.rs` adapts its policy, command, containment, environment, experimental authorization, and telemetry opt-in into `mxc_sdk::SandboxRequest`. This keeps binding-shape parsing at the FFI boundary instead of requiring each binding to emit canonical MXC configuration JSON. `mxc_run_request` fills a `#[repr(C)] MxcRunResult` (status + exit_code + timed_out, owned stdout/stderr/output-metadata C strings, and an `MxcErrorDetail` carrying the failure message plus the failing API call and its platform status); every entry point is `catch_unwind`-wrapped so a panic becomes a status code, never an unwind. Its `build.rs` runs **csbindgen** to generate the C# P/Invoke (`sdk/dotnet/Microsoft.Mxc.Sdk/Native/NativeMethods.g.cs`), gated behind the crate's **`dotnetsdk`** feature (off by default, so the whole-workspace backend build matrix doesn't compile csbindgen). The generated file is **not committed** (gitignored); the C# csproj regenerates it at build time and `scripts/check-dotnet-bindings-codegen.js` runs the codegen in CI and asserts the expected entry points are produced. The C ABI is **not a stable external contract** (native + binding are co-versioned and generated together; see the crate docs). It exposes three surfaces: **run-to-completion** (`mxc_run_request`), **streaming** (`mxc_spawn_request` → opaque `MxcSandbox` handle; `mxc_stream_read`/`write`/`flush`, `mxc_sandbox_take_stdin`/`stdout`/`stderr`, `mxc_sandbox_id`/`try_wait`/`wait`/`kill`/`output_metadata_json`/`free`, in `src/streaming.rs`), and the **state-aware lifecycle** (`mxc_state_aware` for the envelope phases, `mxc_state_aware_exec` returning a live streaming handle, and `mxc_state_aware_exec_attached` relaying onto this process's stdio and returning an outcome, in `src/state_aware.rs`). All four `.rs` files are csbindgen inputs in `build.rs` (the shared `MxcErrorDetail` lives in `src/error_detail.rs`); the `MXC_STATUS_*` space already reserves the state-aware phase codes.
- `mxc_pty` is the shared pty bridge used by the LXC backend (`lxc_common::lxc_bindings::attach_run`) so the inner shell sees a real TTY and host stdio is streamed live. (Seatbelt and Bubblewrap no longer use it: they spawn directly and let the child inherit the host's stdio — a TTY when the executor binary runs under a pty — via `SandboxBackend::spawn(StdioMode::Inherit)`.)
- `learning_mode_core` is the **cross-platform learning-mode / captureDenials model + output emitter**: `DeniedResource` (+ `ResourceType`/`AccessType`), `DenialSummary`, the `DenialAnalyzer` decode trait, actionable `DenialsDocument` emission, bounded sensitive-value-redacted `VerboseLoggingDocument` emission, and transactional paired-file write/relocation helpers. Every successful analysis writes the actionable artifact plus its deterministic `*.verbose.json` sibling; relocation reports destination commit separately from source-cleanup warnings so callers keep metadata truthful. The crate also defines the serializable `DenialsOutputPointer` and carries no OS-specific code (it must not depend on any `backends/*` crate). The Windows ETL decoder implementing `DenialAnalyzer` lives in `backends/learning_mode/windows`. When `processContainer.captureDenials` is set, native PSEC/V2 seals and decodes a managed ETL locally, while guarded WPR relogs its host-wide source into a process-scoped ETL before analysis; both routes write the same output pair through shared plumbing and return neutral `wxc_common` metadata. Explicit `retainEtl` preserves the native sealed trace or the guarded process-scoped relogged trace after a terminal wait; abandonment discards it. `wxc-exec` serializes the metadata as the one-line stderr pointer at the CLI boundary; Rust/C#/FFI callers receive it programmatically. Each actionable denial's `resource` field holds an authorable file path, UI category, or AppContainer capability name; registry writes and recognized Section, SymbolicLink, and Timer checks remain verbose-only because MXC has no corresponding config grants. Capability denials resolve their capability SID to a friendly name via `backends/learning_mode/windows`'s `capability_names` (well-known `S-1-15-3-…` SID → policy name; custom hashed SIDs fall back to the SID string).
- `mxc_build_common` is a build-time helper crate — all Windows binary crates use it in their `build.rs` to embed VersionInfo (ProductName, FileDescription, copyright, version+commit). When adding a new Windows binary crate, add `mxc_build_common` as a build-dependency and call `mxc_build_common::embed_version_info()` from `build.rs`
Expand Down
170 changes: 88 additions & 82 deletions scripts/check-dotnet-api-parity.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ function enumVariants(source, enumName, language) {
return enumMembers(source, enumName, language).map((member) => member.name);
}

function managedDerivedTypes(source, baseType) {
return [
...source.matchAll(
new RegExp(`public\\s+sealed\\s+class\\s+(\\w+)\\s*:\\s*${baseType}\\b`, "g")
),
].map((match) => match[1]);
}

function compare(label, actual, expected) {
const actualSorted = [...new Set(actual)].sort();
const expectedSorted = [...new Set(expected)].sort();
Expand Down Expand Up @@ -206,28 +214,23 @@ function managedJsonFields(source, className) {
});
}

// `legacyManagedOnly` lists managed JSON fields that intentionally have no Rust
// counterpart because they are deprecated compatibility aliases. They must stay
// serializable so legacy JSON round-trips, but the managed request path strips
// them before the native layer sees them.
function compareStructFields(
label,
rustSource,
rustName,
managedSource,
managedName,
legacyManagedOnly = []
) {
function compareStructFields(label, rustSource, rustName, managedSource, managedName) {
compare(
`${label} fields`,
managedJsonFields(managedSource, managedName).filter(
(field) => !legacyManagedOnly.includes(field)
),
managedJsonFields(managedSource, managedName),
rustStructFields(rustSource, rustName)
);
}

const rustPolicy = read("src", "core", "mxc_engine", "src", "policy.rs");
const rustRequest = read("src", "ffi", "mxc_ffi", "src", "request.rs");
const rustWire = read(
"src",
"core",
"wxc_common",
"src",
"wire.rs"
);
const rustNetworkPolicy = read(
"src",
"core",
Expand All @@ -236,13 +239,6 @@ const rustNetworkPolicy = read(
"policy",
"network.rs"
);
const rustBindingRequest = read(
"src",
"ffi",
"mxc_ffi",
"src",
"request.rs"
);
const managedRequest = read(
"sdk",
"dotnet",
Expand All @@ -258,66 +254,92 @@ const managedPolicy = read(
const rustOneShot = enumVariants(rustPolicy, "Containment", "rust").filter(
(variant) => variant !== "IsolationSession"
);
const bindingContainment = enumMembers(
rustBindingRequest,
"RequestContainment",
"rust"
);
compare(
"Rust SDK vs binding containment variants",
bindingContainment.map((member) => member.name),
"Rust SDK vs managed containment variants",
managedDerivedTypes(managedRequest, "SandboxContainment").map((name) =>
name.replace(/Containment$/, "")
),
rustOneShot
Comment thread
RamonArjona4 marked this conversation as resolved.
);
compareStructFields(
"one-shot request",
rustBindingRequest,
"RequestSpec",
"process-container canonical fields",
rustWire,
"ProcessContainer",
managedRequest,
"SandboxRequest"
);
compare(
"process-container request fields",
managedJsonFields(managedRequest, "ProcessContainerContainment"),
rustVariantFields(rustBindingRequest, "RequestContainment", "ProcessContainer")
);
compare(
"WSLC request fields",
managedJsonFields(managedRequest, "WslcContainment"),
rustVariantFields(rustBindingRequest, "RequestContainment", "Wslc")
"ProcessContainerContainment"
);
compareStructFields(
"process-container UI",
rustBindingRequest,
"ProcessContainerUiSpec",
"process-container UI canonical fields",
rustWire,
"BaseProcessUi",
managedRequest,
"ProcessContainerUiPolicy"
);
compareStructFields(
"process-container network",
rustBindingRequest,
"ProcessContainerNetworkSpec",
"process-container network canonical fields",
rustWire,
"ProcessContainerNetwork",
managedRequest,
"ProcessContainerNetworkPolicy"
);
compareStructFields(
"WSLC port mapping",
rustBindingRequest,
"WslcPortMappingSpec",
managedRequest,
"WslcPortMapping"
"capture-denials canonical fields",
rustWire,
"CaptureDenials",
managedPolicy,
"CaptureDenialsPolicy"
);
compare(
"WSLC one-shot canonical fields",
managedJsonFields(managedRequest, "WslcContainment"),
rustStructFields(rustWire, "Wslc").filter(
(field) => field !== "targetOs" && field !== "provision"
)
);
compare(
"WSLC port-mapping canonical fields",
[...managedJsonFields(managedRequest, "WslcPortMapping"), "protocol"],
rustStructFields(rustWire, "PortMapping")
);

const managedSandboxPolicyFields = managedJsonFields(
managedPolicy,
"SandboxPolicy"
);

for (const [
label,
rustSource,
rustName,
managedName,
legacyManagedOnly,
] of [
// `captureDenials` is an obsolete managed-only alias (MXC0001, removed in
// 1.0). Rust only accepts it under `containment.captureDenials`, and
// MxcSandbox.PrepareRequest strips it from the policy before serialization.
["sandbox policy", rustPolicy, "SandboxPolicy", "SandboxPolicy", ["captureDenials"]],
// `captureDenials` is the one existing managed compatibility alias (MXC0001,
// removed in 1.0). MxcSandbox.PrepareRequest migrates it to
// processContainer.captureDenials and strips it before native serialization.
compare(
"sandbox policy compatibility aliases",
managedSandboxPolicyFields.filter((field) => field === "captureDenials"),
["captureDenials"]
);
compare(
"sandbox policy fields",
managedSandboxPolicyFields.filter(
(field) => field !== "captureDenials" && field !== "telemetry"
),
rustStructFields(rustPolicy, "SandboxPolicy")
);

// Telemetry is execution metadata, not a containment restriction. The FFI
// RequestSpec adapter removes it from the binding policy before constructing
// the Rust SandboxPolicy, then applies it to SandboxRequest explicitly.
compare(
"sandbox policy binding telemetry field",
managedSandboxPolicyFields.filter((field) => field === "telemetry"),
["telemetry"]
);
Comment thread
Copilot marked this conversation as resolved.
compareStructFields(
"telemetry settings",
rustRequest,
"TelemetrySpec",
managedPolicy,
"TelemetrySettings"
);

for (const [label, rustSource, rustName, managedName] of [
["filesystem policy", rustPolicy, "FilesystemSection", "FilesystemPolicy"],
["UI policy", rustPolicy, "UiSection", "UiPolicy"],
["network policy", rustNetworkPolicy, "NetworkSection", "NetworkPolicy"],
Expand All @@ -333,26 +355,10 @@ for (const [
rustSource,
rustName,
managedPolicy,
managedName,
legacyManagedOnly
managedName
);
}
const managedOneShot = [
...managedRequest.matchAll(
/\[JsonDerivedType\(typeof\(\w+\),\s*"([^"]+)"\)\]/g
),
].map((match) => match[1]);
const camelCase = (value) => value[0].toLowerCase() + value.slice(1);
compare(
"one-shot containment discriminators",
managedOneShot,
bindingContainment.map((member) => {
const renamed = /#\[serde\([^]]*\brename\s*=\s*"([^"]+)"/s.exec(
member.source
);
return renamed?.[1] ?? camelCase(member.name);
})
);

const rustProbeFull = read("src", "core", "mxc_engine", "src", "probe.rs");
const rustProbe = rustProbeFull.split("#[cfg(test)]")[0];
Expand Down
15 changes: 15 additions & 0 deletions scripts/check-dotnet-bindings-codegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ if (missing.length > 0) {
process.exit(1);
}

const requiredSignatures = [
"mxc_run_request(byte* request_json_utf8, MxcRunResult* @out)",
"mxc_spawn_request(byte* request_json_utf8, MxcSandbox** out_handle, MxcErrorDetail* out_error)",
];
const missingSignatures = requiredSignatures.filter(
(signature) => !content.includes(signature)
);
if (missingSignatures.length > 0) {
console.error(
"ERROR: generated C# bindings have unexpected request signature(s):\n " +
missingSignatures.join("\n ")
);
process.exit(1);
}

// build.rs names the same set of Rust sources twice: once for csbindgen to
// read, and once as `cargo:rerun-if-changed`. Emitting any `rerun-if-changed`
// replaces cargo's default "re-run when any package file changed" with exactly
Expand Down
14 changes: 10 additions & 4 deletions sdk/dotnet/Microsoft.Mxc.Sdk.Tests/JsonAssert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@ namespace Microsoft.Mxc.Sdk.Tests;
internal static class JsonAssert
{
internal static void MatchesGolden(string actualJson, string fixtureName)
{
var expectedJson = ReadGolden(fixtureName);
using var expected = JsonDocument.Parse(expectedJson);
using var actual = JsonDocument.Parse(actualJson);

Equivalent(expected.RootElement, actual.RootElement, fixtureName);
}

internal static string ReadGolden(string fixtureName)
{
var assembly = typeof(JsonAssert).Assembly;
var resourceName = assembly.GetManifestResourceNames()
.Single(name => name.EndsWith(fixtureName, StringComparison.Ordinal));
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Missing embedded fixture {resourceName}.");
using var reader = new StreamReader(stream);
using var expected = JsonDocument.Parse(reader.ReadToEnd());
using var actual = JsonDocument.Parse(actualJson);

Equivalent(expected.RootElement, actual.RootElement, fixtureName);
return reader.ReadToEnd();
}

private static void Equivalent(JsonElement expected, JsonElement actual, string path)
Expand Down
Loading
Loading