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..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-05-08" +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 0caca3b7a..83e8c7a6f 100644 --- a/schemas/dev/mxc-config.schema.0.8.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.8.0-dev.json @@ -259,29 +259,15 @@ }, "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)." + "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": [ @@ -344,16 +330,12 @@ "IsolationSessionPhase": { "description": "Per-phase IsolationSession configuration (state-aware lifecycle).", "properties": { - "configurationId": { - "anyOf": [ - { - "$ref": "#/definitions/IsolationConfigurationId" - }, - { - "type": "null" - } - ], - "description": "Sizing profile for this phase." + "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/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..d1009357b 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -107,18 +107,13 @@ 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). + * 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:". */ - configurationId?: IsolationConfigurationId | null; + appId?: string | null; /** * State-aware deprovision-phase configuration. */ @@ -147,9 +142,9 @@ export interface IsolationSession { */ export interface IsolationSessionPhase { /** - * Sizing profile for this phase. + * 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:". */ - configurationId?: IsolationConfigurationId | null; + appId?: string | 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/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/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..63018f62e 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,120 +341,103 @@ 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 + IIsoSessionOpsPreview2, + IIsoSessionOpsPreview2_Vtbl, + 0x2e26214d_02c1_5dc5_9ce7_f9a41cddd5b1 ); -impl windows_core::RuntimeType for IIsoSessionAppConfig { +impl windows_core::RuntimeType for IIsoSessionOpsPreview2 { 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 IIsoSessionOpsPreview2 { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionOpsPreview2"; } -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 IIsoSessionOpsPreview2_Impl: windows_core::IUnknownImpl { + fn AddUserAsync2( + &self, + optAppId: &windows_core::HSTRING, + optEntraAccountName: &windows_core::HSTRING, + optWamToken: &windows_core::HSTRING, + ) -> windows_core::Result>; } -impl IIsoSessionAppConfig_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn ConfigId< - Identity: IIsoSessionAppConfig_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - result__: *mut IsoSessionConfigId, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionAppConfig_Impl::ConfigId(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } - } - } - unsafe extern "system" fn SetConfigId< - Identity: IIsoSessionAppConfig_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - value: IsoSessionConfigId, - ) -> windows_core::HRESULT { - unsafe { - let this: &Identity = - &*((this as *const *const ()).offset(OFFSET) as *const Identity); - IIsoSessionAppConfig_Impl::SetConfigId(this, value).into() - } - } - unsafe extern "system" fn RegistrationId< - Identity: IIsoSessionAppConfig_Impl, +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 IIsoSessionAppConfig_Impl::RegistrationId(this) { + 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__); @@ -373,248 +447,193 @@ impl IIsoSessionAppConfig_Vtbl { } } } - unsafe extern "system" fn SetRegistrationId< - Identity: IIsoSessionAppConfig_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); - IIsoSessionAppConfig_Impl::SetRegistrationId(this, core::mem::transmute(&value)) - .into() - } - } Self { - base__: windows_core::IInspectable_Vtbl::new::( + base__: windows_core::IInspectable_Vtbl::new::( ), - ConfigId: ConfigId::, - SetConfigId: SetConfigId::, - RegistrationId: RegistrationId::, - SetRegistrationId: SetRegistrationId::, + AddUserAsync2: AddUserAsync2::, } } pub fn matches(iid: &windows_core::GUID) -> bool { - iid == &::IID + iid == &::IID } } #[repr(C)] #[doc(hidden)] -pub struct IIsoSessionAppConfig_Vtbl { +pub struct IIsoSessionOpsPreview2_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( + pub AddUserAsync2: 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, + *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } windows_core::imp::define_interface!( - IIsoSessionAppConfig2, - IIsoSessionAppConfig2_Vtbl, - 0x051c29de_ca75_5cdb_af2c_3012cf1a62ec + IIsoSessionProcess, + IIsoSessionProcess_Vtbl, + 0x1cccc074_ce04_527c_8b2f_d367930af570 ); -impl windows_core::RuntimeType for IIsoSessionAppConfig2 { +impl windows_core::RuntimeType for IIsoSessionProcess { 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"; +impl windows_core::RuntimeName for IIsoSessionProcess { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionProcess"; } -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<()>; +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 IIsoSessionAppConfig2_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn ProvisionId< - Identity: IIsoSessionAppConfig2_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 *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 IIsoSessionAppConfig2_Impl::ProvisionId(this) { + match IIsoSessionProcess_Impl::ErrorHandle(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 SetProvisionId< - Identity: IIsoSessionAppConfig2_Impl, + unsafe extern "system" fn ExitCode< + Identity: IIsoSessionProcess_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, - value: *mut core::ffi::c_void, + result__: *mut i32, ) -> 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() + 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 WamToken< - Identity: IIsoSessionAppConfig2_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 IIsoSessionAppConfig2_Impl::WamToken(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 SetWamToken< - Identity: IIsoSessionAppConfig2_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); - IIsoSessionAppConfig2_Impl::SetWamToken(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::( - ), - ProvisionId: ProvisionId::, - SetProvisionId: SetProvisionId::, - WamToken: WamToken::, - SetWamToken: SetWamToken::, + 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 IIsoSessionAppConfig2_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, -} -windows_core::imp::define_interface!( - IIsoSessionAppConnectResult, - IIsoSessionAppConnectResult_Vtbl, - 0x951478b0_1667_5bc2_9eb9_63d4e402cf04 -); -impl windows_core::RuntimeType for IIsoSessionAppConnectResult { - 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"; -} -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; -} -impl IIsoSessionAppConnectResult_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn AgentName< - Identity: IIsoSessionAppConnectResult_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 IIsoSessionAppConnectResult_Impl::AgentName(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 AgentUserName< - Identity: IIsoSessionAppConnectResult_Impl, + unsafe extern "system" fn SendCtrlClose< + 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 IIsoSessionAppConnectResult_Impl::AgentUserName(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } + IIsoSessionProcess_Impl::SendCtrlClose(this).into() } } - unsafe extern "system" fn Error< - Identity: IIsoSessionAppConnectResult_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 IIsoSessionAppConnectResult_Impl::Error(this) { + 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__)); - core::mem::forget(ok__); windows_core::HRESULT(0) } Err(err) => err.into(), @@ -622,82 +641,105 @@ impl IIsoSessionAppConnectResult_Vtbl { } } Self { - base__: windows_core::IInspectable_Vtbl::new::< - Identity, - IIsoSessionAppConnectResult, - OFFSET, - >(), - AgentName: AgentName::, - AgentUserName: AgentUserName::, - Error: Error::, + 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 IIsoSessionAppConnectResult_Vtbl { +pub struct IIsoSessionProcess_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, + 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!( - IIsoSessionError, - IIsoSessionError_Vtbl, - 0xd9dcc33b_b415_52ff_a0ef_000c956feced + IIsoSessionProcessOptions, + IIsoSessionProcessOptions_Vtbl, + 0x20e0f2f6_3b48_5f60_b97c_e856754ded74 ); -impl windows_core::RuntimeType for IIsoSessionError { +impl windows_core::RuntimeType for IIsoSessionProcessOptions { 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"; +impl windows_core::RuntimeName for IIsoSessionProcessOptions { + const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionProcessOptions"; } -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; +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 IIsoSessionError_Vtbl { - pub const fn new() -> Self { - unsafe extern "system" fn Code( +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 windows_core::HRESULT, + 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::Code(this) { + 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 IsError( + 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 IIsoSessionError_Impl::IsError(this) { + match IIsoSessionProcessOptions_Impl::InteractiveConsole(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); windows_core::HRESULT(0) @@ -706,146 +748,126 @@ impl IIsoSessionError_Vtbl { } } } - unsafe extern "system" fn Message( + 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 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(), - } + IIsoSessionProcessOptions_Impl::SetInteractiveConsole(this, value).into() } } - unsafe extern "system" fn Remediation< - Identity: IIsoSessionError_Impl, + unsafe extern "system" fn RedirectStandardError< + 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::Remediation(this) { + match IIsoSessionProcessOptions_Impl::RedirectStandardError(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 Format( + unsafe extern "system" fn SetRedirectStandardError< + 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::Format(this) { - Ok(ok__) => { - result__.write(core::mem::transmute_copy(&ok__)); - core::mem::forget(ok__); - windows_core::HRESULT(0) - } - Err(err) => err.into(), - } + IIsoSessionProcessOptions_Impl::SetRedirectStandardError(this, value).into() } } - Self { - base__: windows_core::IInspectable_Vtbl::new::(), - Code: Code::, - IsError: IsError::, - Message: Message::, - Remediation: Remediation::, - Format: Format::, + 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(), + } + } } - } - 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 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); + 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 IIsoSessionFolderSharingResult_Impl::FolderPath(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(), } } } - unsafe extern "system" fn Status< - Identity: IIsoSessionFolderSharingResult_Impl, + 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 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 +876,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 +899,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 +909,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 +1026,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 +1046,58 @@ impl IIsoSessionOps_Vtbl { } } } - unsafe extern "system" fn RemoveUserAsync< - 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::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(), - } - } + Self { + base__: windows_core::IInspectable_Vtbl::new::< + Identity, + IIsoSessionProcessResult, + OFFSET, + >(), + Error: Error::, + Process: Process::, } - unsafe extern "system" fn ShareFolderBatchAsync< - Identity: IIsoSessionOps_Impl, - const OFFSET: isize, - >( + } + 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, - 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 +1108,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 +1165,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,1031 +1185,8 @@ 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, - 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, - ) -> 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, - ) { - 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 GetAllAgentUsers< - Identity: IIsoSessionOps3_Impl, - const OFFSET: isize, - >( - this: *mut core::ffi::c_void, - includeinactive: bool, - 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::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, + unsafe extern "system" fn EphemeralWorkspacePath< + Identity: IIsoSessionUserResult_Impl, const OFFSET: isize, >( this: *mut core::ffi::c_void, @@ -2418,7 +1195,7 @@ impl IIsoSessionUserResult2_Vtbl { unsafe { let this: &Identity = &*((this as *const *const ()).offset(OFFSET) as *const Identity); - match IIsoSessionUserResult2_Impl::RegistrationId(this) { + match IIsoSessionUserResult_Impl::EphemeralWorkspacePath(this) { Ok(ok__) => { result__.write(core::mem::transmute_copy(&ok__)); core::mem::forget(ok__); @@ -2428,366 +1205,59 @@ impl IIsoSessionUserResult2_Vtbl { } } } - 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__)) + 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(), + } + } } - } - 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 +1327,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 +1337,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 +1381,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 +1438,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 +1451,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 +1462,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,53 +1479,14 @@ 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(), + core::mem::transmute_copy(agentusername), &mut result__, ) .and_then(|| windows_core::Type::from_abi(result__)) @@ -3184,98 +1494,23 @@ impl IsoSessionOps { } pub fn AddUserAsync2( &self, - registrationid: &windows_core::HSTRING, - entraaccountname: &windows_core::HSTRING, - wamtoken: &windows_core::HSTRING, + optappid: &windows_core::HSTRING, + optentraaccountname: &windows_core::HSTRING, + optwamtoken: &windows_core::HSTRING, ) -> windows_core::Result> { - let this = &windows_core::Interface::cast::(self)?; + 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(optappid), + core::mem::transmute_copy(optentraaccountname), + core::mem::transmute_copy(optwamtoken), &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 +1521,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 +1533,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 +1586,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 +1644,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 +1822,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 +1867,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 +1901,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 +1914,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 +1958,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 +1968,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/app_id.rs b/src/backends/isolation_session/common/src/app_id.rs new file mode 100644 index 000000000..6b0ade013 --- /dev/null +++ b/src/backends/isolation_session/common/src/app_id.rs @@ -0,0 +1,141 @@ +// 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/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..c66d9eb8a 100644 --- a/src/backends/isolation_session/common/src/lib.rs +++ b/src/backends/isolation_session/common/src/lib.rs @@ -2,17 +2,19 @@ // 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. +#[cfg(target_os = "windows")] +mod app_id; #[cfg(target_os = "windows")] mod console_mode; #[cfg(target_os = "windows")] @@ -20,8 +22,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 +32,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..311f110f0 100644 --- a/src/backends/isolation_session/common/src/manager.rs +++ b/src/backends/isolation_session/common/src/manager.rs @@ -2,59 +2,30 @@ // 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::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}; -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 +35,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). + /// + /// `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 in `AddUserAsync`. - pub(super) fn provision_agent_user(&self) -> Result { - let async_op = self - .ops - .AddUserAsync(&self.registration_id, &self.provision_id) - .map_err(|e| lifecycle_err(format!("AddUserAsync call failed: {}", e)))?; - let user_result: IsoSessionUserResult = async_op - .join() - .map_err(|e| lifecycle_err(format!("AddUserAsync wait failed: {}", e)))?; - - let err = user_result - .Error() - .map_err(|e| lifecycle_err(format!("AddUserAsync: get Error failed: {}", e)))?; - let is_error = err - .IsError() - .map_err(|e| lifecycle_err(format!("AddUserAsync: get IsError failed: {}", e)))?; - if is_error { - return Err(format_iso_error("AddUserAsync", &err)); - } - - let 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))), - }; + /// 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 + .AddUserAsync2( + &HSTRING::from(app_id.as_str()), + &HSTRING::from(opt_entra_account_name), + &HSTRING::from(opt_wam_token), + ) + .map_err(|e| lifecycle_err(format!("AddUserAsync2 call failed: {}", e)))?; let user_result: IsoSessionUserResult = async_op .join() .map_err(|e| lifecycle_err(format!("AddUserAsync2 wait failed: {}", e)))?; @@ -186,58 +143,34 @@ impl IsolationSessionManager { return Err(format_iso_error("AddUserAsync2", &err)); } - let name = user_result.AgentUserName().map_err(|e| { + let agent_user_name = user_result.AgentUserName().map_err(|e| { lifecycle_err(format!("AddUserAsync2: get AgentUserName failed: {}", e)) })?; - Ok(name.to_string()) - } + let agent_user_sid = user_result + .AgentUserSid() + .map_err(|e| lifecycle_err(format!("AddUserAsync2: get AgentUserSid failed: {}", e)))?; + let ephemeral_workspace_path = user_result.EphemeralWorkspacePath().map_err(|e| { + lifecycle_err(format!( + "AddUserAsync2: get EphemeralWorkspacePath failed: {}", + e + )) + })?; - /// 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 +178,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 +190,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 +442,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 +454,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 +540,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..531107f90 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,40 @@ 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) + // One-shot runs are local agent users only (state-aware handles + // 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, + "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 +94,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 +104,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 +137,7 @@ mod tests { experimental: ExperimentalConfig { isolation_session: Some(IsolationSessionConfig { user: Some(well_formed_user()), - ..Default::default() + app_id: None, }), ..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..638e7bd09 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,35 @@ 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(), + let (user, app_id) = match config { + Some(c) => (c.user, c.app_id), + None => (None, 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)); - } + // 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 => ("", ""), }; - - // 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)); - } + // 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, 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 +118,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 +142,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 +185,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 +236,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 +288,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 +300,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 +334,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,13 +415,14 @@ 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() { let runner = IsolationSessionRunner::new(); let cfg = IsolationSessionProvisionConfig { user: Some(well_formed_user()), + app_id: None, }; runner .validate_provision(&ExecutionRequest::default(), Some(&cfg)) @@ -444,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)) @@ -452,102 +446,58 @@ mod tests { } #[test] - fn validate_start_entra_sandbox_with_matching_user_accepts() { - 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(); + 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 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 - ); + 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_local_sandbox_with_user_is_malformed() { + 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() + app_id: None, }; - 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) + .validate_start("iso:wxc-abcd1234", &ExecutionRequest::default(), Some(&cfg)) .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() + app_id: None, }; 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..0c8065853 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -999,14 +999,12 @@ 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), + app_id: as_cfg.app_id, + }); if raw_exp.seatbelt.is_some() { let msg = "'experimental.seatbelt' has moved to the stable section; \ use top-level 'seatbelt' instead." @@ -1336,7 +1334,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 +1348,7 @@ mod tests { assert_eq!( exp, serde_json::json!({ - "isolation_session": {"start": {"configurationId": "small"}} + "isolation_session": {"start": {"user": {"upn": "alice@contoso.com"}}} }) ); } @@ -3589,125 +3587,65 @@ mod tests { } #[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"}}}"#; + fn isolation_session_absent_from_experimental() { + let json = r#"{"process": {"commandLine": "echo hi"}, "experimental": {}}"#; 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 - ); + assert!(req.experimental.isolation_session.is_none()); } #[test] - fn isolation_session_config_medium() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "medium"}}}"#; + fn isolation_session_user_field_round_trips_through_one_shot_parser() { + let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"user": {"upn": "alice@contoso.com", "wamToken": "tok"}}}}"#; 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 - ); + let user = cfg + .user + .expect("user field should round-trip through the one-shot parser"); + assert_eq!(user.upn, "alice@contoso.com"); + assert_eq!(user.wam_token, "tok"); } #[test] - fn isolation_session_config_large() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "large"}}}"#; + fn isolation_session_user_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_eq!( - cfg.configuration_id, - crate::models::IsolationSessionConfigurationId::Large - ); + assert!(cfg.user.is_none()); } #[test] - fn isolation_session_config_composable() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "composable"}}}"#; + 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.configuration_id, - crate::models::IsolationSessionConfigurationId::Composable + cfg.app_id.as_deref(), + Some("PFN:Contoso.App_8wekyb3d8bbwe"), + "appId should round-trip through the one-shot parser" ); } #[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": {}}"#; - let encoded = base64_encode(json.as_bytes()); - let mut logger = test_logger(); - - let req = load_request(&encoded, &mut logger, true).unwrap(); - assert!(req.experimental.isolation_session.is_none()); - } - - #[test] - fn isolation_session_user_field_round_trips_through_one_shot_parser() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"user": {"upn": "alice@contoso.com", "wamToken": "tok"}}}}"#; - 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(); - let user = cfg - .user - .expect("user field should round-trip through the one-shot parser"); - assert_eq!(user.upn, "alice@contoso.com"); - assert_eq!(user.wam_token, "tok"); - } - - #[test] - fn isolation_session_user_absent_when_field_omitted() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "isolation_session", "experimental": {"isolation_session": {"configurationId": "medium"}}}"#; + 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.user.is_none()); + assert!(cfg.app_id.is_none()); } #[test] diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index d54c604f8..f9a4807e5 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,46 +253,17 @@ 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)] +#[serde(default, rename_all = "camelCase")] 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, + /// 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, } /// Entra cloud-agent credentials. Both fields are required when the bundle @@ -327,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. 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, } /// Configuration specific to the LXC container backend. @@ -863,14 +838,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..a26c665ef 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -483,17 +483,19 @@ 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, + /// 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, /// State-aware provision-phase configuration. pub provision: Option, /// State-aware start-phase configuration. @@ -509,21 +511,12 @@ 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, + /// 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, } /// Entra cloud-agent user bundle. Reachable only under the permissive 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