From b4786ddc67dcf69489323fe9c7316359f37dc0be Mon Sep 17 00:00:00 2001 From: adpa-ms <104169682+adpa-ms@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:47:38 -0700 Subject: [PATCH 1/3] Migrate isolation_session backend + SDK to the IsolationSession Preview API (#592) * Fix isolation_session_bindings build.rs version-check path resolution * Migrate isolation_session backend + SDK to the IsolationSession Preview API The IsolationSession WinRT surface MXC consumes is now frozen as the Preview namespace. Regenerate the Rust bindings against it and reshape the consumers to the reduced, stable API: - bindings: regenerate from the Preview WinMD; add the windows-crate Foundation feature (the Preview surface references IClosable). - backend (manager/policy/state_aware/one_shot): the OS now assigns an opaque agent user name at provision and validates identity/token at the service, so the sandbox id tail is that opaque name. Collapse the local/Entra provision and start paths into single token-carrying calls, drop host-folder sharing and the per-session sizing profile, and reject all filesystem/network/proxy policy at every phase. Entra is carried by the start config's user bundle rather than inferred from the sandbox id. - domain/wire: remove the sizing-profile config id; regenerate the dev schema and the SDK wire types. - probe: advertise isolation-session availability via `wxc-exec --probe` (probes.isolationSessionAvailable) instead of a registry build pin. - SDK: drop filesystem/configurationId from the typed configs and gate the isolation_session method on the probe fact. Retail CI green: fmt, clippy --all-features, build+test with the feature on and off, SDK unit, schema/sdk-types codegen, and config validation. VM end-to-end validation is pending. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rework the isolation_session VM E2E corpus for the Preview API The one-shot and state-aware PowerShell suites and their JSON fixtures asserted behavior the Preview migration removed. Bring them in line with the new backend: - drop the filesystem-sharing, path-filter, sizing-profile (configurationId) and start-identity cross-check tests (and their fixtures); - assert that filesystem policy is now rejected (policy_validation) at provision as well as the post-provision phases; - assert the sandbox id tail is the opaque OS-assigned agent user name rather than a client-minted token; - rework the simultaneous-sandbox and concurrent one-shot tests to use per-sandbox %TEMP% markers / a host ACL grant instead of folder sharing; - add a fixture proving an unknown configurationId is gracefully ignored. Verified end-to-end on an isolation-capable VM: one-shot 11/11, state-aware 42/42, SDK node integration 2/2 (55/55). Config schema validation green (157 configs). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refresh isolation_session docs and comments after the Preview migration Post-migration review found documentation and comment staleness left behind by the Preview migration (no functional defects). Bring the prose in line with the shipped backend: - rename the three isolation-session docs to drop the "initial-plan" framing (now living specs): initial-bringup-plan -> oneshot and state-aware-{rust,typescript}-initial-plan -> state-aware-{rust,typescript}; update all inbound links (copilot-instructions, sdk/README). - correct the stale policy matrix and prose: filesystem policy is now rejected (policy_validation) at every phase; remove the deleted configurationId / v2-interface / UPN-match / registration content; the sandbox id tail is the opaque OS-assigned agent user name. - scrub residual internal names from MXC prose/comments: IsoEnvBroker, IsoSessionApp.dll, and the pre-Preview Windows.AI.IsolationEnvironment namespace -> Windows.AI.IsolationSession.Preview; genericize bringup-era OS-side names (agent-user format, host binary, worker-process interface). - rewrite the Lifecycle E "registration leak" test comments to the per-agent-user isolation rationale (RemoveUserAsync is per user) and disambiguate two identical assert messages. - refresh the stale configurationId sample in a content-agnostic config_parser test to a user bundle. - fix the IsolationSession row in copilot-instructions (filesystem rejected at every phase; drop ShareFolderBatchAsync/IsoSessionApp.dll). Retail CI green: fmt, clippy, build x64+arm64 with the feature on, unit tests feature on (359) and off (397), wxc_host_prep (17, elevated). Re-verified end-to-end on an isolation-capable VM: 55/55 (one-shot 11, state-aware 42, SDK node 2). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Surface agent SID and shared ephemeral workspace from the Preview API The IsolationSession Preview API gained two provision-time getters on `IsoSessionUserResult` (`AgentUserSid`, `EphemeralWorkspacePath`). Regenerate the bindings and surface both as provision metadata: - bindings: regenerate from the newer Preview WinMD; the only surface change is the two additive getters (the `IIsoSessionUserResult` IID recomputes accordingly). No other interface changed. - manager: `add_user` now returns a `ProvisionedUser` carrying the agent user name, the agent SID, and the shared ephemeral workspace path (read from the three `IsoSessionUserResult` getters). - state-aware: extend `IsolationSessionProvisionMetadata` with `agentUserSid` and `ephemeralWorkspacePath` and populate them at provision. The `sandboxId` tail (the addressing key) is unchanged. - one-shot: adapt the `add_user` call site; one-shot still returns no provision metadata, so it surfaces nothing new. - SDK: add the two fields to the `IsolationSessionProvisionMetadata` type and refresh the unit-test fixtures. The ephemeral workspace is a directory shared between the calling user and the isolated agent user (the caller can stage files into the session through it); each isolated user can access only its own workspace, and it is deleted when the sandbox is deprovisioned. It does not change the workload's working directory. Tests: - Rust unit: provision metadata serializes to exactly the three camelCase wire keys. - VM state-aware E2E (Lifecycle F): metadata presence, caller<->session file sharing, cross-session workspace isolation (a session cannot read a peer's workspace), and workspace deletion on deprovision. - SDK integration: asserts the new metadata fields are present. Validation: fmt, clippy (all-features), build + unit tests feature on and off, wxc_host_prep (elevated), SDK unit, schema/sdk-types codegen, config validation -- all green. Clean-room package build (x64 + arm64) green. VM end-to-end on an isolation-capable build: 62/62 (one-shot 11, state-aware 49, SDK node 2); manual TTY operator-confirmed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: adpa-ms <> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 8 +- .../{initial-bringup-plan.md => oneshot.md} | 120 +- ...st-initial-plan.md => state-aware-rust.md} | 148 +- ...tial-plan.md => state-aware-typescript.md} | 37 +- .../isolation-session/GENERATION_INFO.toml | 2 +- schemas/dev/mxc-config.schema.0.8.0-dev.json | 34 +- sdk/node/README.md | 2 +- sdk/node/src/generated/wire.ts | 15 +- sdk/node/src/platform.ts | 98 +- sdk/node/src/state-aware-types.ts | 22 +- .../isolation-session-state-aware.test.ts | 148 +- sdk/node/tests/unit/platform.test.ts | 54 +- sdk/node/tests/unit/state-aware-types.test.ts | 27 +- sdk/node/tests/unit/state-aware.test.ts | 31 +- .../unit/wire-conformance-state-aware.test.ts | 30 +- src/Cargo.lock | 1 + src/backends/appcontainer/common/src/probe.rs | 22 + .../isolation_session/bindings/Cargo.toml | 2 +- .../isolation_session/bindings/build.rs | 11 +- .../bindings/src/bindings.rs | 3159 ++++------------- .../isolation_session/bindings/src/lib.rs | 4 +- .../isolation_session/common/Cargo.toml | 3 + .../isolation_session/common/src/error.rs | 10 +- .../common/src/folder_sharing.rs | 300 -- .../isolation_session/common/src/lib.rs | 13 +- .../isolation_session/common/src/manager.rs | 331 +- .../isolation_session/common/src/one_shot.rs | 68 +- .../isolation_session/common/src/policy.rs | 43 +- .../common/src/protected_paths_filter.rs | 509 --- .../common/src/state_aware.rs | 289 +- src/core/mxc_engine/src/lib.rs | 2 + src/core/mxc_engine/src/platform.rs | 11 + src/core/wxc/src/main.rs | 10 + src/core/wxc_common/src/config_parser.rs | 105 +- src/core/wxc_common/src/models.rs | 40 +- src/core/wxc_common/src/wire.rs | 21 +- .../isolation_session_concurrent_A.json | 3 - .../isolation_session_concurrent_B.json | 3 - .../isolation_session_concurrent_C.json | 3 - ...> isolation_session_configid_ignored.json} | 6 +- .../configs/isolation_session_filesystem.json | 12 - tests/configs/isolation_session_filtered.json | 12 - ...ession_state_aware_exec_read_readonly.json | 8 - ...sion_state_aware_exec_read_restricted.json | 8 - ..._session_state_aware_exec_read_shared.json | 8 - ...tate_aware_exec_write_readonly_denied.json | 8 - ...session_state_aware_exec_write_shared.json | 8 - ...ion_state_aware_provision_with_filter.json | 7 - ..._state_aware_start_entra_missing_user.json | 4 - ...ion_state_aware_start_local_with_user.json | 14 - ...tion_session_state_aware_start_medium.json | 11 - ...ession_state_aware_start_upn_mismatch.json | 14 - ...un_isolation_session_state_aware_tests.ps1 | 682 +--- tests/scripts/run_isolation_session_tests.ps1 | 109 +- 54 files changed, 1455 insertions(+), 5195 deletions(-) rename docs/isolation-session/{initial-bringup-plan.md => oneshot.md} (76%) rename docs/isolation-session/{state-aware-rust-initial-plan.md => state-aware-rust.md} (55%) rename docs/isolation-session/{state-aware-typescript-initial-plan.md => state-aware-typescript.md} (78%) delete mode 100644 src/backends/isolation_session/common/src/folder_sharing.rs delete mode 100644 src/backends/isolation_session/common/src/protected_paths_filter.rs rename tests/configs/{isolation_session_hello_medium.json => isolation_session_configid_ignored.json} (76%) delete mode 100644 tests/configs/isolation_session_filesystem.json delete mode 100644 tests/configs/isolation_session_filtered.json delete mode 100644 tests/configs/isolation_session_state_aware_exec_read_readonly.json delete mode 100644 tests/configs/isolation_session_state_aware_exec_read_restricted.json delete mode 100644 tests/configs/isolation_session_state_aware_exec_read_shared.json delete mode 100644 tests/configs/isolation_session_state_aware_exec_write_readonly_denied.json delete mode 100644 tests/configs/isolation_session_state_aware_exec_write_shared.json delete mode 100644 tests/configs/isolation_session_state_aware_provision_with_filter.json delete mode 100644 tests/configs/isolation_session_state_aware_start_entra_missing_user.json delete mode 100644 tests/configs/isolation_session_state_aware_start_local_with_user.json delete mode 100644 tests/configs/isolation_session_state_aware_start_medium.json delete mode 100644 tests/configs/isolation_session_state_aware_start_upn_mismatch.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 340766543..b0fea874d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -113,7 +113,7 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the ` | Windows Sandbox | `wxc-exec.exe` | Windows | `backends/windows_sandbox/lifecycle/src/` (live transient one-shot `WindowsSandboxRunner` + state-aware `StatefulSandboxBackend`). Experimental — requires `--experimental`. Supports both **one-shot** (a fresh, disposable VM per invocation with guaranteed teardown, via `ScriptRunner`) and **state-aware** (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. State-aware holds a single live VM across separate `wxc-exec` phase processes behind a persistent detached host-side daemon (`backends/windows_sandbox/daemon/`); the OS enforces a single running Windows Sandbox VM per host, so the daemon owns it and reclaims an orphaned VM on restart only via positive process-identity proof. The shared boot sequence (write per-launch nonce, launch VM, capture ownership proof, wait rendezvous, connect) lives in `backends/windows_sandbox/lifecycle/src/vm.rs::launch_managed_vm`; each mode plugs in its own `LaunchObserver` for the per-caller ownership / proof bookkeeping. Honors `readwritePaths`/`readonlyPaths`/`deniedPaths` (HOST paths) at provision via `.wsb` `` entries (mapped at the same absolute host path inside the guest; rejects `deniedPaths` equal-to or nested-within a mapped share since `.wsb` has no Deny primitive); filesystem policy is immutable post-provision. Network isolation is enforced unconditionally by the in-guest agent; `network`/`ui` and the Entra `user` bundle are not honored. ID prefix `wsb` (strict `wsb:<8-hex>` grammar). Per-launch handshake: 32-byte `Nonce` + 1-byte `ChannelRole` tag on every TCP connection (boot + reconnect); the guest pairs accepted sockets by declared role, not by accept order. The guest agent binary `wxc-windows-sandbox-guest.exe` (`backends/windows_sandbox/guest/`) is injected into the VM. | | MicroVM (NanVix) | `wxc-exec.exe` | Windows | `backends/nanvix/runner/src/lib.rs` — feature-gated behind `microvm` | | Hyperlight | `wxc-exec.exe` | Windows | `backends/hyperlight/common/src/lib.rs` — Hyperlight + Unikraft micro-VM backend | -| IsolationSession | `wxc-exec.exe` | Windows | `backends/isolation_session/common/src/` — feature-gated behind `isolation_session`, experimental, uses the in-proc `Windows.AI.IsolationSession` `IsoSessionOps` API (loaded from `IsoSessionApp.dll`). Supports both one-shot (single-invocation lifecycle, via `ScriptRunner`) and state-aware (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. Honors `readwritePaths` and `readonlyPaths` at provision via `ShareFolderBatchAsync` (rejects `deniedPaths` since the API has no Deny ACE primitive); filesystem policy is immutable post-provision and rejected at later phases. State-aware additionally accepts an optional `user` bundle (`upn`, `wamToken`) at provision and start to provision Entra cloud-agent sandboxes; one-shot rejects the bundle, and hosts that don't support Entra agents surface `backend_unavailable`. Streams stdout/stderr, forwards stdin, and switches to ConPTY mode when wxc-exec's stdout is a TTY for `spawnSandbox` parity. | +| IsolationSession | `wxc-exec.exe` | Windows | `backends/isolation_session/common/src/` — feature-gated behind `isolation_session`, experimental, uses the in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` API. Supports both one-shot (single-invocation lifecycle, via `ScriptRunner`) and state-aware (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. Rejects all filesystem policy (`readwritePaths`/`readonlyPaths`/`deniedPaths`) plus network and proxy policy at every phase with `policy_validation` — the backend has no host-folder-sharing, network, or proxy primitive. State-aware additionally accepts an optional `user` bundle (`upn`, `wamToken`) at provision and start to provision Entra cloud-agent sandboxes; one-shot rejects the bundle, and hosts that don't support Entra agents surface `backend_unavailable`. Streams stdout/stderr, forwards stdin, and switches to ConPTY mode when wxc-exec's stdout is a TTY for `spawnSandbox` parity. | | LXC | `lxc-exec` | Linux | `core/lxc/src/main.rs` + `backends/lxc/common/` | | Seatbelt | `mxc-exec-mac` | macOS | `core/mxc_darwin/src/main.rs` + `backends/seatbelt/common/` — uses macOS App Sandbox (Seatbelt) profiles for process containment. Requires schema `0.7.0-alpha`+. Supports `network.proxy` via the same cooperative env-var model as Bubblewrap (injects `HTTP_PROXY`/`HTTPS_PROXY` into the sandbox, reusing `wxc_common::unix_proxy_coordinator`; `builtinTestServer` spawns the shared `unix-test-proxy`). See `docs/macos-support/seatbelt-backend.md`. | | Bubblewrap | `lxc-exec` | Linux | `backends/bubblewrap/common/src/bwrap_runner.rs` — unprivileged sandboxing via Linux user namespaces and `bwrap`. Experimental — requires `--experimental`. Uses shared filesystem/network policy fields; per-host network filtering via `NetworkIptablesManager` from `backends/lxc/common`. See `docs/bwrap-support/bubblewrap-backend.md`. | @@ -169,9 +169,9 @@ State-aware lifecycle: - `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md` — state-aware sandbox lifecycle API (cross-backend wire format, Rust `StatefulSandboxBackend` trait, and dispatcher contract) - `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md` — companion overview to the full state-aware design -- `docs/isolation-session/initial-bringup-plan.md` — IsolationSession backend, one-shot bringup (experimental, isolated user account per execution via the OS-side service) -- `docs/isolation-session/state-aware-rust-initial-plan.md` — IsolationSession state-aware lifecycle, Rust-layer plan (per-phase config / metadata, policy honor matrix, idempotence, concurrency, error mapping) -- `docs/isolation-session/state-aware-typescript-initial-plan.md` — IsolationSession state-aware lifecycle, TypeScript SDK plan +- `docs/isolation-session/oneshot.md` — IsolationSession backend, one-shot bringup (experimental, isolated user account per execution via the OS-side service) +- `docs/isolation-session/state-aware-rust.md` — IsolationSession state-aware lifecycle, Rust-layer spec (per-phase config / metadata, policy matrix, idempotence, concurrency, error mapping) +- `docs/isolation-session/state-aware-typescript.md` — IsolationSession state-aware lifecycle, TypeScript SDK spec ## Key Conventions diff --git a/docs/isolation-session/initial-bringup-plan.md b/docs/isolation-session/oneshot.md similarity index 76% rename from docs/isolation-session/initial-bringup-plan.md rename to docs/isolation-session/oneshot.md index bbd578b4f..8b092c3ad 100644 --- a/docs/isolation-session/initial-bringup-plan.md +++ b/docs/isolation-session/oneshot.md @@ -1,4 +1,4 @@ -# MXC IsolationSession Backend — Initial Bringup Plan +# MXC IsolationSession Backend — One-Shot Bringup ## Problem @@ -11,7 +11,7 @@ session. Use cases that need this — per the broader claw-on-MXC scenario — c - **OS-managed session lifecycle** that the OS-side service tears down cleanly when the calling process exits, and - a **path toward stateful execution** where one provisioned user and session - can host multiple sequential exec calls without re-paying the registration / + can host multiple sequential exec calls without re-paying the provisioning / session-start cost each time. ## Proposed Solution @@ -20,10 +20,10 @@ Add an **IsolationSession runner** to `wxc-exec.exe`, behind `--experimental`. When the JSON config specifies `"containment": "isolation_session"` and the experimental flag is set, the binary routes to a new `IsolationSessionRunner` (implementing the existing `ScriptRunner` trait). The runner orchestrates the -full lifecycle against the OS-side Isolation Session API: register the -calling app, provision an agent user, start a session, run the script -(capturing stdout / stderr / exit code into `ScriptResponse`), then stop the -session, deprovision the agent, and unregister. All of this happens through +full lifecycle against the OS-side Isolation Session API: provision an +agent user, start a session, run the script (capturing stdout / stderr / +exit code into `ScriptResponse`), then stop the session and deprovision the +agent. All of this happens through Rust bindings auto-generated from a private WinMD; the OS-side API is gated on an internal Windows feature flag. @@ -44,21 +44,19 @@ wxc-exec.exe (Rust — single binary, multiple backends) ├── Parses JSON config → sees containment = "isolation_session" ├── Checks --experimental flag → instantiates IsolationSessionRunner ├── Calls IsolationSessionManager methods 1:1 with the OS-side service: - │ register_client(regId) → Step 0 - │ provision_agent_user(...) → Step 1 — creates agent user - │ start_session(..., configId) → Step 2 — boots session - │ create_process(..., path, args, opts) → Step 3 — launches in session - │ [Read stdout + stderr handles] → drives ScriptResponse - │ [WaitForExitAsync + ExitCode] → drives exit_code - │ stop_session(...) → Step 4 - │ deprovision_agent_user(...) → Step 5 - │ unregister_client(regId) → Step 6 + │ add_user(...) → Step 1 — creates agent user + │ start_session(...) → Step 2 — boots session + │ create_process(...) → Step 3 — launches in session + │ [Read stdout + stderr] → drives ScriptResponse + │ [WaitForExitAsync + ExitCode] → drives exit_code + │ stop_session(...) → Step 4 + │ deprovision_agent_user(...) → Step 5 └── Returns ScriptResponse with stdout, stderr, exit_code ``` -The OS-side service does the heavy lifting: it provisions a Windows agent user account -(named `-IEB-`), launches an `IsolationProxy.exe` per -session, and exposes the running script as an `IIsolationSessionWorkerProcess` +The OS-side service does the heavy lifting: it provisions a fresh per-execution +Windows agent user account (an opaque, OS-assigned name), launches a per-session +host process, and exposes the running script as an `IsoSessionProcess` handle from which the runner reads pipe handles for I/O. ## Architecture @@ -83,9 +81,8 @@ let mut runner: Box = match request.containment { The runner is split into two layers: - **`IsolationSessionManager`** — reusable, lifecycle methods that map 1:1 - to the OS-side API. Methods: `new`, `register_client`, - `provision_agent_user`, `start_session`, `create_process`, `stop_session`, - `deprovision_agent_user`, `unregister_client`. + to the OS-side API. Methods: `new`, `add_user`, `start_session`, + `create_process`, `stop_session`, `deprovision_agent_user`. - **`IsolationSessionRunner`** — thin one-shot `ScriptRunner` impl that drives the manager's methods in order. Disposable when a stateful path lands. @@ -135,46 +132,45 @@ interface. "timeout": 30000 }, "experimental": { - "isolation_session": { - "configurationId": "small" - } + "isolation_session": {} } } ``` -The only `experimental.isolation_session` knob is `configurationId`, which -selects the OS-side session size: `"small"` (1, default), `"medium"` (2), -`"large"` (3), or `"commandline"` (4). Other process options (`cwd`, `env`, -`timeout`) read from the existing top-level `process` section, matching the -contract every other backend honors. +The one-shot `experimental.isolation_session` block currently carries no +honored knobs — the Entra `user` bundle is a state-aware-only field and is +rejected on the one-shot path. Process options (`cwd`, `env`, `timeout`) read +from the existing top-level `process` section, matching the contract every +other backend honors. Run with: `wxc-exec.exe --experimental config.json`. ## OS API Dependency The runner calls into the WinRT API namespaced -`Windows.AI.IsolationEnvironment.Session`, exposed by the OS-side Isolation +`Windows.AI.IsolationSession.Preview`, exposed by the OS-side Isolation Session service (running as SYSTEM via `svchost.exe`). The API is gated on an internal Windows feature flag. -Activation goes through -`RoGetActivationFactory(RuntimeClass_Windows_AI_IsolationEnvironment_Session_IsolationSessionClient)`. +Activation goes through the WinRT activation factory for the +`Windows.AI.IsolationSession.Preview` `IsoSessionOps` runtime class. Activation requires `RoInitialize(RO_INIT_MULTITHREADED)` (handled in `main.rs` at startup, applied unconditionally because it's benign for other backends). -The API surface includes seven lifecycle methods plus -`IIsolationSessionWorkerProcess` (the running-process handle). The runner -uses a minimal subset of the worker-process surface: stdout pipe, stderr -pipe, `WaitForExitAsync`, `ExitCode`. It does not use stdin, terminate, +The API surface includes the lifecycle methods plus +`IsoSessionProcess` (the running-process handle). The runner +uses a minimal subset of the process surface: stdout pipe, stderr +pipe, exit-wait, and exit code. It does not use stdin, terminate, control signals, or interactive ConPTY mode. ## Bindings Workflow -**Why a private WinMD.** The OS-side API ships its WinMD -(`windows.ai.isolationenvironment.winmd`) as part of an internal Windows OS -build. There is no public NuGet or release distribution today. MXC stores -generated Rust bindings in the workspace and tracks their provenance. +**Why a private WinMD.** The OS-side API ships its WinMD as part of an +internal Windows OS build (the exact file name and provenance are recorded +in `GENERATION_INFO.toml`). There is no public NuGet or release distribution +today. MXC stores generated Rust bindings in the workspace and tracks their +provenance. **Future direction.** The OS API is expected to land in the public Windows SDK eventually, at which point the `windows` crate (auto-generated from @@ -194,15 +190,12 @@ moves), the bindings must be regenerated by a Microsoft engineer with access to the private WinMD. `windows-bindgen` X.Y generates code that targets the `windows` X.Y crate, so a regenerator must use a `windows-bindgen` release whose major.minor matches the workspace -`windows` crate. For avoidance of doubt: although earlier draft text in -this document may refer to the WinRT namespace -`Windows.AI.IsolationEnvironment.Session`, the generated bindings and the -Rust code in this repo use `Windows.AI.IsolationSession` (for example, -`IsoSessionOps`), and that is the naming to use when diagnosing -regeneration or version-coupling issues. The build-time check below -catches the most common slip — bumping the workspace `windows` crate -without regenerating — by comparing the workspace version against the -recorded `target_windows_crate`. +`windows` crate. The generated bindings and the Rust code in this repo use +the `Windows.AI.IsolationSession.Preview` namespace (for example, +`IsoSessionOps`) — that is the naming to use when diagnosing regeneration or +version-coupling issues. The build-time check below catches the most common +slip — bumping the workspace `windows` crate without regenerating — by +comparing the workspace version against the recorded `target_windows_crate`. **`build.rs` version check.** `isolation_session_bindings/build.rs` reads the expected `windows` crate version from `GENERATION_INFO.toml` @@ -214,17 +207,15 @@ versions and stating that the bindings must be regenerated. **Implemented:** -- Single-shot `register → provision → start → run → stop → deprovision → - unregister` lifecycle, gated by `--experimental`. +- Single-shot `provision → start → run → stop → deprovision` lifecycle, + gated by `--experimental`. - `process.commandLine` (the script command, wrapped via `cmd.exe /c "..."` — the same pattern the LXC runner uses with `/bin/sh -c`). - `process.cwd` (working directory inside the session). -- `process.env` (environment variables forwarded via - `IIsolationSessionWorkerProcessCreateOptions::SetEnvironmentVariables`). +- `process.env` (environment variables forwarded via the OS-side + `IsoSessionProcessOptions`). - `process.timeout` (forwarded to the OS-side per-process timeout enforcement). -- `experimental.isolation_session.configurationId` - (Small / Medium / Large / CommandLine). - `lifecycle.destroyOnExit` (mapped to the OS-side `LifetimePolicy`: `true` → `CallerProcess`, `false` → `Indefinite`; matches how other backends interpret this field). @@ -250,8 +241,8 @@ versions and stating that the bindings must be regenerated. | Category | Count | Location | What it verifies | |---|---:|---|---| -| Config parsing | ~8 | `config_parser.rs` | `"isolation_session"` containment value, `experimental.isolation_session` section, `configurationId` values + defaults | -| Policy validation | ~15 | `isolation_session_runner.rs` | Phase-specific behaviour: provision accepts `readwritePaths` / `readonlyPaths` and rejects `deniedPaths`; non-provision phases reject every filesystem field; network and proxy are rejected at every phase | +| Config parsing | ~8 | `config_parser.rs` | `"isolation_session"` containment value and `experimental.isolation_session` section parsing | +| Policy validation | ~15 | `policy.rs` | Every filesystem field (`readwritePaths` / `readonlyPaths` / `deniedPaths`), network, and proxy policy is rejected at every phase | | Option building | ~6 | `isolation_session_runner.rs` | `ExecutionRequest` → `ProcessOptions` mapping (timeout, cwd, env vars, redirect flags) | | Feature unavailable | 1 | `isolation_session_runner.rs` | Runner returns a clean error on machines without the IsolationSession feature enabled, so the test passes everywhere | @@ -265,7 +256,7 @@ Two end-to-end configs live under `tests/configs/`: - `isolation_session_hello.json` — happy path. Prints `USERNAME`, `MYVAR`, `CWD`, and `whoami` from inside the session. Validates the - agent identity (`-IEB-`), env-var pass-through, + agent identity (the freshly-provisioned agent account), env-var pass-through, working-directory pass-through, and that the running account differs from the caller. - `isolation_session_exit42.json` — runs `exit 42` and validates that @@ -318,7 +309,7 @@ The following were observed during VM testing and are accepted for v0.1. | OS API not present on older Windows builds | the IsolationSession feature is OS-side; runner reports a clean error when the activation factory fails. Feature-unavailable test exercises this on CI | | New Cargo feature increases coupling | The `isolation_session` feature is off by default in the workspace; default builds and existing CI are unaffected | | Manual VM testing required | The OS-side service has the same constraint for any consumer (it rejects network-logon tokens). Automated suite covers what it can without the OS-side service | -| One-shot lifecycle is heavy (full register → provision → start per call) | Accepted for v0.1; experimental flag indicates rough edges. Stateful API is the planned mitigation | +| One-shot lifecycle is heavy (full provision → start per call) | Accepted for v0.1; experimental flag indicates rough edges. Stateful API is the planned mitigation | | `ProvisionAgentUserAsync` re-provision hang under `Indefinite` lifetime | Manager calls `GetAgentUser` first and skips a redundant provision when the user already exists | | `DeprovisionAgentUserAsync` failure under `Indefinite` lifetime | Manager re-provisions with `CallerProcess` lifetime as part of teardown so the OS-side process-exit callback handles cleanup naturally | @@ -327,8 +318,9 @@ The following were observed during VM testing and are accepted for v0.1. **For end users:** - A Windows build with the IsolationSession feature enabled. -- `IsolationProxy.exe` present in `%SystemRoot%\System32\` (ships with - Windows as part of the OS-side service). +- The OS-side isolation-session host binary present in + `%SystemRoot%\System32\` (ships with Windows as part of the OS-side + service). - WinRT initialized as MTA (handled by `wxc-exec`). **For developers:** @@ -358,13 +350,13 @@ wxc-exec.exe --experimental hello.json "timeout": 30000 }, "experimental": { - "isolation_session": { "configurationId": "small" } + "isolation_session": {} } } ``` -Expected stdout: `\-ieb-` (the freshly-provisioned agent -user, distinct from whichever account ran `wxc-exec`). +Expected stdout: the freshly-provisioned agent account name (an opaque, +OS-assigned account, distinct from whichever account ran `wxc-exec`). ## Supported Workloads diff --git a/docs/isolation-session/state-aware-rust-initial-plan.md b/docs/isolation-session/state-aware-rust.md similarity index 55% rename from docs/isolation-session/state-aware-rust-initial-plan.md rename to docs/isolation-session/state-aware-rust.md index d675600a2..0765ba5e1 100644 --- a/docs/isolation-session/state-aware-rust-initial-plan.md +++ b/docs/isolation-session/state-aware-rust.md @@ -1,10 +1,10 @@ -# MXC IsolationSession Backend — State-Aware Rust Initial Plan +# MXC IsolationSession Backend — State-Aware (Rust) This document describes the IsolationSession backend's behaviour under the state-aware lifecycle API ([design](../state-aware-lifecycle/mxc-state-aware-sandbox-api.md)). -It is the per-backend plan required by §11.6 of that design and covers the -five state-aware phases — provision, start, exec, stop, deprovision — -plus the cross-cutting policy honor matrix, idempotence behaviour, +It is the per-backend specification required by §11.6 of that design and +covers the five state-aware phases — provision, start, exec, stop, +deprovision — plus the cross-cutting policy matrix, idempotence behaviour, concurrency story, and error mapping. ## Scope @@ -20,7 +20,7 @@ concurrency story, and error mapping. - Mapping from the OS-side service's HRESULTs to the wire-format `MxcError` codes. -### Out of scope (for this initial plan) +### Out of scope (for v1) - **Explicit `AbortSignal` plumbing.** v1 cancellation is OS-level: the caller kills `wxc-exec.exe`, the OS-side service's per-process timer or @@ -49,22 +49,23 @@ without metadata use `()`. | Field | Type | Default | Description | |---|---|---|---| -| `user` | `IsolationSessionUser` (object) \| absent | absent | Optional Entra cloud-agent credentials. When present, provision routes through `IIsoSessionOps2::AddUserAsync2` and the resulting sandbox is Entra-backed. When absent, provision uses the v1 `AddUserAsync` path and produces a local-agent sandbox. The bundle is `{ upn: string, wamToken: string }`; both fields required when supplied. `wamToken` is passed verbatim to the OS-side service and never stored by MXC. The wire path is `experimental.isolation_session.provision.user`. | +| `user` | `IsolationSessionUser` (object) \| absent | absent | Optional Entra cloud-agent credentials. When present, the UPN and WAM token are passed to `AddUserAsync` and the resulting sandbox is Entra-backed. When absent, provision calls `AddUserAsync` with empty strings and produces a local-agent sandbox. The bundle is `{ upn: string, wamToken: string }`; both fields required when supplied. `wamToken` is passed verbatim to the OS-side service and never stored by MXC. The wire path is `experimental.isolation_session.provision.user`. | **Metadata (`IsolationSessionProvisionMetadata`):** | Field | Type | Description | |---|---|---| -| `agentUserName` | string | The OS-assigned agent account name returned by `AddUserAsync` / `AddUserAsync2`. Diagnostic only — not used as an addressing key. Format is OS-internal and not stable across builds. | - -The provisioned `sandboxId` shape depends on whether `user` was supplied: - -- **Local sandbox** (no `user`): `iso:wxc-<8-hex>`, where the 8-hex suffix is - `mint_random_token()`. Example: `iso:wxc-1b65bd11`. -- **Entra sandbox** (`user` supplied): `iso:`. The UPN is the OS-layer - `provisionId` for Entra agents — no separate identifier exists — so encoding - it as the tail keeps every later phase stateless. Example: - `iso:alice@contoso.com`. +| `agentUserName` | string | The OS-assigned agent account name returned by `AddUserAsync`. Diagnostic only — not used as an addressing key. Format is OS-internal and not stable across builds. | +| `agentUserSid` | string | The security identifier (SID) of the agent user, returned by `AddUserAsync`. Diagnostic only. | +| `ephemeralWorkspacePath` | string | A directory shared between the calling user and this isolated agent user, through which the caller can stage files into the session. Each isolated user can access only its own workspace; the caller can access every concurrent sandbox's workspace. Created at provision and deleted when the sandbox is deprovisioned. It does **not** change the workload's working directory. | + +The provisioned `sandboxId` is always `iso:`, where +`agentUserName` is the opaque account name the OS assigns at `AddUserAsync` +(also returned in `IsolationSessionProvisionMetadata.agentUserName`). The +same shape is used for local and Entra sandboxes alike — the tail is opaque, +so no later phase can infer Entra-ness from it; the Entra WAM token is +re-supplied at start instead. The exact `agentUserName` format is OS-internal +and not stable across builds. ### Start @@ -72,15 +73,13 @@ The provisioned `sandboxId` shape depends on whether `user` was supplied: | Field | Type | Default | Description | |---|---|---|---| -| `configurationId` | `"small" \| "medium" \| "large" \| "composable"` | `"composable"` | Maps to the OS-side `IsoSessionConfigId`. `composable` is the lightweight, ConPTY-friendly default; `small` triggers a known cache-teardown bug on the current OS build (see [Known issues](#known-issues)) and is not recommended. | -| `user` | `IsolationSessionUser` (object) \| absent | absent | Required when starting an Entra sandbox (one whose `sandboxId` tail contains `@`); rejected for local sandboxes. When required, `user.upn` must match the `sandboxId` tail (case-insensitive) and `wamToken` must be non-empty — `validate_start` enforces this matrix and surfaces mismatches as `malformed_request`. Routes start through `IIsoSessionOps2::StartSessionAsync2`. The wire path is `experimental.isolation_session.start.user`. | +| `user` | `IsolationSessionUser` (object) \| absent | absent | Optional. Supply for an Entra sandbox to re-provide the WAM token (the opaque `sandboxId` tail can't carry it); omit for a local sandbox. When supplied it is shape-validated (`upn` contains `@`, `wamToken` non-empty) by `validate_start`, surfacing shape errors as `policy_validation`; the OS validates the token against the agent user assigned at provision. The wire path is `experimental.isolation_session.start.user`. | This is the same `IsolationSessionConfig` shape used by the one-shot `experimental.isolation_session` block, with one mode difference: `user` is honoured here at state-aware start, but rejected on the one-shot path (`validate_runner` returns `policy_validation` if a one-shot request carries -it). The wire path for `configurationId` is -`experimental.isolation_session.start.configurationId`. +it). **Metadata (none).** Start returns an empty `result: {}` envelope on success. @@ -103,25 +102,24 @@ described in [Idempotence](#idempotence-per-phase). ### Deprovision -**Config (none).** Deprovision removes the agent user *and* unregisters the -client app. After this returns, `sandboxId` is no longer addressable — any -subsequent op against it surfaces `stale_id`. +**Config (none).** Deprovision removes the agent user. After this returns, +`sandboxId` is no longer addressable — any subsequent op against it surfaces +`stale_id`. **Metadata (none).** Empty `result: {}` envelope. ## Cross-cutting policy honor matrix -IsolationSession honors `readwritePaths` and `readonlyPaths` at provision -(applied via `ShareFolderBatchAsync`), and rejects everything else at -every phase. The grant lifecycle is bound to the agent user, so -filesystem policy is bound to provision and immutable thereafter — every -non-provision phase rejects any non-empty filesystem field. The OS-side -service has no equivalent for `deniedPaths`, network, or UI policy, so -those are rejected at every phase including provision. +IsolationSession rejects every `policy.filesystem` field (`readwritePaths`, +`readonlyPaths`, `deniedPaths`), all `policy.network` policy, and +`policy.network.proxy` at every phase — provision included. The backend has +no host-folder-sharing, network, or proxy primitive, so there is nothing to +honor. The only caller-supplied knob it accepts is the optional Entra `user` +bundle, at provision and start. | Field | provision | start | exec | stop | deprovision | |---|---|---|---|---|---| -| `policy.filesystem.{readwritePaths,readonlyPaths}` | **honored** | rejected | rejected | rejected | rejected | +| `policy.filesystem.{readwritePaths,readonlyPaths}` | rejected | rejected | rejected | rejected | rejected | | `policy.filesystem.deniedPaths` | rejected | rejected | rejected | rejected | rejected | | `policy.network.{allowedHosts,blockedHosts,defaultPolicy}` | rejected | rejected | rejected | rejected | rejected | | `policy.network.proxy` | rejected | rejected | rejected | rejected | rejected | @@ -129,10 +127,11 @@ those are rejected at every phase including provision. | `experimental.isolation_session.{provision,start}.user` | **honored** | **honored** | n/a | n/a | n/a | Rejection of `policy.*` fields surfaces as `error.code = "policy_validation"`. -Rejection of malformed `user` shape (UPN missing `@`, empty `wamToken`) surfaces -as `policy_validation`; rejection at start due to a sandboxId/user inconsistency -(missing user for Entra sandbox, user supplied for local sandbox, or UPN -mismatch) surfaces as `malformed_request`. +A malformed `user` shape (UPN missing `@`, empty `wamToken`) likewise surfaces +as `policy_validation`. Start does not cross-check the `user` bundle against +the `sandboxId` tail — the tail is opaque — so there is no identity-mismatch +`malformed_request` path; the OS validates the WAM token against the agent +user it assigned at provision. ## Mode-specific fields @@ -143,39 +142,23 @@ mismatch) surfaces as `malformed_request`. absent for non-exec phases). - `process.cwd`, `process.env`, `process.timeout` — optional in both modes, honoured per-process (each exec receives its own block). -- `experimental.isolation_session.configurationId` (one-shot) / - `experimental.isolation_session.start.configurationId` (state-aware) — - same enum (`small` / `medium` / `large` / `composable`). ### Policy fields and mode parity -Both modes share the same policy-honor matrix above: - -- `policy.filesystem.readwritePaths` / `readonlyPaths` are honored at - provision (state-aware) or at the start of the lifecycle (one-shot, - via `ScriptRunner::validate_runner` then `share_folders` in - `IsolationSessionRunner::execute`). Rejected at all later state-aware - phases. - - Before forwarding to `ShareFolderBatchAsync`, `share_folders` runs - the entries through a small filter (`filter_protected_paths` in - `isolation_session_runner.rs`, bracketed by `BEGIN:` / `END:` - markers) that silently drops a fixed set of system-folder paths — - drive roots, `SystemRoot`, parent of `USERPROFILE`, `ProgramFiles`, - `ProgramFiles(x86)`, and `ProgramData`. The mitigation exists - because `ShareFolderBatchAsync` applies ACEs with subtree - inheritance; the proper fix belongs in the OS API. See the region - comment for removal conditions. -- `policy.filesystem.deniedPaths`, `policy.network`, `policy.ui`, and - `policy.network.proxy` are rejected at every phase (one-shot via - `validate_runner`, state-aware via `validate_` hooks). +Both modes share the same policy matrix above. Every `policy.filesystem` +field (`readwritePaths`, `readonlyPaths`, `deniedPaths`), all `policy.network` +policy, `policy.network.proxy`, and `policy.ui` are rejected at every phase — +the backend has no host-folder-sharing, network, or proxy primitive. One-shot +enforces this via `validate_runner`; state-aware enforces it via the +`validate_` hooks. ### Fields valid in state-aware only - `phase` — the discriminator. Required for state-aware; absent for one-shot. - `sandboxId` — required for non-provision phases. - `experimental.isolation_session.` — typed per-phase config blocks - (`provision` carries optional `user`; `start` carries `configurationId` and - optional `user`; `exec` / `stop` / `deprovision` use `()`). + (`provision` carries optional `user`; `start` carries optional `user`; + `exec` / `stop` / `deprovision` use `()`). - `experimental.isolation_session.{provision,start}.user` — Entra cloud-agent credentials. Honoured here; the same field on a one-shot `experimental.isolation_session` is rejected with `policy_validation`. @@ -187,18 +170,16 @@ Both modes share the same policy-honor matrix above: | provision | non-idempotent | Each provision mints a fresh `provisionId` / agent user. Two provision calls produce two distinct sandboxes. Acceptable: callers manage `sandboxId` state themselves. | | start | OS-side dependent | Starting an already-started session surfaces an HRESULT from `StartSessionAsync`; mapped to `backend_error` (no specific MXC code). Callers should not call start twice; if they do, the second call's failure does not corrupt the first session. | | exec | per-call | Each exec creates a fresh agent process via `RunProcessWithOptionsAsync`. No deduplication — repeated `commandLine` runs the command repeatedly. | -| stop | OS-side dependent | Stopping an already-stopped session surfaces an HRESULT from `StopSessionAsync`; mapped to `backend_error`. The cohort registration and agent user remain — only the running session is gone. | -| deprovision | becomes `stale_id` | After a successful deprovision, the agent user and registration are gone. A second deprovision on the same `sandboxId` triggers the OS-side `FindActiveAgentUserByProvisionId` lookup failure (`HRESULT_FROM_WIN32(ERROR_NOT_FOUND)`), which the runner maps to `MxcError::StaleId`. | +| stop | OS-side dependent | Stopping an already-stopped session surfaces an HRESULT from `StopSessionAsync`; mapped to `backend_error`. The agent user remains — only the running session is gone. | +| deprovision | becomes `stale_id` | After a successful deprovision, the agent user is gone. A second deprovision on the same `sandboxId` triggers the OS-side `FindActiveAgentUserByProvisionId` lookup failure (`HRESULT_FROM_WIN32(ERROR_NOT_FOUND)`), which the runner maps to `MxcError::StaleId`. | ## Concurrency ### Multiple sandboxes -Distinct `sandboxId`s have distinct `provisionId`s (each minted by -`mint_random_token`). They share a single registration string (`"regid"`, -hardcoded by the `IsoSessionOps` wrapper). The OS-side service's -`RegisterApp` is idempotent for duplicate calls (returns -`ALREADY_REGISTERED` as success), so concurrent provisions all succeed. +Distinct `sandboxId`s map to distinct OS agent users (each `AddUserAsync` +mints a fresh account). There is no shared registration between them, so +concurrent provisions are independent and all succeed. ### Multiple exec calls against the same sandbox @@ -209,15 +190,12 @@ same `sandboxId` from two `wxc-exec` processes are not coordinated by MXC; the OS-side service serialises (or rejects, depending on session state) at its own layer. -### Deprovision side-effect on concurrent sandboxes +### Deprovision and concurrent sandboxes -`deprovision` calls both `deprovision_agent_user` and `unregister_client`. -The second call tears down the *shared* registration, so any other -concurrent state-aware sandbox using the same registration breaks at its -next op (sees `stale_id` from `FindClientIdentity` lookup failure). v1 does -not target concurrent state-aware sandboxes; if that becomes a real -requirement, this needs either reference-counting on the registration or a -"leave-registration-alone" deprovision mode. +`deprovision` removes only its own agent user (`deprovision_agent_user`). +Because each sandbox is a distinct OS agent user with no shared registration, +deprovisioning one sandbox does not affect any other concurrent sandbox — +they remain independently addressable until each is deprovisioned in turn. ## Error mapping @@ -227,7 +205,7 @@ wire-format `MxcError` codes via `map_lifecycle_error`: | `IsolationSessionError` variant | Wire `error.code` | Trigger | |---|---|---| | `Policy(...)` | `policy_validation` | Caller-supplied policy field that this phase does not accept — see the honor matrix above. Rejected by `validate_` hooks (state-aware) or `validate_runner` (one-shot). | -| `ServiceUnavailable(...)` | `backend_unavailable` | `IsoSessionOps` activation failure: `IsoSessionApp.dll` not registered, or `Feature_IsoBrokerSessionApis` disabled at the OS-side. HRESULTs `CLASS_E_CLASSNOTAVAILABLE` (`0x80040111`) or `REGDB_E_CLASSNOTREG` (`0x80040154`). | +| `ServiceUnavailable(...)` | `backend_unavailable` | `IsoSessionOps` activation failure: the `Windows.AI.IsolationSession.Preview` API is unavailable on this OS build (not registered, or the OS feature gate is off). HRESULTs `CLASS_E_CLASSNOTAVAILABLE` (`0x80040111`) or `REGDB_E_CLASSNOTREG` (`0x80040154`). | | `Stale(...)` | `stale_id` | OS-side `AgentManager::FindActiveAgentUserByProvisionId` returns `HRESULT_FROM_WIN32(ERROR_NOT_FOUND)` (`0x80070490`) — the `provisionId` is missing from both the in-memory cache and the persisted registry. After `deprovision`, every non-provision op against the dead `sandboxId` triggers this. | | `Lifecycle(...)` | `backend_error` | Any other HRESULT from a lifecycle op. The error message embeds the operation name, HRESULT, OS-side message, and remediation hint where present. | @@ -255,26 +233,20 @@ waiter, with `terminator` invoking `IsoSessionProcess::Terminate()`. ## Known issues -### `Small` configurationId - -Selecting `configurationId: "small"` triggers a cache-teardown bug in the -OS-side service: the `RemoveUserAsync` call against a cached `Small` agent -user causes the service's RPC endpoint to disconnect with `0x800706be` -(`RPC_S_CALL_FAILED`), and subsequent calls fail until the service -restarts. `Composable` is unaffected. Use `composable` (the default). - ### Concurrent state-aware sandboxes -See [Concurrency](#concurrency) — `deprovision` tears down the shared -registration and breaks any other in-flight state-aware sandbox. v1's -single-sandbox-per-consumer model is the workaround. +v1 targets a single state-aware sandbox per consumer (see the +[Out of scope](#out-of-scope-for-v1) note). The earlier cross-sandbox +deprovision hazard no longer applies — each sandbox is an independent OS +agent user with no shared registration — so this is a v1 scoping choice, +not an OS limitation. ## References - [State-aware design (full)](../state-aware-lifecycle/mxc-state-aware-sandbox-api.md) - [State-aware design (overview)](../state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md) -- [TypeScript initial plan](state-aware-typescript-initial-plan.md) — SDK companion +- [TypeScript spec](state-aware-typescript.md) — SDK companion to this doc; covers SDK API surface, types, and TS usage examples. -- [Initial bringup plan (one-shot)](initial-bringup-plan.md) — the +- [One-shot bringup](oneshot.md) — the predecessor doc for IsolationSession's first integration; this doc covers state-aware on top of that foundation. diff --git a/docs/isolation-session/state-aware-typescript-initial-plan.md b/docs/isolation-session/state-aware-typescript.md similarity index 78% rename from docs/isolation-session/state-aware-typescript-initial-plan.md rename to docs/isolation-session/state-aware-typescript.md index b944de45d..0d178e97e 100644 --- a/docs/isolation-session/state-aware-typescript-initial-plan.md +++ b/docs/isolation-session/state-aware-typescript.md @@ -1,8 +1,8 @@ -# MXC IsolationSession Backend — State-Aware TypeScript Initial Plan +# MXC IsolationSession Backend — State-Aware (TypeScript) This document describes the IsolationSession backend's TypeScript SDK surface under the state-aware lifecycle API ([design](../state-aware-lifecycle/mxc-state-aware-sandbox-api.md)). -It is the SDK companion to the [Rust initial plan](state-aware-rust-initial-plan.md). +It is the SDK companion to the [Rust spec](state-aware-rust.md). The Rust doc covers runtime semantics (validation, error mapping, idempotence, concurrency); this doc covers SDK API surface, types, and consumer usage patterns. @@ -18,8 +18,8 @@ concurrency); this doc covers SDK API surface, types, and consumer usage pattern ### Out of scope -- Runtime validation rules — see the [Rust plan](state-aware-rust-initial-plan.md) - for the Entra `user` validation matrix, policy honor matrix, idempotence, +- Runtime validation rules — see the [Rust spec](state-aware-rust.md) + for the Entra `user` validation, policy matrix, idempotence, concurrency, and error mapping. - The wire-format envelope — see the [main design doc](../state-aware-lifecycle/mxc-state-aware-sandbox-api.md) §7. @@ -29,7 +29,7 @@ concurrency); this doc covers SDK API surface, types, and consumer usage pattern ## Per-phase Configs and Metadata The SDK exposes only the fields the IsolationSession runtime currently honors at each -phase. See the [Rust plan](state-aware-rust-initial-plan.md) for the full Rust-side +phase. See the [Rust spec](state-aware-rust.md) for the full Rust-side contract (including fields not yet exposed via the SDK). | Phase | Config | Metadata | @@ -47,7 +47,6 @@ contract (including fields not yet exposed via the SDK). | Field | Type | Default | Description | |---|---|---|---| | `version` | string | SDK `SUPPORTED_VERSION` | Schema-version override. | -| `filesystem` | `FilesystemConfig` | absent | `readwritePaths` and `readonlyPaths` honored at provision; `deniedPaths` rejected. | | `user` | `IsolationSessionUserConfig` | absent | Optional Entra credentials (see below). | **Metadata (`IsolationSessionProvisionMetadata`):** @@ -55,6 +54,8 @@ contract (including fields not yet exposed via the SDK). | Field | Type | Description | |---|---|---| | `agentUserName` | string | OS-assigned account name. Diagnostic only — not used as an addressing key. | +| `agentUserSid` | string | SID of the agent user. Diagnostic only. | +| `ephemeralWorkspacePath` | string | A directory shared between the caller and this isolated user for staging files into the session. Each isolated user sees only its own workspace; the caller can access every concurrent sandbox's workspace. Deleted when the sandbox is deprovisioned. Does not change the working directory. | ### Start @@ -63,8 +64,7 @@ contract (including fields not yet exposed via the SDK). | Field | Type | Default | Description | |---|---|---|---| | `version` | string | SDK `SUPPORTED_VERSION` | Schema-version override. | -| `configurationId` | `'small' \| 'medium' \| 'large' \| 'composable'` | runtime default `'composable'` | Session size profile. | -| `user` | `IsolationSessionUserConfig` | absent | Required when the sandbox was provisioned with a `user` bundle; rejected otherwise. When required, `upn` must match the UPN supplied at provision (case-insensitive). | +| `user` | `IsolationSessionUserConfig` | absent | Optional. For an Entra sandbox, re-supply the `user` (same UPN, current WAM token) so the OS can validate the token against the agent user assigned at provision. Omit for a local sandbox. Shape-validated only — MXC does not match the UPN against the sandbox id. | **Metadata:** none. @@ -120,11 +120,11 @@ const opts: SandboxSpawnOptions = { experimental: true }; const { sandboxId } = await provisionSandbox( 'isolation_session', - { filesystem: { readwritePaths: ['C:\\workspace'] } }, + {}, opts, ); -await startSandbox(sandboxId, { configurationId: 'composable' }, opts); +await startSandbox(sandboxId, {}, opts); const r = await execInSandboxAsync(sandboxId, { process: { commandLine: 'echo hi' } }, opts); console.log(r.stdout); // "hi" @@ -134,8 +134,9 @@ await deprovisionSandbox(sandboxId, undefined, opts); ### Entra -Provisioning with a `user` bundle selects the Entra path; the returned id encodes -the UPN, and every subsequent start on that sandbox must carry a matching `user`: +Provisioning with a `user` bundle selects the Entra path. The returned id is an +opaque OS-assigned handle (it does not encode the UPN); re-supply the `user` at +start so the OS can validate the current WAM token: ```typescript import { IsolationSessionUserConfig } from '@microsoft/mxc-sdk'; @@ -144,16 +145,16 @@ const user = new IsolationSessionUserConfig('alice@contoso.com', wamToken); const { sandboxId } = await provisionSandbox( 'isolation_session', - { filesystem: { readwritePaths: ['C:\\workspace'] }, user }, + { user }, opts, ); -await startSandbox(sandboxId, { configurationId: 'composable', user }, opts); +await startSandbox(sandboxId, { user }, opts); // exec / stop / deprovision unchanged from the local example above. ``` -Validation rules for the Entra path (UPN matching, malformed-bundle handling, error -codes) live in the [Rust plan](state-aware-rust-initial-plan.md). +Validation rules for the Entra path (bundle shape-validation, error +codes) live in the [Rust spec](state-aware-rust.md). ## Test helpers @@ -189,5 +190,5 @@ covers the validation rejections. - [State-aware design (main)](../state-aware-lifecycle/mxc-state-aware-sandbox-api.md) - [State-aware design (overview)](../state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md) -- [Rust initial plan](state-aware-rust-initial-plan.md) — runtime semantics -- [Initial bringup plan (one-shot)](initial-bringup-plan.md) +- [Rust spec](state-aware-rust.md) — runtime semantics +- [One-shot bringup](oneshot.md) diff --git a/external/windows-sdk/isolation-session/GENERATION_INFO.toml b/external/windows-sdk/isolation-session/GENERATION_INFO.toml index b7463ead4..7a22c92d9 100644 --- a/external/windows-sdk/isolation-session/GENERATION_INFO.toml +++ b/external/windows-sdk/isolation-session/GENERATION_INFO.toml @@ -4,4 +4,4 @@ [tool] windows_bindgen_version = "0.62.1" target_windows_crate = "0.62" -generated_date = "2026-05-08" +generated_date = "2026-07-01" diff --git a/schemas/dev/mxc-config.schema.0.8.0-dev.json b/schemas/dev/mxc-config.schema.0.8.0-dev.json index 0caca3b7a..e37786f2e 100644 --- a/schemas/dev/mxc-config.schema.0.8.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.8.0-dev.json @@ -259,30 +259,9 @@ }, "type": "object" }, - "IsolationConfigurationId": { - "description": "IsolationSession sizing profile.", - "enum": [ - "small", - "medium", - "large", - "composable" - ], - "type": "string" - }, "IsolationSession": { - "description": "IsolationSession backend config. Carries both the one-shot fields (`configurationId`, `user`) and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`).", + "description": "IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`).", "properties": { - "configurationId": { - "anyOf": [ - { - "$ref": "#/definitions/IsolationConfigurationId" - }, - { - "type": "null" - } - ], - "description": "Sizing profile (one-shot)." - }, "deprovision": { "anyOf": [ { @@ -344,17 +323,6 @@ "IsolationSessionPhase": { "description": "Per-phase IsolationSession configuration (state-aware lifecycle).", "properties": { - "configurationId": { - "anyOf": [ - { - "$ref": "#/definitions/IsolationConfigurationId" - }, - { - "type": "null" - } - ], - "description": "Sizing profile for this phase." - }, "user": { "anyOf": [ { diff --git a/sdk/node/README.md b/sdk/node/README.md index cfda97366..ce047978c 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -200,7 +200,7 @@ console.log(result.stdout); | `windows_sandbox` | `vm` | Windows | Experimental | [`docs/windows-sandbox/windows-sandbox.md`](https://github.com/microsoft/mxc/blob/main/docs/windows-sandbox/windows-sandbox.md) | | `microvm` | `microvm` | Windows | Experimental | [`docs/nanvix-microvm/nanvix.md`](https://github.com/microsoft/mxc/blob/main/docs/nanvix-microvm/nanvix.md) — MicroVM via NanVix on Windows Hypervisor Platform | | `wslc` | (concrete only) | Windows | Experimental | [`docs/wsl/wsl-container-getting-started.md`](https://github.com/microsoft/mxc/blob/main/docs/wsl/wsl-container-getting-started.md) | -| `isolation_session` | (concrete only) | Windows | Experimental | [`docs/isolation-session/initial-bringup-plan.md`](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/initial-bringup-plan.md) | +| `isolation_session` | (concrete only) | Windows | Experimental | [`docs/isolation-session/oneshot.md`](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/oneshot.md) | Experimental backends require `{ experimental: true }` in `SandboxSpawnOptions`: diff --git a/sdk/node/src/generated/wire.ts b/sdk/node/src/generated/wire.ts index 788cd4238..8d5d23f7a 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -107,18 +107,9 @@ export interface Filesystem { } /** - * IsolationSession sizing profile. - */ -export type IsolationConfigurationId = "small" | "medium" | "large" | "composable"; - -/** - * IsolationSession backend config. Carries both the one-shot fields (`configurationId`, `user`) and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`). + * IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`). */ export interface IsolationSession { - /** - * Sizing profile (one-shot). - */ - configurationId?: IsolationConfigurationId | null; /** * State-aware deprovision-phase configuration. */ @@ -146,10 +137,6 @@ export interface IsolationSession { * Per-phase IsolationSession configuration (state-aware lifecycle). */ export interface IsolationSessionPhase { - /** - * Sizing profile for this phase. - */ - configurationId?: IsolationConfigurationId | null; /** * Entra cloud-agent user bundle for this phase. */ diff --git a/sdk/node/src/platform.ts b/sdk/node/src/platform.ts index 4a2e60d0b..9150398e6 100644 --- a/sdk/node/src/platform.ts +++ b/sdk/node/src/platform.ts @@ -23,87 +23,6 @@ function getSdkPackageRoot(): string { } } -/** - * Query Windows Registry for a value - * @param key - Registry key path (e.g., "HKLM\\Software\\...") - * @param valueName - Name of the value to query - * @returns The registry value as a string, or null if not found - */ -function queryWindowsRegistry(key: string, valueName: string): string | null { - try { - const command = `reg query "${key}" /v "${valueName}"`; - const output = execSync(command, { encoding: 'utf-8', stdio: 'pipe' }); - - // Parse output - format is: - // HKEY_LOCAL_MACHINE\... - // ValueName REG_SZ Value - const lines = output.split('\n'); - for (const line of lines) { - if (line.includes(valueName)) { - // Extract value after REG_SZ or REG_DWORD - const match = line.match(/REG_\w+\s+(.+)/); - if (match) { - return match[1].trim(); - } - } - } - return null; - } catch { - return null; - } -} - -/** - * Result of querying the host's Windows build number, or `null` when the - * registry values are missing or unparseable. - */ -type WindowsBuild = { major: number; minor: number } | null; - -/** - * Default implementation that reads `CurrentBuild` / `UBR` from the - * registry. Replaceable via {@link _setWindowsBuildQuery} in tests so we - * can exercise the IsolationSession version gate deterministically. - */ -function defaultWindowsBuildQuery(): WindowsBuild { - const registryPath = 'HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion'; - const currentBuild = queryWindowsRegistry(registryPath, 'CurrentBuild'); - const ubrValue = queryWindowsRegistry(registryPath, 'UBR'); - if (!currentBuild || !ubrValue) { - return null; - } - const major = parseInt(currentBuild, 10); - const minor = Number(ubrValue); - if (isNaN(major) || isNaN(minor)) { - return null; - } - return { major, minor }; -} - -let windowsBuildQuery: () => WindowsBuild = defaultWindowsBuildQuery; - -/** @internal Test-only: override the Windows build lookup. */ -export function _setWindowsBuildQuery(fn: (() => WindowsBuild) | null): void { - windowsBuildQuery = fn ?? defaultWindowsBuildQuery; -} - -/** - * Check whether the host supports the IsolationSession backend. - * Requires Windows Insider Preview build 26300.8553 or later. - * - * No internal cache — `getPlatformSupport` memoizes the full result, and - * registry reads are cheap relative to the rest of the probe. - */ -function isIsoSessionSupported(): boolean { - const build = windowsBuildQuery(); - if (!build) { - return false; - } - - // Pin to the Windows Insider Preview build that introduced IsolationSession - // (26300.8553+). Other major builds are not yet supported. - return build.major === 26300 && build.minor >= 8553; -} - let windowsSandboxAvailableCache: boolean | undefined; /** @@ -217,11 +136,12 @@ function isUiCapabilitySupport(value: unknown): value is UiCapabilitySupport { } /** - * Run the probe binary and merge its results into `support`. On any - * failure (binary missing, timeout, malformed JSON, unknown tier), the - * function silently leaves `support.isolationTier` and - * `support.isolationWarnings` unset — callers see the same contract as - * pre-Phase-5 SDKs. + * Run the probe binary and merge its results into `support`: the isolation + * tier, any warnings, portable UI capabilities, and — when the probe reports + * the isolation-session service available — the `isolation_session` method. + * On any failure (binary missing, timeout, malformed JSON), the function + * silently leaves those fields unset, so callers see the same contract as + * pre-probe SDKs. */ function populateIsolationFromProbe(support: PlatformSupport): void { try { @@ -242,6 +162,9 @@ function populateIsolationFromProbe(support: PlatformSupport): void { if (isUiCapabilitySupport(facts.uiCapabilities)) { support.uiCapabilities = facts.uiCapabilities; } + if (facts.isolationSessionAvailable === true) { + support.availableMethods.push('isolation_session'); + } } } } catch { @@ -293,9 +216,6 @@ function computeSupport(): PlatformSupport { if (isWindowsSandboxAvailable()) { support.availableMethods.push('windows_sandbox'); } - if (isIsoSessionSupported()) { - support.availableMethods.push('isolation_session'); - } populateIsolationFromProbe(support); return support; } diff --git a/sdk/node/src/state-aware-types.ts b/sdk/node/src/state-aware-types.ts index 8d0709b90..5f648c12e 100644 --- a/sdk/node/src/state-aware-types.ts +++ b/sdk/node/src/state-aware-types.ts @@ -3,7 +3,6 @@ import { ContainmentBackend, - FilesystemConfig, ProcessConfig, } from './types.js'; @@ -58,7 +57,6 @@ export class IsolationSessionUserConfig { export interface IsolationSessionProvisionConfig { /** Schema version (semver). When omitted, the SDK fills in its own SUPPORTED_VERSION. */ version?: string; - filesystem?: FilesystemConfig; /** * Optional Entra credentials. When supplied, provisioning uses the Entra * identity for the sandbox; the same `user` must be supplied to @@ -71,14 +69,9 @@ export interface IsolationSessionStartConfig { /** Schema version (semver). */ version?: string; /** - * Selected IsoSession size profile. Unknown values are warned and - * downgraded to `'composable'` on the Rust side. - */ - configurationId?: 'small' | 'medium' | 'large' | 'composable'; - /** - * Entra credentials. Required when the sandbox was provisioned with a - * `user` bundle; rejected otherwise. When required, `upn` must match the - * UPN supplied at provision (case-insensitive). + * Entra credentials for an Entra-backed sandbox. Supply the same UPN used + * at provision, with a current WAM token; the OS validates the token + * against the agent user assigned at provision. */ user?: IsolationSessionUserConfig; } @@ -100,11 +93,16 @@ export interface IsolationSessionDeprovisionConfig { } /** - * IsolationSession's provision-phase metadata: the per-instance agent user - * account name minted for this sandbox. + * IsolationSession's provision-phase metadata surfaced to the caller: the + * per-instance agent user account name minted for this sandbox, the agent + * user's SID, and the ephemeral workspace directory shared between the caller + * and this isolated user (through which the caller can stage files into the + * session; deleted when the sandbox is deprovisioned). */ export interface IsolationSessionProvisionMetadata { agentUserName: string; + agentUserSid: string; + ephemeralWorkspacePath: string; } /** diff --git a/sdk/node/tests/integration/isolation-session-state-aware.test.ts b/sdk/node/tests/integration/isolation-session-state-aware.test.ts index 49ee2c781..a27efd1f1 100644 --- a/sdk/node/tests/integration/isolation-session-state-aware.test.ts +++ b/sdk/node/tests/integration/isolation-session-state-aware.test.ts @@ -15,16 +15,16 @@ import { describe, it } from 'node:test'; import assert from 'node:assert'; -import fs from 'fs'; +import fs from 'node:fs'; +import path from 'node:path'; import os from 'os'; -import path from 'path'; import { execInSandboxAsync, provisionSandbox, startSandbox, stopSandbox, } from '@microsoft/mxc-sdk'; -import { createTempDir, probeStateAwareRuntime, safeDeprovision, sandboxSkipReason } from './test-helpers.js'; +import { probeStateAwareRuntime, safeDeprovision, sandboxSkipReason } from './test-helpers.js'; const skipReason = os.platform() !== 'win32' ? 'IsolationSession is Windows-only' @@ -39,6 +39,23 @@ describe('IsolationSession state-aware lifecycle E2E', { skip: skipReason }, () `Expected sandboxId to start with 'iso:', got '${sandboxId}'`, ); + // Provision now returns the agent SID and the shared ephemeral workspace + // path alongside the agent user name. + const metadata = provisionResult.metadata; + assert.ok(metadata, 'provision result carries metadata'); + assert.ok( + (metadata?.agentUserName?.length ?? 0) > 0, + `metadata.agentUserName is non-empty: ${JSON.stringify(metadata)}`, + ); + assert.ok( + (metadata?.agentUserSid?.length ?? 0) > 0, + `metadata.agentUserSid is non-empty: ${JSON.stringify(metadata)}`, + ); + assert.ok( + (metadata?.ephemeralWorkspacePath?.length ?? 0) > 0, + `metadata.ephemeralWorkspacePath is non-empty: ${JSON.stringify(metadata)}`, + ); + try { await startSandbox(sandboxId, {}, { experimental: true }); @@ -60,105 +77,68 @@ describe('IsolationSession state-aware lifecycle E2E', { skip: skipReason }, () } }); - it('exec surfaces a non-zero script exit as ExecResult.exitCode', async () => { + it('shares files with the session through the ephemeral workspace', async () => { const provisionResult = await provisionSandbox('isolation_session', {}, { experimental: true }); const sandboxId = provisionResult.sandboxId; - - try { - await startSandbox(sandboxId, {}, { experimental: true }); - - const result = await execInSandboxAsync( - sandboxId, - { process: { commandLine: 'cmd /c exit 7' } }, - { experimental: true }, - ); - - assert.strictEqual(result.exitCode, 7, `expected exit 7, got ${result.exitCode}`); - - await stopSandbox(sandboxId, undefined, { experimental: true }); - } finally { - await safeDeprovision(sandboxId); - } - }); - - it('honors readwritePaths at provision: agent writes are visible on the host', async () => { - const rwDir = createTempDir('mxc-iso-rw'); - const markerName = 'agent-write.txt'; - const markerHostPath = path.join(rwDir, markerName); - const markerExpected = 'agent-wrote-this'; - - const provisionResult = await provisionSandbox( - 'isolation_session', - { filesystem: { readwritePaths: [rwDir] } }, - { experimental: true }, + const workspace = provisionResult.metadata?.ephemeralWorkspacePath; + assert.ok( + workspace && workspace.length > 0, + `provision did not return an ephemeralWorkspacePath: ${JSON.stringify(provisionResult.metadata)}`, ); - const sandboxId = provisionResult.sandboxId; + // node:assert's `ok` does not narrow the type, so pin it explicitly for tsc. + const ws = workspace as string; try { await startSandbox(sandboxId, {}, { experimental: true }); - const result = await execInSandboxAsync( + // Caller -> session: the test (the calling user) stages a file into the + // shared workspace and the session reads it back. Proves the SDK surfaces + // a *usable* path a consumer can share files through, not just a non-empty + // string. + fs.writeFileSync(path.join(ws, 'caller_to_session.txt'), 'from-caller', 'ascii'); + const readResult = await execInSandboxAsync( sandboxId, - { process: { commandLine: `cmd /c echo ${markerExpected}> "${rwDir}\\${markerName}"` } }, + { process: { commandLine: `cmd /c type "${ws}\\caller_to_session.txt"` } }, { experimental: true }, ); + assert.strictEqual( + readResult.exitCode, + 0, + `read exit: stdout=${readResult.stdout}, stderr=${readResult.stderr}`, + ); + assert.ok( + readResult.stdout.includes('from-caller'), + `session did not see the caller's file: ${readResult.stdout}`, + ); - assert.strictEqual(result.exitCode, 0, `exec exit code: stdout=${result.stdout}, stderr=${result.stderr}`); - assert.ok(fs.existsSync(markerHostPath), `expected ${markerHostPath} to exist after agent write`); - assert.strictEqual(fs.readFileSync(markerHostPath, 'utf-8').trim(), markerExpected); - - await stopSandbox(sandboxId, undefined, { experimental: true }); - } finally { - await safeDeprovision(sandboxId); - fs.rmSync(rwDir, { recursive: true, force: true }); - } - }); - - it('honors readonlyPaths at provision: agent reads pre-seeded content', async () => { - const roDir = createTempDir('mxc-iso-ro'); - const markerName = 'host-seeded.txt'; - const markerHostPath = path.join(roDir, markerName); - const markerExpected = 'host-seeded-content'; - fs.writeFileSync(markerHostPath, markerExpected, 'utf-8'); - - const provisionResult = await provisionSandbox( - 'isolation_session', - { filesystem: { readonlyPaths: [roDir] } }, - { experimental: true }, - ); - const sandboxId = provisionResult.sandboxId; - - try { - await startSandbox(sandboxId, {}, { experimental: true }); - - const result = await execInSandboxAsync( + // Session -> caller: the session writes into its workspace and the caller + // reads it back on the host. + const writeResult = await execInSandboxAsync( sandboxId, - { process: { commandLine: `cmd /c type "${roDir}\\${markerName}"` } }, + { process: { commandLine: `cmd /c echo from-session> "${ws}\\session_to_caller.txt"` } }, { experimental: true }, ); - - assert.strictEqual(result.exitCode, 0, `exec exit code: stdout=${result.stdout}, stderr=${result.stderr}`); - assert.ok( - result.stdout.includes(markerExpected), - `stdout did not contain seeded content '${markerExpected}': ${result.stdout}`, + assert.strictEqual( + writeResult.exitCode, + 0, + `write exit: stdout=${writeResult.stdout}, stderr=${writeResult.stderr}`, + ); + const backFile = path.join(ws, 'session_to_caller.txt'); + assert.ok(fs.existsSync(backFile), 'caller does not see the file the session wrote'); + assert.match( + fs.readFileSync(backFile, 'ascii'), + /from-session/, + 'caller could not read the session output', ); await stopSandbox(sandboxId, undefined, { experimental: true }); } finally { await safeDeprovision(sandboxId); - fs.rmSync(roDir, { recursive: true, force: true }); } }); - it('honors readonlyPaths at provision: agent writes to a readonly path fail', async () => { - const roDir = createTempDir('mxc-iso-ro-write'); - const markerName = 'agent-should-not-write.txt'; - - const provisionResult = await provisionSandbox( - 'isolation_session', - { filesystem: { readonlyPaths: [roDir] } }, - { experimental: true }, - ); + it('exec surfaces a non-zero script exit as ExecResult.exitCode', async () => { + const provisionResult = await provisionSandbox('isolation_session', {}, { experimental: true }); const sandboxId = provisionResult.sandboxId; try { @@ -166,19 +146,15 @@ describe('IsolationSession state-aware lifecycle E2E', { skip: skipReason }, () const result = await execInSandboxAsync( sandboxId, - { process: { commandLine: `cmd /c echo nope> "${roDir}\\${markerName}"` } }, + { process: { commandLine: 'cmd /c exit 7' } }, { experimental: true }, ); - assert.notStrictEqual( - result.exitCode, 0, - `expected non-zero exit when writing to a readonly path, got ${result.exitCode}; stdout=${result.stdout}, stderr=${result.stderr}`, - ); + assert.strictEqual(result.exitCode, 7, `expected exit 7, got ${result.exitCode}`); await stopSandbox(sandboxId, undefined, { experimental: true }); } finally { await safeDeprovision(sandboxId); - fs.rmSync(roDir, { recursive: true, force: true }); } }); }); diff --git a/sdk/node/tests/unit/platform.test.ts b/sdk/node/tests/unit/platform.test.ts index bcb36c9b4..8dd69657d 100644 --- a/sdk/node/tests/unit/platform.test.ts +++ b/sdk/node/tests/unit/platform.test.ts @@ -9,7 +9,6 @@ import { getPlatformSupport, _resetPlatformSupportCache, _setProbeRunner, - _setWindowsBuildQuery, findWxcExecutable, } from '../../src/platform.js'; @@ -324,57 +323,62 @@ describe('findWxcExecutable failure modes', () => { }); }); -// IsolationSession availability is gated on Windows Insider Preview build -// 26300.8553+. These tests stub the build-query seam so the gate can be -// exercised deterministically without depending on the host's actual build. +// IsolationSession availability is now reported by the native probe +// (`wxc-exec --probe` -> probes.isolationSessionAvailable). These tests stub +// the probe runner so the gate can be exercised deterministically without +// depending on the host's actual build. describe('isolation_session availability gate', () => { beforeEach(() => { _resetPlatformSupportCache(); }); afterEach(() => { - _setWindowsBuildQuery(null); + _setProbeRunner(null); _resetPlatformSupportCache(); }); - it('omits isolation_session when minor build is below 8553', { skip: !isWindows }, () => { - _setWindowsBuildQuery(() => ({ major: 26300, minor: 8552 })); + it('includes isolation_session when the probe reports it available', { skip: !isWindows }, () => { + _setProbeRunner(() => + JSON.stringify({ tier: 'base-container', probes: { isolationSessionAvailable: true } }), + ); const support = getPlatformSupport(); assert.ok(support.isSupported, 'Windows is supported regardless of iso gate'); assert.ok( - !support.availableMethods.includes('isolation_session'), - `expected isolation_session absent, got: ${support.availableMethods.join(',')}`, + support.availableMethods.includes('isolation_session'), + `expected isolation_session present, got: ${support.availableMethods.join(',')}`, ); }); - it('includes isolation_session when build is exactly 26300.8553', { skip: !isWindows }, () => { - _setWindowsBuildQuery(() => ({ major: 26300, minor: 8553 })); - const support = getPlatformSupport(); - assert.ok(support.availableMethods.includes('isolation_session')); - }); - - it('includes isolation_session when minor is newer (26300.9999)', { skip: !isWindows }, () => { - _setWindowsBuildQuery(() => ({ major: 26300, minor: 9999 })); + it('omits isolation_session when the probe reports it unavailable', { skip: !isWindows }, () => { + _setProbeRunner(() => + JSON.stringify({ tier: 'base-container', probes: { isolationSessionAvailable: false } }), + ); const support = getPlatformSupport(); - assert.ok(support.availableMethods.includes('isolation_session')); + assert.ok( + !support.availableMethods.includes('isolation_session'), + `expected isolation_session absent, got: ${support.availableMethods.join(',')}`, + ); }); - it('omits isolation_session when major is newer than 26300 (gate is pinned to the Insider Preview)', { skip: !isWindows }, () => { - _setWindowsBuildQuery(() => ({ major: 26400, minor: 0 })); + it('omits isolation_session when the probes block omits the field', { skip: !isWindows }, () => { + _setProbeRunner(() => JSON.stringify({ tier: 'base-container', probes: {} })); const support = getPlatformSupport(); assert.ok(!support.availableMethods.includes('isolation_session')); }); - it('omits isolation_session when the registry query returns null', { skip: !isWindows }, () => { - _setWindowsBuildQuery(() => null); + it('omits isolation_session when the probe fails', { skip: !isWindows }, () => { + _setProbeRunner(() => { + throw new Error('probe failed'); + }); const support = getPlatformSupport(); + assert.ok(support.isSupported, 'Windows support is independent of the probe'); assert.ok(!support.availableMethods.includes('isolation_session')); }); it('always reports processcontainer as the default on Windows (no build gate)', { skip: !isWindows }, () => { - // Even on a hypothetical sub-24H2 build the SDK now reports support; - // the runtime gate has moved into the native binary. - _setWindowsBuildQuery(() => ({ major: 22000, minor: 0 })); + // The runtime gate lives in the native binary; the SDK reports Windows + // support regardless of isolation-session availability. + _setProbeRunner(() => JSON.stringify({ probes: { isolationSessionAvailable: false } })); const support = getPlatformSupport(); assert.ok(support.isSupported); assert.strictEqual(support.availableMethods[0], 'processcontainer'); diff --git a/sdk/node/tests/unit/state-aware-types.test.ts b/sdk/node/tests/unit/state-aware-types.test.ts index f5f8e63f4..be0fa3bab 100644 --- a/sdk/node/tests/unit/state-aware-types.test.ts +++ b/sdk/node/tests/unit/state-aware-types.test.ts @@ -39,12 +39,13 @@ describe('SandboxId brand', () => { }); describe('IsolationSessionProvisionConfig', () => { - it('accepts version and filesystem', () => { + it('accepts version and rejects filesystem', () => { const cfg: IsolationSessionProvisionConfig = { version: '0.6.0-alpha', + // @ts-expect-error — filesystem is rejected at provision; the backend has no host-folder-sharing primitive. filesystem: { readwritePaths: ['C:\\workspace'] }, }; - assert.deepStrictEqual(cfg.filesystem?.readwritePaths, ['C:\\workspace']); + assert.ok(cfg); }); it('rejects network and ui until those features land Rust-side', () => { @@ -82,20 +83,16 @@ describe('IsolationSessionStartConfig', () => { assert.ok(cfg); }); - it('accepts configurationId only from the closed enum', () => { - const ok: IsolationSessionStartConfig = { configurationId: 'composable' }; - assert.strictEqual(ok.configurationId, 'composable'); - - const bogus: IsolationSessionStartConfig = { - // @ts-expect-error — configurationId must be in the closed enum. - configurationId: 'xlarge', + it('rejects configurationId (sizing profiles were removed)', () => { + const cfg: IsolationSessionStartConfig = { + // @ts-expect-error — configurationId is no longer part of the start config. + configurationId: 'composable', }; - assert.ok(bogus); + assert.ok(cfg); }); it('accepts user only as an IsolationSessionUserConfig instance', () => { const ok: IsolationSessionStartConfig = { - configurationId: 'composable', user: new IsolationSessionUserConfig('alice@contoso.com', 'tok'), }; const bare: IsolationSessionStartConfig = { @@ -169,8 +166,14 @@ describe('ProvisionResult', () => { it('carries backend-typed metadata for isolation_session', () => { const result: ProvisionResult<'isolation_session'> = { sandboxId: 'iso:abcd' as SandboxId<'isolation_session'>, - metadata: { agentUserName: 'iso\\agent' }, + metadata: { + agentUserName: 'iso\\agent', + agentUserSid: 'S-1-5-21-1001', + ephemeralWorkspacePath: 'C:\\ProgramData\\ws', + }, }; assert.strictEqual(result.metadata?.agentUserName, 'iso\\agent'); + assert.strictEqual(result.metadata?.agentUserSid, 'S-1-5-21-1001'); + assert.strictEqual(result.metadata?.ephemeralWorkspacePath, 'C:\\ProgramData\\ws'); }); }); diff --git a/sdk/node/tests/unit/state-aware.test.ts b/sdk/node/tests/unit/state-aware.test.ts index 48d9f88a4..06e18025b 100644 --- a/sdk/node/tests/unit/state-aware.test.ts +++ b/sdk/node/tests/unit/state-aware.test.ts @@ -42,18 +42,15 @@ describe('buildStateAwareEnvelope', () => { assert.strictEqual(env.sandboxId, undefined); }); - it('produces a start envelope with backend-specific configurationId nested under experimental', () => { + it('produces a start envelope with no experimental block when no backend config is supplied', () => { const env = buildStateAwareEnvelope({ phase: 'start', backendKey: 'isolation_session', sandboxId: 'iso:reg-abc:prov-123', - config: { configurationId: 'small' }, }); assert.strictEqual(env.phase, 'start'); assert.strictEqual(env.sandboxId, 'iso:reg-abc:prov-123'); - assert.deepStrictEqual(env.experimental, { - isolation_session: { start: { configurationId: 'small' } }, - }); + assert.strictEqual(env.experimental, undefined); }); it('produces an exec envelope with process at top-level and no experimental block', () => { @@ -107,13 +104,12 @@ describe('buildStateAwareEnvelope', () => { }); }); - it('nests start user under experimental.isolation_session.start alongside configurationId', () => { + it('nests start user under experimental.isolation_session.start', () => { const env = buildStateAwareEnvelope({ phase: 'start', backendKey: 'isolation_session', sandboxId: 'iso:alice@contoso.com', config: { - configurationId: 'composable', user: new IsolationSessionUserConfig('alice@contoso.com', 'tok'), }, }); @@ -121,7 +117,6 @@ describe('buildStateAwareEnvelope', () => { assert.deepStrictEqual(wire.experimental, { isolation_session: { start: { - configurationId: 'composable', user: { upn: 'alice@contoso.com', wamToken: 'tok' }, }, }, @@ -207,21 +202,22 @@ describe('provisionSandbox', { skip: platformSkip }, () => { it('builds a provision envelope and unwraps the SandboxId from the response', async () => { const fake = fakeSpawn({ - stdout: '{"result":{"sandboxId":"iso:reg-abc:prov-1","metadata":{"agentUserName":"agent\\\\u1"}}}', + stdout: '{"result":{"sandboxId":"iso:reg-abc:prov-1","metadata":{"agentUserName":"agent\\\\u1","agentUserSid":"S-1-5-21-1001","ephemeralWorkspacePath":"C:\\\\ProgramData\\\\ws"}}}', exitCode: 0, }); activeFake = fake; _setSpawnImpl(fake.spawn); const result = await provisionSandbox( 'isolation_session', - { filesystem: { readwritePaths: ['C:\\workspace'] } }, + { user: new IsolationSessionUserConfig('alice@contoso.com', 'tok') }, testOptions(), ); assert.strictEqual(result.sandboxId, 'iso:reg-abc:prov-1'); assert.strictEqual(result.metadata?.agentUserName, 'agent\\u1'); + assert.strictEqual(result.metadata?.agentUserSid, 'S-1-5-21-1001'); + assert.strictEqual(result.metadata?.ephemeralWorkspacePath, 'C:\\ProgramData\\ws'); assert.strictEqual(fake.captured.envelope?.phase, 'provision'); assert.strictEqual(fake.captured.envelope?.containment, 'isolation_session'); - assert.deepStrictEqual(fake.captured.envelope?.filesystem, { readwritePaths: ['C:\\workspace'] }); assert.ok(fake.captured.args?.includes('--experimental')); }); @@ -270,15 +266,20 @@ describe('provisionSandbox', { skip: platformSkip }, () => { describe('startSandbox', { skip: platformSkip }, () => { afterEach(() => { _resetSpawnImpl(); }); - it('infers backend from sandboxId prefix and nests configurationId under experimental', async () => { + it('infers backend from sandboxId prefix and nests start user under experimental', async () => { const fake = fakeSpawn({ stdout: '{"result":{}}', exitCode: 0 }); _setSpawnImpl(fake.spawn); const id = 'iso:reg-abc:prov-1' as SandboxId<'isolation_session'>; - await startSandbox(id, { configurationId: 'small' }, testOptions()); + await startSandbox( + id, + { user: new IsolationSessionUserConfig('alice@contoso.com', 'tok') }, + testOptions(), + ); assert.strictEqual(fake.captured.envelope?.phase, 'start'); assert.strictEqual(fake.captured.envelope?.sandboxId, 'iso:reg-abc:prov-1'); - assert.deepStrictEqual(fake.captured.envelope?.experimental, { - isolation_session: { start: { configurationId: 'small' } }, + const wire = JSON.parse(JSON.stringify(fake.captured.envelope)); + assert.deepStrictEqual(wire.experimental, { + isolation_session: { start: { user: { upn: 'alice@contoso.com', wamToken: 'tok' } } }, }); }); diff --git a/sdk/node/tests/unit/wire-conformance-state-aware.test.ts b/sdk/node/tests/unit/wire-conformance-state-aware.test.ts index d9f8e2b7a..118e540ba 100644 --- a/sdk/node/tests/unit/wire-conformance-state-aware.test.ts +++ b/sdk/node/tests/unit/wire-conformance-state-aware.test.ts @@ -7,9 +7,9 @@ // `sdk/src/types.ts` conforms to the generated wire types. This companion does // the same for the STATE-AWARE lifecycle public types in // `sdk/src/state-aware-types.ts`, against the generated wire state-aware defs -// (`Phase`, `IsolationConfigurationId`, `IsolationUser`, `IsolationSessionPhase`). -// Without it, a wire-model change to the state-aware surface — a new sizing -// profile, a field added to the Entra user bundle, a `Phase` change — would +// (`Phase`, `IsolationUser`, `IsolationSessionPhase`). +// Without it, a wire-model change to the state-aware surface — a field added to +// the Entra user bundle, a `Phase` change — would // regenerate `wire.ts`, pass the codegen gate, and still leave the SDK silently // lagging with no CI signal. // @@ -20,22 +20,19 @@ // public field wire location // ------------------------------------ -------------------------------------- // *Config.version top-level `version` (SDK fills default) -// ProvisionConfig.filesystem top-level `Filesystem` // ExecConfig.process top-level `Process` -// StartConfig.configurationId IsolationSessionPhase.configurationId // {Provision,Start}Config.user IsolationSessionPhase.user / IsolationUser // // The top-level fields are already covered by the one-shot oracle; here we (a) // assert the per-phase configs REUSE those same public leaf types (so the // delegation is real, not a re-derived shape that could escape the one-shot // oracle), and (b) directly check the genuinely state-aware shapes (the phase -// enum, the sizing-profile enum, the user bundle, and the `IsolationSessionPhase` -// field set). The runtime body is a no-op; the guarantee is enforced at `tsc` -// time. +// enum, the user bundle, and the `IsolationSessionPhase` field set). The runtime +// body is a no-op; the guarantee is enforced at `tsc` time. import { test } from 'node:test'; -import type { ProcessConfig, FilesystemConfig } from '../../src/types.js'; +import type { ProcessConfig } from '../../src/types.js'; import type { Phase, @@ -50,7 +47,6 @@ import type { import type { Phase as WirePhase, IsolationUser as WireIsolationUser, - IsolationConfigurationId as WireIsolationConfigurationId, IsolationSessionPhase as WireIsolationSessionPhase, } from '../../src/generated/wire.js'; @@ -85,8 +81,8 @@ type _UserBundlePublicKeys = AssertTrue, never>>; // Matching public/wire phase field names must also carry matching value types. type _PhaseFieldValueTypes = AssertTrue>; -// Sizing profile remains named explicitly because it is an important SDK-facing -// enum, but the broad value-type guard above is the primary drift check. -type _ConfigurationId = AssertTrue, WireIsolationConfigurationId>>; // Phases that accept a user bundle must reuse the same public type. type _PhaseUserBundleReuse = AssertTrue, IsolationSessionUserConfig>>; @@ -143,9 +136,6 @@ type _PhaseUserBundleReuse = AssertTrue // re-declared an inline shape instead, it would escape that coverage — these // assertions fail if that ever happens. type _ExecProcessReuse = AssertTrue>; -type _ProvisionFilesystemReuse = AssertTrue< - Equivalent, FilesystemConfig> ->; // Reference the assertion aliases so they read as intentionally load-bearing. export type StateAwareWireConformanceAssertions = [ @@ -156,10 +146,8 @@ export type StateAwareWireConformanceAssertions = [ _PhaseWireKeys, _PhasePublicKeys, _PhaseFieldValueTypes, - _ConfigurationId, _PhaseUserBundleReuse, _ExecProcessReuse, - _ProvisionFilesystemReuse, ]; test('public state-aware SDK types conform to the generated wire schema (compile-time)', () => { diff --git a/src/Cargo.lock b/src/Cargo.lock index b05d4dbe5..5ec05dd03 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1288,6 +1288,7 @@ version = "0.7.0" dependencies = [ "isolation_session_bindings", "serde", + "serde_json", "windows", "windows-collections", "windows-core", diff --git a/src/backends/appcontainer/common/src/probe.rs b/src/backends/appcontainer/common/src/probe.rs index af60926ce..3d6850e09 100644 --- a/src/backends/appcontainer/common/src/probe.rs +++ b/src/backends/appcontainer/common/src/probe.rs @@ -65,6 +65,11 @@ pub struct ProbeFacts { /// Tier 3 (AppContainer + DACL) enforces `deniedPaths` via DENY ACEs /// regardless of this bit; it is meaningful only for the BaseContainer tier. pub base_container_supports_deny_paths: bool, + /// Whether the in-proc IsolationSession service can be activated on this + /// host. Always `false` here — `appcontainer_common` has no dependency on + /// the isolation-session backend; `wxc-exec --probe` overrides it when + /// that backend is compiled in. + pub isolation_session_available: bool, /// Platform-agnostic UI restrictions this host can enforce. pub ui_capabilities: UiCapabilitySupport, } @@ -126,6 +131,7 @@ pub fn run_probe(policy: &ContainerPolicy) -> ProbeOutput { bfs_compiled_in: cfg!(feature = "tier2_bfs"), base_container_supports_deny_paths: crate::base_container_runner::BaseContainerRunner::base_container_supports_deny_paths(), + isolation_session_available: false, ui_capabilities: crate::job_object::supported_ui_restrictions().into(), }; match fallback_detector::detect(policy, /* prefer_base_container */ true) { @@ -201,6 +207,7 @@ mod tests { bfscfg_present: false, bfs_compiled_in: false, base_container_supports_deny_paths: false, + isolation_session_available: true, ui_capabilities: all_ui_capabilities(), }, error: None, @@ -213,6 +220,7 @@ mod tests { assert_eq!(v["probes"]["baseContainerApiPresent"], true); assert_eq!(v["probes"]["bfscfgPresent"], false); assert_eq!(v["probes"]["bfsCompiledIn"], false); + assert_eq!(v["probes"]["isolationSessionAvailable"], true); assert_eq!(v["probes"]["uiCapabilities"]["canBlockClipboardRead"], true); assert_eq!( v["probes"]["uiCapabilities"]["canBlockInputInjection"], @@ -236,6 +244,7 @@ mod tests { bfscfg_present: false, bfs_compiled_in: false, base_container_supports_deny_paths: false, + isolation_session_available: false, ui_capabilities: UiCapabilitySupport { can_block_input_injection: false, can_block_input_method_changes: false, @@ -314,4 +323,17 @@ mod tests { assert!(obj.contains_key("warnings")); assert!(obj.contains_key("probes")); } + + #[test] + fn probe_always_emits_isolation_session_available() { + // The SDK's isolation-session gate reads this non-optional field, so + // it must always serialize (never omitted), even when false. + let out = run_probe(&ContainerPolicy::default()); + let v = serde_json::to_value(&out).expect("to_value"); + let probes = v["probes"].as_object().expect("probes object"); + assert!( + probes.contains_key("isolationSessionAvailable"), + "isolationSessionAvailable must always be present, got: {v}" + ); + } } diff --git a/src/backends/isolation_session/bindings/Cargo.toml b/src/backends/isolation_session/bindings/Cargo.toml index ed4ccee0c..b61268d0c 100644 --- a/src/backends/isolation_session/bindings/Cargo.toml +++ b/src/backends/isolation_session/bindings/Cargo.toml @@ -11,7 +11,7 @@ windows-collections = "0.3" windows-future = "0.3" [target.'cfg(target_os = "windows")'.dependencies] -windows = { workspace = true } +windows = { workspace = true, features = ["Foundation"] } [build-dependencies] semver = "1" diff --git a/src/backends/isolation_session/bindings/build.rs b/src/backends/isolation_session/bindings/build.rs index 8412f2280..f377a0644 100644 --- a/src/backends/isolation_session/bindings/build.rs +++ b/src/backends/isolation_session/bindings/build.rs @@ -5,9 +5,13 @@ //! the version the generated bindings were produced for. fn main() { - // Path to the generation provenance file. + // Path to the generation provenance file. This crate lives at + // /src/backends/isolation_session/bindings, so four `..` segments + // walk back up to the repo root, where `external/` lives. let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let info_path = std::path::Path::new(&manifest_dir) + .join("..") + .join("..") .join("..") .join("..") .join("external") @@ -40,8 +44,11 @@ fn main() { return; }; - // Read the actual windows crate version from Cargo.lock. + // Read the actual windows crate version from Cargo.lock. The workspace root + // (and its Cargo.lock) is at /src, three `..` up from this crate. let lock_path = std::path::Path::new(&manifest_dir) + .join("..") + .join("..") .join("..") .join("Cargo.lock"); diff --git a/src/backends/isolation_session/bindings/src/bindings.rs b/src/backends/isolation_session/bindings/src/bindings.rs index 0e6d1209a..cec1dd581 100644 --- a/src/backends/isolation_session/bindings/src/bindings.rs +++ b/src/backends/isolation_session/bindings/src/bindings.rs @@ -9,123 +9,204 @@ )] windows_core::imp::define_interface!( - IClosable, - IClosable_Vtbl, - 0x30d5a829_7fa4_4026_83bb_d75bae4ea99e + IIsoSessionError, + IIsoSessionError_Vtbl, + 0x39e104da_6f4b_5fe8_a63f_f28fd8c3d606 ); -impl windows_core::RuntimeType for IClosable { +impl windows_core::RuntimeType for IIsoSessionError { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::for_interface::(); } -windows_core::imp::interface_hierarchy!( - IClosable, - windows_core::IUnknown, - windows_core::IInspectable -); -impl IClosable { - pub fn Close(&self) -> windows_core::Result<()> { - let this = self; - unsafe { - (windows_core::Interface::vtable(this).Close)(windows_core::Interface::as_raw(this)) - .ok() - } - } -} -impl windows_core::RuntimeName for IClosable { - const NAME: &'static str = "Windows.Foundation.IClosable"; +impl windows_core::RuntimeName for IIsoSessionError { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionError"; } -pub trait IClosable_Impl: windows_core::IUnknownImpl { - fn Close(&self) -> windows_core::Result<()>; +pub trait IIsoSessionError_Impl: windows_core::IUnknownImpl { + fn Code(&self) -> windows_core::Result; + fn IsError(&self) -> windows_core::Result; + fn Message(&self) -> windows_core::Result; + fn Remediation(&self) -> windows_core::Result; } -impl IClosable_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn Close( +impl IIsoSessionError_Vtbl { + pub const fn new() -> Self { + unsafe extern "system" fn Code( + this: *mut core::ffi::c_void, + result__: *mut windows_core::HRESULT, + ) -> windows_core::HRESULT { + unsafe { + let this: &Identity = + &*((this as *const *const ()).offset(OFFSET) as *const Identity); + match IIsoSessionError_Impl::Code(this) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } + } + } + unsafe extern "system" fn IsError( + this: *mut core::ffi::c_void, + result__: *mut bool, + ) -> windows_core::HRESULT { + unsafe { + let this: &Identity = + &*((this as *const *const ()).offset(OFFSET) as *const Identity); + match IIsoSessionError_Impl::IsError(this) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } + } + } + unsafe extern "system" fn Message( + this: *mut core::ffi::c_void, + result__: *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT { + unsafe { + let this: &Identity = + &*((this as *const *const ()).offset(OFFSET) as *const Identity); + match IIsoSessionError_Impl::Message(this) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + core::mem::forget(ok__); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } + } + } + unsafe extern "system" fn Remediation< + Identity: IIsoSessionError_Impl, + const OFFSET: isize, + >( this: *mut core::ffi::c_void, + result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IClosable_Impl::Close(this).into() + match IIsoSessionError_Impl::Remediation(this) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + core::mem::forget(ok__); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } } } Self { - base__: windows_core::IInspectable_Vtbl::new::(), - Close: Close::, + base__: windows_core::IInspectable_Vtbl::new::(), + Code: Code::, + IsError: IsError::, + Message: Message::, + Remediation: Remediation::, } } pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID + iid == &::IID } } #[repr(C)] #[doc(hidden)] -pub struct IClosable_Vtbl { +pub struct IIsoSessionError_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - pub Close: unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, + pub Code: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut windows_core::HRESULT, + ) -> windows_core::HRESULT, + pub IsError: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, + pub Message: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub Remediation: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, } windows_core::imp::define_interface!( - IIsoSessionApp, - IIsoSessionApp_Vtbl, - 0x1e5ddb96_ca2d_572f_8f5d_34d9773712c6 + IIsoSessionOps, + IIsoSessionOps_Vtbl, + 0xabe3e450_1dd6_5c8f_88d1_f5522b785b96 ); -impl windows_core::RuntimeType for IIsoSessionApp { +impl windows_core::RuntimeType for IIsoSessionOps { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::for_interface::(); } -impl windows_core::RuntimeName for IIsoSessionApp { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionApp"; +impl windows_core::RuntimeName for IIsoSessionOps { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionOps"; } -pub trait IIsoSessionApp_Impl: windows_core::IUnknownImpl { - fn AgentName(&self) -> windows_core::Result; - fn AgentUserName(&self) -> windows_core::Result; - fn IsConnected(&self) -> windows_core::Result; - fn ConnectAsync( +pub trait IIsoSessionOps_Impl: windows_core::IUnknownImpl { + fn BehaviorVersion(&self) -> windows_core::Result; + fn AddUserAsync( &self, - config: windows_core::Ref<'_, IsoSessionAppConfig>, - ) -> windows_core::Result>; - fn LaunchProcessAsync( + optEntraAccountName: &windows_core::HSTRING, + optWamToken: &windows_core::HSTRING, + ) -> windows_core::Result>; + fn GetFeatureLevel(&self, feature: IsoSessionFeature) -> windows_core::Result; + fn RemoveUserAsync( &self, - processPath: &windows_core::HSTRING, - arguments: &windows_core::HSTRING, - ) -> windows_core::Result>; - fn LaunchProcessWithOptionsAsync( + agentUserName: &windows_core::HSTRING, + ) -> windows_core::Result>; + fn RunProcessWithOptionsAsync( &self, + agentUserName: &windows_core::HSTRING, processPath: &windows_core::HSTRING, arguments: &windows_core::HSTRING, options: windows_core::Ref<'_, IsoSessionProcessOptions>, ) -> windows_core::Result>; - fn TeardownAsync(&self) -> windows_core::Result; + fn StartSessionAsync( + &self, + agentUserName: &windows_core::HSTRING, + optWamToken: &windows_core::HSTRING, + ) -> windows_core::Result>; + fn StopSessionAsync( + &self, + agentUserName: &windows_core::HSTRING, + ) -> windows_core::Result>; } -impl IIsoSessionApp_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn AgentName( +impl IIsoSessionOps_Vtbl { + pub const fn new() -> Self { + unsafe extern "system" fn BehaviorVersion< + Identity: IIsoSessionOps_Impl, + const OFFSET: isize, + >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + result__: *mut i32, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionApp_Impl::AgentName(this) { + match IIsoSessionOps_Impl::BehaviorVersion(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); windows_core::HRESULT(0) } Err(err) => err.into(), } } } - unsafe extern "system" fn AgentUserName< - Identity: IIsoSessionApp_Impl, + unsafe extern "system" fn AddUserAsync< + Identity: IIsoSessionOps_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, + optentraaccountname: *mut core::ffi::c_void, + optwamtoken: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionApp_Impl::AgentUserName(this) { + match IIsoSessionOps_Impl::AddUserAsync( + this, + core::mem::transmute(&optentraaccountname), + core::mem::transmute(&optwamtoken), + ) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -135,17 +216,18 @@ impl IIsoSessionApp_Vtbl { } } } - unsafe extern "system" fn IsConnected< - Identity: IIsoSessionApp_Impl, + unsafe extern "system" fn GetFeatureLevel< + Identity: IIsoSessionOps_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut bool, + feature: IsoSessionFeature, + result__: *mut i32, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionApp_Impl::IsConnected(this) { + match IIsoSessionOps_Impl::GetFeatureLevel(this, feature) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); windows_core::HRESULT(0) @@ -154,18 +236,21 @@ impl IIsoSessionApp_Vtbl { } } } - unsafe extern "system" fn ConnectAsync< - Identity: IIsoSessionApp_Impl, + unsafe extern "system" fn RemoveUserAsync< + Identity: IIsoSessionOps_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - config: *mut core::ffi::c_void, + agentusername: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionApp_Impl::ConnectAsync(this, core::mem::transmute_copy(&config)) { + match IIsoSessionOps_Impl::RemoveUserAsync( + this, + core::mem::transmute(&agentusername), + ) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -175,22 +260,26 @@ impl IIsoSessionApp_Vtbl { } } } - unsafe extern "system" fn LaunchProcessAsync< - Identity: IIsoSessionApp_Impl, + unsafe extern "system" fn RunProcessWithOptionsAsync< + Identity: IIsoSessionOps_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, + agentusername: *mut core::ffi::c_void, processpath: *mut core::ffi::c_void, arguments: *mut core::ffi::c_void, + options: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionApp_Impl::LaunchProcessAsync( + match IIsoSessionOps_Impl::RunProcessWithOptionsAsync( this, + core::mem::transmute(&agentusername), core::mem::transmute(&processpath), core::mem::transmute(&arguments), + core::mem::transmute_copy(&options), ) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); @@ -201,24 +290,22 @@ impl IIsoSessionApp_Vtbl { } } } - unsafe extern "system" fn LaunchProcessWithOptionsAsync< - Identity: IIsoSessionApp_Impl, + unsafe extern "system" fn StartSessionAsync< + Identity: IIsoSessionOps_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - processpath: *mut core::ffi::c_void, - arguments: *mut core::ffi::c_void, - options: *mut core::ffi::c_void, + agentusername: *mut core::ffi::c_void, + optwamtoken: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionApp_Impl::LaunchProcessWithOptionsAsync( + match IIsoSessionOps_Impl::StartSessionAsync( this, - core::mem::transmute(&processpath), - core::mem::transmute(&arguments), - core::mem::transmute_copy(&options), + core::mem::transmute(&agentusername), + core::mem::transmute(&optwamtoken), ) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); @@ -229,17 +316,21 @@ impl IIsoSessionApp_Vtbl { } } } - unsafe extern "system" fn TeardownAsync< - Identity: IIsoSessionApp_Impl, + unsafe extern "system" fn StopSessionAsync< + Identity: IIsoSessionOps_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, + agentusername: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionApp_Impl::TeardownAsync(this) { + match IIsoSessionOps_Impl::StopSessionAsync( + this, + core::mem::transmute(&agentusername), + ) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -250,88 +341,98 @@ impl IIsoSessionApp_Vtbl { } } Self { - base__: windows_core::IInspectable_Vtbl::new::(), - AgentName: AgentName::, - AgentUserName: AgentUserName::, - IsConnected: IsConnected::, - ConnectAsync: ConnectAsync::, - LaunchProcessAsync: LaunchProcessAsync::, - LaunchProcessWithOptionsAsync: LaunchProcessWithOptionsAsync::, - TeardownAsync: TeardownAsync::, + base__: windows_core::IInspectable_Vtbl::new::(), + BehaviorVersion: BehaviorVersion::, + AddUserAsync: AddUserAsync::, + GetFeatureLevel: GetFeatureLevel::, + RemoveUserAsync: RemoveUserAsync::, + RunProcessWithOptionsAsync: RunProcessWithOptionsAsync::, + StartSessionAsync: StartSessionAsync::, + StopSessionAsync: StopSessionAsync::, } } pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID + iid == &::IID } } #[repr(C)] #[doc(hidden)] -pub struct IIsoSessionApp_Vtbl { +pub struct IIsoSessionOps_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - pub AgentName: unsafe extern "system" fn( + pub BehaviorVersion: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut i32) -> windows_core::HRESULT, + pub AddUserAsync: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, *mut core::ffi::c_void, *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, - pub AgentUserName: unsafe extern "system" fn( + pub GetFeatureLevel: unsafe extern "system" fn( *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, + IsoSessionFeature, + *mut i32, ) -> windows_core::HRESULT, - pub IsConnected: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, - pub ConnectAsync: unsafe extern "system" fn( + pub RemoveUserAsync: unsafe extern "system" fn( *mut core::ffi::c_void, *mut core::ffi::c_void, *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, - pub LaunchProcessAsync: unsafe extern "system" fn( + pub RunProcessWithOptionsAsync: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, *mut core::ffi::c_void, *mut core::ffi::c_void, *mut core::ffi::c_void, *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, - pub LaunchProcessWithOptionsAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, + pub StartSessionAsync: unsafe extern "system" fn( *mut core::ffi::c_void, *mut core::ffi::c_void, *mut core::ffi::c_void, *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, - pub TeardownAsync: unsafe extern "system" fn( + pub StopSessionAsync: unsafe extern "system" fn( + *mut core::ffi::c_void, *mut core::ffi::c_void, *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } windows_core::imp::define_interface!( - IIsoSessionAppConfig, - IIsoSessionAppConfig_Vtbl, - 0xc2eec193_7e9c_59af_92d4_073b027273ef + IIsoSessionProcess, + IIsoSessionProcess_Vtbl, + 0x1cccc074_ce04_527c_8b2f_d367930af570 ); -impl windows_core::RuntimeType for IIsoSessionAppConfig { +impl windows_core::RuntimeType for IIsoSessionProcess { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::for_interface::(); } -impl windows_core::RuntimeName for IIsoSessionAppConfig { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionAppConfig"; +impl windows_core::RuntimeName for IIsoSessionProcess { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionProcess"; } -pub trait IIsoSessionAppConfig_Impl: windows_core::IUnknownImpl { - fn ConfigId(&self) -> windows_core::Result; - fn SetConfigId(&self, value: IsoSessionConfigId) -> windows_core::Result<()>; - fn RegistrationId(&self) -> windows_core::Result; - fn SetRegistrationId(&self, value: &windows_core::HSTRING) -> windows_core::Result<()>; +pub trait IIsoSessionProcess_Impl: windows_core::IUnknownImpl { + fn ErrorHandle(&self) -> windows_core::Result; + fn ExitCode(&self) -> windows_core::Result; + fn InputHandle(&self) -> windows_core::Result; + fn OutputHandle(&self) -> windows_core::Result; + fn CloseStandardInput(&self) -> windows_core::Result<()>; + fn ResizeConsole(&self, columns: u16, rows: u16) -> windows_core::Result<()>; + fn SendCtrlClose(&self) -> windows_core::Result<()>; + fn Terminate(&self) -> windows_core::Result<()>; + fn WaitForExit(&self, timeoutMs: u32) -> windows_core::Result; } -impl IIsoSessionAppConfig_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn ConfigId< - Identity: IIsoSessionAppConfig_Impl, +impl IIsoSessionProcess_Vtbl { + pub const fn new() -> Self { + unsafe extern "system" fn ErrorHandle< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut IsoSessionConfigId, + result__: *mut u64, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionAppConfig_Impl::ConfigId(this) { + match IIsoSessionProcess_Impl::ErrorHandle(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); windows_core::HRESULT(0) @@ -340,229 +441,204 @@ impl IIsoSessionAppConfig_Vtbl { } } } - unsafe extern "system" fn SetConfigId< - Identity: IIsoSessionAppConfig_Impl, + unsafe extern "system" fn ExitCode< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - value: IsoSessionConfigId, + result__: *mut i32, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionAppConfig_Impl::SetConfigId(this, value).into() + match IIsoSessionProcess_Impl::ExitCode(this) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } } } - unsafe extern "system" fn RegistrationId< - Identity: IIsoSessionAppConfig_Impl, + unsafe extern "system" fn InputHandle< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + result__: *mut u64, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionAppConfig_Impl::RegistrationId(this) { + match IIsoSessionProcess_Impl::InputHandle(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); windows_core::HRESULT(0) } Err(err) => err.into(), } } } - unsafe extern "system" fn SetRegistrationId< - Identity: IIsoSessionAppConfig_Impl, + unsafe extern "system" fn OutputHandle< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - value: *mut core::ffi::c_void, + result__: *mut u64, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionAppConfig_Impl::SetRegistrationId(this, core::mem::transmute(&value)) - .into() + match IIsoSessionProcess_Impl::OutputHandle(this) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } } } - Self { - base__: windows_core::IInspectable_Vtbl::new::( - ), - ConfigId: ConfigId::, - SetConfigId: SetConfigId::, - RegistrationId: RegistrationId::, - SetRegistrationId: SetRegistrationId::, + unsafe extern "system" fn CloseStandardInput< + Identity: IIsoSessionProcess_Impl, + const OFFSET: isize, + >( + this: *mut core::ffi::c_void, + ) -> windows_core::HRESULT { + unsafe { + let this: &Identity = + &*((this as *const *const ()).offset(OFFSET) as *const Identity); + IIsoSessionProcess_Impl::CloseStandardInput(this).into() + } } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionAppConfig_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub ConfigId: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut IsoSessionConfigId, - ) -> windows_core::HRESULT, - pub SetConfigId: unsafe extern "system" fn( - *mut core::ffi::c_void, - IsoSessionConfigId, - ) -> windows_core::HRESULT, - pub RegistrationId: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub SetRegistrationId: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionAppConfig2, - IIsoSessionAppConfig2_Vtbl, - 0x051c29de_ca75_5cdb_af2c_3012cf1a62ec -); -impl windows_core::RuntimeType for IIsoSessionAppConfig2 { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionAppConfig2 { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionAppConfig2"; -} -pub trait IIsoSessionAppConfig2_Impl: windows_core::IUnknownImpl { - fn ProvisionId(&self) -> windows_core::Result; - fn SetProvisionId(&self, value: &windows_core::HSTRING) -> windows_core::Result<()>; - fn WamToken(&self) -> windows_core::Result; - fn SetWamToken(&self, value: &windows_core::HSTRING) -> windows_core::Result<()>; -} -impl IIsoSessionAppConfig2_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn ProvisionId< - Identity: IIsoSessionAppConfig2_Impl, + unsafe extern "system" fn ResizeConsole< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + columns: u16, + rows: u16, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionAppConfig2_Impl::ProvisionId(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } + IIsoSessionProcess_Impl::ResizeConsole(this, columns, rows).into() } } - unsafe extern "system" fn SetProvisionId< - Identity: IIsoSessionAppConfig2_Impl, + unsafe extern "system" fn SendCtrlClose< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - value: *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionAppConfig2_Impl::SetProvisionId(this, core::mem::transmute(&value)) - .into() + IIsoSessionProcess_Impl::SendCtrlClose(this).into() } } - unsafe extern "system" fn WamToken< - Identity: IIsoSessionAppConfig2_Impl, + unsafe extern "system" fn Terminate< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionAppConfig2_Impl::WamToken(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } + IIsoSessionProcess_Impl::Terminate(this).into() } } - unsafe extern "system" fn SetWamToken< - Identity: IIsoSessionAppConfig2_Impl, + unsafe extern "system" fn WaitForExit< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - value: *mut core::ffi::c_void, + timeoutms: u32, + result__: *mut i32, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionAppConfig2_Impl::SetWamToken(this, core::mem::transmute(&value)).into() + match IIsoSessionProcess_Impl::WaitForExit(this, timeoutms) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } } } Self { - base__: windows_core::IInspectable_Vtbl::new::( - ), - ProvisionId: ProvisionId::, - SetProvisionId: SetProvisionId::, - WamToken: WamToken::, - SetWamToken: SetWamToken::, + base__: windows_core::IInspectable_Vtbl::new::(), + ErrorHandle: ErrorHandle::, + ExitCode: ExitCode::, + InputHandle: InputHandle::, + OutputHandle: OutputHandle::, + CloseStandardInput: CloseStandardInput::, + ResizeConsole: ResizeConsole::, + SendCtrlClose: SendCtrlClose::, + Terminate: Terminate::, + WaitForExit: WaitForExit::, } } pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID + iid == &::IID } } #[repr(C)] #[doc(hidden)] -pub struct IIsoSessionAppConfig2_Vtbl { +pub struct IIsoSessionProcess_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - pub ProvisionId: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub SetProvisionId: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub WamToken: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub SetWamToken: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - ) -> windows_core::HRESULT, + pub ErrorHandle: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut u64) -> windows_core::HRESULT, + pub ExitCode: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut i32) -> windows_core::HRESULT, + pub InputHandle: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut u64) -> windows_core::HRESULT, + pub OutputHandle: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut u64) -> windows_core::HRESULT, + pub CloseStandardInput: + unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, + pub ResizeConsole: + unsafe extern "system" fn(*mut core::ffi::c_void, u16, u16) -> windows_core::HRESULT, + pub SendCtrlClose: unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, + pub Terminate: unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, + pub WaitForExit: + unsafe extern "system" fn(*mut core::ffi::c_void, u32, *mut i32) -> windows_core::HRESULT, } windows_core::imp::define_interface!( - IIsoSessionAppConnectResult, - IIsoSessionAppConnectResult_Vtbl, - 0x951478b0_1667_5bc2_9eb9_63d4e402cf04 + IIsoSessionProcessOptions, + IIsoSessionProcessOptions_Vtbl, + 0x20e0f2f6_3b48_5f60_b97c_e856754ded74 ); -impl windows_core::RuntimeType for IIsoSessionAppConnectResult { +impl windows_core::RuntimeType for IIsoSessionProcessOptions { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::for_interface::(); } -impl windows_core::RuntimeName for IIsoSessionAppConnectResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionAppConnectResult"; +impl windows_core::RuntimeName for IIsoSessionProcessOptions { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionProcessOptions"; } -pub trait IIsoSessionAppConnectResult_Impl: windows_core::IUnknownImpl { - fn AgentName(&self) -> windows_core::Result; - fn AgentUserName(&self) -> windows_core::Result; - fn Error(&self) -> windows_core::Result; +pub trait IIsoSessionProcessOptions_Impl: windows_core::IUnknownImpl { + fn Environment( + &self, + ) -> windows_core::Result>; + fn InteractiveConsole(&self) -> windows_core::Result; + fn SetInteractiveConsole(&self, value: bool) -> windows_core::Result<()>; + fn RedirectStandardError(&self) -> windows_core::Result; + fn SetRedirectStandardError(&self, value: bool) -> windows_core::Result<()>; + fn RedirectStandardInput(&self) -> windows_core::Result; + fn SetRedirectStandardInput(&self, value: bool) -> windows_core::Result<()>; + fn RedirectStandardOutput(&self) -> windows_core::Result; + fn SetRedirectStandardOutput(&self, value: bool) -> windows_core::Result<()>; + fn TimeoutMilliseconds(&self) -> windows_core::Result; + fn SetTimeoutMilliseconds(&self, value: u32) -> windows_core::Result<()>; + fn WorkingDirectory(&self) -> windows_core::Result; + fn SetWorkingDirectory(&self, value: &windows_core::HSTRING) -> windows_core::Result<()>; } -impl IIsoSessionAppConnectResult_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn AgentName< - Identity: IIsoSessionAppConnectResult_Impl, +impl IIsoSessionProcessOptions_Vtbl { + pub const fn new() -> Self { + unsafe extern "system" fn Environment< + Identity: IIsoSessionProcessOptions_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, @@ -571,7 +647,7 @@ impl IIsoSessionAppConnectResult_Vtbl { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionAppConnectResult_Impl::AgentName(this) { + match IIsoSessionProcessOptions_Impl::Environment(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -581,107 +657,49 @@ impl IIsoSessionAppConnectResult_Vtbl { } } } - unsafe extern "system" fn AgentUserName< - Identity: IIsoSessionAppConnectResult_Impl, + unsafe extern "system" fn InteractiveConsole< + Identity: IIsoSessionProcessOptions_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + result__: *mut bool, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionAppConnectResult_Impl::AgentUserName(this) { + match IIsoSessionProcessOptions_Impl::InteractiveConsole(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); windows_core::HRESULT(0) } Err(err) => err.into(), } } } - unsafe extern "system" fn Error< - Identity: IIsoSessionAppConnectResult_Impl, + unsafe extern "system" fn SetInteractiveConsole< + Identity: IIsoSessionProcessOptions_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + value: bool, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionAppConnectResult_Impl::Error(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } + IIsoSessionProcessOptions_Impl::SetInteractiveConsole(this, value).into() } } - Self { - base__: windows_core::IInspectable_Vtbl::new::< - Identity, - IIsoSessionAppConnectResult, - OFFSET, - >(), - AgentName: AgentName::, - AgentUserName: AgentUserName::, - Error: Error::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionAppConnectResult_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub AgentName: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub AgentUserName: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub Error: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionError, - IIsoSessionError_Vtbl, - 0xd9dcc33b_b415_52ff_a0ef_000c956feced -); -impl windows_core::RuntimeType for IIsoSessionError { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionError { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionError"; -} -pub trait IIsoSessionError_Impl: windows_core::IUnknownImpl { - fn Code(&self) -> windows_core::Result; - fn IsError(&self) -> windows_core::Result; - fn Message(&self) -> windows_core::Result; - fn Remediation(&self) -> windows_core::Result; - fn Format(&self) -> windows_core::Result; -} -impl IIsoSessionError_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn Code( + unsafe extern "system" fn RedirectStandardError< + Identity: IIsoSessionProcessOptions_Impl, + const OFFSET: isize, + >( this: *mut core::ffi::c_void, - result__: *mut windows_core::HRESULT, + result__: *mut bool, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionError_Impl::Code(this) { + match IIsoSessionProcessOptions_Impl::RedirectStandardError(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); windows_core::HRESULT(0) @@ -690,162 +708,94 @@ impl IIsoSessionError_Vtbl { } } } - unsafe extern "system" fn IsError( + unsafe extern "system" fn SetRedirectStandardError< + Identity: IIsoSessionProcessOptions_Impl, + const OFFSET: isize, + >( this: *mut core::ffi::c_void, - result__: *mut bool, + value: bool, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionError_Impl::IsError(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } + IIsoSessionProcessOptions_Impl::SetRedirectStandardError(this, value).into() } } - unsafe extern "system" fn Message( + unsafe extern "system" fn RedirectStandardInput< + Identity: IIsoSessionProcessOptions_Impl, + const OFFSET: isize, + >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + result__: *mut bool, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionError_Impl::Message(this) { + match IIsoSessionProcessOptions_Impl::RedirectStandardInput(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); windows_core::HRESULT(0) } Err(err) => err.into(), } } } - unsafe extern "system" fn Remediation< - Identity: IIsoSessionError_Impl, + unsafe extern "system" fn SetRedirectStandardInput< + Identity: IIsoSessionProcessOptions_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + value: bool, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionError_Impl::Remediation(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } + IIsoSessionProcessOptions_Impl::SetRedirectStandardInput(this, value).into() } } - unsafe extern "system" fn Format( + unsafe extern "system" fn RedirectStandardOutput< + Identity: IIsoSessionProcessOptions_Impl, + const OFFSET: isize, + >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + result__: *mut bool, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionError_Impl::Format(this) { + match IIsoSessionProcessOptions_Impl::RedirectStandardOutput(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); windows_core::HRESULT(0) } Err(err) => err.into(), } } } - Self { - base__: windows_core::IInspectable_Vtbl::new::(), - Code: Code::, - IsError: IsError::, - Message: Message::, - Remediation: Remediation::, - Format: Format::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionError_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub Code: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut windows_core::HRESULT, - ) -> windows_core::HRESULT, - pub IsError: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, - pub Message: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub Remediation: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub Format: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionFolderSharingResult, - IIsoSessionFolderSharingResult_Vtbl, - 0xbb5dbdfa_fb03_5b95_b6ef_d92efe9409f4 -); -impl windows_core::RuntimeType for IIsoSessionFolderSharingResult { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionFolderSharingResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionFolderSharingResult"; -} -pub trait IIsoSessionFolderSharingResult_Impl: windows_core::IUnknownImpl { - fn FolderPath(&self) -> windows_core::Result; - fn Status(&self) -> windows_core::Result; - fn Error(&self) -> windows_core::Result; -} -impl IIsoSessionFolderSharingResult_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn FolderPath< - Identity: IIsoSessionFolderSharingResult_Impl, + unsafe extern "system" fn SetRedirectStandardOutput< + Identity: IIsoSessionProcessOptions_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, + value: bool, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionFolderSharingResult_Impl::FolderPath(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } + IIsoSessionProcessOptions_Impl::SetRedirectStandardOutput(this, value).into() } } - unsafe extern "system" fn Status< - Identity: IIsoSessionFolderSharingResult_Impl, + unsafe extern "system" fn TimeoutMilliseconds< + Identity: IIsoSessionProcessOptions_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - result__: *mut IsoSessionFolderSharingStatus, + result__: *mut u32, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionFolderSharingResult_Impl::Status(this) { + match IIsoSessionProcessOptions_Impl::TimeoutMilliseconds(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); windows_core::HRESULT(0) @@ -854,8 +804,21 @@ impl IIsoSessionFolderSharingResult_Vtbl { } } } - unsafe extern "system" fn Error< - Identity: IIsoSessionFolderSharingResult_Impl, + unsafe extern "system" fn SetTimeoutMilliseconds< + Identity: IIsoSessionProcessOptions_Impl, + const OFFSET: isize, + >( + this: *mut core::ffi::c_void, + value: u32, + ) -> windows_core::HRESULT { + unsafe { + let this: &Identity = + &*((this as *const *const ()).offset(OFFSET) as *const Identity); + IIsoSessionProcessOptions_Impl::SetTimeoutMilliseconds(this, value).into() + } + } + unsafe extern "system" fn WorkingDirectory< + Identity: IIsoSessionProcessOptions_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, @@ -864,7 +827,7 @@ impl IIsoSessionFolderSharingResult_Vtbl { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionFolderSharingResult_Impl::Error(this) { + match IIsoSessionProcessOptions_Impl::WorkingDirectory(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -874,144 +837,114 @@ impl IIsoSessionFolderSharingResult_Vtbl { } } } + unsafe extern "system" fn SetWorkingDirectory< + Identity: IIsoSessionProcessOptions_Impl, + const OFFSET: isize, + >( + this: *mut core::ffi::c_void, + value: *mut core::ffi::c_void, + ) -> windows_core::HRESULT { + unsafe { + let this: &Identity = + &*((this as *const *const ()).offset(OFFSET) as *const Identity); + IIsoSessionProcessOptions_Impl::SetWorkingDirectory( + this, + core::mem::transmute(&value), + ) + .into() + } + } Self { base__: windows_core::IInspectable_Vtbl::new::< Identity, - IIsoSessionFolderSharingResult, + IIsoSessionProcessOptions, OFFSET, >(), - FolderPath: FolderPath::, - Status: Status::, - Error: Error::, + Environment: Environment::, + InteractiveConsole: InteractiveConsole::, + SetInteractiveConsole: SetInteractiveConsole::, + RedirectStandardError: RedirectStandardError::, + SetRedirectStandardError: SetRedirectStandardError::, + RedirectStandardInput: RedirectStandardInput::, + SetRedirectStandardInput: SetRedirectStandardInput::, + RedirectStandardOutput: RedirectStandardOutput::, + SetRedirectStandardOutput: SetRedirectStandardOutput::, + TimeoutMilliseconds: TimeoutMilliseconds::, + SetTimeoutMilliseconds: SetTimeoutMilliseconds::, + WorkingDirectory: WorkingDirectory::, + SetWorkingDirectory: SetWorkingDirectory::, } } pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID + iid == &::IID } } #[repr(C)] #[doc(hidden)] -pub struct IIsoSessionFolderSharingResult_Vtbl { +pub struct IIsoSessionProcessOptions_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - pub FolderPath: unsafe extern "system" fn( + pub Environment: unsafe extern "system" fn( *mut core::ffi::c_void, *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, - pub Status: unsafe extern "system" fn( + pub InteractiveConsole: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, + pub SetInteractiveConsole: + unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, + pub RedirectStandardError: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, + pub SetRedirectStandardError: + unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, + pub RedirectStandardInput: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, + pub SetRedirectStandardInput: + unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, + pub RedirectStandardOutput: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, + pub SetRedirectStandardOutput: + unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, + pub TimeoutMilliseconds: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut u32) -> windows_core::HRESULT, + pub SetTimeoutMilliseconds: + unsafe extern "system" fn(*mut core::ffi::c_void, u32) -> windows_core::HRESULT, + pub WorkingDirectory: unsafe extern "system" fn( *mut core::ffi::c_void, - *mut IsoSessionFolderSharingStatus, + *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, - pub Error: unsafe extern "system" fn( + pub SetWorkingDirectory: unsafe extern "system" fn( + *mut core::ffi::c_void, *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } windows_core::imp::define_interface!( - IIsoSessionOps, - IIsoSessionOps_Vtbl, - 0x9331fb6b_1f20_545e_a6f5_8cefdb2ed5a6 + IIsoSessionProcessResult, + IIsoSessionProcessResult_Vtbl, + 0x30c5f8a5_efb8_5872_b8f9_be625090b9ba ); -impl windows_core::RuntimeType for IIsoSessionOps { +impl windows_core::RuntimeType for IIsoSessionProcessResult { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::for_interface::(); } -impl windows_core::RuntimeName for IIsoSessionOps { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionOps"; +impl windows_core::RuntimeName for IIsoSessionProcessResult { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionProcessResult"; } -pub trait IIsoSessionOps_Impl: windows_core::IUnknownImpl { - fn AddUserAsync( - &self, - registrationId: &windows_core::HSTRING, - provisionId: &windows_core::HSTRING, - ) -> windows_core::Result>; - fn GetUser( - &self, - agentName: &windows_core::HSTRING, - ) -> windows_core::Result; - fn RegisterApp( - &self, - registrationId: &windows_core::HSTRING, - ) -> windows_core::Result; - fn RemoveUserAsync( - &self, - agentName: &windows_core::HSTRING, - ) -> windows_core::Result>; - fn RunProcessAsync( - &self, - agentName: &windows_core::HSTRING, - processPath: &windows_core::HSTRING, - arguments: &windows_core::HSTRING, - ) -> windows_core::Result>; - fn RunProcessWithOptionsAsync( - &self, - agentName: &windows_core::HSTRING, - processPath: &windows_core::HSTRING, - arguments: &windows_core::HSTRING, - options: windows_core::Ref<'_, IsoSessionProcessOptions>, - ) -> windows_core::Result>; - fn StartSessionAsync( - &self, - agentName: &windows_core::HSTRING, - configId: IsoSessionConfigId, - ) -> windows_core::Result>; - fn StopSessionAsync( - &self, - agentName: &windows_core::HSTRING, - ) -> windows_core::Result>; - fn UnregisterAppAsync( - &self, - registrationId: &windows_core::HSTRING, - ) -> windows_core::Result>; - fn ShareFolderBatchAsync( - &self, - agentName: &windows_core::HSTRING, - requests: windows_core::Ref< - '_, - windows_collections::IVectorView, - >, - ) -> windows_core::Result< - windows_future::IAsyncOperation< - windows_collections::IVectorView, - >, - >; +pub trait IIsoSessionProcessResult_Impl: windows_core::IUnknownImpl { + fn Error(&self) -> windows_core::Result; + fn Process(&self) -> windows_core::Result; } -impl IIsoSessionOps_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn AddUserAsync< - Identity: IIsoSessionOps_Impl, +impl IIsoSessionProcessResult_Vtbl { + pub const fn new() -> Self { + unsafe extern "system" fn Error< + Identity: IIsoSessionProcessResult_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - registrationid: *mut core::ffi::c_void, - provisionid: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::AddUserAsync( - this, - core::mem::transmute(®istrationid), - core::mem::transmute(&provisionid), - ) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn GetUser( - this: *mut core::ffi::c_void, - agentname: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::GetUser(this, core::mem::transmute(&agentname)) { + match IIsoSessionProcessResult_Impl::Error(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -1021,19 +954,17 @@ impl IIsoSessionOps_Vtbl { } } } - unsafe extern "system" fn RegisterApp< - Identity: IIsoSessionOps_Impl, + unsafe extern "system" fn Process< + Identity: IIsoSessionProcessResult_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - registrationid: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::RegisterApp(this, core::mem::transmute(®istrationid)) - { + match IIsoSessionProcessResult_Impl::Process(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -1043,174 +974,58 @@ impl IIsoSessionOps_Vtbl { } } } - unsafe extern "system" fn RemoveUserAsync< - Identity: IIsoSessionOps_Impl, - const OFFSET: isize, - >( + Self { + base__: windows_core::IInspectable_Vtbl::new::< + Identity, + IIsoSessionProcessResult, + OFFSET, + >(), + Error: Error::, + Process: Process::, + } + } + pub fn matches(iid: &windows_core::GUID) -> bool { + iid == &::IID + } +} +#[repr(C)] +#[doc(hidden)] +pub struct IIsoSessionProcessResult_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub Error: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub Process: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} +windows_core::imp::define_interface!( + IIsoSessionResult, + IIsoSessionResult_Vtbl, + 0xc0f5de66_c487_571c_87a0_13a0854a7db0 +); +impl windows_core::RuntimeType for IIsoSessionResult { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl windows_core::RuntimeName for IIsoSessionResult { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionResult"; +} +pub trait IIsoSessionResult_Impl: windows_core::IUnknownImpl { + fn Error(&self) -> windows_core::Result; +} +impl IIsoSessionResult_Vtbl { + pub const fn new() -> Self { + unsafe extern "system" fn Error( this: *mut core::ffi::c_void, - agentname: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::RemoveUserAsync(this, core::mem::transmute(&agentname)) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn RunProcessAsync< - Identity: IIsoSessionOps_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - agentname: *mut core::ffi::c_void, - processpath: *mut core::ffi::c_void, - arguments: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::RunProcessAsync( - this, - core::mem::transmute(&agentname), - core::mem::transmute(&processpath), - core::mem::transmute(&arguments), - ) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn RunProcessWithOptionsAsync< - Identity: IIsoSessionOps_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - agentname: *mut core::ffi::c_void, - processpath: *mut core::ffi::c_void, - arguments: *mut core::ffi::c_void, - options: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::RunProcessWithOptionsAsync( - this, - core::mem::transmute(&agentname), - core::mem::transmute(&processpath), - core::mem::transmute(&arguments), - core::mem::transmute_copy(&options), - ) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn StartSessionAsync< - Identity: IIsoSessionOps_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - agentname: *mut core::ffi::c_void, - configid: IsoSessionConfigId, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::StartSessionAsync( - this, - core::mem::transmute(&agentname), - configid, - ) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn StopSessionAsync< - Identity: IIsoSessionOps_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - agentname: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::StopSessionAsync(this, core::mem::transmute(&agentname)) - { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn UnregisterAppAsync< - Identity: IIsoSessionOps_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - registrationid: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::UnregisterAppAsync( - this, - core::mem::transmute(®istrationid), - ) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn ShareFolderBatchAsync< - Identity: IIsoSessionOps_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - agentname: *mut core::ffi::c_void, - requests: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps_Impl::ShareFolderBatchAsync( - this, - core::mem::transmute(&agentname), - core::mem::transmute_copy(&requests), - ) { + match IIsoSessionResult_Impl::Error(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -1221,133 +1036,54 @@ impl IIsoSessionOps_Vtbl { } } Self { - base__: windows_core::IInspectable_Vtbl::new::(), - AddUserAsync: AddUserAsync::, - GetUser: GetUser::, - RegisterApp: RegisterApp::, - RemoveUserAsync: RemoveUserAsync::, - RunProcessAsync: RunProcessAsync::, - RunProcessWithOptionsAsync: RunProcessWithOptionsAsync::, - StartSessionAsync: StartSessionAsync::, - StopSessionAsync: StopSessionAsync::, - UnregisterAppAsync: UnregisterAppAsync::, - ShareFolderBatchAsync: ShareFolderBatchAsync::, + base__: windows_core::IInspectable_Vtbl::new::(), + Error: Error::, } } pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID + iid == &::IID } } #[repr(C)] #[doc(hidden)] -pub struct IIsoSessionOps_Vtbl { +pub struct IIsoSessionResult_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - pub AddUserAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub GetUser: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub RegisterApp: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub RemoveUserAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub RunProcessAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub RunProcessWithOptionsAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub StartSessionAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - IsoSessionConfigId, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub StopSessionAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub UnregisterAppAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub ShareFolderBatchAsync: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, + pub Error: unsafe extern "system" fn( *mut core::ffi::c_void, *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } windows_core::imp::define_interface!( - IIsoSessionOps2, - IIsoSessionOps2_Vtbl, - 0x78940d90_d410_5998_9a3c_9a696045ca0d + IIsoSessionUserResult, + IIsoSessionUserResult_Vtbl, + 0x8733fb07_cce2_5483_95a7_0a68cbd7641a ); -impl windows_core::RuntimeType for IIsoSessionOps2 { +impl windows_core::RuntimeType for IIsoSessionUserResult { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::for_interface::(); } -impl windows_core::RuntimeName for IIsoSessionOps2 { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionOps2"; +impl windows_core::RuntimeName for IIsoSessionUserResult { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionUserResult"; } -pub trait IIsoSessionOps2_Impl: windows_core::IUnknownImpl { - fn AddUserAsync2( - &self, - registrationId: &windows_core::HSTRING, - entraAccountName: &windows_core::HSTRING, - wamToken: &windows_core::HSTRING, - ) -> windows_core::Result>; - fn StartSessionAsync2( - &self, - entraAccountName: &windows_core::HSTRING, - configId: IsoSessionConfigId, - wamToken: &windows_core::HSTRING, - ) -> windows_core::Result>; +pub trait IIsoSessionUserResult_Impl: windows_core::IUnknownImpl { + fn AgentUserName(&self) -> windows_core::Result; + fn AgentUserSid(&self) -> windows_core::Result; + fn EphemeralWorkspacePath(&self) -> windows_core::Result; + fn Error(&self) -> windows_core::Result; } -impl IIsoSessionOps2_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn AddUserAsync2< - Identity: IIsoSessionOps2_Impl, +impl IIsoSessionUserResult_Vtbl { + pub const fn new() -> Self { + unsafe extern "system" fn AgentUserName< + Identity: IIsoSessionUserResult_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - registrationid: *mut core::ffi::c_void, - entraaccountname: *mut core::ffi::c_void, - wamtoken: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps2_Impl::AddUserAsync2( - this, - core::mem::transmute(®istrationid), - core::mem::transmute(&entraaccountname), - core::mem::transmute(&wamtoken), - ) { + match IIsoSessionUserResult_Impl::AgentUserName(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -1357,25 +1093,17 @@ impl IIsoSessionOps2_Vtbl { } } } - unsafe extern "system" fn StartSessionAsync2< - Identity: IIsoSessionOps2_Impl, + unsafe extern "system" fn AgentUserSid< + Identity: IIsoSessionUserResult_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - entraaccountname: *mut core::ffi::c_void, - configid: IsoSessionConfigId, - wamtoken: *mut core::ffi::c_void, result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps2_Impl::StartSessionAsync2( - this, - core::mem::transmute(&entraaccountname), - configid, - core::mem::transmute(&wamtoken), - ) { + match IIsoSessionUserResult_Impl::AgentUserSid(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -1385,1409 +1113,79 @@ impl IIsoSessionOps2_Vtbl { } } } - Self { - base__: windows_core::IInspectable_Vtbl::new::(), - AddUserAsync2: AddUserAsync2::, - StartSessionAsync2: StartSessionAsync2::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionOps2_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub AddUserAsync2: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub StartSessionAsync2: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - IsoSessionConfigId, - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionOps3, - IIsoSessionOps3_Vtbl, - 0xdd069f2d_ff63_539f_85ff_e54e1881cd47 -); -impl windows_core::RuntimeType for IIsoSessionOps3 { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionOps3 { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionOps3"; -} -pub trait IIsoSessionOps3_Impl: windows_core::IUnknownImpl { - fn GetAgentUsers( - &self, - registrationId: &windows_core::HSTRING, - includeInactive: bool, - ) -> windows_core::Result>; - fn GetAllAgentUsers( - &self, - includeInactive: bool, - ) -> windows_core::Result>; - fn GetProcesses( - &self, - agentName: &windows_core::HSTRING, - ) -> windows_core::Result>; -} -impl IIsoSessionOps3_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn GetAgentUsers< - Identity: IIsoSessionOps3_Impl, + unsafe extern "system" fn EphemeralWorkspacePath< + Identity: IIsoSessionUserResult_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - registrationid: *mut core::ffi::c_void, - includeinactive: bool, - result_size__: *mut u32, - result__: *mut *mut *mut core::ffi::c_void, + result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps3_Impl::GetAgentUsers( - this, - core::mem::transmute(®istrationid), - includeinactive, - ) { + match IIsoSessionUserResult_Impl::EphemeralWorkspacePath(this) { Ok(ok__) => { - let (ok_data__, ok_data_len__) = ok__.into_abi(); - result__.write(core::mem::transmute(ok_data__)); - result_size__.write(ok_data_len__); + result__.write(core::mem::transmute_copy(&ok__)); + core::mem::forget(ok__); windows_core::HRESULT(0) } Err(err) => err.into(), } } } - unsafe extern "system" fn GetAllAgentUsers< - Identity: IIsoSessionOps3_Impl, + unsafe extern "system" fn Error< + Identity: IIsoSessionUserResult_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - includeinactive: bool, - result_size__: *mut u32, - result__: *mut *mut *mut core::ffi::c_void, + result__: *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps3_Impl::GetAllAgentUsers(this, includeinactive) { - Ok(ok__) => { - let (ok_data__, ok_data_len__) = ok__.into_abi(); - result__.write(core::mem::transmute(ok_data__)); - result_size__.write(ok_data_len__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn GetProcesses< - Identity: IIsoSessionOps3_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - agentname: *mut core::ffi::c_void, - result_size__: *mut u32, - result__: *mut *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionOps3_Impl::GetProcesses(this, core::mem::transmute(&agentname)) { - Ok(ok__) => { - let (ok_data__, ok_data_len__) = ok__.into_abi(); - result__.write(core::mem::transmute(ok_data__)); - result_size__.write(ok_data_len__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - Self { - base__: windows_core::IInspectable_Vtbl::new::(), - GetAgentUsers: GetAgentUsers::, - GetAllAgentUsers: GetAllAgentUsers::, - GetProcesses: GetProcesses::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionOps3_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub GetAgentUsers: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - bool, - *mut u32, - *mut *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub GetAllAgentUsers: unsafe extern "system" fn( - *mut core::ffi::c_void, - bool, - *mut u32, - *mut *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub GetProcesses: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - *mut u32, - *mut *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionProcess, - IIsoSessionProcess_Vtbl, - 0x68230e9d_bde4_5076_8c5d_5b08610805a4 -); -impl windows_core::RuntimeType for IIsoSessionProcess { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionProcess { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionProcess"; -} -pub trait IIsoSessionProcess_Impl: windows_core::IUnknownImpl { - fn ExitCode(&self) -> windows_core::Result; - fn ErrorHandle(&self) -> windows_core::Result; - fn InputHandle(&self) -> windows_core::Result; - fn OutputHandle(&self) -> windows_core::Result; - fn ProcessId(&self) -> windows_core::Result; - fn ProcessPath(&self) -> windows_core::Result; - fn CloseStandardInput(&self) -> windows_core::Result<()>; - fn ResizeConsole(&self, columns: u16, rows: u16) -> windows_core::Result<()>; - fn SendCtrlClose(&self) -> windows_core::Result<()>; - fn Terminate(&self) -> windows_core::Result<()>; - fn WaitForExit(&self, timeoutMs: u32) -> windows_core::Result; -} -impl IIsoSessionProcess_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn ExitCode< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut i32, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcess_Impl::ExitCode(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn ErrorHandle< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut u64, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcess_Impl::ErrorHandle(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn InputHandle< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut u64, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcess_Impl::InputHandle(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn OutputHandle< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut u64, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcess_Impl::OutputHandle(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn ProcessId< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut u32, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcess_Impl::ProcessId(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn ProcessPath< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcess_Impl::ProcessPath(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn CloseStandardInput< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcess_Impl::CloseStandardInput(this).into() - } - } - unsafe extern "system" fn ResizeConsole< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - columns: u16, - rows: u16, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcess_Impl::ResizeConsole(this, columns, rows).into() - } - } - unsafe extern "system" fn SendCtrlClose< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcess_Impl::SendCtrlClose(this).into() - } - } - unsafe extern "system" fn Terminate< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcess_Impl::Terminate(this).into() - } - } - unsafe extern "system" fn WaitForExit< - Identity: IIsoSessionProcess_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - timeoutms: u32, - result__: *mut i32, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcess_Impl::WaitForExit(this, timeoutms) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - Self { - base__: windows_core::IInspectable_Vtbl::new::(), - ExitCode: ExitCode::, - ErrorHandle: ErrorHandle::, - InputHandle: InputHandle::, - OutputHandle: OutputHandle::, - ProcessId: ProcessId::, - ProcessPath: ProcessPath::, - CloseStandardInput: CloseStandardInput::, - ResizeConsole: ResizeConsole::, - SendCtrlClose: SendCtrlClose::, - Terminate: Terminate::, - WaitForExit: WaitForExit::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionProcess_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub ExitCode: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut i32) -> windows_core::HRESULT, - pub ErrorHandle: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut u64) -> windows_core::HRESULT, - pub InputHandle: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut u64) -> windows_core::HRESULT, - pub OutputHandle: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut u64) -> windows_core::HRESULT, - pub ProcessId: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut u32) -> windows_core::HRESULT, - pub ProcessPath: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub CloseStandardInput: - unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, - pub ResizeConsole: - unsafe extern "system" fn(*mut core::ffi::c_void, u16, u16) -> windows_core::HRESULT, - pub SendCtrlClose: unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, - pub Terminate: unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, - pub WaitForExit: - unsafe extern "system" fn(*mut core::ffi::c_void, u32, *mut i32) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionProcessOptions, - IIsoSessionProcessOptions_Vtbl, - 0x2f3e5bf6_6adb_5d23_bb5e_c042543292cf -); -impl windows_core::RuntimeType for IIsoSessionProcessOptions { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionProcessOptions { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionProcessOptions"; -} -pub trait IIsoSessionProcessOptions_Impl: windows_core::IUnknownImpl { - fn Environment( - &self, - ) -> windows_core::Result>; - fn InteractiveConsole(&self) -> windows_core::Result; - fn SetInteractiveConsole(&self, value: bool) -> windows_core::Result<()>; - fn RedirectStandardError(&self) -> windows_core::Result; - fn SetRedirectStandardError(&self, value: bool) -> windows_core::Result<()>; - fn RedirectStandardInput(&self) -> windows_core::Result; - fn SetRedirectStandardInput(&self, value: bool) -> windows_core::Result<()>; - fn RedirectStandardOutput(&self) -> windows_core::Result; - fn SetRedirectStandardOutput(&self, value: bool) -> windows_core::Result<()>; - fn TimeoutMilliseconds(&self) -> windows_core::Result; - fn SetTimeoutMilliseconds(&self, value: u32) -> windows_core::Result<()>; - fn WorkingDirectory(&self) -> windows_core::Result; - fn SetWorkingDirectory(&self, value: &windows_core::HSTRING) -> windows_core::Result<()>; -} -impl IIsoSessionProcessOptions_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn Environment< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessOptions_Impl::Environment(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn InteractiveConsole< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut bool, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessOptions_Impl::InteractiveConsole(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn SetInteractiveConsole< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - value: bool, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcessOptions_Impl::SetInteractiveConsole(this, value).into() - } - } - unsafe extern "system" fn RedirectStandardError< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut bool, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessOptions_Impl::RedirectStandardError(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn SetRedirectStandardError< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - value: bool, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcessOptions_Impl::SetRedirectStandardError(this, value).into() - } - } - unsafe extern "system" fn RedirectStandardInput< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut bool, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessOptions_Impl::RedirectStandardInput(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn SetRedirectStandardInput< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - value: bool, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcessOptions_Impl::SetRedirectStandardInput(this, value).into() - } - } - unsafe extern "system" fn RedirectStandardOutput< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut bool, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessOptions_Impl::RedirectStandardOutput(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn SetRedirectStandardOutput< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - value: bool, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcessOptions_Impl::SetRedirectStandardOutput(this, value).into() - } - } - unsafe extern "system" fn TimeoutMilliseconds< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut u32, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessOptions_Impl::TimeoutMilliseconds(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn SetTimeoutMilliseconds< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - value: u32, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcessOptions_Impl::SetTimeoutMilliseconds(this, value).into() - } - } - unsafe extern "system" fn WorkingDirectory< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessOptions_Impl::WorkingDirectory(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn SetWorkingDirectory< - Identity: IIsoSessionProcessOptions_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - value: *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionProcessOptions_Impl::SetWorkingDirectory( - this, - core::mem::transmute(&value), - ) - .into() - } - } - Self { - base__: windows_core::IInspectable_Vtbl::new::< - Identity, - IIsoSessionProcessOptions, - OFFSET, - >(), - Environment: Environment::, - InteractiveConsole: InteractiveConsole::, - SetInteractiveConsole: SetInteractiveConsole::, - RedirectStandardError: RedirectStandardError::, - SetRedirectStandardError: SetRedirectStandardError::, - RedirectStandardInput: RedirectStandardInput::, - SetRedirectStandardInput: SetRedirectStandardInput::, - RedirectStandardOutput: RedirectStandardOutput::, - SetRedirectStandardOutput: SetRedirectStandardOutput::, - TimeoutMilliseconds: TimeoutMilliseconds::, - SetTimeoutMilliseconds: SetTimeoutMilliseconds::, - WorkingDirectory: WorkingDirectory::, - SetWorkingDirectory: SetWorkingDirectory::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionProcessOptions_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub Environment: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub InteractiveConsole: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, - pub SetInteractiveConsole: - unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, - pub RedirectStandardError: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, - pub SetRedirectStandardError: - unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, - pub RedirectStandardInput: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, - pub SetRedirectStandardInput: - unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, - pub RedirectStandardOutput: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, - pub SetRedirectStandardOutput: - unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, - pub TimeoutMilliseconds: - unsafe extern "system" fn(*mut core::ffi::c_void, *mut u32) -> windows_core::HRESULT, - pub SetTimeoutMilliseconds: - unsafe extern "system" fn(*mut core::ffi::c_void, u32) -> windows_core::HRESULT, - pub WorkingDirectory: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub SetWorkingDirectory: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionProcessResult, - IIsoSessionProcessResult_Vtbl, - 0xa3fa8242_cabe_5297_825d_50e41834ba2e -); -impl windows_core::RuntimeType for IIsoSessionProcessResult { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionProcessResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionProcessResult"; -} -pub trait IIsoSessionProcessResult_Impl: windows_core::IUnknownImpl { - fn Error(&self) -> windows_core::Result; - fn Process(&self) -> windows_core::Result; -} -impl IIsoSessionProcessResult_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn Error< - Identity: IIsoSessionProcessResult_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessResult_Impl::Error(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn Process< - Identity: IIsoSessionProcessResult_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionProcessResult_Impl::Process(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - Self { - base__: windows_core::IInspectable_Vtbl::new::< - Identity, - IIsoSessionProcessResult, - OFFSET, - >(), - Error: Error::, - Process: Process::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionProcessResult_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub Error: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub Process: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionResult, - IIsoSessionResult_Vtbl, - 0xdc64999f_df73_5561_b58e_003ad94f2b9f -); -impl windows_core::RuntimeType for IIsoSessionResult { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionResult"; -} -pub trait IIsoSessionResult_Impl: windows_core::IUnknownImpl { - fn Error(&self) -> windows_core::Result; -} -impl IIsoSessionResult_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn Error( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionResult_Impl::Error(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - Self { - base__: windows_core::IInspectable_Vtbl::new::(), - Error: Error::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionResult_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub Error: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionUserResult, - IIsoSessionUserResult_Vtbl, - 0x88d0d89f_4b21_5da8_947b_62d957d8e509 -); -impl windows_core::RuntimeType for IIsoSessionUserResult { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionUserResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionUserResult"; -} -pub trait IIsoSessionUserResult_Impl: windows_core::IUnknownImpl { - fn AgentName(&self) -> windows_core::Result; - fn AgentUserName(&self) -> windows_core::Result; - fn AgentUserSid(&self) -> windows_core::Result; - fn Error(&self) -> windows_core::Result; -} -impl IIsoSessionUserResult_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn AgentName< - Identity: IIsoSessionUserResult_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionUserResult_Impl::AgentName(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn AgentUserName< - Identity: IIsoSessionUserResult_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionUserResult_Impl::AgentUserName(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn AgentUserSid< - Identity: IIsoSessionUserResult_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionUserResult_Impl::AgentUserSid(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn Error< - Identity: IIsoSessionUserResult_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionUserResult_Impl::Error(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - Self { - base__: windows_core::IInspectable_Vtbl::new::( - ), - AgentName: AgentName::, - AgentUserName: AgentUserName::, - AgentUserSid: AgentUserSid::, - Error: Error::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionUserResult_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub AgentName: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub AgentUserName: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub AgentUserSid: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, - pub Error: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -windows_core::imp::define_interface!( - IIsoSessionUserResult2, - IIsoSessionUserResult2_Vtbl, - 0x00cd4400_fa69_5ccb_9b4f_1837752b1f93 -); -impl windows_core::RuntimeType for IIsoSessionUserResult2 { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_interface::(); -} -impl windows_core::RuntimeName for IIsoSessionUserResult2 { - const NAME: &'static str = "Windows.AI.IsolationSession.IIsoSessionUserResult2"; -} -pub trait IIsoSessionUserResult2_Impl: windows_core::IUnknownImpl { - fn RegistrationId(&self) -> windows_core::Result; -} -impl IIsoSessionUserResult2_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn RegistrationId< - Identity: IIsoSessionUserResult2_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionUserResult2_Impl::RegistrationId(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - Self { - base__: windows_core::IInspectable_Vtbl::new::( - ), - RegistrationId: RegistrationId::, - } - } - pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID - } -} -#[repr(C)] -#[doc(hidden)] -pub struct IIsoSessionUserResult2_Vtbl { - pub base__: windows_core::IInspectable_Vtbl, - pub RegistrationId: unsafe extern "system" fn( - *mut core::ffi::c_void, - *mut *mut core::ffi::c_void, - ) -> windows_core::HRESULT, -} -#[repr(transparent)] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct IsoSessionApp(windows_core::IUnknown); -windows_core::imp::interface_hierarchy!( - IsoSessionApp, - windows_core::IUnknown, - windows_core::IInspectable -); -windows_core::imp::required_hierarchy!(IsoSessionApp, IClosable); -impl IsoSessionApp { - pub fn new() -> windows_core::Result { - Self::IActivationFactory(|f| f.ActivateInstance::()) - } - fn IActivationFactory< - R, - F: FnOnce(&windows_core::imp::IGenericFactory) -> windows_core::Result, - >( - callback: F, - ) -> windows_core::Result { - static SHARED: windows_core::imp::FactoryCache< - IsoSessionApp, - windows_core::imp::IGenericFactory, - > = windows_core::imp::FactoryCache::new(); - SHARED.call(callback) - } - pub fn Close(&self) -> windows_core::Result<()> { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - (windows_core::Interface::vtable(this).Close)(windows_core::Interface::as_raw(this)) - .ok() - } - } - pub fn AgentName(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AgentName)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } - pub fn AgentUserName(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AgentUserName)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } - pub fn IsConnected(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).IsConnected)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| result__) - } - } - pub fn ConnectAsync( - &self, - config: P0, - ) -> windows_core::Result> - where - P0: windows_core::Param, - { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).ConnectAsync)( - windows_core::Interface::as_raw(this), - config.param().abi(), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } - pub fn LaunchProcessAsync( - &self, - processpath: &windows_core::HSTRING, - arguments: &windows_core::HSTRING, - ) -> windows_core::Result> { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).LaunchProcessAsync)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(processpath), - core::mem::transmute_copy(arguments), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } - pub fn LaunchProcessWithOptionsAsync( - &self, - processpath: &windows_core::HSTRING, - arguments: &windows_core::HSTRING, - options: P2, - ) -> windows_core::Result> - where - P2: windows_core::Param, - { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).LaunchProcessWithOptionsAsync)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(processpath), - core::mem::transmute_copy(arguments), - options.param().abi(), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } - pub fn TeardownAsync(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).TeardownAsync)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } -} -impl windows_core::RuntimeType for IsoSessionApp { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_class::(); -} -unsafe impl windows_core::Interface for IsoSessionApp { - type Vtable = ::Vtable; - const IID: windows_core::GUID = ::IID; -} -impl windows_core::RuntimeName for IsoSessionApp { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionApp"; -} -unsafe impl Send for IsoSessionApp {} -unsafe impl Sync for IsoSessionApp {} -#[repr(transparent)] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct IsoSessionAppConfig(windows_core::IUnknown); -windows_core::imp::interface_hierarchy!( - IsoSessionAppConfig, - windows_core::IUnknown, - windows_core::IInspectable -); -impl IsoSessionAppConfig { - pub fn new() -> windows_core::Result { - Self::IActivationFactory(|f| f.ActivateInstance::()) - } - fn IActivationFactory< - R, - F: FnOnce(&windows_core::imp::IGenericFactory) -> windows_core::Result, - >( - callback: F, - ) -> windows_core::Result { - static SHARED: windows_core::imp::FactoryCache< - IsoSessionAppConfig, - windows_core::imp::IGenericFactory, - > = windows_core::imp::FactoryCache::new(); - SHARED.call(callback) - } - pub fn ConfigId(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).ConfigId)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| result__) - } - } - pub fn SetConfigId(&self, value: IsoSessionConfigId) -> windows_core::Result<()> { - let this = self; - unsafe { - (windows_core::Interface::vtable(this).SetConfigId)( - windows_core::Interface::as_raw(this), - value, - ) - .ok() - } - } - pub fn RegistrationId(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).RegistrationId)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } - pub fn SetRegistrationId(&self, value: &windows_core::HSTRING) -> windows_core::Result<()> { - let this = self; - unsafe { - (windows_core::Interface::vtable(this).SetRegistrationId)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(value), - ) - .ok() - } - } - pub fn ProvisionId(&self) -> windows_core::Result { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).ProvisionId)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } - pub fn SetProvisionId(&self, value: &windows_core::HSTRING) -> windows_core::Result<()> { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - (windows_core::Interface::vtable(this).SetProvisionId)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(value), - ) - .ok() - } - } - pub fn WamToken(&self) -> windows_core::Result { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).WamToken)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } - pub fn SetWamToken(&self, value: &windows_core::HSTRING) -> windows_core::Result<()> { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - (windows_core::Interface::vtable(this).SetWamToken)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(value), - ) - .ok() - } - } -} -impl windows_core::RuntimeType for IsoSessionAppConfig { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_class::(); -} -unsafe impl windows_core::Interface for IsoSessionAppConfig { - type Vtable = ::Vtable; - const IID: windows_core::GUID = ::IID; -} -impl windows_core::RuntimeName for IsoSessionAppConfig { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionAppConfig"; -} -unsafe impl Send for IsoSessionAppConfig {} -unsafe impl Sync for IsoSessionAppConfig {} -#[repr(transparent)] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct IsoSessionAppConnectResult(windows_core::IUnknown); -windows_core::imp::interface_hierarchy!( - IsoSessionAppConnectResult, - windows_core::IUnknown, - windows_core::IInspectable -); -impl IsoSessionAppConnectResult { - pub fn AgentName(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AgentName)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) + match IIsoSessionUserResult_Impl::Error(this) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + core::mem::forget(ok__); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } + } } - } - pub fn AgentUserName(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AgentUserName)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) + Self { + base__: windows_core::IInspectable_Vtbl::new::( + ), + AgentUserName: AgentUserName::, + AgentUserSid: AgentUserSid::, + EphemeralWorkspacePath: EphemeralWorkspacePath::, + Error: Error::, } } - pub fn Error(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).Error)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } + pub fn matches(iid: &windows_core::GUID) -> bool { + iid == &::IID } } -impl windows_core::RuntimeType for IsoSessionAppConnectResult { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_class::(); -} -unsafe impl windows_core::Interface for IsoSessionAppConnectResult { - type Vtable = ::Vtable; - const IID: windows_core::GUID = ::IID; -} -impl windows_core::RuntimeName for IsoSessionAppConnectResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionAppConnectResult"; -} -unsafe impl Send for IsoSessionAppConnectResult {} -unsafe impl Sync for IsoSessionAppConnectResult {} -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct IsoSessionConfigId(pub i32); -impl IsoSessionConfigId { - pub const Small: Self = Self(1i32); - pub const Medium: Self = Self(2i32); - pub const Large: Self = Self(3i32); - pub const Composable: Self = Self(4i32); -} -impl windows_core::TypeKind for IsoSessionConfigId { - type TypeKind = windows_core::CopyType; -} -impl windows_core::RuntimeType for IsoSessionConfigId { - const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::from_slice( - b"enum(Windows.AI.IsolationSession.IsoSessionConfigId;i4)", - ); +#[repr(C)] +#[doc(hidden)] +pub struct IIsoSessionUserResult_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub AgentUserName: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub AgentUserSid: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub EphemeralWorkspacePath: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub Error: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, } #[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] @@ -2857,17 +1255,6 @@ impl IsoSessionError { .map(|| core::mem::transmute(result__)) } } - pub fn Format(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).Format)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } } impl windows_core::RuntimeType for IsoSessionError { const SIGNATURE: windows_core::imp::ConstBuffer = @@ -2878,108 +1265,24 @@ unsafe impl windows_core::Interface for IsoSessionError { const IID: windows_core::GUID = ::IID; } impl windows_core::RuntimeName for IsoSessionError { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionError"; + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IsoSessionError"; } unsafe impl Send for IsoSessionError {} unsafe impl Sync for IsoSessionError {} #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct IsoSessionFolderSharingAccessLevel(pub i32); -impl IsoSessionFolderSharingAccessLevel { - pub const Read: Self = Self(0i32); - pub const ReadWrite: Self = Self(1i32); - pub const Remove: Self = Self(2i32); -} -impl windows_core::TypeKind for IsoSessionFolderSharingAccessLevel { - type TypeKind = windows_core::CopyType; -} -impl windows_core::RuntimeType for IsoSessionFolderSharingAccessLevel { - const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::from_slice( - b"enum(Windows.AI.IsolationSession.IsoSessionFolderSharingAccessLevel;i4)", - ); -} -#[repr(C)] -#[derive(Clone, Debug, Default, PartialEq)] -pub struct IsoSessionFolderSharingRequest { - pub FolderPath: windows_core::HSTRING, - pub AccessLevel: IsoSessionFolderSharingAccessLevel, -} -impl windows_core::TypeKind for IsoSessionFolderSharingRequest { - type TypeKind = windows_core::CloneType; -} -impl windows_core::RuntimeType for IsoSessionFolderSharingRequest { - const SIGNATURE :windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::from_slice ( b"struct(Windows.AI.IsolationSession.IsoSessionFolderSharingRequest;string;enum(Windows.AI.IsolationSession.IsoSessionFolderSharingAccessLevel;i4))" ) ; -} -#[repr(transparent)] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct IsoSessionFolderSharingResult(windows_core::IUnknown); -windows_core::imp::interface_hierarchy!( - IsoSessionFolderSharingResult, - windows_core::IUnknown, - windows_core::IInspectable -); -impl IsoSessionFolderSharingResult { - pub fn FolderPath(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).FolderPath)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } - pub fn Status(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).Status)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| result__) - } - } - pub fn Error(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).Error)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } -} -impl windows_core::RuntimeType for IsoSessionFolderSharingResult { - const SIGNATURE: windows_core::imp::ConstBuffer = - windows_core::imp::ConstBuffer::for_class::(); -} -unsafe impl windows_core::Interface for IsoSessionFolderSharingResult { - type Vtable = ::Vtable; - const IID: windows_core::GUID = - ::IID; -} -impl windows_core::RuntimeName for IsoSessionFolderSharingResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionFolderSharingResult"; -} -unsafe impl Send for IsoSessionFolderSharingResult {} -unsafe impl Sync for IsoSessionFolderSharingResult {} -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct IsoSessionFolderSharingStatus(pub i32); -impl IsoSessionFolderSharingStatus { - pub const Succeeded: Self = Self(0i32); - pub const Failed: Self = Self(1i32); +pub struct IsoSessionFeature(pub i32); +impl IsoSessionFeature { + pub const None: Self = Self(0i32); + pub const LocalAgentUser: Self = Self(1i32); + pub const EntraAgentUser: Self = Self(2i32); } -impl windows_core::TypeKind for IsoSessionFolderSharingStatus { +impl windows_core::TypeKind for IsoSessionFeature { type TypeKind = windows_core::CopyType; } -impl windows_core::RuntimeType for IsoSessionFolderSharingStatus { +impl windows_core::RuntimeType for IsoSessionFeature { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::from_slice( - b"enum(Windows.AI.IsolationSession.IsoSessionFolderSharingStatus;i4)", + b"enum(Windows.AI.IsolationSession.Preview.IsoSessionFeature;i4)", ); } #[repr(transparent)] @@ -3006,82 +1309,56 @@ impl IsoSessionOps { > = windows_core::imp::FactoryCache::new(); SHARED.call(callback) } - pub fn AddUserAsync( - &self, - registrationid: &windows_core::HSTRING, - provisionid: &windows_core::HSTRING, - ) -> windows_core::Result> { + pub fn BehaviorVersion(&self) -> windows_core::Result { let this = self; unsafe { let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AddUserAsync)( + (windows_core::Interface::vtable(this).BehaviorVersion)( windows_core::Interface::as_raw(this), - core::mem::transmute_copy(registrationid), - core::mem::transmute_copy(provisionid), &mut result__, ) - .and_then(|| windows_core::Type::from_abi(result__)) + .map(|| result__) } } - pub fn GetUser( + pub fn AddUserAsync( &self, - agentname: &windows_core::HSTRING, - ) -> windows_core::Result { + optentraaccountname: &windows_core::HSTRING, + optwamtoken: &windows_core::HSTRING, + ) -> windows_core::Result> { let this = self; unsafe { let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).GetUser)( + (windows_core::Interface::vtable(this).AddUserAsync)( windows_core::Interface::as_raw(this), - core::mem::transmute_copy(agentname), + core::mem::transmute_copy(optentraaccountname), + core::mem::transmute_copy(optwamtoken), &mut result__, ) .and_then(|| windows_core::Type::from_abi(result__)) } } - pub fn RegisterApp( - &self, - registrationid: &windows_core::HSTRING, - ) -> windows_core::Result { + pub fn GetFeatureLevel(&self, feature: IsoSessionFeature) -> windows_core::Result { let this = self; unsafe { let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).RegisterApp)( + (windows_core::Interface::vtable(this).GetFeatureLevel)( windows_core::Interface::as_raw(this), - core::mem::transmute_copy(registrationid), + feature, &mut result__, ) - .and_then(|| windows_core::Type::from_abi(result__)) + .map(|| result__) } } pub fn RemoveUserAsync( &self, - agentname: &windows_core::HSTRING, + agentusername: &windows_core::HSTRING, ) -> windows_core::Result> { let this = self; unsafe { let mut result__ = core::mem::zeroed(); (windows_core::Interface::vtable(this).RemoveUserAsync)( windows_core::Interface::as_raw(this), - core::mem::transmute_copy(agentname), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } - pub fn RunProcessAsync( - &self, - agentname: &windows_core::HSTRING, - processpath: &windows_core::HSTRING, - arguments: &windows_core::HSTRING, - ) -> windows_core::Result> { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).RunProcessAsync)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(agentname), - core::mem::transmute_copy(processpath), - core::mem::transmute_copy(arguments), + core::mem::transmute_copy(agentusername), &mut result__, ) .and_then(|| windows_core::Type::from_abi(result__)) @@ -3089,7 +1366,7 @@ impl IsoSessionOps { } pub fn RunProcessWithOptionsAsync( &self, - agentname: &windows_core::HSTRING, + agentusername: &windows_core::HSTRING, processpath: &windows_core::HSTRING, arguments: &windows_core::HSTRING, options: P3, @@ -3102,7 +1379,7 @@ impl IsoSessionOps { let mut result__ = core::mem::zeroed(); (windows_core::Interface::vtable(this).RunProcessWithOptionsAsync)( windows_core::Interface::as_raw(this), - core::mem::transmute_copy(agentname), + core::mem::transmute_copy(agentusername), core::mem::transmute_copy(processpath), core::mem::transmute_copy(arguments), options.param().abi(), @@ -3113,16 +1390,16 @@ impl IsoSessionOps { } pub fn StartSessionAsync( &self, - agentname: &windows_core::HSTRING, - configid: IsoSessionConfigId, + agentusername: &windows_core::HSTRING, + optwamtoken: &windows_core::HSTRING, ) -> windows_core::Result> { let this = self; unsafe { let mut result__ = core::mem::zeroed(); (windows_core::Interface::vtable(this).StartSessionAsync)( windows_core::Interface::as_raw(this), - core::mem::transmute_copy(agentname), - configid, + core::mem::transmute_copy(agentusername), + core::mem::transmute_copy(optwamtoken), &mut result__, ) .and_then(|| windows_core::Type::from_abi(result__)) @@ -3130,152 +1407,19 @@ impl IsoSessionOps { } pub fn StopSessionAsync( &self, - agentname: &windows_core::HSTRING, + agentusername: &windows_core::HSTRING, ) -> windows_core::Result> { let this = self; unsafe { let mut result__ = core::mem::zeroed(); (windows_core::Interface::vtable(this).StopSessionAsync)( windows_core::Interface::as_raw(this), - core::mem::transmute_copy(agentname), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } - pub fn UnregisterAppAsync( - &self, - registrationid: &windows_core::HSTRING, - ) -> windows_core::Result> { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).UnregisterAppAsync)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(registrationid), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } - pub fn ShareFolderBatchAsync( - &self, - agentname: &windows_core::HSTRING, - requests: P1, - ) -> windows_core::Result< - windows_future::IAsyncOperation< - windows_collections::IVectorView, - >, - > - where - P1: windows_core::Param>, - { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).ShareFolderBatchAsync)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(agentname), - requests.param().abi(), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } - pub fn AddUserAsync2( - &self, - registrationid: &windows_core::HSTRING, - entraaccountname: &windows_core::HSTRING, - wamtoken: &windows_core::HSTRING, - ) -> windows_core::Result> { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AddUserAsync2)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(registrationid), - core::mem::transmute_copy(entraaccountname), - core::mem::transmute_copy(wamtoken), - &mut result__, - ) - .and_then(|| windows_core::Type::from_abi(result__)) - } - } - pub fn StartSessionAsync2( - &self, - entraaccountname: &windows_core::HSTRING, - configid: IsoSessionConfigId, - wamtoken: &windows_core::HSTRING, - ) -> windows_core::Result> { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).StartSessionAsync2)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(entraaccountname), - configid, - core::mem::transmute_copy(wamtoken), + core::mem::transmute_copy(agentusername), &mut result__, ) .and_then(|| windows_core::Type::from_abi(result__)) } } - pub fn GetAgentUsers( - &self, - registrationid: &windows_core::HSTRING, - includeinactive: bool, - ) -> windows_core::Result> { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - let mut result__ = core::mem::MaybeUninit::zeroed(); - (windows_core::Interface::vtable(this).GetAgentUsers)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(registrationid), - includeinactive, - windows_core::Array::::set_abi_len(core::mem::transmute( - &mut result__, - )), - result__.as_mut_ptr() as *mut _ as _, - ) - .map(|| result__.assume_init()) - } - } - pub fn GetAllAgentUsers( - &self, - includeinactive: bool, - ) -> windows_core::Result> { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - let mut result__ = core::mem::MaybeUninit::zeroed(); - (windows_core::Interface::vtable(this).GetAllAgentUsers)( - windows_core::Interface::as_raw(this), - includeinactive, - windows_core::Array::::set_abi_len(core::mem::transmute( - &mut result__, - )), - result__.as_mut_ptr() as *mut _ as _, - ) - .map(|| result__.assume_init()) - } - } - pub fn GetProcesses( - &self, - agentname: &windows_core::HSTRING, - ) -> windows_core::Result> { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - let mut result__ = core::mem::MaybeUninit::zeroed(); - (windows_core::Interface::vtable(this).GetProcesses)( - windows_core::Interface::as_raw(this), - core::mem::transmute_copy(agentname), - windows_core::Array::::set_abi_len(core::mem::transmute( - &mut result__, - )), - result__.as_mut_ptr() as *mut _ as _, - ) - .map(|| result__.assume_init()) - } - } } impl windows_core::RuntimeType for IsoSessionOps { const SIGNATURE: windows_core::imp::ConstBuffer = @@ -3286,7 +1430,7 @@ unsafe impl windows_core::Interface for IsoSessionOps { const IID: windows_core::GUID = ::IID; } impl windows_core::RuntimeName for IsoSessionOps { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionOps"; + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IsoSessionOps"; } unsafe impl Send for IsoSessionOps {} unsafe impl Sync for IsoSessionOps {} @@ -3298,31 +1442,31 @@ windows_core::imp::interface_hierarchy!( windows_core::IUnknown, windows_core::IInspectable ); -windows_core::imp::required_hierarchy!(IsoSessionProcess, IClosable); +windows_core::imp::required_hierarchy!(IsoSessionProcess, windows::Foundation::IClosable); impl IsoSessionProcess { pub fn Close(&self) -> windows_core::Result<()> { - let this = &windows_core::Interface::cast::(self)?; + let this = &windows_core::Interface::cast::(self)?; unsafe { (windows_core::Interface::vtable(this).Close)(windows_core::Interface::as_raw(this)) .ok() } } - pub fn ExitCode(&self) -> windows_core::Result { + pub fn ErrorHandle(&self) -> windows_core::Result { let this = self; unsafe { let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).ExitCode)( + (windows_core::Interface::vtable(this).ErrorHandle)( windows_core::Interface::as_raw(this), &mut result__, ) .map(|| result__) } } - pub fn ErrorHandle(&self) -> windows_core::Result { + pub fn ExitCode(&self) -> windows_core::Result { let this = self; unsafe { let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).ErrorHandle)( + (windows_core::Interface::vtable(this).ExitCode)( windows_core::Interface::as_raw(this), &mut result__, ) @@ -3351,28 +1495,6 @@ impl IsoSessionProcess { .map(|| result__) } } - pub fn ProcessId(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).ProcessId)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| result__) - } - } - pub fn ProcessPath(&self) -> windows_core::Result { - let this = self; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).ProcessPath)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } pub fn CloseStandardInput(&self) -> windows_core::Result<()> { let this = self; unsafe { @@ -3431,7 +1553,7 @@ unsafe impl windows_core::Interface for IsoSessionProcess { const IID: windows_core::GUID = ::IID; } impl windows_core::RuntimeName for IsoSessionProcess { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionProcess"; + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IsoSessionProcess"; } unsafe impl Send for IsoSessionProcess {} unsafe impl Sync for IsoSessionProcess {} @@ -3609,7 +1731,7 @@ unsafe impl windows_core::Interface for IsoSessionProcessOptions { const IID: windows_core::GUID = ::IID; } impl windows_core::RuntimeName for IsoSessionProcessOptions { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionProcessOptions"; + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IsoSessionProcessOptions"; } unsafe impl Send for IsoSessionProcessOptions {} unsafe impl Sync for IsoSessionProcessOptions {} @@ -3654,7 +1776,7 @@ unsafe impl windows_core::Interface for IsoSessionProcessResult { const IID: windows_core::GUID = ::IID; } impl windows_core::RuntimeName for IsoSessionProcessResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionProcessResult"; + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IsoSessionProcessResult"; } unsafe impl Send for IsoSessionProcessResult {} unsafe impl Sync for IsoSessionProcessResult {} @@ -3688,7 +1810,7 @@ unsafe impl windows_core::Interface for IsoSessionResult { const IID: windows_core::GUID = ::IID; } impl windows_core::RuntimeName for IsoSessionResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionResult"; + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IsoSessionResult"; } unsafe impl Send for IsoSessionResult {} unsafe impl Sync for IsoSessionResult {} @@ -3701,33 +1823,33 @@ windows_core::imp::interface_hierarchy!( windows_core::IInspectable ); impl IsoSessionUserResult { - pub fn AgentName(&self) -> windows_core::Result { + pub fn AgentUserName(&self) -> windows_core::Result { let this = self; unsafe { let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AgentName)( + (windows_core::Interface::vtable(this).AgentUserName)( windows_core::Interface::as_raw(this), &mut result__, ) .map(|| core::mem::transmute(result__)) } } - pub fn AgentUserName(&self) -> windows_core::Result { + pub fn AgentUserSid(&self) -> windows_core::Result { let this = self; unsafe { let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AgentUserName)( + (windows_core::Interface::vtable(this).AgentUserSid)( windows_core::Interface::as_raw(this), &mut result__, ) .map(|| core::mem::transmute(result__)) } } - pub fn AgentUserSid(&self) -> windows_core::Result { + pub fn EphemeralWorkspacePath(&self) -> windows_core::Result { let this = self; unsafe { let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).AgentUserSid)( + (windows_core::Interface::vtable(this).EphemeralWorkspacePath)( windows_core::Interface::as_raw(this), &mut result__, ) @@ -3745,17 +1867,6 @@ impl IsoSessionUserResult { .and_then(|| windows_core::Type::from_abi(result__)) } } - pub fn RegistrationId(&self) -> windows_core::Result { - let this = &windows_core::Interface::cast::(self)?; - unsafe { - let mut result__ = core::mem::zeroed(); - (windows_core::Interface::vtable(this).RegistrationId)( - windows_core::Interface::as_raw(this), - &mut result__, - ) - .map(|| core::mem::transmute(result__)) - } - } } impl windows_core::RuntimeType for IsoSessionUserResult { const SIGNATURE: windows_core::imp::ConstBuffer = @@ -3766,7 +1877,7 @@ unsafe impl windows_core::Interface for IsoSessionUserResult { const IID: windows_core::GUID = ::IID; } impl windows_core::RuntimeName for IsoSessionUserResult { - const NAME: &'static str = "Windows.AI.IsolationSession.IsoSessionUserResult"; + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IsoSessionUserResult"; } unsafe impl Send for IsoSessionUserResult {} unsafe impl Sync for IsoSessionUserResult {} diff --git a/src/backends/isolation_session/bindings/src/lib.rs b/src/backends/isolation_session/bindings/src/lib.rs index 718a4177b..afbe414d5 100644 --- a/src/backends/isolation_session/bindings/src/lib.rs +++ b/src/backends/isolation_session/bindings/src/lib.rs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Generated WinRT bindings for the IsoEnvBroker Session API. +//! Generated WinRT bindings for the IsolationSession Preview API. //! //! This crate contains Rust projections generated from the -//! `Windows.AI.IsolationSession` WinMD using `windows-bindgen`. +//! `Windows.AI.IsolationSession.Preview` WinMD using `windows-bindgen`. //! //! See `external/windows-sdk/isolation-session/GENERATION_INFO.toml` //! for provenance details. diff --git a/src/backends/isolation_session/common/Cargo.toml b/src/backends/isolation_session/common/Cargo.toml index 97bd7a2b8..f4f7ab0c5 100644 --- a/src/backends/isolation_session/common/Cargo.toml +++ b/src/backends/isolation_session/common/Cargo.toml @@ -8,6 +8,9 @@ license.workspace = true wxc_common = { workspace = true } serde = { workspace = true } +[dev-dependencies] +serde_json = { workspace = true } + [target.'cfg(target_os = "windows")'.dependencies] windows = { workspace = true } windows-core = { workspace = true } diff --git a/src/backends/isolation_session/common/src/error.rs b/src/backends/isolation_session/common/src/error.rs index 89599f6ed..5e5404003 100644 --- a/src/backends/isolation_session/common/src/error.rs +++ b/src/backends/isolation_session/common/src/error.rs @@ -15,14 +15,14 @@ pub(super) enum IsolationSessionError { /// Caller-supplied container policy carries a field this backend does /// not support (filesystem rules, network rules, proxy). Policy(String), - /// The in-proc `Windows.AI.IsolationSession` `IsoSessionOps` API is not + /// The in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` API is not /// available on this host (DLL not registered or the OS feature gate /// is off). ServiceUnavailable(String), - /// A lifecycle step (register / provision / start / exec / stop / - /// deprovision) returned a failure from the OS API. + /// A lifecycle step (provision / start / exec / stop / deprovision) + /// returned a failure from the OS API. Lifecycle(String), - /// The OS API could not find the provisionId — the sandbox has been + /// The OS API could not find the agent user — the sandbox has been /// deprovisioned (or never existed in this user's session). Surfaces /// as `MxcError::StaleId` at the dispatch boundary. Stale(String), @@ -53,7 +53,7 @@ pub(super) fn lifecycle_err(msg: impl Into) -> IsolationSessionError { /// `HRESULT_FROM_WIN32(ERROR_NOT_FOUND)`. Every non-provision lifecycle op /// (start / exec / stop / deprovision) surfaces this HRESULT when the -/// provisionId is unknown to the OS API; we promote it to `Stale` so a +/// agent user is unknown to the OS API; we promote it to `Stale` so a /// deprovisioned `sandbox_id` reads as `MxcError::StaleId` at the dispatch /// boundary, not a generic backend error. const ERROR_NOT_FOUND_HRESULT: u32 = 0x80070490; diff --git a/src/backends/isolation_session/common/src/folder_sharing.rs b/src/backends/isolation_session/common/src/folder_sharing.rs deleted file mode 100644 index 682674e26..000000000 --- a/src/backends/isolation_session/common/src/folder_sharing.rs +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Folder-sharing helpers for `ShareFolderBatchAsync`. The batch call does -//! not fail as a whole on per-path errors; aggregation is where per-path -//! failures become a single MXC error. - -use std::fmt::Write; - -use isolation_session_bindings::bindings::{ - IsoSessionFolderSharingAccessLevel, IsoSessionFolderSharingRequest, - IsoSessionFolderSharingResult, IsoSessionFolderSharingStatus, -}; -use windows_collections::IVectorView; -use windows_core::HSTRING; - -use super::error::{lifecycle_err, IsolationSessionError}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) struct ShareFolderFailure { - pub message: String, - pub remediation: String, - pub hresult: u32, -} - -/// Per-path outcome from a folder-share batch. The WinRT batch result type -/// can't be built in unit tests; this struct is the test-friendly -/// equivalent that aggregation logic operates on. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) struct ShareFolderOutcome { - pub folder_path: String, - /// `Some` iff the per-path status was `Failed`. - pub failure: Option, -} - -/// Builds the per-path WinRT requests with rw paths first, ro paths second. -/// A path appearing in both slices ends up read-only (the ro request is -/// applied second and overwrites the earlier rw ACE for the same SID). -/// Callers should keep the slices disjoint to avoid relying on this. -pub(super) fn build_share_folder_requests( - rw: &[String], - ro: &[String], -) -> Vec { - let mut requests = Vec::with_capacity(rw.len() + ro.len()); - for path in rw { - requests.push(IsoSessionFolderSharingRequest { - FolderPath: HSTRING::from(path), - AccessLevel: IsoSessionFolderSharingAccessLevel::ReadWrite, - }); - } - for path in ro { - requests.push(IsoSessionFolderSharingRequest { - FolderPath: HSTRING::from(path), - AccessLevel: IsoSessionFolderSharingAccessLevel::Read, - }); - } - requests -} - -/// Extracts MXC-internal per-path outcomes from the WinRT result vector. -pub(super) fn extract_share_folder_outcomes( - results: &IVectorView, -) -> Result, IsolationSessionError> { - let size = results - .Size() - .map_err(|e| lifecycle_err(format!("ShareFolderBatch results.Size: {}", e)))?; - let mut outcomes = Vec::with_capacity(size as usize); - for i in 0..size { - let result = results - .GetAt(i) - .map_err(|e| lifecycle_err(format!("ShareFolderBatch results.GetAt({}): {}", i, e)))?; - let folder_path = result - .FolderPath() - .map_err(|e| lifecycle_err(format!("ShareFolderBatch result.FolderPath: {}", e)))? - .to_string(); - let status = result - .Status() - .map_err(|e| lifecycle_err(format!("ShareFolderBatch result.Status: {}", e)))?; - let failure = if status == IsoSessionFolderSharingStatus::Failed { - let err = result - .Error() - .map_err(|e| lifecycle_err(format!("ShareFolderBatch result.Error: {}", e)))?; - Some(ShareFolderFailure { - message: err.Message().map(|h| h.to_string()).unwrap_or_default(), - remediation: err.Remediation().map(|h| h.to_string()).unwrap_or_default(), - hresult: err.Code().map(|h| h.0 as u32).unwrap_or(0), - }) - } else { - None - }; - outcomes.push(ShareFolderOutcome { - folder_path, - failure, - }); - } - Ok(outcomes) -} - -/// Aggregates per-path outcomes into a single `Result`. Ok iff every path -/// succeeded; otherwise a `Lifecycle` error listing every failed path with -/// its message, HRESULT, and (when non-empty) remediation hint. -pub(super) fn aggregate_share_folder_outcomes( - outcomes: &[ShareFolderOutcome], -) -> Result<(), IsolationSessionError> { - let any_failure = outcomes.iter().any(|o| o.failure.is_some()); - if !any_failure { - return Ok(()); - } - let mut msg = String::from("ShareFolderBatchAsync had per-path failures:"); - for outcome in outcomes { - let Some(f) = &outcome.failure else { - continue; - }; - let _ = write!( - msg, - "\n {}: {} (HRESULT: {:#010x})", - outcome.folder_path, f.message, f.hresult, - ); - if !f.remediation.is_empty() { - let _ = write!(msg, " -- remediation: {}", f.remediation); - } - } - Err(IsolationSessionError::Lifecycle(msg)) -} - -#[cfg(test)] -mod tests { - use super::*; - - // The runtime path (`share_folders` itself) needs a live `IsoSessionOps`, - // exercised on the VM. These unit tests cover the two pure helpers that - // bracket the COM call: request-building and outcome aggregation. - - #[test] - fn build_requests_empty_inputs_returns_empty_vec() { - let requests = build_share_folder_requests(&[], &[]); - assert!(requests.is_empty()); - } - - #[test] - fn build_requests_rw_only() { - let rw = vec!["C:\\rw1".to_string(), "C:\\rw2".to_string()]; - let requests = build_share_folder_requests(&rw, &[]); - assert_eq!(requests.len(), 2); - assert_eq!(requests[0].FolderPath.to_string(), "C:\\rw1"); - assert_eq!( - requests[0].AccessLevel, - IsoSessionFolderSharingAccessLevel::ReadWrite - ); - assert_eq!(requests[1].FolderPath.to_string(), "C:\\rw2"); - assert_eq!( - requests[1].AccessLevel, - IsoSessionFolderSharingAccessLevel::ReadWrite - ); - } - - #[test] - fn build_requests_ro_only() { - let ro = vec!["C:\\ro1".to_string()]; - let requests = build_share_folder_requests(&[], &ro); - assert_eq!(requests.len(), 1); - assert_eq!(requests[0].FolderPath.to_string(), "C:\\ro1"); - assert_eq!( - requests[0].AccessLevel, - IsoSessionFolderSharingAccessLevel::Read - ); - } - - #[test] - fn build_requests_rw_then_ro_in_input_order() { - let rw = vec!["C:\\a".to_string()]; - let ro = vec!["C:\\b".to_string(), "C:\\c".to_string()]; - let requests = build_share_folder_requests(&rw, &ro); - assert_eq!(requests.len(), 3); - assert_eq!(requests[0].FolderPath.to_string(), "C:\\a"); - assert_eq!( - requests[0].AccessLevel, - IsoSessionFolderSharingAccessLevel::ReadWrite - ); - assert_eq!(requests[1].FolderPath.to_string(), "C:\\b"); - assert_eq!( - requests[1].AccessLevel, - IsoSessionFolderSharingAccessLevel::Read - ); - assert_eq!(requests[2].FolderPath.to_string(), "C:\\c"); - assert_eq!( - requests[2].AccessLevel, - IsoSessionFolderSharingAccessLevel::Read - ); - } - - fn ok_outcome(path: &str) -> ShareFolderOutcome { - ShareFolderOutcome { - folder_path: path.to_string(), - failure: None, - } - } - - fn fail_outcome(path: &str, msg: &str, hr: u32, remediation: &str) -> ShareFolderOutcome { - ShareFolderOutcome { - folder_path: path.to_string(), - failure: Some(ShareFolderFailure { - message: msg.to_string(), - remediation: remediation.to_string(), - hresult: hr, - }), - } - } - - #[test] - fn aggregate_empty_outcomes_is_ok() { - // Defensive: the runtime path returns Ok early on empty inputs, but - // if `extract_share_folder_outcomes` ever returns an empty Vec the - // aggregator should still report success. - assert!(matches!(aggregate_share_folder_outcomes(&[]), Ok(()))); - } - - #[test] - fn aggregate_all_succeeded_is_ok() { - let outcomes = vec![ok_outcome("C:\\a"), ok_outcome("C:\\b")]; - assert!(matches!(aggregate_share_folder_outcomes(&outcomes), Ok(()))); - } - - #[test] - fn aggregate_single_failure_includes_path_message_and_hresult() { - let outcomes = vec![fail_outcome("C:\\bad", "denied", 0x80070005, "")]; - let err = aggregate_share_folder_outcomes(&outcomes).unwrap_err(); - let IsolationSessionError::Lifecycle(msg) = err else { - panic!("expected Lifecycle, got {:?}", err); - }; - assert!(msg.contains("C:\\bad"), "missing path in: {}", msg); - assert!(msg.contains("denied"), "missing message in: {}", msg); - assert!(msg.contains("0x80070005"), "missing hresult in: {}", msg); - } - - #[test] - fn aggregate_mixed_outcomes_includes_all_failures_only() { - let outcomes = vec![ - ok_outcome("C:\\good"), - fail_outcome("C:\\bad1", "first failure", 0xdeadbeef, ""), - ok_outcome("C:\\good2"), - fail_outcome("C:\\bad2", "second failure", 0xfeedface, ""), - ]; - let err = aggregate_share_folder_outcomes(&outcomes).unwrap_err(); - let IsolationSessionError::Lifecycle(msg) = err else { - panic!("expected Lifecycle, got {:?}", err); - }; - assert!(msg.contains("C:\\bad1"), "missing bad1 in: {}", msg); - assert!( - msg.contains("first failure"), - "missing first msg in: {}", - msg - ); - assert!(msg.contains("C:\\bad2"), "missing bad2 in: {}", msg); - assert!( - msg.contains("second failure"), - "missing second msg in: {}", - msg - ); - // Successful paths must not appear in the error message. - assert!( - !msg.contains("C:\\good"), - "good path leaked into error: {}", - msg - ); - assert!( - !msg.contains("C:\\good2"), - "good2 path leaked into error: {}", - msg - ); - } - - #[test] - fn aggregate_failure_with_remediation_appends_remediation() { - let outcomes = vec![fail_outcome("C:\\rd", "denied", 0x80070005, "run as admin")]; - let err = aggregate_share_folder_outcomes(&outcomes).unwrap_err(); - let IsolationSessionError::Lifecycle(msg) = err else { - panic!("expected Lifecycle, got {:?}", err); - }; - assert!( - msg.contains("remediation: run as admin"), - "missing remediation in: {}", - msg - ); - } - - #[test] - fn aggregate_failure_with_empty_remediation_omits_suffix() { - let outcomes = vec![fail_outcome("C:\\nor", "msg", 0x80004005, "")]; - let err = aggregate_share_folder_outcomes(&outcomes).unwrap_err(); - let IsolationSessionError::Lifecycle(msg) = err else { - panic!("expected Lifecycle, got {:?}", err); - }; - assert!( - !msg.contains("remediation:"), - "unexpected remediation suffix in: {}", - msg - ); - } -} diff --git a/src/backends/isolation_session/common/src/lib.rs b/src/backends/isolation_session/common/src/lib.rs index e6f265530..15a4a6ae9 100644 --- a/src/backends/isolation_session/common/src/lib.rs +++ b/src/backends/isolation_session/common/src/lib.rs @@ -2,14 +2,14 @@ // Licensed under the MIT License. //! IsolationSession backend — executes scripts in an isolated Windows -//! session via the in-proc `Windows.AI.IsolationSession` `IsoSessionOps` +//! session via the in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` //! API. `IsolationSessionRunner` is the only externally-reachable type; //! the granular lifecycle wrapper (`IsolationSessionManager`) and helpers //! are module-private. //! //! Trait impls split by lifecycle shape: -//! - `one_shot`: `ScriptRunner` — register → provision → start → exec → -//! stop → deprovision in a single process. +//! - `one_shot`: `ScriptRunner` — provision → start → exec → stop → +//! deprovision in a single process. //! - `state_aware`: `StatefulSandboxBackend` — per-phase methods called //! across multiple `wxc-exec` invocations by an external orchestrator. @@ -20,8 +20,6 @@ mod console_relay; #[cfg(target_os = "windows")] mod error; #[cfg(target_os = "windows")] -mod folder_sharing; -#[cfg(target_os = "windows")] mod manager; #[cfg(target_os = "windows")] mod one_shot; @@ -32,10 +30,11 @@ mod policy; #[cfg(target_os = "windows")] mod process_options; #[cfg(target_os = "windows")] -mod protected_paths_filter; -#[cfg(target_os = "windows")] mod state_aware; +#[cfg(target_os = "windows")] +pub use manager::is_service_available; + /// Stateless marker type. Trait impls live in `one_shot` (`ScriptRunner`) /// and `state_aware` (`StatefulSandboxBackend`). #[cfg(target_os = "windows")] diff --git a/src/backends/isolation_session/common/src/manager.rs b/src/backends/isolation_session/common/src/manager.rs index 586aacd27..d11a5952f 100644 --- a/src/backends/isolation_session/common/src/manager.rs +++ b/src/backends/isolation_session/common/src/manager.rs @@ -2,59 +2,29 @@ // Licensed under the MIT License. //! `IsolationSessionManager` — granular wrapper over the in-proc -//! `IsoSessionOps` lifecycle. Each method maps 1:1 to a single WinRT op, -//! plus the `share_folders` non-lifecycle op. `create_process` also drives -//! the ConPTY relay setup + shutdown ladder against the local console. +//! isolation session lifecycle. Each method maps 1:1 to a single WinRT op. +//! `create_process` also drives the ConPTY relay setup + shutdown ladder +//! against the local console. -use wxc_common::models::IsolationSessionConfigurationId; use wxc_common::process_util::OwnedHandle; use isolation_session_bindings::bindings::{ - IsoSessionConfigId, IsoSessionFolderSharingRequest, IsoSessionFolderSharingResult, IsoSessionOps, IsoSessionProcess, IsoSessionProcessResult, IsoSessionUserResult, }; -use windows::Win32::Foundation::{ - CLASS_E_CLASSNOTAVAILABLE, E_NOINTERFACE, HANDLE, REGDB_E_CLASSNOTREG, -}; +use windows::Win32::Foundation::{CLASS_E_CLASSNOTAVAILABLE, HANDLE, REGDB_E_CLASSNOTREG}; use windows::Win32::System::Console::{ GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, }; use windows::Win32::System::Threading::{CreateEventW, SetEvent, WaitForSingleObject}; -use windows_collections::IVectorView; use windows_core::{HSTRING, PCWSTR}; use super::console_mode::{get_local_console_size, ConsoleModeRestorer, CtrlHandlerGuard}; use super::console_relay::{create_console_relay_thread, ConsoleRelayParams}; use super::error::{check_result, format_iso_error, lifecycle_err, IsolationSessionError}; -use super::folder_sharing::{ - aggregate_share_folder_outcomes, build_share_folder_requests, extract_share_folder_outcomes, -}; use super::pipe_relay::{ create_relay_thread, create_relay_thread_with_stop, PipeRelayParams, PipeRelayWithStopParams, }; use super::process_options::{build_iso_process_options, ProcessOptions}; -use super::protected_paths_filter::filter_protected_paths; - -/// Registration ID used with the in-proc `IsoSessionOps` API. Must be the -/// literal string `"regid"`: the in-proc API uses the same string -/// internally for every agent-name-keyed op, so registering under any -/// other value causes subsequent calls to miss the registration. Do not -/// parameterise. -/// -/// The id is effectively shared across all concurrent MXC isolation-session -/// sandboxes for the calling user; see `IsolationSessionManager::register_client` -/// (idempotent) and `unregister_client` (intentional no-op) for the -/// lifecycle implications. -const REGISTRATION_ID: &str = "regid"; - -fn to_iso_config_id(value: IsolationSessionConfigurationId) -> IsoSessionConfigId { - match value { - IsolationSessionConfigurationId::Small => IsoSessionConfigId::Small, - IsolationSessionConfigurationId::Medium => IsoSessionConfigId::Medium, - IsolationSessionConfigurationId::Large => IsoSessionConfigId::Large, - IsolationSessionConfigurationId::Composable => IsoSessionConfigId::Composable, - } -} /// Activates the in-proc `IsoSessionOps` factory and returns the instance. fn check_service_available_and_activate() -> Result { @@ -64,9 +34,9 @@ fn check_service_available_and_activate() -> Result Result bool { + check_service_available_and_activate().is_ok() +} + +/// The provision-time facts the OS assigns to a freshly-created agent user, +/// read from `IsoSessionUserResult` at `add_user`. The addressing key for +/// every later lifecycle op remains `agent_user_name`; the other two fields +/// are provision metadata surfaced to the caller. +pub(super) struct ProvisionedUser { + /// The OS-assigned agent account name (also the `sandboxId` tail). + pub agent_user_name: String, + /// The security identifier (SID) of the agent user. Diagnostic only. + pub agent_user_sid: String, + /// A directory shared between the calling user and this isolated agent + /// user, through which the caller can stage files into the session. Each + /// isolated user can access only its own workspace; the caller can access + /// all of them. Deleted when the agent user is deprovisioned. + pub ephemeral_workspace_path: String, +} + +/// Manages the isolation session lifecycle. Methods map 1:1 to the granular /// API steps. pub struct IsolationSessionManager { - /// Registration identifier used in `RegisterApp` / `AddUserAsync` / - /// `UnregisterAppAsync`. Pegged to the literal `"regid"` — required - /// by the OS API. - registration_id: HSTRING, - /// Provision identifier. Used as `provisionId` to `AddUserAsync` and - /// as the `agentName` argument to every subsequent op (the OS API - /// aliases them at the COM layer). - provision_id: HSTRING, - /// The activated `IsoSessionOps` instance. Held for the manager's - /// lifetime so the WinRT factory is reused across calls. + /// The OS-assigned agent user name returned by `add_user`. Used as the + /// `agentUserName` argument to every subsequent lifecycle op. + agent_user_name: HSTRING, + /// The activated service instance. Held for the manager's lifetime so + /// the WinRT factory is reused across calls. ops: IsoSessionOps, } impl IsolationSessionManager { - /// Activates the `IsoSessionOps` factory, verifies the service is - /// available, and pegs the manager to the supplied `provisionId`. - /// Both one-shot and state-aware callers mint a dynamic id per - /// invocation (e.g. `wxc-<8-hex>`). - pub(super) fn new(provision_id: &str) -> Result { + /// Pegs a manager to an existing OS-assigned agent user name (the value + /// returned by `add_user`). Activates the service factory once and + /// reuses it for the manager's lifetime. + pub(super) fn new(agent_user_name: &str) -> Result { let ops = check_service_available_and_activate()?; Ok(Self { - registration_id: HSTRING::from(REGISTRATION_ID), - provision_id: HSTRING::from(provision_id), + agent_user_name: HSTRING::from(agent_user_name), ops, }) } - /// Registers the app with the OS API. Safe to call repeatedly with - /// the same regid — the OS API treats duplicates as success. - pub(super) fn register_client(&self) -> Result<(), IsolationSessionError> { - let result = self - .ops - .RegisterApp(&self.registration_id) - .map_err(|e| lifecycle_err(format!("RegisterApp call failed: {}", e)))?; - check_result(&result, "RegisterApp") - } - - /// Step 1: Provision an agent user. Returns the OS-assigned agent - /// account name for logging only — addressing for subsequent ops - /// continues to use the configured `provision_id`. + /// Provisions an agent user and returns its provision-time facts (the + /// OS-assigned account name — which addresses every subsequent lifecycle + /// op — plus the agent SID and the shared ephemeral workspace path). + /// + /// Pass empty strings for a local agent user, or the Entra account name + /// and its WAM token for an Entra-backed agent; the OS validates + /// token/identity consistency. Because the account name is not known + /// until this returns, the caller constructs the manager via `new` + /// afterward — hence an associated function rather than a method. /// /// Note: `lifecycle.destroyOnExit` is silently ignored on this backend. - /// The in-proc API hardcodes `Indefinite` lifetime in `AddUserAsync`. - pub(super) fn provision_agent_user(&self) -> Result { - let async_op = self - .ops - .AddUserAsync(&self.registration_id, &self.provision_id) + /// The in-proc API hardcodes `Indefinite` lifetime. + pub(super) fn add_user( + opt_entra_account_name: &str, + opt_wam_token: &str, + ) -> Result { + let ops = check_service_available_and_activate()?; + let async_op = ops + .AddUserAsync( + &HSTRING::from(opt_entra_account_name), + &HSTRING::from(opt_wam_token), + ) .map_err(|e| lifecycle_err(format!("AddUserAsync call failed: {}", e)))?; let user_result: IsoSessionUserResult = async_op .join() @@ -144,100 +132,34 @@ impl IsolationSessionManager { return Err(format_iso_error("AddUserAsync", &err)); } - let name = user_result + let agent_user_name = user_result .AgentUserName() .map_err(|e| lifecycle_err(format!("AddUserAsync: get AgentUserName failed: {}", e)))?; - Ok(name.to_string()) - } - - /// Step 1 (Entra): Provision an agent user backed by Entra cloud - /// credentials. Calls `IIsoSessionOps2::AddUserAsync2` with the - /// caller-supplied `wam_token`. Returns `ServiceUnavailable` when the - /// host OS lacks the v2 interface; the caller does not fall back to v1. - pub(super) fn provision_agent_user_v2( - &self, - wam_token: &str, - ) -> Result { - let async_op = match self.ops.AddUserAsync2( - &self.registration_id, - &self.provision_id, - &HSTRING::from(wam_token), - ) { - Ok(op) => op, - Err(e) if e.code() == E_NOINTERFACE => { - return Err(IsolationSessionError::ServiceUnavailable( - "IsoSessionOps2 (Entra agent support) is not available on this OS build" - .to_string(), - )); - } - Err(e) => return Err(lifecycle_err(format!("AddUserAsync2 call failed: {}", e))), - }; - let user_result: IsoSessionUserResult = async_op - .join() - .map_err(|e| lifecycle_err(format!("AddUserAsync2 wait failed: {}", e)))?; - - let err = user_result - .Error() - .map_err(|e| lifecycle_err(format!("AddUserAsync2: get Error failed: {}", e)))?; - let is_error = err - .IsError() - .map_err(|e| lifecycle_err(format!("AddUserAsync2: get IsError failed: {}", e)))?; - if is_error { - return Err(format_iso_error("AddUserAsync2", &err)); - } - - let name = user_result.AgentUserName().map_err(|e| { - lifecycle_err(format!("AddUserAsync2: get AgentUserName failed: {}", e)) + let agent_user_sid = user_result + .AgentUserSid() + .map_err(|e| lifecycle_err(format!("AddUserAsync: get AgentUserSid failed: {}", e)))?; + let ephemeral_workspace_path = user_result.EphemeralWorkspacePath().map_err(|e| { + lifecycle_err(format!( + "AddUserAsync: get EphemeralWorkspacePath failed: {}", + e + )) })?; - Ok(name.to_string()) - } - /// Grants the agent user access to host folders. `readwrite_paths` get - /// read+write access, `readonly_paths` get read-only. Both apply - /// recursively to each subtree. - /// - /// Independent of session start: requires only that the agent user - /// exists (call after `provision_agent_user`, before - /// `deprovision_agent_user`). - /// - /// The MXC process needs `WRITE_DAC` on each target folder. Returns - /// `Ok` on all-success; on any per-path failure returns a `Lifecycle` - /// error listing every failed path. Empty input on both slices is a - /// no-op. - pub(super) fn share_folders( - &self, - readwrite_paths: &[String], - readonly_paths: &[String], - logger: Option<&mut wxc_common::logger::Logger>, - ) -> Result<(), IsolationSessionError> { - // Emergency mitigation (MXC issue #330): drop protected paths - // before forwarding. See `protected_paths_filter.rs`. - let (rw_kept, ro_kept) = filter_protected_paths(readwrite_paths, readonly_paths, logger); - let requests = build_share_folder_requests(&rw_kept, &ro_kept); - if requests.is_empty() { - return Ok(()); - } - let view: IVectorView = requests.into(); - let async_op = self - .ops - .ShareFolderBatchAsync(&self.provision_id, &view) - .map_err(|e| lifecycle_err(format!("ShareFolderBatchAsync call failed: {}", e)))?; - let results: IVectorView = async_op - .join() - .map_err(|e| lifecycle_err(format!("ShareFolderBatchAsync wait failed: {}", e)))?; - let outcomes = extract_share_folder_outcomes(&results)?; - aggregate_share_folder_outcomes(&outcomes) + Ok(ProvisionedUser { + agent_user_name: agent_user_name.to_string(), + agent_user_sid: agent_user_sid.to_string(), + ephemeral_workspace_path: ephemeral_workspace_path.to_string(), + }) } - /// Step 2: Start the isolation session. - pub(super) fn start_session( - &self, - config_id: IsolationSessionConfigurationId, - ) -> Result<(), IsolationSessionError> { - let cfg: IsoSessionConfigId = to_iso_config_id(config_id); + /// Step 2: Start the isolation session for the pegged agent user. + /// + /// `opt_wam_token` is empty for a local agent or the Entra WAM token for + /// an Entra-backed agent. + pub(super) fn start_session(&self, opt_wam_token: &str) -> Result<(), IsolationSessionError> { let async_op = self .ops - .StartSessionAsync(&self.provision_id, cfg) + .StartSessionAsync(&self.agent_user_name, &HSTRING::from(opt_wam_token)) .map_err(|e| lifecycle_err(format!("StartSessionAsync call failed: {}", e)))?; let result = async_op .join() @@ -245,40 +167,6 @@ impl IsolationSessionManager { check_result(&result, "StartSessionAsync") } - /// Step 2 (Entra): Start an Entra-backed isolation session via - /// `IIsoSessionOps2::StartSessionAsync2`. Returns `ServiceUnavailable` - /// when the host OS lacks the v2 interface. - pub(super) fn start_session_v2( - &self, - config_id: IsolationSessionConfigurationId, - wam_token: &str, - ) -> Result<(), IsolationSessionError> { - let cfg: IsoSessionConfigId = to_iso_config_id(config_id); - let async_op = - match self - .ops - .StartSessionAsync2(&self.provision_id, cfg, &HSTRING::from(wam_token)) - { - Ok(op) => op, - Err(e) if e.code() == E_NOINTERFACE => { - return Err(IsolationSessionError::ServiceUnavailable( - "IsoSessionOps2 (Entra agent support) is not available on this OS build" - .to_string(), - )); - } - Err(e) => { - return Err(lifecycle_err(format!( - "StartSessionAsync2 call failed: {}", - e - ))); - } - }; - let result = async_op - .join() - .map_err(|e| lifecycle_err(format!("StartSessionAsync2 wait failed: {}", e)))?; - check_result(&result, "StartSessionAsync2") - } - /// Step 3: Create a process inside the started isolation session. /// Output is streamed live to wxc-exec's stdio via internal relay /// threads; only the exit code is returned to the caller. @@ -291,7 +179,7 @@ impl IsolationSessionManager { let async_op = self .ops .RunProcessWithOptionsAsync( - &self.provision_id, + &self.agent_user_name, &HSTRING::from(&options.process_path), &HSTRING::from(&options.arguments), &proc_options, @@ -543,7 +431,7 @@ impl IsolationSessionManager { pub(super) fn stop_session(&self) -> Result<(), IsolationSessionError> { let async_op = self .ops - .StopSessionAsync(&self.provision_id) + .StopSessionAsync(&self.agent_user_name) .map_err(|e| lifecycle_err(format!("StopSessionAsync call failed: {}", e)))?; let result = async_op .join() @@ -555,34 +443,13 @@ impl IsolationSessionManager { pub(super) fn deprovision_agent_user(&self) -> Result<(), IsolationSessionError> { let async_op = self .ops - .RemoveUserAsync(&self.provision_id) + .RemoveUserAsync(&self.agent_user_name) .map_err(|e| lifecycle_err(format!("RemoveUserAsync call failed: {}", e)))?; let result = async_op .join() .map_err(|e| lifecycle_err(format!("RemoveUserAsync wait failed: {}", e)))?; check_result(&result, "RemoveUserAsync") } - - /// Tears down the client registration set up by `register_client` — - /// currently a no-op. - pub(super) fn unregister_client(&self) -> Result<(), IsolationSessionError> { - // Intentional no-op. The `"regid"` literal is shared across all - // concurrent MXC isolation-session sandboxes for the calling user; - // calling `UnregisterAppAsync` would tear down the registration for - // every other still-running one. Reversible when the OS API - // eliminates registration IDs entirely; do not uncomment without - // verifying OS API behavior has changed. - // - // let async_op = self - // .ops - // .UnregisterAppAsync(&self.registration_id) - // .map_err(|e| lifecycle_err(format!("UnregisterAppAsync call failed: {}", e)))?; - // let result = async_op - // .join() - // .map_err(|e| lifecycle_err(format!("UnregisterAppAsync wait failed: {}", e)))?; - // check_result(&result, "UnregisterAppAsync") - Ok(()) - } } /// Three-tier graceful shutdown for an `IsoSessionProcess` that's still @@ -662,34 +529,4 @@ mod tests { } } } - - // The `to_iso_config_id` free function is the sole bridge between - // MXC's internal enum and the WinRT enum. If a new variant is added - // to either side without updating the function, these tests catch - // the drift. - - #[test] - fn config_id_conversion_small() { - let iso_id: IsoSessionConfigId = to_iso_config_id(IsolationSessionConfigurationId::Small); - assert_eq!(iso_id, IsoSessionConfigId::Small); - } - - #[test] - fn config_id_conversion_medium() { - let iso_id: IsoSessionConfigId = to_iso_config_id(IsolationSessionConfigurationId::Medium); - assert_eq!(iso_id, IsoSessionConfigId::Medium); - } - - #[test] - fn config_id_conversion_large() { - let iso_id: IsoSessionConfigId = to_iso_config_id(IsolationSessionConfigurationId::Large); - assert_eq!(iso_id, IsoSessionConfigId::Large); - } - - #[test] - fn config_id_conversion_composable() { - let iso_id: IsoSessionConfigId = - to_iso_config_id(IsolationSessionConfigurationId::Composable); - assert_eq!(iso_id, IsoSessionConfigId::Composable); - } } diff --git a/src/backends/isolation_session/common/src/one_shot.rs b/src/backends/isolation_session/common/src/one_shot.rs index 166535b54..c510867ec 100644 --- a/src/backends/isolation_session/common/src/one_shot.rs +++ b/src/backends/isolation_session/common/src/one_shot.rs @@ -2,15 +2,14 @@ // Licensed under the MIT License. //! `ScriptRunner` impl for `IsolationSessionRunner`. Runs the full -//! register → provision → share → start → exec → stop → deprovision -//! lifecycle in a single process. +//! provision → start → exec → stop → deprovision lifecycle in a single +//! process. use std::fmt::Write; use std::io::IsTerminal; -use wxc_common::id::mint_random_token; use wxc_common::logger::Logger; -use wxc_common::models::{ExecutionRequest, IsolationSessionConfigurationId, ScriptResponse}; +use wxc_common::models::{ExecutionRequest, ScriptResponse}; use wxc_common::script_runner::ScriptRunner; use super::manager::IsolationSessionManager; @@ -53,55 +52,31 @@ impl ScriptRunner for IsolationSessionRunner { let _ = writeln!(logger, "Isolation Session: arguments={}", options.arguments); let _ = writeln!(logger, "Isolation Session: interactive={}", interactive); - let session_cfg = request.experimental.isolation_session.as_ref(); - let config_id: IsolationSessionConfigurationId = session_cfg - .map(|cfg| cfg.configuration_id) - .unwrap_or_default(); + // One-shot runs are local agent users only (state-aware handles + // Entra). Provision returns the OS-assigned account name; the + // manager is then pegged to it for the rest of the lifecycle. + let agent_user_name = match IsolationSessionManager::add_user("", "") { + Ok(provisioned) => { + let _ = writeln!( + logger, + "Isolation Session: agent user = {}", + provisioned.agent_user_name + ); + provisioned.agent_user_name + } + Err(e) => return e.into(), + }; - // Mint a per-invocation provisionId so concurrent MXC - // isolation-session processes do not collide on agent identity. - let provision_id = format!("wxc-{}", mint_random_token()); - let manager = match IsolationSessionManager::new(&provision_id) { + let manager = match IsolationSessionManager::new(&agent_user_name) { Ok(m) => m, Err(e) => return e.into(), }; - if let Err(e) = manager.register_client() { - return e.into(); - } - - match manager.provision_agent_user() { - Ok(agent_name) => { - let _ = writeln!(logger, "Isolation Session: agent user = {}", agent_name); - } - Err(e) => { - // provision_agent_user may return Err *after* a successful - // OS-side provision (e.g., the AgentUserName fetch fails on - // a non-error result). Defensively deprovision so an - // Indefinite-lifetime agent user does not leak. - // No-ops on absent state. - let _ = manager.deprovision_agent_user(); - let _ = manager.unregister_client(); - return e.into(); - } - } - - if let Err(e) = manager.share_folders( - &request.policy.readwrite_paths, - &request.policy.readonly_paths, - Some(logger), - ) { - let _ = manager.deprovision_agent_user(); - let _ = manager.unregister_client(); - return e.into(); - } - - if let Err(e) = manager.start_session(config_id) { + if let Err(e) = manager.start_session("") { // Provision succeeded; start did not. Clean up. stop_session // is a no-op on an unstarted session. let _ = manager.stop_session(); let _ = manager.deprovision_agent_user(); - let _ = manager.unregister_client(); return e.into(); } @@ -110,7 +85,6 @@ impl ScriptRunner for IsolationSessionRunner { Err(e) => { let _ = manager.stop_session(); let _ = manager.deprovision_agent_user(); - let _ = manager.unregister_client(); return e.into(); } }; @@ -121,9 +95,6 @@ impl ScriptRunner for IsolationSessionRunner { if let Err(e) = manager.deprovision_agent_user() { let _ = writeln!(logger, "Warning: deprovision_agent_user failed: {}", e); } - if let Err(e) = manager.unregister_client() { - let _ = writeln!(logger, "Warning: unregister_client failed: {}", e); - } // Output already streamed live to wxc-exec's stdio via relay // threads in `create_process` — captured fields intentionally @@ -157,7 +128,6 @@ mod tests { experimental: ExperimentalConfig { isolation_session: Some(IsolationSessionConfig { user: Some(well_formed_user()), - ..Default::default() }), ..Default::default() }, diff --git a/src/backends/isolation_session/common/src/policy.rs b/src/backends/isolation_session/common/src/policy.rs index 0da250b3f..94d10f94a 100644 --- a/src/backends/isolation_session/common/src/policy.rs +++ b/src/backends/isolation_session/common/src/policy.rs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Policy validation for the IsolationSession backend. Filesystem `rw` and -//! `ro` paths are honored at provision (applied via `share_folders`); every -//! other filesystem field is rejected, and network / proxy policy is -//! rejected at every phase — the backend has no equivalent primitive. +//! Policy validation for the IsolationSession backend. Every filesystem +//! field (`rw`, `ro`, `denied`) and all network / proxy policy is rejected +//! at every phase — the backend has no host-folder-sharing, network, or +//! proxy primitive. use wxc_common::models::{ExecutionRequest, IsolationSessionUser, NetworkPolicy}; use wxc_common::mxc_error::MxcError; @@ -16,18 +16,14 @@ const ERR_FILESYSTEM_POLICY: &str = const ERR_NETWORK_POLICY: &str = "network policy is not supported by the isolation session backend"; const ERR_PROXY_POLICY: &str = "network proxy is not supported by the isolation session backend"; -/// Validates the request for the provision phase. `rw` and `ro` paths are -/// honored (applied later via `share_folders`); `denied_paths` is rejected -/// because the underlying API has no equivalent primitive. +/// Validates the request for the provision phase. The isolation session +/// backend has no host-folder-sharing primitive, so every filesystem field +/// (`rw`, `ro`, `denied`) is rejected — identically to the post-provision +/// phases. pub(super) fn validate_provision_policy( request: &ExecutionRequest, ) -> Result<(), IsolationSessionError> { - if !request.policy.denied_paths.is_empty() { - return Err(IsolationSessionError::Policy( - ERR_FILESYSTEM_POLICY.to_string(), - )); - } - validate_network_and_proxy_policy(request) + validate_post_provision_policy(request) } /// Validates the request for any non-provision phase. All filesystem fields @@ -108,7 +104,7 @@ mod tests { // every branch of the shared helper runs at least once. #[test] - fn provision_policy_accepts_readwrite_paths() { + fn provision_policy_rejects_readwrite_paths() { let request = ExecutionRequest { policy: ContainerPolicy { readwrite_paths: vec!["C:\\src".to_string()], @@ -116,11 +112,14 @@ mod tests { }, ..Default::default() }; - assert!(validate_provision_policy(&request).is_ok()); + assert_policy_err_contains( + validate_provision_policy(&request).unwrap_err(), + ERR_FILESYSTEM_POLICY, + ); } #[test] - fn provision_policy_accepts_readonly_paths() { + fn provision_policy_rejects_readonly_paths() { let request = ExecutionRequest { policy: ContainerPolicy { readonly_paths: vec!["C:\\data".to_string()], @@ -128,11 +127,14 @@ mod tests { }, ..Default::default() }; - assert!(validate_provision_policy(&request).is_ok()); + assert_policy_err_contains( + validate_provision_policy(&request).unwrap_err(), + ERR_FILESYSTEM_POLICY, + ); } #[test] - fn provision_policy_accepts_readwrite_and_readonly_together() { + fn provision_policy_rejects_readwrite_and_readonly_together() { let request = ExecutionRequest { policy: ContainerPolicy { readwrite_paths: vec!["C:\\src".to_string()], @@ -141,7 +143,10 @@ mod tests { }, ..Default::default() }; - assert!(validate_provision_policy(&request).is_ok()); + assert_policy_err_contains( + validate_provision_policy(&request).unwrap_err(), + ERR_FILESYSTEM_POLICY, + ); } #[test] diff --git a/src/backends/isolation_session/common/src/protected_paths_filter.rs b/src/backends/isolation_session/common/src/protected_paths_filter.rs deleted file mode 100644 index d45215cd2..000000000 --- a/src/backends/isolation_session/common/src/protected_paths_filter.rs +++ /dev/null @@ -1,509 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Emergency mitigation (MXC issue #330) — silently drops a fixed set of -//! system-folder paths from filesystem-policy requests before they reach -//! `ShareFolderBatchAsync`. The OS API applies grants with subtree -//! inheritance, so granting these top-level folders propagates an agent -//! SID's ACE through their entire subtrees — catastrophic for drive roots, -//! the Windows directory, the Users root, ProgramFiles, and ProgramData. -//! -//! The proper fix belongs in the OS API. When that lands, delete this -//! file and the call site in `manager.rs::share_folders`. -//! -//! Known not covered (caller would have to actively pursue these spellings -//! to evade the text match): 8.3 short names (e.g. `PROGRA~1`), symlinks / -//! junctions (no `canonicalize` disk access), UNC paths, -//! `CommonProgramFiles` / `CommonProgramFiles(x86)`. The `\\?\` long-path -//! prefix on a disk path (`\\?\C:\foo`) IS handled — it normalizes to the -//! same canonical form as `C:\foo` so it cannot be used as a textual bypass. - -use std::collections::HashSet; -use std::fmt::Write; -use std::sync::OnceLock; - -use wxc_common::logger::Logger; - -/// Returns the lazily-computed, cached set of canonical paths the filter -/// rejects. Contents: -/// -/// - 26 drive roots (`A:\` through `Z:\`) — static, no `GetLogicalDrives` -/// call so future-mounted drives are also caught. -/// - Normalized env-var-derived paths: `SystemRoot`, parent of `USERPROFILE`, -/// `ProgramFiles`, `ProgramFiles(x86)`, `ProgramData`, plus the aliases -/// `windir`, `ProgramW6432`, `AllUsersProfile`, `SYSTEMDRIVE` (each of -/// which collapses to one of the preceding via normalization-and-dedup). -/// -/// Missing or empty env vars are silently skipped — pathological host -/// configuration is not the runner's failure mode. -fn protected_paths_set() -> &'static HashSet { - static SET: OnceLock> = OnceLock::new(); - SET.get_or_init(|| build_protected_paths_set(|var| std::env::var(var).ok())) -} - -/// Construction logic for the protected-paths set, parameterised on an -/// env-var lookup so tests can inject synthetic environments without -/// touching the process-wide `OnceLock` cache. -fn build_protected_paths_set(env: impl Fn(&str) -> Option) -> HashSet { - let mut set = HashSet::new(); - // 26 drive roots, regardless of current mount state. - for letter in b'A'..=b'Z' { - if let Some(p) = normalize_protected_path(&format!("{}:\\", letter as char)) { - set.insert(p); - } - } - // env-var-derived entries; (var name, take-parent flag). - for (var, take_parent) in [ - ("SystemRoot", false), - ("windir", false), - ("USERPROFILE", true), - ("ProgramFiles", false), - ("ProgramFiles(x86)", false), - ("ProgramW6432", false), - ("ProgramData", false), - ("AllUsersProfile", false), - ("SYSTEMDRIVE", false), - ] { - let raw = match env(var) { - Some(v) if !v.is_empty() => v, - _ => continue, - }; - let candidate = if take_parent { - std::path::Path::new(&raw) - .parent() - .map(|p| p.to_string_lossy().into_owned()) - } else { - Some(raw) - }; - if let Some(c) = candidate { - if let Some(n) = normalize_protected_path(&c) { - set.insert(n); - } - } - } - set -} - -/// Normalizes a Windows path for filter-set comparison. Returns `None` for -/// strings without a recognised disk prefix (only local absolute paths are -/// filtered). Applied to both filter-set entries and caller-supplied paths -/// so they compare on the same canonical form. -/// -/// Handles (so accidental spellings still match): -/// - Forward slashes → backslashes. -/// - Trailing-slash discipline: drive `C:` → `c:\`; non-root paths drop the -/// trailing `\` via component re-assembly. -/// - `.` / `..` collapse, clamped at root (so `C:\..` stays `c:\`). -/// - Per-component trailing whitespace and dots trim (Win32 silently strips -/// these at the file-open boundary, making them a real bypass shape). -/// - Case-insensitive ASCII compare via lowercase-on-output. -/// - `\\?\` long-path prefix on a disk path (`\\?\C:\foo`): the `\\?\` is -/// dropped and the rest normalizes the same as `C:\foo`. -fn normalize_protected_path(input: &str) -> Option { - let trimmed = input.trim(); - if trimmed.is_empty() { - return None; - } - let replaced = trimmed.replace('/', "\\"); - let path = std::path::Path::new(&replaced); - - let mut prefix: Option = None; - let mut has_root = false; - let mut comps: Vec = Vec::new(); - for component in path.components() { - match component { - std::path::Component::Prefix(pc) => { - // `Disk(c)` is the plain `C:` form; `VerbatimDisk(c)` is the - // `\\?\C:` long-path form, which Win32 strips at file-open - // time — accept it as the same canonical disk prefix so it - // cannot be used as a textual bypass. UNC (`\\server\share`) - // and verbatim UNC remain in the documented skip list — - // return early so the caller's path passes through. - match pc.kind() { - std::path::Prefix::Disk(_) => { - prefix = Some(pc.as_os_str().to_string_lossy().into_owned()); - } - std::path::Prefix::VerbatimDisk(letter) => { - prefix = Some(format!("{}:", letter as char)); - } - _ => return None, - } - } - std::path::Component::RootDir => { - has_root = true; - } - std::path::Component::CurDir => { /* skip */ } - std::path::Component::ParentDir => { - // Pop within the components we've collected; clamps at root - // because there's nothing to pop when comps is empty. - comps.pop(); - } - std::path::Component::Normal(s) => { - let mut name = s.to_string_lossy().into_owned(); - while let Some(ch) = name.chars().last() { - if ch.is_ascii_whitespace() || ch == '.' { - name.pop(); - } else { - break; - } - } - if !name.is_empty() { - comps.push(name); - } - } - } - } - - let prefix = prefix?; - let mut out = prefix.to_ascii_lowercase(); - // Always end the prefix with `\` so drive-root spellings normalize to - // `c:\` regardless of whether the input had the explicit RootDir - // component (`C:` and `C:\` collapse to the same canonical form). - out.push('\\'); - if !comps.is_empty() { - // `C:foo` (prefix + components but no RootDir) is a drive-relative - // path — `foo` resolves against the per-drive current directory. - // That's not an absolute spelling of any filter entry; out of scope. - if !has_root { - return None; - } - let lowered: Vec = comps.iter().map(|s| s.to_ascii_lowercase()).collect(); - out.push_str(&lowered.join("\\")); - } - Some(out) -} - -/// Drops protected paths from `rw` / `ro` slices before they reach -/// `ShareFolderBatchAsync`. Returns the kept paths in their original -/// caller spelling so the OS API sees verbatim input. -/// -/// When `logger` is provided AND at least one entry was dropped, emits a -/// single trace line summarising originals + canonical matches. The -/// state-aware `provision` path passes `None` and silently filters. -pub(super) fn filter_protected_paths( - rw: &[String], - ro: &[String], - logger: Option<&mut Logger>, -) -> (Vec, Vec) { - let set = protected_paths_set(); - let mut dropped: Vec<(String, String, &'static str)> = Vec::new(); - - let mut partition = |list: &'static str, paths: &[String]| -> Vec { - let mut kept = Vec::with_capacity(paths.len()); - for p in paths { - match normalize_protected_path(p) { - Some(n) if set.contains(&n) => { - dropped.push((p.clone(), n, list)); - } - _ => kept.push(p.clone()), - } - } - kept - }; - let rw_kept = partition("rw", rw); - let ro_kept = partition("ro", ro); - - if let Some(logger) = logger { - if !dropped.is_empty() { - let summary: Vec = dropped - .iter() - .map(|(orig, canon, list)| format!("'{}' ({}, matched {})", orig, list, canon)) - .collect(); - let _ = writeln!( - logger, - "filesystem policy filter dropped {} paths: {}", - dropped.len(), - summary.join(", "), - ); - } - } - - (rw_kept, ro_kept) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalize_drive_root_variants_collapse_to_canonical_form() { - assert_eq!(normalize_protected_path("C:").as_deref(), Some("c:\\")); - assert_eq!(normalize_protected_path("C:\\").as_deref(), Some("c:\\")); - assert_eq!(normalize_protected_path("c:\\").as_deref(), Some("c:\\")); - assert_eq!(normalize_protected_path("C:/").as_deref(), Some("c:\\")); - } - - #[test] - fn normalize_handles_case_and_slash_direction() { - assert_eq!( - normalize_protected_path("C:\\Windows").as_deref(), - Some("c:\\windows") - ); - assert_eq!( - normalize_protected_path("c:/windows").as_deref(), - Some("c:\\windows") - ); - assert_eq!( - normalize_protected_path("C:\\WINDOWS").as_deref(), - Some("c:\\windows") - ); - } - - #[test] - fn normalize_drops_trailing_slash_for_non_root() { - assert_eq!( - normalize_protected_path("C:\\Windows\\").as_deref(), - Some("c:\\windows") - ); - } - - #[test] - fn normalize_collapses_dot_components() { - assert_eq!( - normalize_protected_path("C:\\Windows\\.").as_deref(), - Some("c:\\windows") - ); - assert_eq!( - normalize_protected_path("C:\\.\\Windows").as_deref(), - Some("c:\\windows") - ); - } - - #[test] - fn normalize_pops_parent_components_clamped_at_root() { - assert_eq!( - normalize_protected_path("C:\\Windows\\..").as_deref(), - Some("c:\\") - ); - assert_eq!( - normalize_protected_path("C:\\Windows\\System32\\..").as_deref(), - Some("c:\\windows") - ); - // Excess pops clamp at root. - assert_eq!( - normalize_protected_path("C:\\..\\..").as_deref(), - Some("c:\\") - ); - } - - #[test] - fn normalize_trims_trailing_whitespace_and_dots_per_component() { - // Win32 silently strips these at the file-open boundary, so they're - // a real bypass shape (not just a curiosity). - assert_eq!( - normalize_protected_path("C:\\Windows ").as_deref(), - Some("c:\\windows") - ); - assert_eq!( - normalize_protected_path("C:\\Windows.").as_deref(), - Some("c:\\windows") - ); - assert_eq!( - normalize_protected_path("C:\\Windows...").as_deref(), - Some("c:\\windows") - ); - assert_eq!( - normalize_protected_path("C:\\Windows . .").as_deref(), - Some("c:\\windows") - ); - // Trim applies to every component, not only the last. - assert_eq!( - normalize_protected_path("C:\\Windows \\System32").as_deref(), - Some("c:\\windows\\system32") - ); - } - - #[test] - fn normalize_rejects_paths_without_drive_prefix() { - assert_eq!(normalize_protected_path(""), None); - assert_eq!(normalize_protected_path(" "), None); - assert_eq!(normalize_protected_path("relative\\path"), None); - // UNC: prefix is `\\server\share`, not a drive — out of filter scope. - assert_eq!(normalize_protected_path("\\\\server\\share\\Windows"), None); - // Verbatim UNC (`\\?\UNC\server\share\...`): not a disk prefix. - assert_eq!( - normalize_protected_path("\\\\?\\UNC\\server\\share\\Windows"), - None - ); - // Drive-relative (`C:foo`, no RootDir): `foo` resolves against the - // per-drive cwd, not absolute. Out of filter scope. - assert_eq!(normalize_protected_path("C:foo"), None); - } - - #[test] - fn normalize_verbatim_disk_prefix_collapses_to_canonical_form() { - // `\\?\C:\foo` normalizes to the same canonical form as `C:\foo`, - // so the long-path prefix cannot be used as a textual filter bypass. - assert_eq!( - normalize_protected_path("\\\\?\\C:\\Windows").as_deref(), - Some("c:\\windows") - ); - assert_eq!( - normalize_protected_path("\\\\?\\c:\\windows\\").as_deref(), - Some("c:\\windows") - ); - assert_eq!( - normalize_protected_path("\\\\?\\Z:\\").as_deref(), - Some("z:\\") - ); - } - - #[test] - fn build_set_includes_all_26_drive_roots_with_empty_env() { - let set = build_protected_paths_set(|_| None); - assert_eq!(set.len(), 26); - for letter in b'A'..=b'Z' { - let key = format!("{}:\\", (letter as char).to_ascii_lowercase()); - assert!(set.contains(&key), "missing drive-root entry {}", key); - } - } - - #[test] - fn build_set_includes_canonical_forms_of_supplied_env_vars() { - let env = |var: &str| { - Some(match var { - "SystemRoot" => "C:\\Windows".to_string(), - "USERPROFILE" => "C:\\Users\\me".to_string(), - "ProgramFiles" => "C:\\Program Files".to_string(), - "ProgramFiles(x86)" => "C:\\Program Files (x86)".to_string(), - "ProgramData" => "C:\\ProgramData".to_string(), - _ => return None, - }) - }; - let set = build_protected_paths_set(env); - assert!(set.contains("c:\\windows")); - assert!(set.contains("c:\\users")); // USERPROFILE parent - assert!(set.contains("c:\\program files")); - assert!(set.contains("c:\\program files (x86)")); - assert!(set.contains("c:\\programdata")); - } - - #[test] - fn build_set_aliases_dedupe_into_their_canonical_entries() { - let env = |var: &str| { - Some(match var { - "SystemRoot" => "C:\\Windows".to_string(), - "windir" => "C:\\Windows".to_string(), - "ProgramFiles" => "C:\\Program Files".to_string(), - "ProgramW6432" => "C:\\Program Files".to_string(), - "ProgramData" => "C:\\ProgramData".to_string(), - "AllUsersProfile" => "C:\\ProgramData".to_string(), - "SYSTEMDRIVE" => "C:".to_string(), - _ => return None, - }) - }; - let set = build_protected_paths_set(env); - // 26 drive roots + 3 unique env entries. SYSTEMDRIVE folds into the - // drive-root set; the named aliases collapse into their primary. - assert_eq!(set.len(), 26 + 3); - } - - #[test] - fn build_set_silently_skips_missing_or_empty_env_vars() { - let env = |var: &str| match var { - "SystemRoot" => Some(String::new()), // empty - "ProgramFiles" => Some("C:\\Program Files".to_string()), - _ => None, // everything else missing - }; - let set = build_protected_paths_set(env); - // Drive roots + only ProgramFiles (SystemRoot was empty, others missing). - assert_eq!(set.len(), 26 + 1); - assert!(set.contains("c:\\program files")); - } - - #[test] - fn build_set_handles_userprofile_at_drive_root() { - // Pathological: USERPROFILE = "C:\" → parent is None → silently skipped. - let env = |var: &str| match var { - "USERPROFILE" => Some("C:\\".to_string()), - _ => None, - }; - let set = build_protected_paths_set(env); - assert_eq!(set.len(), 26); // no extra entry from USERPROFILE - } - - #[test] - fn filter_drops_protected_drive_roots() { - // Drive roots are always in the real set regardless of env, so this - // composition test is hermetic. - let (rw, _) = filter_protected_paths( - &[ - "C:\\".to_string(), - "C:\\Users\\Alice\\work".to_string(), - "Z:\\".to_string(), - ], - &[], - None, - ); - assert!(!rw.iter().any(|p| p == "C:\\")); - assert!(!rw.iter().any(|p| p == "Z:\\")); - assert!(rw.contains(&"C:\\Users\\Alice\\work".to_string())); - } - - #[test] - fn filter_applies_to_readonly_paths_too() { - let (_, ro) = filter_protected_paths( - &[], - &["D:\\".to_string(), "C:\\Users\\Alice\\data".to_string()], - None, - ); - assert!(!ro.iter().any(|p| p == "D:\\")); - assert!(ro.contains(&"C:\\Users\\Alice\\data".to_string())); - } - - #[test] - fn filter_preserves_caller_spelling_for_kept_entries() { - // Kept paths come out byte-for-byte as the caller supplied them - // (the OS API gets the verbatim string, not the canonical form). - let (rw, _) = filter_protected_paths(&["C:/Users/Alice/work".to_string()], &[], None); - assert_eq!(rw, vec!["C:/Users/Alice/work".to_string()]); - } - - #[test] - fn filter_empty_input_yields_empty_output() { - let (rw, ro) = filter_protected_paths(&[], &[], None); - assert!(rw.is_empty()); - assert!(ro.is_empty()); - } - - #[test] - fn filter_subdirectory_of_protected_path_passes_through() { - // Exact-match-only: subdirectories of protected folders are not filtered. - let (rw, _) = filter_protected_paths( - &[ - "C:\\subdir-of-drive-root".to_string(), - "Z:\\my\\subdir".to_string(), - ], - &[], - None, - ); - assert!(rw.contains(&"C:\\subdir-of-drive-root".to_string())); - assert!(rw.contains(&"Z:\\my\\subdir".to_string())); - } - - #[test] - fn filter_catches_drive_root_bypass_attempts() { - // Exercises the composition of normalize + set lookup for the - // always-present drive-root entries. - let (rw, _) = filter_protected_paths( - &[ - "c:\\".to_string(), - "C:/".to_string(), - "C:".to_string(), - "c:".to_string(), - "C:\\..".to_string(), // .. clamps at root - "C:\\.".to_string(), // . skipped - "C:\\ ".to_string(), // trailing space (after slash, this empties → root) - "\\\\?\\C:\\".to_string(), // verbatim disk drive root - "\\\\?\\Z:\\".to_string(), - ], - &[], - None, - ); - assert!( - rw.is_empty(), - "drive-root bypass variants should drop, got {:?}", - rw - ); - } -} diff --git a/src/backends/isolation_session/common/src/state_aware.rs b/src/backends/isolation_session/common/src/state_aware.rs index fa5e50b73..8472e82b4 100644 --- a/src/backends/isolation_session/common/src/state_aware.rs +++ b/src/backends/isolation_session/common/src/state_aware.rs @@ -10,7 +10,6 @@ use std::io::IsTerminal; use serde::Serialize; -use wxc_common::id::mint_random_token; use wxc_common::models::{ ExecutionRequest, IsolationSessionConfig, IsolationSessionProvisionConfig, }; @@ -29,8 +28,10 @@ use super::policy::{ use super::process_options::build_process_options; use super::IsolationSessionRunner; -/// Provision-phase metadata. Carries the OS-assigned agent account name -/// for diagnostics; the SID is omitted (can be added when a caller needs it). +/// Provision-phase metadata surfaced to the caller: the OS-assigned agent +/// account name, the agent user's SID, and the shared ephemeral workspace +/// path. All are diagnostic/metadata — the addressing key remains the +/// `sandboxId` tail (the agent user name). /// /// `pub` is required because the trait associated type slot /// (`StatefulSandboxBackend::ProvisionMetadata`) reaches public callers via @@ -39,17 +40,22 @@ use super::IsolationSessionRunner; pub struct IsolationSessionProvisionMetadata { #[serde(rename = "agentUserName")] pub agent_user_name: String, + #[serde(rename = "agentUserSid")] + pub agent_user_sid: String, + #[serde(rename = "ephemeralWorkspacePath")] + pub ephemeral_workspace_path: String, } -/// Parses the `iso:` form of a state-aware sandbox_id and -/// returns the inner `provisionId` segment. Surfaces format mismatches as +/// Parses the `iso:` form of a state-aware sandbox_id and +/// returns the inner `agentUserName` segment — the opaque, OS-assigned +/// account name minted at provision. Surfaces format mismatches as /// `MxcError::MalformedId`. -fn extract_provision_id(sandbox_id: &str) -> Result<&str, MxcError> { +fn extract_agent_user_name(sandbox_id: &str) -> Result<&str, MxcError> { let prefix = ::ID_PREFIX; match sandbox_id.split_once(':') { Some((p, rest)) if p == prefix && !rest.is_empty() => Ok(rest), _ => Err(MxcError::malformed_id(format!( - "expected {}:, got {:?}", + "expected {}:, got {:?}", prefix, sandbox_id ))), } @@ -74,53 +80,28 @@ impl StatefulSandboxBackend for IsolationSessionRunner { fn provision( &mut self, - request: &ExecutionRequest, + _request: &ExecutionRequest, config: Option, ) -> Result, MxcError> { let user = config.and_then(|c| c.user); - // For Entra sandboxes the UPN IS the OS-layer provisionId; encoding - // it as the sandboxId tail keeps subsequent phases stateless. - let provision_id = match &user { - Some(u) => u.upn.clone(), - None => format!("wxc-{}", mint_random_token()), - }; - let manager = IsolationSessionManager::new(&provision_id).map_err(map_lifecycle_error)?; - manager.register_client().map_err(map_lifecycle_error)?; - - let provision_result = match &user { - Some(u) => manager.provision_agent_user_v2(&u.wam_token), - None => manager.provision_agent_user(), + // Local agent users pass empty strings; Entra agents pass the UPN + + // WAM token. Either way the OS assigns an opaque agent account name, + // which becomes the sandboxId tail — start cannot infer Entra-ness + // from it, so the token is re-supplied at start. + let (entra_account, wam_token) = match &user { + Some(u) => (u.upn.as_str(), u.wam_token.as_str()), + None => ("", ""), }; - let agent_user_name = match provision_result { - Ok(name) => name, - Err(e) => { - // provision_agent_user can fail after the OS-side provision - // succeeded, leaving an Indefinite-lifetime agent user. - // Defensive cleanup mirrors the one-shot path; no-ops on - // absent state. - let _ = manager.deprovision_agent_user(); - let _ = manager.unregister_client(); - return Err(map_lifecycle_error(e)); - } - }; - - // Apply filesystem policy (rw + ro paths) before returning. A - // failure here leaves the agent user provisioned but no folders - // accessible to it; tear it down so the caller does not see a - // half-provisioned sandboxId. - if let Err(e) = manager.share_folders( - &request.policy.readwrite_paths, - &request.policy.readonly_paths, - None, - ) { - let _ = manager.deprovision_agent_user(); - let _ = manager.unregister_client(); - return Err(map_lifecycle_error(e)); - } + let provisioned = IsolationSessionManager::add_user(entra_account, wam_token) + .map_err(map_lifecycle_error)?; Ok(ProvisionResult { - sandbox_id: format!("{}:{}", Self::ID_PREFIX, provision_id), - metadata: Some(IsolationSessionProvisionMetadata { agent_user_name }), + sandbox_id: format!("{}:{}", Self::ID_PREFIX, provisioned.agent_user_name), + metadata: Some(IsolationSessionProvisionMetadata { + agent_user_name: provisioned.agent_user_name, + agent_user_sid: provisioned.agent_user_sid, + ephemeral_workspace_path: provisioned.ephemeral_workspace_path, + }), }) } @@ -130,18 +111,21 @@ impl StatefulSandboxBackend for IsolationSessionRunner { _request: &ExecutionRequest, config: Option, ) -> Result, MxcError> { - let provision_id = extract_provision_id(sandbox_id)?; - let manager = IsolationSessionManager::new(provision_id).map_err(map_lifecycle_error)?; - // Config absent → Composable (the one-shot default). The OS API - // does not call back into MXC after start; a return here means the - // session is ready to host process launches. + let agent_user_name = extract_agent_user_name(sandbox_id)?; + let manager = IsolationSessionManager::new(agent_user_name).map_err(map_lifecycle_error)?; + // The sandboxId tail is opaque, so Entra-ness is carried by the + // start config's user bundle: present → re-supply the WAM token; + // absent → local session (empty token). The OS validates the token + // against the agent user it assigned at provision. let cfg = config.unwrap_or_default(); - let configuration_id = cfg.configuration_id; - let start_result = match cfg.user { - Some(u) => manager.start_session_v2(configuration_id, &u.wam_token), - None => manager.start_session(configuration_id), - }; - start_result.map_err(map_lifecycle_error)?; + let wam_token = cfg + .user + .as_ref() + .map(|u| u.wam_token.as_str()) + .unwrap_or(""); + manager + .start_session(wam_token) + .map_err(map_lifecycle_error)?; Ok(StartResult { metadata: None }) } @@ -151,36 +135,31 @@ impl StatefulSandboxBackend for IsolationSessionRunner { _request: &ExecutionRequest, _config: Option<()>, ) -> Result, MxcError> { - let provision_id = extract_provision_id(sandbox_id)?; - let manager = IsolationSessionManager::new(provision_id).map_err(map_lifecycle_error)?; + let agent_user_name = extract_agent_user_name(sandbox_id)?; + let manager = IsolationSessionManager::new(agent_user_name).map_err(map_lifecycle_error)?; manager.stop_session().map_err(map_lifecycle_error)?; Ok(StopResult { metadata: None }) } - /// Removes the agent user. The `unregister_client` step is currently a - /// no-op so concurrent MXC isolation-session sandboxes coexist on the - /// shared regid; deprovisioning one does not tear down another. + /// Removes the agent user. fn deprovision( &mut self, sandbox_id: &str, _request: &ExecutionRequest, _config: Option<()>, ) -> Result, MxcError> { - let provision_id = extract_provision_id(sandbox_id)?; - let manager = IsolationSessionManager::new(provision_id).map_err(map_lifecycle_error)?; + let agent_user_name = extract_agent_user_name(sandbox_id)?; + let manager = IsolationSessionManager::new(agent_user_name).map_err(map_lifecycle_error)?; manager .deprovision_agent_user() .map_err(map_lifecycle_error)?; - manager.unregister_client().map_err(map_lifecycle_error)?; Ok(DeprovisionResult { metadata: None }) } - // Filesystem rw/ro paths are honoured at provision (applied via - // `share_folders`) and rejected at every later phase, because the grant - // lifecycle is bound to the agent user. `denied_paths`, network, and - // proxy policy are rejected at every phase: the backend has no - // equivalent primitive. Anything rejected produces a - // `policy_validation` envelope rather than silent ignore. + // Filesystem rw/ro/denied paths, network, and proxy policy are rejected + // at every phase: the backend has no host-folder-sharing, network, or + // proxy primitive. Anything rejected produces a `policy_validation` + // envelope rather than silent ignore. fn validate_provision( &self, @@ -199,29 +178,13 @@ impl StatefulSandboxBackend for IsolationSessionRunner { request: &ExecutionRequest, config: Option<&IsolationSessionConfig>, ) -> Result<(), MxcError> { - let provision_id = extract_provision_id(sandbox_id)?; - let is_entra = provision_id.contains('@'); - let user = config.and_then(|c| c.user.as_ref()); - match (is_entra, user) { - (true, None) => { - return Err(MxcError::malformed_request( - "Entra sandbox requires user at start", - )); - } - (false, Some(_)) => { - return Err(MxcError::malformed_request( - "user is not allowed for local sandbox", - )); - } - (true, Some(u)) => { - validate_isolation_session_user(u)?; - if !u.upn.eq_ignore_ascii_case(provision_id) { - return Err(MxcError::malformed_request( - "user.upn does not match sandboxId", - )); - } - } - (false, None) => {} + // The sandboxId tail is opaque, so start no longer cross-checks it + // against the user bundle. A user bundle (Entra) is optional at + // start; when present it must be well-formed. The OS validates the + // token against the agent user it assigned at provision. + extract_agent_user_name(sandbox_id)?; + if let Some(user) = config.and_then(|c| c.user.as_ref()) { + validate_isolation_session_user(user)?; } validate_post_provision_policy(request).map_err(map_lifecycle_error) } @@ -266,8 +229,8 @@ impl StatefulSandboxBackend for IsolationSessionRunner { request: &ExecutionRequest, _config: Option<()>, ) -> Result { - let provision_id = extract_provision_id(sandbox_id)?; - let manager = IsolationSessionManager::new(provision_id).map_err(map_lifecycle_error)?; + let agent_user_name = extract_agent_user_name(sandbox_id)?; + let manager = IsolationSessionManager::new(agent_user_name).map_err(map_lifecycle_error)?; let interactive = std::io::stdout().is_terminal(); let options = build_process_options(request, interactive); @@ -318,9 +281,9 @@ mod tests { ); } - // `ID_PREFIX` is the `:` tag the dispatcher + // `ID_PREFIX` is the `:` tag the dispatcher // matches against in `backend_from_prefix`. Indirectly covered by - // every `extract_provision_id_*` test that uses an `"iso:..."` + // every `extract_agent_user_name_*` test that uses an `"iso:..."` // literal; pinned explicitly here so the dependence is visible. #[test] fn id_prefix_matches_wire_format() { @@ -330,6 +293,27 @@ mod tests { ); } + // Provision metadata must serialize to exactly the three camelCase wire + // keys the SDK's `IsolationSessionProvisionMetadata` reads. A missing or + // misnamed field would silently strip provision data from the result. + #[test] + fn provision_metadata_serializes_all_fields() { + let meta = IsolationSessionProvisionMetadata { + agent_user_name: "agent-1".to_string(), + agent_user_sid: "S-1-5-21-1001".to_string(), + ephemeral_workspace_path: "C:\\ProgramData\\ws\\agent-1".to_string(), + }; + let v = serde_json::to_value(&meta).unwrap(); + assert_eq!(v["agentUserName"], "agent-1"); + assert_eq!(v["agentUserSid"], "S-1-5-21-1001"); + assert_eq!(v["ephemeralWorkspacePath"], "C:\\ProgramData\\ws\\agent-1"); + assert_eq!( + v.as_object().unwrap().len(), + 3, + "unexpected fields in provision metadata: {v}" + ); + } + fn request_with_filesystem_policy() -> ExecutionRequest { ExecutionRequest { policy: ContainerPolicy { @@ -343,38 +327,39 @@ mod tests { // ====== sandbox_id parsing ====== #[test] - fn extract_provision_id_unwraps_iso_prefix() { + fn extract_agent_user_name_unwraps_iso_prefix() { assert_eq!( - extract_provision_id("iso:wxc-abcd1234").unwrap(), + extract_agent_user_name("iso:wxc-abcd1234").unwrap(), "wxc-abcd1234" ); } #[test] - fn extract_provision_id_rejects_other_prefix() { - let err = extract_provision_id("wsb:abc").unwrap_err(); + fn extract_agent_user_name_rejects_other_prefix() { + let err = extract_agent_user_name("wsb:abc").unwrap_err(); assert_eq!(err.code, MxcErrorCode::MalformedId); } #[test] - fn extract_provision_id_rejects_missing_colon() { - let err = extract_provision_id("no-colon").unwrap_err(); + fn extract_agent_user_name_rejects_missing_colon() { + let err = extract_agent_user_name("no-colon").unwrap_err(); assert_eq!(err.code, MxcErrorCode::MalformedId); } #[test] - fn extract_provision_id_rejects_empty_payload() { - let err = extract_provision_id("iso:").unwrap_err(); + fn extract_agent_user_name_rejects_empty_payload() { + let err = extract_agent_user_name("iso:").unwrap_err(); assert_eq!(err.code, MxcErrorCode::MalformedId); } // ====== validation-hook phase routing ====== #[test] - fn validate_provision_hook_accepts_filesystem_policy() { + fn validate_provision_hook_rejects_filesystem_policy() { let runner = IsolationSessionRunner::new(); let req = request_with_filesystem_policy(); - runner.validate_provision(&req, None).unwrap(); + let err = runner.validate_provision(&req, None).unwrap_err(); + assert_eq!(err.code, MxcErrorCode::PolicyValidation); } #[test] @@ -423,7 +408,7 @@ mod tests { runner.validate_deprovision("iso:abc", &req, None).unwrap(); } - // ====== Entra user bundle + sandbox-id consistency matrix ====== + // ====== Entra user bundle validation ====== #[test] fn validate_provision_accepts_well_formed_user() { @@ -452,102 +437,38 @@ mod tests { } #[test] - fn validate_start_entra_sandbox_with_matching_user_accepts() { + fn validate_start_accepts_well_formed_user() { + // A user bundle is now allowed at start regardless of the opaque + // sandboxId; it only needs to be well-formed. let runner = IsolationSessionRunner::new(); let cfg = IsolationSessionConfig { user: Some(well_formed_user()), - ..Default::default() }; runner - .validate_start( - "iso:alice@contoso.com", - &ExecutionRequest::default(), - Some(&cfg), - ) - .unwrap(); - } - - #[test] - fn validate_start_entra_sandbox_without_user_is_malformed() { - let runner = IsolationSessionRunner::new(); - let err = runner - .validate_start("iso:alice@contoso.com", &ExecutionRequest::default(), None) - .unwrap_err(); - assert_eq!(err.code, MxcErrorCode::MalformedRequest); - assert!( - err.message.contains("Entra sandbox requires user"), - "got {}", - err.message - ); - } - - #[test] - fn validate_start_local_sandbox_with_user_is_malformed() { - let runner = IsolationSessionRunner::new(); - let cfg = IsolationSessionConfig { - user: Some(well_formed_user()), - ..Default::default() - }; - let err = runner .validate_start("iso:wxc-abcd1234", &ExecutionRequest::default(), Some(&cfg)) - .unwrap_err(); - assert_eq!(err.code, MxcErrorCode::MalformedRequest); - assert!( - err.message.contains("not allowed for local sandbox"), - "got {}", - err.message - ); - } - - #[test] - fn validate_start_local_sandbox_without_user_accepts() { - let runner = IsolationSessionRunner::new(); - runner - .validate_start("iso:wxc-abcd1234", &ExecutionRequest::default(), None) .unwrap(); } #[test] - fn validate_start_entra_user_upn_mismatch_is_malformed() { + fn validate_start_rejects_malformed_user() { let runner = IsolationSessionRunner::new(); let cfg = IsolationSessionConfig { user: Some(IsolationSessionUser { - upn: "bob@contoso.com".to_string(), + upn: "no-at-sign".to_string(), wam_token: "tok".to_string(), }), - ..Default::default() }; let err = runner - .validate_start( - "iso:alice@contoso.com", - &ExecutionRequest::default(), - Some(&cfg), - ) + .validate_start("iso:wxc-abcd1234", &ExecutionRequest::default(), Some(&cfg)) .unwrap_err(); - assert_eq!(err.code, MxcErrorCode::MalformedRequest); - assert!( - err.message.contains("does not match sandboxId"), - "got {}", - err.message - ); + assert_eq!(err.code, MxcErrorCode::PolicyValidation); } #[test] - fn validate_start_entra_user_upn_match_is_case_insensitive() { + fn validate_start_local_sandbox_without_user_accepts() { let runner = IsolationSessionRunner::new(); - let cfg = IsolationSessionConfig { - user: Some(IsolationSessionUser { - upn: "Alice@Contoso.COM".to_string(), - wam_token: "tok".to_string(), - }), - ..Default::default() - }; runner - .validate_start( - "iso:alice@contoso.com", - &ExecutionRequest::default(), - Some(&cfg), - ) + .validate_start("iso:wxc-abcd1234", &ExecutionRequest::default(), None) .unwrap(); } } diff --git a/src/core/mxc_engine/src/lib.rs b/src/core/mxc_engine/src/lib.rs index 9e5f368c9..040cf26e8 100644 --- a/src/core/mxc_engine/src/lib.rs +++ b/src/core/mxc_engine/src/lib.rs @@ -35,6 +35,8 @@ mod run; mod state_aware; pub use error::{Error, ErrorCode}; +#[cfg(all(target_os = "windows", feature = "isolation_session"))] +pub use platform::isolation_session_available; pub use platform::{platform_support, PlatformSupport}; pub use policy::{ available_tools_policy, build_request, temporary_files_policy, user_profile_policy, diff --git a/src/core/mxc_engine/src/platform.rs b/src/core/mxc_engine/src/platform.rs index 8cbe38b2b..78f748870 100644 --- a/src/core/mxc_engine/src/platform.rs +++ b/src/core/mxc_engine/src/platform.rs @@ -98,3 +98,14 @@ fn command_succeeds(program: &str, args: &[&str]) -> bool { .map(|s| s.success()) .unwrap_or(false) } + +/// Read-only activation probe of the in-process IsolationSession service. +/// +/// Reports whether the isolation-session backend's OS-side service is +/// available on this host. Exposed from the engine so `wxc` reaches the +/// isolation-session backend through `mxc_engine` rather than depending on +/// `isolation_session_common` directly. +#[cfg(all(target_os = "windows", feature = "isolation_session"))] +pub fn isolation_session_available() -> bool { + isolation_session_common::is_service_available() +} diff --git a/src/core/wxc/src/main.rs b/src/core/wxc/src/main.rs index 1e357e6bc..c0a706d91 100644 --- a/src/core/wxc/src/main.rs +++ b/src/core/wxc/src/main.rs @@ -760,6 +760,16 @@ fn main() { wxc_common::models::ContainerPolicy::default() }; let output = appcontainer_common::probe::run_probe(&policy); + // appcontainer_common has no dependency on the isolation-session + // backend, so it reports `isolationSessionAvailable` as `false`. When + // the backend is compiled in, override it with a read-only activation + // probe of the in-proc service. + #[cfg(all(target_os = "windows", feature = "isolation_session"))] + let output = { + let mut output = output; + output.probes.isolation_session_available = mxc_engine::isolation_session_available(); + output + }; match appcontainer_common::probe::to_json_pretty(&output) { Ok(s) => println!("{s}"), Err(e) => { diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index 60df40944..d0e6134e2 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -999,14 +999,11 @@ fn convert_wire_config( } else { None }; - let isolation_session = raw_exp.isolation_session.map(|as_cfg| { - let mut config = IsolationSessionConfig::default(); - if let Some(id) = as_cfg.configuration_id { - config.configuration_id = id.into(); - } - config.user = as_cfg.user.map(Into::into); - config - }); + let isolation_session = raw_exp + .isolation_session + .map(|as_cfg| IsolationSessionConfig { + user: as_cfg.user.map(Into::into), + }); if raw_exp.seatbelt.is_some() { let msg = "'experimental.seatbelt' has moved to the stable section; \ use top-level 'seatbelt' instead." @@ -1336,7 +1333,7 @@ mod tests { "phase": "start", "sandboxId": "iso:abcd1234", "experimental": { - "isolation_session": {"start": {"configurationId": "small"}} + "isolation_session": {"start": {"user": {"upn": "alice@contoso.com"}}} } }"#; match load_mxc(json).unwrap() { @@ -1350,7 +1347,7 @@ mod tests { assert_eq!( exp, serde_json::json!({ - "isolation_session": {"start": {"configurationId": "small"}} + "isolation_session": {"start": {"user": {"upn": "alice@contoso.com"}}} }) ); } @@ -3588,92 +3585,6 @@ mod tests { assert_eq!(req.containment, ContainmentBackend::IsolationSession); } - #[test] - fn isolation_session_config_defaults() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {}}}"#; - let encoded = base64_encode(json.as_bytes()); - let mut logger = test_logger(); - - let req = load_request(&encoded, &mut logger, true).unwrap(); - let cfg = req.experimental.isolation_session.unwrap(); - assert_eq!( - cfg.configuration_id, - crate::models::IsolationSessionConfigurationId::Composable - ); - } - - #[test] - fn isolation_session_config_small() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "small"}}}"#; - let encoded = base64_encode(json.as_bytes()); - let mut logger = test_logger(); - - let req = load_request(&encoded, &mut logger, true).unwrap(); - let cfg = req.experimental.isolation_session.unwrap(); - assert_eq!( - cfg.configuration_id, - crate::models::IsolationSessionConfigurationId::Small - ); - } - - #[test] - fn isolation_session_config_medium() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "medium"}}}"#; - let encoded = base64_encode(json.as_bytes()); - let mut logger = test_logger(); - - let req = load_request(&encoded, &mut logger, true).unwrap(); - let cfg = req.experimental.isolation_session.unwrap(); - assert_eq!( - cfg.configuration_id, - crate::models::IsolationSessionConfigurationId::Medium - ); - } - - #[test] - fn isolation_session_config_large() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "large"}}}"#; - let encoded = base64_encode(json.as_bytes()); - let mut logger = test_logger(); - - let req = load_request(&encoded, &mut logger, true).unwrap(); - let cfg = req.experimental.isolation_session.unwrap(); - assert_eq!( - cfg.configuration_id, - crate::models::IsolationSessionConfigurationId::Large - ); - } - - #[test] - fn isolation_session_config_composable() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "composable"}}}"#; - let encoded = base64_encode(json.as_bytes()); - let mut logger = test_logger(); - - let req = load_request(&encoded, &mut logger, true).unwrap(); - let cfg = req.experimental.isolation_session.unwrap(); - assert_eq!( - cfg.configuration_id, - crate::models::IsolationSessionConfigurationId::Composable - ); - } - - #[test] - fn isolation_session_config_unknown_is_rejected() { - // Strict enums: an unrecognized configurationId is rejected at - // deserialize time rather than silently defaulting to `composable`. - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "xlarge"}}}"#; - let encoded = base64_encode(json.as_bytes()); - let mut logger = test_logger(); - - let err = load_request(&encoded, &mut logger, true).unwrap_err(); - let msg = err.to_string(); - assert!( - msg.contains("unknown variant") && msg.contains("xlarge"), - "expected an unknown-variant rejection for configurationId 'xlarge', got: {msg}" - ); - } - #[test] fn isolation_session_absent_from_experimental() { let json = r#"{"process": {"commandLine": "echo hi"}, "experimental": {}}"#; @@ -3701,7 +3612,7 @@ mod tests { #[test] fn isolation_session_user_absent_when_field_omitted() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "medium"}}}"#; + let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {}}}"#; let encoded = base64_encode(json.as_bytes()); let mut logger = test_logger(); diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index d54c604f8..de8e0a4d3 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -32,7 +32,7 @@ pub enum ContainmentBackend { Hyperlight, /// Windows Sandbox — full VM isolation (experimental, requires --experimental flag). WindowsSandbox, - /// Isolation Session — process isolation via IsoEnvBroker Session API (experimental). + /// Isolation Session — process isolation via the IsolationSession API (experimental). #[serde(rename = "isolation_session")] IsolationSession, /// macOS Seatbelt sandbox backend. @@ -253,43 +253,10 @@ impl Default for WindowsSandboxConfig { } } -/// Session configuration size for the Isolation Session backend. -/// Maps to `IsoSessionConfigId` in the in-proc `Windows.AI.IsolationSession` -/// `IsoSessionOps` APIs, whose values must in turn match `ISOLATION_CONFIG_ID` -/// in `winsta.h`. -#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum IsolationSessionConfigurationId { - /// `Small` (1) — smallest pre-defined configuration. - Small, - /// `Medium` (2) — middle pre-defined configuration. - Medium, - /// `Large` (3) — largest pre-defined configuration. - Large, - /// `Composable` (4) — lightweight configuration with UI subsystems - /// stripped, intended for command-line workloads. The default. - #[default] - Composable, -} - -impl From for IsolationSessionConfigurationId { - fn from(id: crate::wire::IsolationConfigurationId) -> Self { - match id { - crate::wire::IsolationConfigurationId::Small => Self::Small, - crate::wire::IsolationConfigurationId::Medium => Self::Medium, - crate::wire::IsolationConfigurationId::Large => Self::Large, - crate::wire::IsolationConfigurationId::Composable => Self::Composable, - } - } -} - /// Configuration specific to the Isolation Session backend. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default)] pub struct IsolationSessionConfig { - /// Session size/weight. Default: Composable. - #[serde(rename = "configurationId")] - pub configuration_id: IsolationSessionConfigurationId, /// Optional Entra cloud-agent credentials. Honored on the state-aware /// `start` phase; rejected by the one-shot path. pub user: Option, @@ -863,14 +830,9 @@ mod tests { #[test] fn isolation_session_config_carries_optional_user() { let wire = json!({ - "configurationId": "medium", "user": {"upn": "alice@contoso.com", "wamToken": "tok"} }); let parsed: IsolationSessionConfig = serde_json::from_value(wire).unwrap(); - assert_eq!( - parsed.configuration_id, - IsolationSessionConfigurationId::Medium - ); assert!(parsed.user.is_some()); } } diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index 0fe78e849..7030c7f6e 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -483,15 +483,13 @@ pub enum TransportProtocol { Tcp, } -/// IsolationSession backend config. Carries both the one-shot fields -/// (`configurationId`, `user`) and the per-phase state-aware nesting -/// (`provision` / `start` / `stop` / `deprovision`). +/// IsolationSession backend config. Carries the one-shot `user` field and +/// the per-phase state-aware nesting (`provision` / `start` / `stop` / +/// `deprovision`). #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "schema-gen", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub struct IsolationSession { - /// Sizing profile (one-shot). - pub configuration_id: Option, /// Optional Entra cloud-agent user bundle (one-shot). pub user: Option, /// State-aware provision-phase configuration. @@ -509,23 +507,10 @@ pub struct IsolationSession { #[cfg_attr(feature = "schema-gen", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub struct IsolationSessionPhase { - /// Sizing profile for this phase. - pub configuration_id: Option, /// Entra cloud-agent user bundle for this phase. pub user: Option, } -/// IsolationSession sizing profile. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "schema-gen", derive(schemars::JsonSchema))] -#[serde(rename_all = "lowercase")] -pub enum IsolationConfigurationId { - Small, - Medium, - Large, - Composable, -} - /// Entra cloud-agent user bundle. Reachable only under the permissive /// `experimental` surface, so unknown fields are tolerated (forward-compat). #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/tests/configs/isolation_session_concurrent_A.json b/tests/configs/isolation_session_concurrent_A.json index 2f1d4a617..e675824f2 100644 --- a/tests/configs/isolation_session_concurrent_A.json +++ b/tests/configs/isolation_session_concurrent_A.json @@ -5,8 +5,5 @@ "process": { "commandLine": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\\mxc_concurrent_log\\A.ps1", "timeout": 60000 - }, - "filesystem": { - "readwritePaths": ["C:\\mxc_concurrent_log"] } } diff --git a/tests/configs/isolation_session_concurrent_B.json b/tests/configs/isolation_session_concurrent_B.json index d61ac227c..6c2a95823 100644 --- a/tests/configs/isolation_session_concurrent_B.json +++ b/tests/configs/isolation_session_concurrent_B.json @@ -5,8 +5,5 @@ "process": { "commandLine": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\\mxc_concurrent_log\\B.ps1", "timeout": 30000 - }, - "filesystem": { - "readwritePaths": ["C:\\mxc_concurrent_log"] } } diff --git a/tests/configs/isolation_session_concurrent_C.json b/tests/configs/isolation_session_concurrent_C.json index a7f66050b..1f773ddd2 100644 --- a/tests/configs/isolation_session_concurrent_C.json +++ b/tests/configs/isolation_session_concurrent_C.json @@ -5,8 +5,5 @@ "process": { "commandLine": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\\mxc_concurrent_log\\C.ps1", "timeout": 75000 - }, - "filesystem": { - "readwritePaths": ["C:\\mxc_concurrent_log"] } } diff --git a/tests/configs/isolation_session_hello_medium.json b/tests/configs/isolation_session_configid_ignored.json similarity index 76% rename from tests/configs/isolation_session_hello_medium.json rename to tests/configs/isolation_session_configid_ignored.json index 0be30dbe0..c4cbae133 100644 --- a/tests/configs/isolation_session_hello_medium.json +++ b/tests/configs/isolation_session_configid_ignored.json @@ -1,11 +1,13 @@ { "version": "0.6.0-alpha", - "containerId": "isolation-session-hello-medium", + "containerId": "isolation-session-hello", "containment": "isolation_session", "process": { "commandLine": "echo USERNAME=%USERNAME% & echo MYVAR=%MYVAR% & echo CWD=%CD% & whoami", "cwd": "C:\\mxc_workdir_test", - "env": ["MYVAR=IsolationSessionTest"], + "env": [ + "MYVAR=IsolationSessionTest" + ], "timeout": 30000 }, "experimental": { diff --git a/tests/configs/isolation_session_filesystem.json b/tests/configs/isolation_session_filesystem.json deleted file mode 100644 index 1d1aaf5bc..000000000 --- a/tests/configs/isolation_session_filesystem.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "0.6.0-alpha", - "containerId": "isolation-session-filesystem", - "containment": "isolation_session", - "process": { - "commandLine": "type C:\\mxc_share_test_oneshot\\marker.txt", - "timeout": 30000 - }, - "filesystem": { - "readwritePaths": ["C:\\mxc_share_test_oneshot"] - } -} diff --git a/tests/configs/isolation_session_filtered.json b/tests/configs/isolation_session_filtered.json deleted file mode 100644 index 9573c5316..000000000 --- a/tests/configs/isolation_session_filtered.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "0.6.0-alpha", - "containerId": "isolation-session-filtered", - "containment": "isolation_session", - "process": { - "commandLine": "type C:\\mxc_filter_test_oneshot\\marker.txt", - "timeout": 30000 - }, - "filesystem": { - "readwritePaths": ["C:\\Windows", "C:\\", "C:\\mxc_filter_test_oneshot"] - } -} diff --git a/tests/configs/isolation_session_state_aware_exec_read_readonly.json b/tests/configs/isolation_session_state_aware_exec_read_readonly.json deleted file mode 100644 index 693575d64..000000000 --- a/tests/configs/isolation_session_state_aware_exec_read_readonly.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "phase": "exec", - "sandboxId": "{{SANDBOX_ID}}", - "process": { - "commandLine": "type C:\\mxc_share_test\\ro\\marker.txt", - "timeout": 30000 - } -} diff --git a/tests/configs/isolation_session_state_aware_exec_read_restricted.json b/tests/configs/isolation_session_state_aware_exec_read_restricted.json deleted file mode 100644 index 2e5c7c32e..000000000 --- a/tests/configs/isolation_session_state_aware_exec_read_restricted.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "phase": "exec", - "sandboxId": "{{SANDBOX_ID}}", - "process": { - "commandLine": "type C:\\mxc_share_test\\restricted\\marker.txt", - "timeout": 30000 - } -} diff --git a/tests/configs/isolation_session_state_aware_exec_read_shared.json b/tests/configs/isolation_session_state_aware_exec_read_shared.json deleted file mode 100644 index 3a40b66d0..000000000 --- a/tests/configs/isolation_session_state_aware_exec_read_shared.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "phase": "exec", - "sandboxId": "{{SANDBOX_ID}}", - "process": { - "commandLine": "type C:\\mxc_share_test\\rw\\agent_wrote.txt", - "timeout": 30000 - } -} diff --git a/tests/configs/isolation_session_state_aware_exec_write_readonly_denied.json b/tests/configs/isolation_session_state_aware_exec_write_readonly_denied.json deleted file mode 100644 index cf1e2e736..000000000 --- a/tests/configs/isolation_session_state_aware_exec_write_readonly_denied.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "phase": "exec", - "sandboxId": "{{SANDBOX_ID}}", - "process": { - "commandLine": "echo agent-write-attempt > C:\\mxc_share_test\\ro\\agent_should_fail.txt", - "timeout": 30000 - } -} diff --git a/tests/configs/isolation_session_state_aware_exec_write_shared.json b/tests/configs/isolation_session_state_aware_exec_write_shared.json deleted file mode 100644 index 54f3860a7..000000000 --- a/tests/configs/isolation_session_state_aware_exec_write_shared.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "phase": "exec", - "sandboxId": "{{SANDBOX_ID}}", - "process": { - "commandLine": "echo shared-write-content > C:\\mxc_share_test\\rw\\agent_wrote.txt && type C:\\mxc_share_test\\rw\\agent_wrote.txt", - "timeout": 30000 - } -} diff --git a/tests/configs/isolation_session_state_aware_provision_with_filter.json b/tests/configs/isolation_session_state_aware_provision_with_filter.json deleted file mode 100644 index 906498521..000000000 --- a/tests/configs/isolation_session_state_aware_provision_with_filter.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "phase": "provision", - "containment": "isolation_session", - "filesystem": { - "readwritePaths": ["C:\\Windows", "C:\\", "C:\\mxc_filter_test\\rw"] - } -} diff --git a/tests/configs/isolation_session_state_aware_start_entra_missing_user.json b/tests/configs/isolation_session_state_aware_start_entra_missing_user.json deleted file mode 100644 index d2b2e2b66..000000000 --- a/tests/configs/isolation_session_state_aware_start_entra_missing_user.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "phase": "start", - "sandboxId": "iso:alice@contoso.com" -} diff --git a/tests/configs/isolation_session_state_aware_start_local_with_user.json b/tests/configs/isolation_session_state_aware_start_local_with_user.json deleted file mode 100644 index f674705d6..000000000 --- a/tests/configs/isolation_session_state_aware_start_local_with_user.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "phase": "start", - "sandboxId": "iso:wxc-fake1234", - "experimental": { - "isolation_session": { - "start": { - "user": { - "upn": "alice@contoso.com", - "wamToken": "tok" - } - } - } - } -} diff --git a/tests/configs/isolation_session_state_aware_start_medium.json b/tests/configs/isolation_session_state_aware_start_medium.json deleted file mode 100644 index db39e2246..000000000 --- a/tests/configs/isolation_session_state_aware_start_medium.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "phase": "start", - "sandboxId": "{{SANDBOX_ID}}", - "experimental": { - "isolation_session": { - "start": { - "configurationId": "medium" - } - } - } -} diff --git a/tests/configs/isolation_session_state_aware_start_upn_mismatch.json b/tests/configs/isolation_session_state_aware_start_upn_mismatch.json deleted file mode 100644 index 7f5242a4c..000000000 --- a/tests/configs/isolation_session_state_aware_start_upn_mismatch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "phase": "start", - "sandboxId": "iso:alice@contoso.com", - "experimental": { - "isolation_session": { - "start": { - "user": { - "upn": "bob@contoso.com", - "wamToken": "tok" - } - } - } - } -} diff --git a/tests/scripts/run_isolation_session_state_aware_tests.ps1 b/tests/scripts/run_isolation_session_state_aware_tests.ps1 index 641623791..bb38e7049 100644 --- a/tests/scripts/run_isolation_session_state_aware_tests.ps1 +++ b/tests/scripts/run_isolation_session_state_aware_tests.ps1 @@ -11,9 +11,8 @@ .DESCRIPTION Each test invokes wxc-exec.exe with a base64-encoded state-aware request envelope, parses the JSON response on stdout, and asserts on the - envelope's `result` or `error` fields. Subsequent commits extend the - skeleton with start, exec, stop, and deprovision tests; this revision - asserts only the provision phase. + envelope's `result` or `error` fields. The corpus covers lifecycle, + process execution, sandbox-internal persistence, and validation errors. This script must run INTERACTIVELY on the test host. The OS-side service calling-process identity check rejects network-logon tokens, so @@ -193,33 +192,6 @@ function Envelope-Arm { '' } -# Creates a directory with a locked-down DACL: inheritance disabled, -# ACEs reset to current user + SYSTEM + Administrators (FullControl). -# Subdirectories created inside inherit this ACL automatically. -# -# Why: by default, dirs under C:\ inherit Authenticated Users / Users -# read+execute ACEs. The agent user is a member of those groups, so -# without lockdown the "control" test (agent has no share, so cannot -# access the dir) would spuriously pass and the readonly-deny test (B6) -# would not actually prove the ACE granted is read-only. -function Setup-LockedDownTestDir { - param([string]$Path) - - New-Item -Path $Path -ItemType Directory -Force | Out-Null - - $acl = Get-Acl $Path - $acl.SetAccessRuleProtection($true, $false) - $acl.Access | ForEach-Object { [void]$acl.RemoveAccessRule($_) } - - $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name - $inherit = "ContainerInherit,ObjectInherit" - foreach ($principal in @($currentUser, "SYSTEM", "Administrators")) { - $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( - $principal, "FullControl", $inherit, "None", "Allow"))) - } - Set-Acl -Path $Path -AclObject $acl -} - # ---------------- Backend-availability probe ---------------- # Sends a state-aware provision request and surfaces a `backend_unavailable` @@ -300,40 +272,6 @@ function Run-StateAwareTest { return $script:currentTestPassed } -# Test-dir setup. Lifecycle A's "control" test, Lifecycle B's full -# filesystem-policy lifecycle, and the grant-scope check all run against -# a single locked-down host directory tree: -# -# C:\mxc_share_test\ (locked-down: current user + SYSTEM + Admins) -# rw\ (inherits parent ACL; granted to B agent as rw) -# ro\ -# marker.txt (pre-populated; granted to B agent as ro) -# restricted\ -# marker.txt (pre-populated; NOT granted to B agent -- -# proves grants are path-specific, not blanket -# on the parent) -# -# The lockdown is essential -- without it the agent user (a member of -# Authenticated Users / Users) would inherit read access by default, so -# the control test would not actually demonstrate that share_folders is -# what enables access in Lifecycle B. -$script:TestRoot = 'C:\mxc_share_test' -$script:FilterTestRoot = 'C:\mxc_filter_test' -$script:RoMarkerContent = 'readonly-marker-content' -$script:RestrictedMarkerContent = 'restricted-marker-content' -Setup-LockedDownTestDir $script:TestRoot -New-Item -Path "$script:TestRoot\rw" -ItemType Directory -Force | Out-Null -New-Item -Path "$script:TestRoot\ro" -ItemType Directory -Force | Out-Null -New-Item -Path "$script:TestRoot\restricted" -ItemType Directory -Force | Out-Null -$script:RoMarkerContent | Set-Content -Path "$script:TestRoot\ro\marker.txt" -NoNewline -$script:RestrictedMarkerContent | Set-Content -Path "$script:TestRoot\restricted\marker.txt" -NoNewline -Setup-LockedDownTestDir $script:FilterTestRoot -New-Item -Path "$script:FilterTestRoot\rw" -ItemType Directory -Force | Out-Null - -# Outer try-finally: ensures the host directory tree is removed even if a -# test panics. The summary at the end runs after the cleanup. -try { - # try-finally wraps the lifecycle so any mid-flow failure still triggers a # best-effort deprovision. Mirrors the defensive cleanup in # IsolationSessionRunner::execute -- failed runs do not leak Indefinite- @@ -342,8 +280,8 @@ $script:sandboxId = $null $deprovisionedOk = $false try { - # Test 1: provision returns iso:wxc-<8-hex> and a backend_unavailable-free - # envelope. + # Test 1: provision returns iso: and a + # backend_unavailable-free envelope. Run-StateAwareTest "provision (sandbox_id format)" { $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_provision.json' -Experimental $envObj = Parse-Envelope -Stdout $r.Stdout @@ -357,8 +295,12 @@ try { Assert-True ($r.ExitCode -eq 0) "exit code = 0 on success" $script:sandboxId = $envObj.result.sandboxId $agentUserName = $envObj.result.metadata.agentUserName - Assert-True ($script:sandboxId -match '^iso:wxc-[0-9a-f]{8}$') "sandbox_id matches iso:wxc-<8-hex> ($script:sandboxId)" + $agentUserSid = $envObj.result.metadata.agentUserSid + $workspacePath = $envObj.result.metadata.ephemeralWorkspacePath + Assert-True ($script:sandboxId -match '^iso:.+$') "sandbox_id is iso: ($script:sandboxId)" Assert-True ($null -ne $agentUserName) "metadata.agentUserName is present ($agentUserName)" + Assert-True (-not [string]::IsNullOrWhiteSpace($agentUserSid)) "metadata.agentUserSid is present ($agentUserSid)" + Assert-True (-not [string]::IsNullOrWhiteSpace($workspacePath)) "metadata.ephemeralWorkspacePath is present ($workspacePath)" } } | Out-Null @@ -398,20 +340,7 @@ try { } } - # Test 2b: control -- the main sandbox has no fs grants, so the agent - # user must NOT be able to read C:\mxc_share_test\ro\marker.txt. The - # locked-down DACL (no Authenticated Users / Users) is what makes this - # check meaningful: without it the agent would inherit read access via - # default group membership. Pairs with Lifecycle B's B5, where the same - # read SUCCEEDS because B's sandbox was provisioned with readonlyPaths. - if ($startedOk) { - Run-StateAwareTest "control (unshared path inaccessible)" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_read_readonly.json' -SandboxId $script:sandboxId -Experimental - Assert-True ($r.ExitCode -ne 0) "exit code is non-zero (read denied)" - } | Out-Null - } - - # Test 2c: start rejects requests that carry filesystem policy. Filesystem + # Test 2b: start rejects requests that carry filesystem policy. Filesystem # policy is bound to provision and immutable thereafter; a non-empty # readwritePaths on a start request must be rejected with policy_validation. if ($startedOk) { @@ -466,13 +395,12 @@ try { } | Out-Null } - # Test 4: filesystem state continuity across separate wxc-exec invocations - # against the same sandbox_id. exec #1 writes a marker file to the agent - # user's TEMP, exec #2 reads it back. Each exec is a fresh wxc-exec - # process consuming the same sandbox_id, exercising the cross-process - # state-aware path. + # Test 4: sandbox-internal %TEMP% continuity across separate wxc-exec + # invocations against the same sandbox_id. exec #1 writes a marker file + # to the agent user's TEMP, exec #2 reads it back. Each exec is a fresh + # wxc-exec process consuming the same sandbox_id. if ($execedOk) { - Run-StateAwareTest "multi-exec (filesystem state continuity)" { + Run-StateAwareTest "multi-exec (%TEMP% state continuity)" { $w = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_write_marker.json' -SandboxId $script:sandboxId -Experimental Assert-True ($w.ExitCode -eq 0) "exec #1 (write) exit code = 0" $rd = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_read_marker.json' -SandboxId $script:sandboxId -Experimental @@ -678,374 +606,27 @@ try { } } -# ---------------- Lifecycle B: Filesystem policy ---------------- - -# A separate sandbox provisioned with both readwritePaths and readonlyPaths, -# exercising the full read/write semantics of share_folders end-to-end: -# - rw grant: agent can write a file the host then sees, and read it back -# - ro grant: agent can read pre-populated host content, but cannot write -# - no grant: a sibling subdir (restricted) under the same parent is -# inaccessible -- proves grants are path-specific, not blanket on parent -$script:fsSandboxId = $null -$fsDeprovisionedOk = $false -try { - $fsProvisionedOk = Run-StateAwareTest "filesystem: provision (rw + ro)" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_provision_with_filesystem.json' -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Write-Host " Envelope arm: $arm" -ForegroundColor Red - Write-Host " Stdout: $($r.Stdout)" -ForegroundColor Gray - Write-Host " Stderr: $($r.Stderr)" -ForegroundColor Gray - Assert-True $false "filesystem provision returned a result envelope" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0 on success" - $script:fsSandboxId = $envObj.result.sandboxId - Assert-True ($script:fsSandboxId -match '^iso:wxc-[0-9a-f]{8}$') ` - "sandbox_id matches iso:wxc-<8-hex> ($script:fsSandboxId)" - $agentUserName = if ($envObj.result.metadata) { [string]$envObj.result.metadata.agentUserName } else { '' } - Write-Host " filesystem provisioned: agentUserName=$agentUserName" -ForegroundColor DarkGray - } - } - - $fsStartedOk = $false - if ($fsProvisionedOk) { - $fsStartedOk = Run-StateAwareTest "filesystem: start" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_start.json' -SandboxId $script:fsSandboxId -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Assert-True $false "filesystem start returned a result envelope (got $arm)" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0 on success" - } - } - } - - if ($fsStartedOk) { - Run-StateAwareTest "filesystem: exec write-to-rw" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_write_shared.json' -SandboxId $script:fsSandboxId -Experimental - Assert-True ($r.ExitCode -eq 0) "exit code = 0 (agent wrote successfully)" - Assert-True ($r.Stdout -match 'shared-write-content') ` - "agent's own type-back read succeeded ('shared-write-content' in stdout)" - $hostPath = Join-Path $script:TestRoot 'rw\agent_wrote.txt' - $hostExists = Test-Path $hostPath - Assert-True $hostExists "host sees the file the agent wrote ($hostPath)" - if ($hostExists) { - $hostContent = Get-Content $hostPath -Raw - Assert-True ($hostContent -match 'shared-write-content') ` - "host file contains 'shared-write-content'" - } - } | Out-Null - - Run-StateAwareTest "filesystem: exec read-from-rw" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_read_shared.json' -SandboxId $script:fsSandboxId -Experimental - Assert-True ($r.ExitCode -eq 0) "exit code = 0" - Assert-True ($r.Stdout -match 'shared-write-content') ` - "agent reads back the file it wrote earlier" - } | Out-Null - - Run-StateAwareTest "filesystem: exec read-from-ro" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_read_readonly.json' -SandboxId $script:fsSandboxId -Experimental - Assert-True ($r.ExitCode -eq 0) "exit code = 0" - $expected = [regex]::Escape($script:RoMarkerContent) - Assert-True ($r.Stdout -match $expected) ` - "agent reads pre-populated marker.txt ('$($script:RoMarkerContent)')" - } | Out-Null - - Run-StateAwareTest "filesystem: exec write-to-ro denied" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_write_readonly_denied.json' -SandboxId $script:fsSandboxId -Experimental - Assert-True ($r.ExitCode -ne 0) "exit code is non-zero (write to read-only path failed)" - $hostPath = Join-Path $script:TestRoot 'ro\agent_should_fail.txt' - Assert-True (-not (Test-Path $hostPath)) ` - "host did not see the file (write was actually denied, not silently swallowed)" - } | Out-Null - - Run-StateAwareTest "filesystem: exec read-from-restricted denied" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_read_restricted.json' -SandboxId $script:fsSandboxId -Experimental - Assert-True ($r.ExitCode -ne 0) "exit code is non-zero (read from unshared sibling failed)" - $stdoutText = if ($null -eq $r.Stdout) { "" } else { [string]$r.Stdout } - $leaked = $stdoutText.Contains($script:RestrictedMarkerContent) - Assert-True (-not $leaked) ` - "stdout does NOT contain the restricted marker content (grant is path-specific, not blanket)" - } | Out-Null - } - - $fsStoppedOk = $false - if ($fsStartedOk) { - $fsStoppedOk = Run-StateAwareTest "filesystem: stop" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_stop.json' -SandboxId $script:fsSandboxId -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Assert-True $false "filesystem stop returned a result envelope (got $arm)" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0" - } - } - } - - if ($fsStoppedOk) { - $fsDeprovPassed = Run-StateAwareTest "filesystem: deprovision" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $script:fsSandboxId -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Assert-True $false "filesystem deprovision returned a result envelope (got $arm)" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0" - } - } - if ($fsDeprovPassed) { $fsDeprovisionedOk = $true } - } -} finally { - if ($null -ne $script:fsSandboxId -and -not $fsDeprovisionedOk) { - Write-Host "" - Write-Host "[cleanup] best-effort deprovision of $script:fsSandboxId" -ForegroundColor DarkGray - try { - $null = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $script:fsSandboxId -Experimental - } catch { - Write-Host " cleanup deprovision threw: $($_.Exception.Message)" -ForegroundColor DarkGray - } - } -} - -# ---------------- Lifecycle BF: filesystem-policy path filter ---------------- -# -# Verifies the wxc-exec filesystem-policy path filter (MXC issue #330). -# Provisions with a mix of protected (drive root, C:\Windows) and -# non-protected (C:\mxc_filter_test\rw) paths in readwritePaths. Expected: -# provision succeeds (the filter is silent -- no error returned, no -# filter-specific metadata field added), the protected paths get NO new -# agent ACE (filter dropped them before ShareFolderBatchAsync), and the -# non-protected path receives the agent's ACE as normal (positive control: -# legitimate path not accidentally dropped). -# -# Test-dir setup happens at file scope alongside $script:TestRoot so the -# outer try-finally can clean both up between runs; without that cleanup -# a stale $script:FilterTestRoot from a prior run causes Setup-LockedDownTestDir -# to fail with SeSecurityPrivilege (Set-Acl on an existing -# inheritance-disabled directory writes the SACL slot, which non-elevated -# admins cannot do). - -# Translates a local-account name to its SID. Returns $null if the -# translation fails (e.g., the user no longer exists). -function Get-LocalAccountSid { - param([string]$Name) - try { - $nt = New-Object System.Security.Principal.NTAccount($Name) - $nt.Translate([System.Security.Principal.SecurityIdentifier]).Value - } catch { - $null - } -} - -# Returns $true if the ACL on $Path has any access rule whose -# IdentityReference translates to $TargetSid. -function Test-AclContainsSid { - param([string]$Path, [string]$TargetSid) - $acl = Get-Acl $Path - foreach ($ace in $acl.Access) { - try { - $aceSid = $ace.IdentityReference.Translate( - [System.Security.Principal.SecurityIdentifier]).Value - if ($aceSid -eq $TargetSid) { return $true } - } catch { - # IdentityReference may be untranslatable (orphan SID etc.); skip. - } - } - $false -} - -$script:filterSandboxId = $null -$script:filterAgentUserName = $null -$filterDeprovisionedOk = $false -try { - $filterProvisionedOk = Run-StateAwareTest "filter: provision (protected + non-protected paths)" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_provision_with_filter.json' -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Write-Host " Envelope arm: $arm" -ForegroundColor Red - Write-Host " Stdout: $($r.Stdout)" -ForegroundColor Gray - Write-Host " Stderr: $($r.Stderr)" -ForegroundColor Gray - Assert-True $false "filter provision returned a result envelope (filter is silent -- no error expected)" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0 (filter dropped protected paths silently)" - $script:filterSandboxId = $envObj.result.sandboxId - Assert-True ($script:filterSandboxId -match '^iso:wxc-[0-9a-f]{8}$') ` - "sandbox_id matches iso:wxc-<8-hex> ($script:filterSandboxId)" - $script:filterAgentUserName = if ($envObj.result.metadata) { [string]$envObj.result.metadata.agentUserName } else { '' } - Write-Host " filter test provisioned: agentUserName=$($script:filterAgentUserName)" -ForegroundColor DarkGray - } - } - - if ($filterProvisionedOk -and $script:filterAgentUserName -and $script:filterAgentUserName -ne '') { - $filterAgentSid = Get-LocalAccountSid -Name $script:filterAgentUserName - - Run-StateAwareTest "filter: agent user name translates to SID" { - Assert-True ($null -ne $filterAgentSid) ` - "translated '$($script:filterAgentUserName)' to SID ($filterAgentSid)" - } | Out-Null - if ($null -ne $filterAgentSid) { - Run-StateAwareTest "filter: protected drive root receives no agent ACE" { - Assert-True (-not (Test-AclContainsSid -Path 'C:\' -TargetSid $filterAgentSid)) ` - "agent SID is NOT in ACL of C:\ (drive root filter dropped the entry)" - } | Out-Null - - Run-StateAwareTest "filter: protected SystemRoot receives no agent ACE" { - Assert-True (-not (Test-AclContainsSid -Path 'C:\Windows' -TargetSid $filterAgentSid)) ` - "agent SID is NOT in ACL of C:\Windows (SystemRoot filter dropped the entry)" - } | Out-Null - - Run-StateAwareTest "filter: non-protected path receives agent ACE (positive control)" { - $nonProtected = Join-Path $script:FilterTestRoot 'rw' - Assert-True (Test-AclContainsSid -Path $nonProtected -TargetSid $filterAgentSid) ` - "agent SID IS in ACL of $nonProtected (filter passed legitimate path through)" - } | Out-Null - } - } +# ---------------- Lifecycle B: Filesystem policy rejection ---------------- - if ($filterProvisionedOk) { - $filterDeprovPassed = Run-StateAwareTest "filter: deprovision" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $script:filterSandboxId -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Assert-True $false "filter deprovision returned a result envelope (got $arm)" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0" - } - } - if ($filterDeprovPassed) { $filterDeprovisionedOk = $true } - } -} finally { - if ($null -ne $script:filterSandboxId -and -not $filterDeprovisionedOk) { - Write-Host "" - Write-Host "[cleanup] best-effort deprovision of $script:filterSandboxId" -ForegroundColor DarkGray - try { - $null = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $script:filterSandboxId -Experimental - } catch { - Write-Host " cleanup deprovision threw: $($_.Exception.Message)" -ForegroundColor DarkGray - } - } -} - -# ---------------- Lifecycle C: Medium configurationId ---------------- - -# A separate, throwaway sandbox that exercises the Medium config-id end-to-end. -# Lifecycle A defaulted to Composable (no `experimental.isolation_session.start` -# block). This lifecycle proves Medium also works on the target OS build: -# provision -> start with configurationId=medium -> one echo exec -> stop -> -# deprovision. Independent of Lifecycle A's sandbox so a failure here does not -# pollute the main lifecycle's results. -$script:mediumSandboxId = $null -$mediumDeprovisionedOk = $false -try { - Run-StateAwareTest "Medium: provision" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_provision.json' -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Write-Host " Envelope arm: $arm" -ForegroundColor Red - Write-Host " Stdout: $($r.Stdout)" -ForegroundColor Gray - Write-Host " Stderr: $($r.Stderr)" -ForegroundColor Gray - Assert-True $false "Medium provision returned a result envelope" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0 on success" - $script:mediumSandboxId = $envObj.result.sandboxId - Assert-True ($script:mediumSandboxId -match '^iso:wxc-[0-9a-f]{8}$') ` - "sandbox_id matches iso:wxc-<8-hex> ($script:mediumSandboxId)" - $agentUserName = if ($envObj.result.metadata) { [string]$envObj.result.metadata.agentUserName } else { '' } - Write-Host " Medium provisioned: agentUserName=$agentUserName" -ForegroundColor DarkGray - } - } | Out-Null - - $mediumStartedOk = $false - if ($null -ne $script:mediumSandboxId) { - $mediumStartedOk = Run-StateAwareTest "Medium: start (configurationId=medium)" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_start_medium.json' -SandboxId $script:mediumSandboxId -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Write-Host " Envelope arm: $arm" -ForegroundColor Red - Write-Host " Stdout: $($r.Stdout)" -ForegroundColor Gray - Write-Host " Stderr: $($r.Stderr)" -ForegroundColor Gray - Assert-True $false "Medium start returned a result envelope" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0 on success" - } - } - } - - $mediumExecedOk = $false - if ($mediumStartedOk) { - $mediumExecedOk = Run-StateAwareTest "Medium: exec (basic echo)" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_exec_basic.json' -SandboxId $script:mediumSandboxId -Experimental - Assert-True ($r.ExitCode -eq 0) "exit code = 0 on success" - Assert-True ($r.Stdout -match 'state-aware-exec-marker') ` - "stdout contains the script's output (Medium config supports process launch)" - } - } +# Filesystem policy is no longer accepted at any lifecycle phase. Provision +# with readwrite/readonly policy now fails before creating a sandbox, so no +# cleanup is needed. +Run-StateAwareTest "filesystem: provision rejected" { + $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_provision_with_filesystem.json' -Experimental + Assert-True ($r.ExitCode -ne 0) "exit code is non-zero (policy rejected)" + $envObj = Parse-Envelope -Stdout $r.Stdout + Assert-True ($null -ne $envObj) "stdout is a parseable envelope" + $code = if ($envObj) { $envObj.error.code } else { '' } + Assert-True ($code -eq 'policy_validation') "error.code is 'policy_validation' (got '$code')" +} | Out-Null - $mediumStoppedOk = $false - if ($mediumExecedOk) { - $mediumStoppedOk = Run-StateAwareTest "Medium: stop" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_stop.json' -SandboxId $script:mediumSandboxId -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Write-Host " Envelope arm: $arm" -ForegroundColor Red - Write-Host " Stdout: $($r.Stdout)" -ForegroundColor Gray - Write-Host " Stderr: $($r.Stderr)" -ForegroundColor Gray - Assert-True $false "Medium stop returned a result envelope" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0 on success" - } - } - } - if ($mediumStoppedOk) { - $mediumDeprovPassed = Run-StateAwareTest "Medium: deprovision" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $script:mediumSandboxId -Experimental - $envObj = Parse-Envelope -Stdout $r.Stdout - $arm = Envelope-Arm $envObj - if ($arm -ne 'result') { - Write-Host " Envelope arm: $arm" -ForegroundColor Red - Write-Host " Stdout: $($r.Stdout)" -ForegroundColor Gray - Write-Host " Stderr: $($r.Stderr)" -ForegroundColor Gray - Assert-True $false "Medium deprovision returned a result envelope" - } else { - Assert-True ($r.ExitCode -eq 0) "exit code = 0 on success" - } - } - if ($mediumDeprovPassed) { $mediumDeprovisionedOk = $true } - } -} finally { - if ($null -ne $script:mediumSandboxId -and -not $mediumDeprovisionedOk) { - Write-Host "" - Write-Host "[cleanup] best-effort deprovision of $script:mediumSandboxId" -ForegroundColor DarkGray - try { - $cleanupResult = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $script:mediumSandboxId -Experimental - if ($cleanupResult.ExitCode -eq 0) { - Write-Host " cleanup deprovision succeeded" -ForegroundColor DarkGray - } else { - Write-Host " cleanup deprovision exit $($cleanupResult.ExitCode); stdout: $($cleanupResult.Stdout)" -ForegroundColor DarkGray - } - } catch { - Write-Host " cleanup deprovision threw: $($_.Exception.Message)" -ForegroundColor DarkGray - } - } -} -# ---------------- Lifecycle D: Entra agent validation rejections ---------------- +# ---------------- Lifecycle D: Entra user-bundle shape validation ---------------- -# Validation runs before any OS-side call, so these tests never reach the -# IsoEnvBroker service and need no sandbox cleanup. They cover the four -# rejection cells of the validate_provision/validate_start matrix added -# alongside the v2 binding wiring: malformed user shape at provision, and -# the three sandboxId-vs-user mismatches at start. +# These validation tests reject malformed user bundles at provision before a +# sandbox is created, so no cleanup is needed. Run-StateAwareTest "provision (user.upn malformed: missing @)" { $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_provision_user_malformed_upn.json' -Experimental @@ -1067,44 +648,14 @@ Run-StateAwareTest "provision (user.wamToken empty)" { Assert-True ($msg.Contains('wamToken')) "error.message mentions 'wamToken' (got '$msg')" } | Out-Null -Run-StateAwareTest "start (Entra sandbox without user)" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_start_entra_missing_user.json' -Experimental - Assert-True ($r.ExitCode -ne 0) "exit code is non-zero (validation rejected)" - $envObj = Parse-Envelope -Stdout $r.Stdout - $code = if ($envObj) { $envObj.error.code } else { '' } - Assert-True ($code -eq 'malformed_request') "error.code is 'malformed_request' (got '$code')" - $msg = if ($envObj) { [string]$envObj.error.message } else { '' } - Assert-True ($msg.Contains('Entra sandbox requires user')) "error.message mentions Entra-requires-user (got '$msg')" -} | Out-Null -Run-StateAwareTest "start (local sandbox with user)" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_start_local_with_user.json' -Experimental - Assert-True ($r.ExitCode -ne 0) "exit code is non-zero (validation rejected)" - $envObj = Parse-Envelope -Stdout $r.Stdout - $code = if ($envObj) { $envObj.error.code } else { '' } - Assert-True ($code -eq 'malformed_request') "error.code is 'malformed_request' (got '$code')" - $msg = if ($envObj) { [string]$envObj.error.message } else { '' } - Assert-True ($msg.Contains('not allowed for local sandbox')) "error.message mentions local-sandbox restriction (got '$msg')" -} | Out-Null -Run-StateAwareTest "start (user.upn mismatches sandboxId)" { - $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_start_upn_mismatch.json' -Experimental - Assert-True ($r.ExitCode -ne 0) "exit code is non-zero (validation rejected)" - $envObj = Parse-Envelope -Stdout $r.Stdout - $code = if ($envObj) { $envObj.error.code } else { '' } - Assert-True ($code -eq 'malformed_request') "error.code is 'malformed_request' (got '$code')" - $msg = if ($envObj) { [string]$envObj.error.message } else { '' } - Assert-True ($msg.Contains('does not match sandboxId')) "error.message mentions sandboxId mismatch (got '$msg')" -} | Out-Null # ---------------- Lifecycle E: Simultaneous isolation-session sandboxes ---------------- # # Three concurrently-provisioned sandboxes (A, B, C) verify that -# deprovisioning one does not tear down the others. The runner does not -# call UnregisterAppAsync (see IsolationSessionManager::unregister_client) -# so the per-user hardcoded `regid` registration survives a deprovision of -# any individual sandbox. Without that property the first deprovision -# would break every still-running concurrent sandbox. +# deprovisioning one does not tear down the others. The first deprovision +# must not break every still-running concurrent sandbox. # # Per-agent state isolation is verified by having each sandbox write a # unique marker file into its agent's %TEMP% and asserting that each @@ -1171,7 +722,7 @@ try { $script:saSandboxA = Provision-LifecycleESandbox -Label "A" Assert-True ($null -ne $script:saSandboxA) "saSandboxA is non-null" if ($null -ne $script:saSandboxA) { - Assert-True ($script:saSandboxA -match '^iso:wxc-[0-9a-f]{8}$') "saSandboxA matches expected format ($script:saSandboxA)" + Assert-True ($script:saSandboxA -match '^iso:.+$') "saSandboxA is iso: ($script:saSandboxA)" } } | Out-Null Run-StateAwareTest "Lifecycle E: provision B" { @@ -1247,8 +798,8 @@ try { Assert-True (-not $out.Contains("marker_B.txt")) "C does not see marker_B.txt" } | Out-Null - # E5: Stop + deprovision B. The regid leak means the registration - # survives B's deprovision and A / C remain functional. + # E5: Stop + deprovision B. Each sandbox is a distinct OS agent user, + # so deprovisioning B removes only B's user; A / C remain functional. Run-StateAwareTest "Lifecycle E: stop B" { $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_stop.json' -SandboxId $script:saSandboxB -Experimental Assert-True ($r.ExitCode -eq 0) "stop B exit 0" @@ -1260,17 +811,17 @@ try { if ($bDeprovPassed) { $saBDeprov = $true } # E6: Regression check -- A and C remain functional after B - # deprovisioned. Would fail without the regid leak. + # deprovisioned. Per-user isolation means B's teardown cannot affect them. Run-StateAwareTest "Lifecycle E: A still functional after B deprov" { $r = Exec-LifecycleEListMarkers -SandboxId $script:saSandboxA $out = [string]$r.Stdout - Assert-True ($r.ExitCode -eq 0) "list exit 0 (regid not torn down by B's deprovision)" + Assert-True ($r.ExitCode -eq 0) "list exit 0 (A's agent user not torn down by B's deprovision)" Assert-True ($out.Contains("marker_A.txt")) "A still sees marker_A.txt" } | Out-Null Run-StateAwareTest "Lifecycle E: C still functional after B deprov" { $r = Exec-LifecycleEListMarkers -SandboxId $script:saSandboxC $out = [string]$r.Stdout - Assert-True ($r.ExitCode -eq 0) "list exit 0 (regid not torn down by B's deprovision)" + Assert-True ($r.ExitCode -eq 0) "list exit 0 (C's agent user not torn down by B's deprovision)" Assert-True ($out.Contains("marker_C.txt")) "C still sees marker_C.txt" } | Out-Null @@ -1305,8 +856,8 @@ try { if ($cDeprovPassed) { $saCDeprov = $true } } - # E10: Fresh provision D after all three torn down -- the leaked regid - # must not poison new sandboxes either. + # E10: Fresh provision D after all three torn down -- per-user isolation + # means new sandboxes are unaffected by the earlier teardowns. Run-StateAwareTest "Lifecycle E: provision D after all torn down" { $script:saSandboxD = Provision-LifecycleESandbox -Label "D" Assert-True ($null -ne $script:saSandboxD) "saSandboxD is non-null" @@ -1346,16 +897,155 @@ try { } } + +# ---------------- Lifecycle F: Ephemeral workspace sharing ---------------- +# +# Provision now returns `ephemeralWorkspacePath` -- a directory shared between +# the calling user (this harness) and the isolated agent user. This group +# verifies the sharing + isolation contract end to end: +# - the caller can stage a file INTO a session through its workspace, +# - a session can hand a file back to the caller through its workspace, +# - an isolated user can access ONLY its own workspace, not a peer's, +# - the workspace is deleted when the sandbox is deprovisioned. +# +# It also exercises `agentUserSid` (asserted present at provision). + +$script:fA = $null # @{ SandboxId; Workspace; Sid } +$script:fB = $null +$fADeprov = $false +$fBDeprov = $false + +# Provision a sandbox and capture its workspace path + agent SID from the +# provision metadata. Returns $null on failure. +function Provision-LifecycleFSandbox { + param([string]$Label) + $r = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_provision.json' -Experimental + $envObj = Parse-Envelope -Stdout $r.Stdout + if ((Envelope-Arm $envObj) -ne 'result') { + Write-Host " $Label provision arm: $(Envelope-Arm $envObj)" -ForegroundColor Red + Write-Host " Stdout: $($r.Stdout)" -ForegroundColor Gray + return $null + } + $meta = $envObj.result.metadata + $obj = @{ + SandboxId = [string]$envObj.result.sandboxId + Workspace = if ($meta) { [string]$meta.ephemeralWorkspacePath } else { '' } + Sid = if ($meta) { [string]$meta.agentUserSid } else { '' } + } + Write-Host " $Label provisioned: sandboxId=$($obj.SandboxId) workspace=$($obj.Workspace) sid=$($obj.Sid)" -ForegroundColor DarkGray + return $obj +} + +# Exec an arbitrary command inside a started session. +function Exec-InSession { + param([string]$SandboxId, [string]$CommandLine) + $req = @{ + phase = 'exec' + sandboxId = $SandboxId + process = @{ commandLine = $CommandLine; timeout = 30000 } + } + Invoke-StateAware -Request $req -Experimental +} + +try { + Run-StateAwareTest "Lifecycle F: provision A + B with workspaces" { + $script:fA = Provision-LifecycleFSandbox -Label "F-A" + $script:fB = Provision-LifecycleFSandbox -Label "F-B" + Assert-True ($null -ne $script:fA) "A provisioned" + Assert-True ($null -ne $script:fB) "B provisioned" + if ($null -ne $script:fA -and $null -ne $script:fB) { + Assert-True (-not [string]::IsNullOrWhiteSpace($script:fA.Workspace)) "A ephemeralWorkspacePath present ($($script:fA.Workspace))" + Assert-True (-not [string]::IsNullOrWhiteSpace($script:fB.Workspace)) "B ephemeralWorkspacePath present ($($script:fB.Workspace))" + Assert-True (-not [string]::IsNullOrWhiteSpace($script:fA.Sid)) "A agentUserSid present ($($script:fA.Sid))" + Assert-True ($script:fA.Workspace -ne $script:fB.Workspace) "A and B have distinct workspaces" + # The caller (this harness == the provisioning user) can access every + # concurrent sandbox's workspace directory. + Assert-True (Test-Path -LiteralPath $script:fA.Workspace) "caller can access A's workspace dir" + Assert-True (Test-Path -LiteralPath $script:fB.Workspace) "caller can access B's workspace dir" + } + } | Out-Null + + if ($null -ne $script:fA -and $null -ne $script:fB -and $script:fA.Workspace -and $script:fB.Workspace) { + Run-StateAwareTest "Lifecycle F: start A + B" { + $rsa = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_start.json' -SandboxId $script:fA.SandboxId -Experimental + Assert-True ($rsa.ExitCode -eq 0) "start A exit 0" + $rsb = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_start.json' -SandboxId $script:fB.SandboxId -Experimental + Assert-True ($rsb.ExitCode -eq 0) "start B exit 0" + } | Out-Null + + # F2: caller -> session. The caller stages a file into A's workspace; + # session A reads it back. + Run-StateAwareTest "Lifecycle F: caller shares a file into session A" { + $wsA = $script:fA.Workspace + Set-Content -LiteralPath (Join-Path $wsA 'caller_to_A.txt') -Value 'from-caller' -Encoding Ascii + $r = Exec-InSession -SandboxId $script:fA.SandboxId -CommandLine "cmd /c type `"$wsA\caller_to_A.txt`"" + $out = [string]$r.Stdout + Assert-True ($r.ExitCode -eq 0) "session A reads the caller's file (exit 0)" + Assert-True ($out.Contains('from-caller')) "session A sees caller_to_A.txt content" + } | Out-Null + + # F3: session -> caller. Session A writes into its workspace; the caller + # reads it back on the host. + Run-StateAwareTest "Lifecycle F: session A shares a file back to the caller" { + $wsA = $script:fA.Workspace + $r = Exec-InSession -SandboxId $script:fA.SandboxId -CommandLine "cmd /c echo from-session-A> `"$wsA\A_to_caller.txt`"" + Assert-True ($r.ExitCode -eq 0) "session A writes to its workspace (exit 0)" + $backFile = Join-Path $wsA 'A_to_caller.txt' + Assert-True (Test-Path -LiteralPath $backFile) "caller sees the file session A wrote" + $content = Get-Content -LiteralPath $backFile -Raw -ErrorAction SilentlyContinue + Assert-True ($content -match 'from-session-A') "caller reads session A's content" + } | Out-Null + + # F4: cross-session isolation. The caller stages a marker into B's + # workspace; session A must NOT be able to read B's workspace. + Run-StateAwareTest "Lifecycle F: session A cannot access session B's workspace" { + $wsB = $script:fB.Workspace + Set-Content -LiteralPath (Join-Path $wsB 'caller_to_B.txt') -Value 'B-only' -Encoding Ascii + Assert-True (Test-Path -LiteralPath (Join-Path $wsB 'caller_to_B.txt')) "caller can stage into B's workspace" + $r = Exec-InSession -SandboxId $script:fA.SandboxId -CommandLine "cmd /c type `"$wsB\caller_to_B.txt`"" + $out = [string]$r.Stdout + Assert-True (-not $out.Contains('B-only')) "session A does NOT see B's workspace content" + Assert-True ($r.ExitCode -ne 0) "session A's read of B's workspace fails (access denied)" + } | Out-Null + + # F5: teardown deletes the workspace. + $script:fADeprovOk = $false + Run-StateAwareTest "Lifecycle F: deprovision A deletes its workspace" { + $rstop = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_stop.json' -SandboxId $script:fA.SandboxId -Experimental + Assert-True ($rstop.ExitCode -eq 0) "stop A exit 0" + $rdep = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $script:fA.SandboxId -Experimental + Assert-True ($rdep.ExitCode -eq 0) "deprovision A exit 0" + if ($rdep.ExitCode -eq 0) { $script:fADeprovOk = $true } + Assert-True (-not (Test-Path -LiteralPath $script:fA.Workspace)) "A's workspace dir is gone after deprovision" + } | Out-Null + if ($script:fADeprovOk) { $fADeprov = $true } + + $script:fBDeprovOk = $false + Run-StateAwareTest "Lifecycle F: deprovision B" { + $rstop = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_stop.json' -SandboxId $script:fB.SandboxId -Experimental + Assert-True ($rstop.ExitCode -eq 0) "stop B exit 0" + $rdep = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $script:fB.SandboxId -Experimental + Assert-True ($rdep.ExitCode -eq 0) "deprovision B exit 0" + if ($rdep.ExitCode -eq 0) { $script:fBDeprovOk = $true } + } | Out-Null + if ($script:fBDeprovOk) { $fBDeprov = $true } + } } finally { - # Outer-try finally: remove the locked-down test trees. Runs even if a - # test panics so we don't leak the directories between runs. Cleanup is - # necessary: re-running Setup-LockedDownTestDir on an existing locked-down - # directory fails non-elevated because Set-Acl tries to write the SACL - # slot, which requires SeSecurityPrivilege. - Remove-Item -Recurse -Force $script:TestRoot -ErrorAction SilentlyContinue - Remove-Item -Recurse -Force $script:FilterTestRoot -ErrorAction SilentlyContinue + foreach ($entry in @( + @{ obj = $script:fA; done = $fADeprov; label = 'F-A' }, + @{ obj = $script:fB; done = $fBDeprov; label = 'F-B' } + )) { + if ($null -ne $entry.obj -and -not $entry.done) { + Write-Host "" + Write-Host "[cleanup] best-effort deprovision of Lifecycle F sandbox $($entry.label) ($($entry.obj.SandboxId))" -ForegroundColor DarkGray + try { + $null = Invoke-StateAware -ConfigFile 'isolation_session_state_aware_deprovision.json' -SandboxId $entry.obj.SandboxId -Experimental + } catch { } + } + } } + # ---------------- Summary ---------------- $total = $script:TestResults.Count diff --git a/tests/scripts/run_isolation_session_tests.ps1 b/tests/scripts/run_isolation_session_tests.ps1 index 957edff3c..1d39307d5 100644 --- a/tests/scripts/run_isolation_session_tests.ps1 +++ b/tests/scripts/run_isolation_session_tests.ps1 @@ -4,10 +4,7 @@ <# .SYNOPSIS Runs IsolationSession E2E tests. Requires a Windows host with the - in-proc Windows.AI.IsolationSession IsoSessionOps APIs available - (IsoSessionApp.dll registered, Feature_IsoBrokerSessionApis enabled, - and Feature_IsoBrokerCommandLineSessions enabled for the Composable - config-id path). + in-proc IsolationSession service available. .DESCRIPTION - Locates wxc-exec.exe (built with --features isolation_session) @@ -256,55 +253,8 @@ function Run-IsolationSessionTest { return @{ Name = $ConfigFile; Pass = $pass; Skipped = $false; Reason = $reason } } -# Creates a directory with a locked-down DACL: inheritance disabled, ACEs -# reset to current user + SYSTEM + Administrators (FullControl). Used by -# the filesystem-policy test so the agent user has no inherited access by -# default -- the test then proves the share_folders grant is what enables -# read access. -function Setup-LockedDownTestDir { - param([string]$Path) - - New-Item -Path $Path -ItemType Directory -Force | Out-Null - - $acl = Get-Acl $Path - $acl.SetAccessRuleProtection($true, $false) - $acl.Access | ForEach-Object { [void]$acl.RemoveAccessRule($_) } - - $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name - $inherit = "ContainerInherit,ObjectInherit" - foreach ($principal in @($currentUser, "SYSTEM", "Administrators")) { - $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( - $principal, "FullControl", $inherit, "None", "Allow"))) - } - Set-Acl -Path $Path -AclObject $acl -} - [System.Collections.ArrayList]$results = @() -# Filesystem-policy test scaffolding: a locked-down dir the test expects -# the agent to read via a readwritePaths grant. Setup runs at file scope, -# BEFORE the outer try and BEFORE any agent provision/deprovision cycles. -# Empirically, running Setup-LockedDownTestDir AFTER agent lifecycles have -# happened in this cmd.exe session causes Set-Acl to fail with -# SeSecurityPrivilege on non-elevated consoles; running it BEFORE works -# (state-aware uses this same ordering for its host-side dirs). -$FsTestRoot = 'C:\mxc_share_test_oneshot' -$FsMarkerContent = 'oneshot-marker-content' -Setup-LockedDownTestDir $FsTestRoot -$FsMarkerContent | Set-Content -Path (Join-Path $FsTestRoot 'marker.txt') -NoNewline - -# Filter test scaffolding: same file-scope ordering as $FsTestRoot above. -# The outer-try finally also cleans this up between runs so a stale -# inheritance-disabled directory does not require SeSecurityPrivilege on -# re-run (Set-Acl on an existing inheritance-disabled directory writes -# the SACL slot, which non-elevated admins cannot do). -$FilterTestRoot = 'C:\mxc_filter_test_oneshot' -$FilterMarkerContent = 'oneshot-filter-marker-content' -Setup-LockedDownTestDir $FilterTestRoot -$FilterMarkerContent | Set-Content -Path (Join-Path $FilterTestRoot 'marker.txt') -NoNewline - -try { - Write-Host "--- Tests ---" -ForegroundColor Cyan # Setup for isolation_session_hello.json: cwd must exist before agent start. New-Item -Path 'C:\mxc_workdir_test' -ItemType Directory -Force | Out-Null @@ -312,9 +262,9 @@ $HostWhoami = (& whoami).Trim() $null = $results.Add((Run-IsolationSessionTest "isolation_session_hello.json" ` -OutputContains @("MYVAR=IsolationSessionTest", "CWD=C:\mxc_workdir_test") ` -OutputLineNotEqual @($HostWhoami))) -# Same shape as hello.json but with experimental.isolation_session.configurationId=medium. -# Proves the Medium config-id end-to-ends through the one-shot path on the target build. -$null = $results.Add((Run-IsolationSessionTest "isolation_session_hello_medium.json" ` +# Same shape as hello.json with an unknown configurationId in the experimental block. +# The backend should ignore that field and run normally. +$null = $results.Add((Run-IsolationSessionTest "isolation_session_configid_ignored.json" ` -OutputContains @("MYVAR=IsolationSessionTest", "CWD=C:\mxc_workdir_test") ` -OutputLineNotEqual @($HostWhoami))) $null = $results.Add((Run-IsolationSessionTest "isolation_session_exit42.json" ` @@ -332,23 +282,6 @@ $null = $results.Add((Run-IsolationSessionTest "isolation_session_stdout_stderr_ # the agent to exit with code 1. $null = $results.Add((Run-IsolationSessionTest "isolation_session_timeout.json" ` -ExpectedExit 1)) -# Filesystem policy: agent has a readwritePaths grant on $FsTestRoot which -# was locked-down and populated with a marker file at file scope above. -# The `type` exit being 0 + the marker content in stdout proves the grant -# was applied and the agent has read access. -$null = $results.Add((Run-IsolationSessionTest "isolation_session_filesystem.json" ` - -OutputContains @($FsMarkerContent))) - -# Filter test: readwritePaths contains both protected (C:\Windows, C:\) and -# non-protected ($FilterTestRoot, locked-down and populated at file scope -# above) entries. The wxc-exec filesystem-policy path filter (MXC issue -# #330) drops the protected entries silently, leaves the non-protected one -# in place, and provisioning continues. The `type ...marker.txt` exit being -# 0 + marker content in stdout proves the non-protected grant was applied -# (positive control); the absence of any error envelope proves the protected -# entries were dropped without surfacing. -$null = $results.Add((Run-IsolationSessionTest "isolation_session_filtered.json" ` - -OutputContains @($FilterMarkerContent))) # One-shot rejection: experimental.isolation_session.user is only honored on # the state-aware path. validate_runner rejects any one-shot request that @@ -360,16 +293,15 @@ $null = $results.Add((Run-IsolationSessionTest "isolation_session_one_shot_user_ # ---------------- Concurrent one-shot test ---------------- # # Three wxc-exec processes (A, B, C) run a per-agent PowerShell script -# from a shared rw-policy directory. Each script writes timestamped lines -# to its own X.log file: "X-started", "X-still-alive-1" .. "X-still-alive-N", -# "X-done", with one second between iterations. After all three wxc-execs -# exit, the test reads each agent's log file and asserts (a) the wxc-exec -# exited 0, (b) the log contains start / final-still-alive / done markers, -# (c) timestamps are monotonic. This decouples the regid-leak check from -# OS-side teardown timing -- if any of the three isolation sessions were -# torn down mid-run its log would be truncated, and if cleanup failed its -# wxc-exec exit code would be non-zero. A fresh fourth process (D) then -# proves the leak does not poison subsequent sandboxes either. +# from a shared host directory that grants Authenticated Users write access. +# Each script writes timestamped lines to its own X.log file: "X-started", +# "X-still-alive-1" .. "X-still-alive-N", "X-done", with one second +# between iterations. After all three wxc-execs exit, the test reads each +# agent's log file and asserts (a) the wxc-exec exited 0, (b) the log +# contains start / final-still-alive / done markers, (c) timestamps are +# monotonic. If any isolation session is torn down mid-run its log is +# truncated, and if cleanup fails its wxc-exec exit code is non-zero. A +# fresh fourth process (D) then proves subsequent sandboxes still work. # # Each launch is gated on the previously-launched agent's "X-started" # line appearing in its log file. Polling the log file (not wxc-exec's @@ -381,12 +313,17 @@ $null = $results.Add((Run-IsolationSessionTest "isolation_session_one_shot_user_ Write-Host "" Write-Host "--- Concurrent one-shot ---" -ForegroundColor Cyan -# Shared rw directory the three agent PS1 scripts and X.log files live in. -# Each X.json grants the agent rw access to this path via -# policy.readwritePaths. +# Shared host directory the three agent PS1 scripts and X.log files live in. +# The backend no longer accepts filesystem policy, so grant write access at +# the host ACL layer to Authenticated Users (S-1-5-11). Agent users are local +# accounts and are members of that group. $concurrentLogDir = 'C:\mxc_concurrent_log' Remove-Item -Recurse -Force $concurrentLogDir -ErrorAction SilentlyContinue New-Item -Path $concurrentLogDir -ItemType Directory -Force | Out-Null +$aclOutput = & icacls $concurrentLogDir /grant '*S-1-5-11:(OI)(CI)M' 2>&1 +if ($LASTEXITCODE -ne 0) { + throw "Failed to grant Authenticated Users write access to ${concurrentLogDir}: $aclOutput" +} # wxc-exec stdout/stderr capture dir (preserved across runs for inspection; # cleaned at the start of each run). @@ -571,10 +508,6 @@ try { Write-Host " (concurrent stdout/stderr preserved at: $concurrentTempRoot)" -ForegroundColor DarkGray } -} finally { - Remove-Item -Recurse -Force $FsTestRoot -ErrorAction SilentlyContinue - Remove-Item -Recurse -Force $FilterTestRoot -ErrorAction SilentlyContinue -} # Summary -- wrap each filtered pipeline in @(...) to force array context. # Without @(), a Where-Object that returns a single hashtable is unwrapped From cf386e21eb5f2dccfd116154b8bb237be1ba7552 Mon Sep 17 00:00:00 2001 From: Dom Giandinoto Date: Wed, 15 Jul 2026 15:54:02 -0700 Subject: [PATCH 2/3] initial appId integration --- .../isolation-session/GENERATION_INFO.toml | 2 +- schemas/dev/mxc-config.schema.0.8.0-dev.json | 14 ++ sdk/node/src/generated/wire.ts | 8 + src/Cargo.toml | 3 +- .../bindings/src/bindings.rs | 91 ++++++++++++ .../isolation_session/common/src/app_id.rs | 138 ++++++++++++++++++ .../isolation_session/common/src/lib.rs | 2 + .../isolation_session/common/src/manager.rs | 39 +++-- .../isolation_session/common/src/one_shot.rs | 16 +- .../common/src/state_aware.rs | 35 ++++- src/core/wxc_common/src/config_parser.rs | 27 ++++ src/core/wxc_common/src/models.rs | 12 +- src/core/wxc_common/src/wire.rs | 8 + 13 files changed, 371 insertions(+), 24 deletions(-) create mode 100644 src/backends/isolation_session/common/src/app_id.rs diff --git a/external/windows-sdk/isolation-session/GENERATION_INFO.toml b/external/windows-sdk/isolation-session/GENERATION_INFO.toml index 7a22c92d9..87bbb5a76 100644 --- a/external/windows-sdk/isolation-session/GENERATION_INFO.toml +++ b/external/windows-sdk/isolation-session/GENERATION_INFO.toml @@ -4,4 +4,4 @@ [tool] windows_bindgen_version = "0.62.1" target_windows_crate = "0.62" -generated_date = "2026-07-01" +generated_date = "2026-07-11" diff --git a/schemas/dev/mxc-config.schema.0.8.0-dev.json b/schemas/dev/mxc-config.schema.0.8.0-dev.json index e37786f2e..83e8c7a6f 100644 --- a/schemas/dev/mxc-config.schema.0.8.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.8.0-dev.json @@ -262,6 +262,13 @@ "IsolationSession": { "description": "IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`).", "properties": { + "appId": { + "description": "Optional app-scoped registration id. Used verbatim when present; when absent, the backend attempts to auto-detect the invoking process's Package Family Name as \"PFN:\".", + "type": [ + "string", + "null" + ] + }, "deprovision": { "anyOf": [ { @@ -323,6 +330,13 @@ "IsolationSessionPhase": { "description": "Per-phase IsolationSession configuration (state-aware lifecycle).", "properties": { + "appId": { + "description": "Optional app-scoped registration id. Used verbatim when present; when absent, the backend attempts to auto-detect the invoking process's Package Family Name as \"PFN:\".", + "type": [ + "string", + "null" + ] + }, "user": { "anyOf": [ { diff --git a/sdk/node/src/generated/wire.ts b/sdk/node/src/generated/wire.ts index 8d5d23f7a..d1009357b 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -110,6 +110,10 @@ export interface Filesystem { * IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`). */ export interface IsolationSession { + /** + * Optional app-scoped registration id. Used verbatim when present; when absent, the backend attempts to auto-detect the invoking process's Package Family Name as "PFN:". + */ + appId?: string | null; /** * State-aware deprovision-phase configuration. */ @@ -137,6 +141,10 @@ export interface IsolationSession { * Per-phase IsolationSession configuration (state-aware lifecycle). */ export interface IsolationSessionPhase { + /** + * Optional app-scoped registration id. Used verbatim when present; when absent, the backend attempts to auto-detect the invoking process's Package Family Name as "PFN:". + */ + appId?: string | null; /** * Entra cloud-agent user bundle for this phase. */ diff --git a/src/Cargo.toml b/src/Cargo.toml index 1dca1950e..7fa2784e5 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -90,7 +90,8 @@ windows = { version = "0.62", features = [ "Win32_System_Time", "Win32_System_SystemServices", "Win32_System_SystemInformation", - "Win32_System_JobObjects", + "Win32_System_JobObjects", + "Win32_Storage_Packaging_Appx", ] } windows-core = "0.62" serde = { version = "1", features = ["derive"] } diff --git a/src/backends/isolation_session/bindings/src/bindings.rs b/src/backends/isolation_session/bindings/src/bindings.rs index cec1dd581..63018f62e 100644 --- a/src/backends/isolation_session/bindings/src/bindings.rs +++ b/src/backends/isolation_session/bindings/src/bindings.rs @@ -397,6 +397,78 @@ pub struct IIsoSessionOps_Vtbl { *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + IIsoSessionOpsPreview2, + IIsoSessionOpsPreview2_Vtbl, + 0x2e26214d_02c1_5dc5_9ce7_f9a41cddd5b1 +); +impl windows_core::RuntimeType for IIsoSessionOpsPreview2 { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl windows_core::RuntimeName for IIsoSessionOpsPreview2 { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionOpsPreview2"; +} +pub trait IIsoSessionOpsPreview2_Impl: windows_core::IUnknownImpl { + fn AddUserAsync2( + &self, + optAppId: &windows_core::HSTRING, + optEntraAccountName: &windows_core::HSTRING, + optWamToken: &windows_core::HSTRING, + ) -> windows_core::Result>; +} +impl IIsoSessionOpsPreview2_Vtbl { + pub const fn new() -> Self { + unsafe extern "system" fn AddUserAsync2< + Identity: IIsoSessionOpsPreview2_Impl, + const OFFSET: isize, + >( + this: *mut core::ffi::c_void, + optappid: *mut core::ffi::c_void, + optentraaccountname: *mut core::ffi::c_void, + optwamtoken: *mut core::ffi::c_void, + result__: *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT { + unsafe { + let this: &Identity = + &*((this as *const *const ()).offset(OFFSET) as *const Identity); + match IIsoSessionOpsPreview2_Impl::AddUserAsync2( + this, + core::mem::transmute(&optappid), + core::mem::transmute(&optentraaccountname), + core::mem::transmute(&optwamtoken), + ) { + Ok(ok__) => { + result__.write(core::mem::transmute_copy(&ok__)); + core::mem::forget(ok__); + windows_core::HRESULT(0) + } + Err(err) => err.into(), + } + } + } + Self { + base__: windows_core::IInspectable_Vtbl::new::( + ), + AddUserAsync2: AddUserAsync2::, + } + } + pub fn matches(iid: &windows_core::GUID) -> bool { + iid == &::IID + } +} +#[repr(C)] +#[doc(hidden)] +pub struct IIsoSessionOpsPreview2_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub AddUserAsync2: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} windows_core::imp::define_interface!( IIsoSessionProcess, IIsoSessionProcess_Vtbl, @@ -1420,6 +1492,25 @@ impl IsoSessionOps { .and_then(|| windows_core::Type::from_abi(result__)) } } + pub fn AddUserAsync2( + &self, + optappid: &windows_core::HSTRING, + optentraaccountname: &windows_core::HSTRING, + optwamtoken: &windows_core::HSTRING, + ) -> windows_core::Result> { + let this = &windows_core::Interface::cast::(self)?; + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(this).AddUserAsync2)( + windows_core::Interface::as_raw(this), + core::mem::transmute_copy(optappid), + core::mem::transmute_copy(optentraaccountname), + core::mem::transmute_copy(optwamtoken), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + } + } } impl windows_core::RuntimeType for IsoSessionOps { const SIGNATURE: windows_core::imp::ConstBuffer = diff --git a/src/backends/isolation_session/common/src/app_id.rs b/src/backends/isolation_session/common/src/app_id.rs new file mode 100644 index 000000000..c512b3894 --- /dev/null +++ b/src/backends/isolation_session/common/src/app_id.rs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! App-id resolution for the isolation session backend. +//! +//! The OS Preview `AddUserAsync2` takes an app-scoped registration id +//! ("appId"). When the caller supplies one it is used verbatim; otherwise we +//! detect the Package Family Name of wxc-exec's *immediate parent* process — +//! the app that invoked MXC — and pass "PFN:". This mirrors the OS-side +//! `ResolveRegistrationId` format so the resulting registration is identical +//! whether formed here or in the OS client DLL. An unpackaged parent (or any +//! detection failure) yields an empty appId, which lets the OS fall back to +//! its default registration. +//! +//! Note: the OS `ResolveRegistrationId` runs in-proc inside wxc-exec, so an +//! empty appId there would resolve to *wxc-exec's* own PFN — not the invoking +//! app's. Detecting the parent here, and passing a non-empty "PFN:" that +//! the OS then uses verbatim, is what scopes the registration to the caller. + +use wxc_common::process_util::OwnedHandle; + +use windows::Win32::Foundation::{ERROR_SUCCESS, HANDLE}; +use windows::Win32::Storage::Packaging::Appx::GetPackageFamilyName; +use windows::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS, +}; +use windows::Win32::System::Threading::{ + GetCurrentProcessId, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, +}; +use windows_core::PWSTR; + +/// The prefix the OS uses for package-family-scoped registration ids. +const PFN_PREFIX: &str = "PFN:"; + +/// Resolves the appId to pass to `AddUserAsync2`. +/// +/// - Non-empty `explicit` -> returned unchanged (caller opted in). +/// - Empty `explicit` -> detect the immediate parent process's Package Family +/// Name and return "PFN:". +/// - Detection failure (unpackaged parent, PID lookup miss, OS error) -> +/// empty string (OS default registration). +pub(super) fn resolve_app_id(explicit: &str) -> String { + if !explicit.is_empty() { + return explicit.to_string(); + } + match detect_parent_pfn() { + Some(pfn) => format!("{PFN_PREFIX}{pfn}"), + None => String::new(), + } +} + +/// Returns the Package Family Name of wxc-exec's immediate parent process, or +/// `None` if the parent is unpackaged or cannot be inspected. +fn detect_parent_pfn() -> Option { + let parent_pid = parent_process_id()?; + // PROCESS_QUERY_LIMITED_INFORMATION is sufficient for GetPackageFamilyName + // and is grantable across integrity levels. + let handle = + unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, parent_pid) }.ok()?; + let owned = OwnedHandle::new(handle); + package_family_name(owned.get()) +} + +/// Walks a ToolHelp process snapshot to find the current process's +/// `th32ParentProcessID`. Best-effort: PID reuse can race, but this is only an +/// identity hint for registration scoping. +fn parent_process_id() -> Option { + let current = unsafe { GetCurrentProcessId() }; + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?; + let owned = OwnedHandle::new(snapshot); + + let mut entry = PROCESSENTRY32W { + dwSize: std::mem::size_of::() as u32, + ..Default::default() + }; + + if unsafe { Process32FirstW(owned.get(), &mut entry) }.is_err() { + return None; + } + loop { + if entry.th32ProcessID == current { + return Some(entry.th32ParentProcessID); + } + if unsafe { Process32NextW(owned.get(), &mut entry) }.is_err() { + return None; + } + } +} + +/// Queries the Package Family Name for a process handle. `None` when the +/// process is unpackaged (`APPMODEL_ERROR_NO_PACKAGE`) or on any error. +fn package_family_name(process: HANDLE) -> Option { + // First call with a null buffer to obtain the required length. A packaged + // process returns ERROR_INSUFFICIENT_BUFFER with `length` set; an + // unpackaged one returns APPMODEL_ERROR_NO_PACKAGE and leaves length 0. + let mut length: u32 = 0; + let _ = unsafe { GetPackageFamilyName(process, &mut length, None) }; + if length == 0 { + return None; + } + + let mut buffer = vec![0u16; length as usize]; + let rc = + unsafe { GetPackageFamilyName(process, &mut length, Some(PWSTR(buffer.as_mut_ptr()))) }; + if rc != ERROR_SUCCESS { + return None; + } + + // `length` includes the terminating null; trim at the first null. + let end = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len()); + Some(String::from_utf16_lossy(&buffer[..end])) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_app_id_is_passed_through_unchanged() { + assert_eq!(resolve_app_id("PFN:Contoso.App_8wekyb3d8bbwe"), "PFN:Contoso.App_8wekyb3d8bbwe"); + assert_eq!(resolve_app_id("my-literal-id"), "my-literal-id"); + } + + #[test] + fn empty_explicit_resolves_via_parent_detection() { + // Environment-dependent: the test runner's parent is typically an + // unpackaged shell/cargo process, so this yields "". When the parent + // *is* packaged, the value must carry the "PFN:" prefix. Either way it + // must never panic and must be a valid (possibly empty) appId. + let resolved = resolve_app_id(""); + if !resolved.is_empty() { + assert!( + resolved.starts_with(PFN_PREFIX), + "detected appId must be PFN-prefixed, got {resolved:?}" + ); + } + } +} diff --git a/src/backends/isolation_session/common/src/lib.rs b/src/backends/isolation_session/common/src/lib.rs index 15a4a6ae9..c66d9eb8a 100644 --- a/src/backends/isolation_session/common/src/lib.rs +++ b/src/backends/isolation_session/common/src/lib.rs @@ -13,6 +13,8 @@ //! - `state_aware`: `StatefulSandboxBackend` — per-phase methods called //! across multiple `wxc-exec` invocations by an external orchestrator. +#[cfg(target_os = "windows")] +mod app_id; #[cfg(target_os = "windows")] mod console_mode; #[cfg(target_os = "windows")] diff --git a/src/backends/isolation_session/common/src/manager.rs b/src/backends/isolation_session/common/src/manager.rs index d11a5952f..7568ad97f 100644 --- a/src/backends/isolation_session/common/src/manager.rs +++ b/src/backends/isolation_session/common/src/manager.rs @@ -18,6 +18,7 @@ use windows::Win32::System::Console::{ use windows::Win32::System::Threading::{CreateEventW, SetEvent, WaitForSingleObject}; use windows_core::{HSTRING, PCWSTR}; +use super::app_id::resolve_app_id; use super::console_mode::{get_local_console_size, ConsoleModeRestorer, CtrlHandlerGuard}; use super::console_relay::{create_console_relay_thread, ConsoleRelayParams}; use super::error::{check_result, format_iso_error, lifecycle_err, IsolationSessionError}; @@ -99,48 +100,58 @@ impl IsolationSessionManager { /// OS-assigned account name — which addresses every subsequent lifecycle /// op — plus the agent SID and the shared ephemeral workspace path). /// - /// Pass empty strings for a local agent user, or the Entra account name - /// and its WAM token for an Entra-backed agent; the OS validates - /// token/identity consistency. Because the account name is not known - /// until this returns, the caller constructs the manager via `new` - /// afterward — hence an associated function rather than a method. + /// `opt_app_id` is the app-scoped registration id. Pass an explicit value + /// to use it verbatim, or an empty string to auto-detect the invoking + /// (parent) process's Package Family Name as "PFN:"; an unpackaged + /// parent falls back to the OS default registration. Pass empty strings + /// for `opt_entra_account_name` / `opt_wam_token` for a local agent user, + /// or the Entra account name and its WAM token for an Entra-backed agent; + /// the OS validates token/identity consistency. Because the account name + /// is not known until this returns, the caller constructs the manager via + /// `new` afterward — hence an associated function rather than a method. /// /// Note: `lifecycle.destroyOnExit` is silently ignored on this backend. /// The in-proc API hardcodes `Indefinite` lifetime. pub(super) fn add_user( + opt_app_id: &str, opt_entra_account_name: &str, opt_wam_token: &str, ) -> Result { let ops = check_service_available_and_activate()?; + // Resolve the app-scoped registration id: an explicit value is used + // verbatim; an empty value triggers parent-PFN detection ("PFN:") + // or falls back to the OS default registration. + let app_id = resolve_app_id(opt_app_id); let async_op = ops - .AddUserAsync( + .AddUserAsync2( + &HSTRING::from(app_id.as_str()), &HSTRING::from(opt_entra_account_name), &HSTRING::from(opt_wam_token), ) - .map_err(|e| lifecycle_err(format!("AddUserAsync call failed: {}", e)))?; + .map_err(|e| lifecycle_err(format!("AddUserAsync2 call failed: {}", e)))?; let user_result: IsoSessionUserResult = async_op .join() - .map_err(|e| lifecycle_err(format!("AddUserAsync wait failed: {}", e)))?; + .map_err(|e| lifecycle_err(format!("AddUserAsync2 wait failed: {}", e)))?; let err = user_result .Error() - .map_err(|e| lifecycle_err(format!("AddUserAsync: get Error failed: {}", e)))?; + .map_err(|e| lifecycle_err(format!("AddUserAsync2: get Error failed: {}", e)))?; let is_error = err .IsError() - .map_err(|e| lifecycle_err(format!("AddUserAsync: get IsError failed: {}", e)))?; + .map_err(|e| lifecycle_err(format!("AddUserAsync2: get IsError failed: {}", e)))?; if is_error { - return Err(format_iso_error("AddUserAsync", &err)); + return Err(format_iso_error("AddUserAsync2", &err)); } let agent_user_name = user_result .AgentUserName() - .map_err(|e| lifecycle_err(format!("AddUserAsync: get AgentUserName failed: {}", e)))?; + .map_err(|e| lifecycle_err(format!("AddUserAsync2: get AgentUserName failed: {}", e)))?; let agent_user_sid = user_result .AgentUserSid() - .map_err(|e| lifecycle_err(format!("AddUserAsync: get AgentUserSid failed: {}", e)))?; + .map_err(|e| lifecycle_err(format!("AddUserAsync2: get AgentUserSid failed: {}", e)))?; let ephemeral_workspace_path = user_result.EphemeralWorkspacePath().map_err(|e| { lifecycle_err(format!( - "AddUserAsync: get EphemeralWorkspacePath failed: {}", + "AddUserAsync2: get EphemeralWorkspacePath failed: {}", e )) })?; diff --git a/src/backends/isolation_session/common/src/one_shot.rs b/src/backends/isolation_session/common/src/one_shot.rs index c510867ec..4ddb87d30 100644 --- a/src/backends/isolation_session/common/src/one_shot.rs +++ b/src/backends/isolation_session/common/src/one_shot.rs @@ -53,9 +53,18 @@ impl ScriptRunner for IsolationSessionRunner { let _ = writeln!(logger, "Isolation Session: interactive={}", interactive); // One-shot runs are local agent users only (state-aware handles - // Entra). Provision returns the OS-assigned account name; the - // manager is then pegged to it for the rest of the lifecycle. - let agent_user_name = match IsolationSessionManager::add_user("", "") { + // Entra). An explicit appId from config is passed verbatim; when + // absent, the manager auto-detects the invoking (parent) process's + // PFN, if the process is packaged. Provision returns the OS-assigned + // account name; the manager is then pegged to it for the rest of the + // lifecycle. + let app_id = request + .experimental + .isolation_session + .as_ref() + .and_then(|c| c.app_id.clone()) + .unwrap_or_default(); + let agent_user_name = match IsolationSessionManager::add_user(app_id.as_str(), "", "") { Ok(provisioned) => { let _ = writeln!( logger, @@ -128,6 +137,7 @@ mod tests { experimental: ExperimentalConfig { isolation_session: Some(IsolationSessionConfig { user: Some(well_formed_user()), + app_id: None, }), ..Default::default() }, diff --git a/src/backends/isolation_session/common/src/state_aware.rs b/src/backends/isolation_session/common/src/state_aware.rs index 8472e82b4..638e7bd09 100644 --- a/src/backends/isolation_session/common/src/state_aware.rs +++ b/src/backends/isolation_session/common/src/state_aware.rs @@ -83,7 +83,10 @@ impl StatefulSandboxBackend for IsolationSessionRunner { _request: &ExecutionRequest, config: Option, ) -> Result, MxcError> { - let user = config.and_then(|c| c.user); + let (user, app_id) = match config { + Some(c) => (c.user, c.app_id), + None => (None, None), + }; // Local agent users pass empty strings; Entra agents pass the UPN + // WAM token. Either way the OS assigns an opaque agent account name, // which becomes the sandboxId tail — start cannot infer Entra-ness @@ -92,8 +95,12 @@ impl StatefulSandboxBackend for IsolationSessionRunner { Some(u) => (u.upn.as_str(), u.wam_token.as_str()), None => ("", ""), }; - let provisioned = IsolationSessionManager::add_user(entra_account, wam_token) - .map_err(map_lifecycle_error)?; + // An explicit appId is passed verbatim; an empty value lets the + // manager auto-detect the invoking (parent) process's PFN. + let app_id = app_id.unwrap_or_default(); + let provisioned = + IsolationSessionManager::add_user(app_id.as_str(), entra_account, wam_token) + .map_err(map_lifecycle_error)?; Ok(ProvisionResult { sandbox_id: format!("{}:{}", Self::ID_PREFIX, provisioned.agent_user_name), @@ -415,6 +422,7 @@ mod tests { let runner = IsolationSessionRunner::new(); let cfg = IsolationSessionProvisionConfig { user: Some(well_formed_user()), + app_id: None, }; runner .validate_provision(&ExecutionRequest::default(), Some(&cfg)) @@ -429,6 +437,7 @@ mod tests { upn: "no-at-sign".to_string(), wam_token: "tok".to_string(), }), + app_id: None, }; let err = runner .validate_provision(&ExecutionRequest::default(), Some(&cfg)) @@ -436,6 +445,24 @@ mod tests { assert_eq!(err.code, MxcErrorCode::PolicyValidation); } + #[test] + fn provision_config_deserializes_camel_case_app_id() { + // The dispatcher deserializes ProvisionConfig directly from the + // camelCase envelope, so `appId` must map to `app_id`. + let cfg: IsolationSessionProvisionConfig = + serde_json::from_value(serde_json::json!({ "appId": "PFN:Contoso.App_8wekyb3d8bbwe" })) + .unwrap(); + assert_eq!(cfg.app_id.as_deref(), Some("PFN:Contoso.App_8wekyb3d8bbwe")); + assert!(cfg.user.is_none()); + } + + #[test] + fn provision_config_app_id_absent_when_omitted() { + let cfg: IsolationSessionProvisionConfig = + serde_json::from_value(serde_json::json!({})).unwrap(); + assert!(cfg.app_id.is_none()); + } + #[test] fn validate_start_accepts_well_formed_user() { // A user bundle is now allowed at start regardless of the opaque @@ -443,6 +470,7 @@ mod tests { let runner = IsolationSessionRunner::new(); let cfg = IsolationSessionConfig { user: Some(well_formed_user()), + app_id: None, }; runner .validate_start("iso:wxc-abcd1234", &ExecutionRequest::default(), Some(&cfg)) @@ -457,6 +485,7 @@ mod tests { upn: "no-at-sign".to_string(), wam_token: "tok".to_string(), }), + app_id: None, }; let err = runner .validate_start("iso:wxc-abcd1234", &ExecutionRequest::default(), Some(&cfg)) diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index d0e6134e2..0c8065853 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -1003,6 +1003,7 @@ fn convert_wire_config( .isolation_session .map(|as_cfg| IsolationSessionConfig { user: as_cfg.user.map(Into::into), + app_id: as_cfg.app_id, }); if raw_exp.seatbelt.is_some() { let msg = "'experimental.seatbelt' has moved to the stable section; \ @@ -3621,6 +3622,32 @@ mod tests { assert!(cfg.user.is_none()); } + #[test] + fn isolation_session_app_id_round_trips_through_one_shot_parser() { + let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"appId": "PFN:Contoso.App_8wekyb3d8bbwe"}}}"#; + let encoded = base64_encode(json.as_bytes()); + let mut logger = test_logger(); + + let req = load_request(&encoded, &mut logger, true).unwrap(); + let cfg = req.experimental.isolation_session.unwrap(); + assert_eq!( + cfg.app_id.as_deref(), + Some("PFN:Contoso.App_8wekyb3d8bbwe"), + "appId should round-trip through the one-shot parser" + ); + } + + #[test] + fn isolation_session_app_id_absent_when_field_omitted() { + let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {}}}"#; + let encoded = base64_encode(json.as_bytes()); + let mut logger = test_logger(); + + let req = load_request(&encoded, &mut logger, true).unwrap(); + let cfg = req.experimental.isolation_session.unwrap(); + assert!(cfg.app_id.is_none()); + } + #[test] fn containment_seatbelt_accepted() { let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "seatbelt"}"#; diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index de8e0a4d3..d38e56456 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -255,11 +255,15 @@ impl Default for WindowsSandboxConfig { /// Configuration specific to the Isolation Session backend. #[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(default)] +#[serde(default, rename_all = "camelCase")] pub struct IsolationSessionConfig { /// Optional Entra cloud-agent credentials. Honored on the state-aware /// `start` phase; rejected by the one-shot path. pub user: Option, + /// Optional app-scoped registration id (appId). Explicit value is used + /// verbatim; when absent, the backend auto-detects the invoking (parent) + /// process's Package Family Name as "PFN:". + pub app_id: Option, } /// Entra cloud-agent credentials. Both fields are required when the bundle @@ -294,9 +298,13 @@ impl From for IsolationSessionUser { /// credentials when the caller wants a cloud-agent sandbox; absent for /// local sandboxes. #[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(default)] +#[serde(default, rename_all = "camelCase")] pub struct IsolationSessionProvisionConfig { pub user: Option, + /// Optional app-scoped registration id (appId). Explicit value is used + /// verbatim; when absent, the backend auto-detects the invoking (parent) + /// process's Package Family Name as "PFN:". + pub app_id: Option, } /// Configuration specific to the LXC container backend. diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index 7030c7f6e..f5fc8f387 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -492,6 +492,10 @@ pub enum TransportProtocol { pub struct IsolationSession { /// Optional Entra cloud-agent user bundle (one-shot). pub user: Option, + /// Optional app-scoped registration id. Used verbatim when present; when + /// absent, the backend auto-detects the invoking process's Package Family + /// Name as "PFN:". + pub app_id: Option, /// State-aware provision-phase configuration. pub provision: Option, /// State-aware start-phase configuration. @@ -509,6 +513,10 @@ pub struct IsolationSession { pub struct IsolationSessionPhase { /// Entra cloud-agent user bundle for this phase. pub user: Option, + /// Optional app-scoped registration id. Used verbatim when present; when + /// absent, the backend auto-detects the invoking process's Package Family + /// Name as "PFN:". + pub app_id: Option, } /// Entra cloud-agent user bundle. Reachable only under the permissive From 68f09206f330869ce1b468756d75a5b6638aa46a Mon Sep 17 00:00:00 2001 From: Dom Giandinoto Date: Fri, 17 Jul 2026 10:08:58 -0700 Subject: [PATCH 3/3] formatting --- src/backends/isolation_session/common/src/app_id.rs | 5 ++++- src/backends/isolation_session/common/src/manager.rs | 6 +++--- .../isolation_session/common/src/one_shot.rs | 2 +- src/core/wxc_common/src/models.rs | 12 ++++++------ src/core/wxc_common/src/wire.rs | 8 ++++---- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/backends/isolation_session/common/src/app_id.rs b/src/backends/isolation_session/common/src/app_id.rs index c512b3894..6b0ade013 100644 --- a/src/backends/isolation_session/common/src/app_id.rs +++ b/src/backends/isolation_session/common/src/app_id.rs @@ -117,7 +117,10 @@ mod tests { #[test] fn explicit_app_id_is_passed_through_unchanged() { - assert_eq!(resolve_app_id("PFN:Contoso.App_8wekyb3d8bbwe"), "PFN:Contoso.App_8wekyb3d8bbwe"); + assert_eq!( + resolve_app_id("PFN:Contoso.App_8wekyb3d8bbwe"), + "PFN:Contoso.App_8wekyb3d8bbwe" + ); assert_eq!(resolve_app_id("my-literal-id"), "my-literal-id"); } diff --git a/src/backends/isolation_session/common/src/manager.rs b/src/backends/isolation_session/common/src/manager.rs index 7568ad97f..311f110f0 100644 --- a/src/backends/isolation_session/common/src/manager.rs +++ b/src/backends/isolation_session/common/src/manager.rs @@ -143,9 +143,9 @@ impl IsolationSessionManager { return Err(format_iso_error("AddUserAsync2", &err)); } - let agent_user_name = user_result - .AgentUserName() - .map_err(|e| lifecycle_err(format!("AddUserAsync2: get AgentUserName failed: {}", e)))?; + let agent_user_name = user_result.AgentUserName().map_err(|e| { + lifecycle_err(format!("AddUserAsync2: get AgentUserName failed: {}", e)) + })?; let agent_user_sid = user_result .AgentUserSid() .map_err(|e| lifecycle_err(format!("AddUserAsync2: get AgentUserSid failed: {}", e)))?; diff --git a/src/backends/isolation_session/common/src/one_shot.rs b/src/backends/isolation_session/common/src/one_shot.rs index 4ddb87d30..531107f90 100644 --- a/src/backends/isolation_session/common/src/one_shot.rs +++ b/src/backends/isolation_session/common/src/one_shot.rs @@ -56,7 +56,7 @@ impl ScriptRunner for IsolationSessionRunner { // Entra). An explicit appId from config is passed verbatim; when // absent, the manager auto-detects the invoking (parent) process's // PFN, if the process is packaged. Provision returns the OS-assigned - // account name; the manager is then pegged to it for the rest of the + // account name; the manager is then pegged to it for the rest of the // lifecycle. let app_id = request .experimental diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index d38e56456..f9a4807e5 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -260,9 +260,9 @@ pub struct IsolationSessionConfig { /// Optional Entra cloud-agent credentials. Honored on the state-aware /// `start` phase; rejected by the one-shot path. pub user: Option, - /// Optional app-scoped registration id (appId). Explicit value is used - /// verbatim; when absent, the backend auto-detects the invoking (parent) - /// process's Package Family Name as "PFN:". + /// Optional app-scoped registration id. Used verbatim when present; when + /// absent, the backend attempts to auto-detect the invoking process's + /// Package Family Name as "PFN:". pub app_id: Option, } @@ -301,9 +301,9 @@ impl From for IsolationSessionUser { #[serde(default, rename_all = "camelCase")] pub struct IsolationSessionProvisionConfig { pub user: Option, - /// Optional app-scoped registration id (appId). Explicit value is used - /// verbatim; when absent, the backend auto-detects the invoking (parent) - /// process's Package Family Name as "PFN:". + /// Optional app-scoped registration id. Used verbatim when present; when + /// absent, the backend attempts to auto-detect the invoking process's + /// Package Family Name as "PFN:". pub app_id: Option, } diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index f5fc8f387..a26c665ef 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -493,8 +493,8 @@ pub struct IsolationSession { /// Optional Entra cloud-agent user bundle (one-shot). pub user: Option, /// Optional app-scoped registration id. Used verbatim when present; when - /// absent, the backend auto-detects the invoking process's Package Family - /// Name as "PFN:". + /// absent, the backend attempts to auto-detect the invoking process's + /// Package Family Name as "PFN:". pub app_id: Option, /// State-aware provision-phase configuration. pub provision: Option, @@ -514,8 +514,8 @@ pub struct IsolationSessionPhase { /// Entra cloud-agent user bundle for this phase. pub user: Option, /// Optional app-scoped registration id. Used verbatim when present; when - /// absent, the backend auto-detects the invoking process's Package Family - /// Name as "PFN:". + /// absent, the backend attempts to auto-detect the invoking process's + /// Package Family Name as "PFN:". pub app_id: Option, }