Skip to content

Commit 852d81d

Browse files
author
Dom Giandinoto
committed
initial appId integration
1 parent 542928e commit 852d81d

13 files changed

Lines changed: 371 additions & 24 deletions

File tree

external/windows-sdk/isolation-session/GENERATION_INFO.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
[tool]
55
windows_bindgen_version = "0.62.1"
66
target_windows_crate = "0.62"
7-
generated_date = "2026-07-01"
7+
generated_date = "2026-07-11"

schemas/dev/mxc-config.schema.0.8.0-dev.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@
262262
"IsolationSession": {
263263
"description": "IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`).",
264264
"properties": {
265+
"appId": {
266+
"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:<pfn>\".",
267+
"type": [
268+
"string",
269+
"null"
270+
]
271+
},
265272
"deprovision": {
266273
"anyOf": [
267274
{
@@ -323,6 +330,13 @@
323330
"IsolationSessionPhase": {
324331
"description": "Per-phase IsolationSession configuration (state-aware lifecycle).",
325332
"properties": {
333+
"appId": {
334+
"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:<pfn>\".",
335+
"type": [
336+
"string",
337+
"null"
338+
]
339+
},
326340
"user": {
327341
"anyOf": [
328342
{

sdk/src/generated/wire.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ export interface Filesystem {
110110
* IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`).
111111
*/
112112
export interface IsolationSession {
113+
/**
114+
* 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:<pfn>".
115+
*/
116+
appId?: string | null;
113117
/**
114118
* State-aware deprovision-phase configuration.
115119
*/
@@ -137,6 +141,10 @@ export interface IsolationSession {
137141
* Per-phase IsolationSession configuration (state-aware lifecycle).
138142
*/
139143
export interface IsolationSessionPhase {
144+
/**
145+
* 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:<pfn>".
146+
*/
147+
appId?: string | null;
140148
/**
141149
* Entra cloud-agent user bundle for this phase.
142150
*/

src/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ windows = { version = "0.62", features = [
8787
"Win32_System_Time",
8888
"Win32_System_SystemServices",
8989
"Win32_System_SystemInformation",
90-
"Win32_System_JobObjects",
90+
"Win32_System_JobObjects",
91+
"Win32_Storage_Packaging_Appx",
9192
] }
9293
windows-core = "0.62"
9394
serde = { version = "1", features = ["derive"] }

src/backends/isolation_session/bindings/src/bindings.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,78 @@ pub struct IIsoSessionOps_Vtbl {
397397
*mut *mut core::ffi::c_void,
398398
) -> windows_core::HRESULT,
399399
}
400+
windows_core::imp::define_interface!(
401+
IIsoSessionOpsPreview2,
402+
IIsoSessionOpsPreview2_Vtbl,
403+
0x2e26214d_02c1_5dc5_9ce7_f9a41cddd5b1
404+
);
405+
impl windows_core::RuntimeType for IIsoSessionOpsPreview2 {
406+
const SIGNATURE: windows_core::imp::ConstBuffer =
407+
windows_core::imp::ConstBuffer::for_interface::<Self>();
408+
}
409+
impl windows_core::RuntimeName for IIsoSessionOpsPreview2 {
410+
const NAME: &'static str = "Windows.AI.IsolationSession.Preview.IIsoSessionOpsPreview2";
411+
}
412+
pub trait IIsoSessionOpsPreview2_Impl: windows_core::IUnknownImpl {
413+
fn AddUserAsync2(
414+
&self,
415+
optAppId: &windows_core::HSTRING,
416+
optEntraAccountName: &windows_core::HSTRING,
417+
optWamToken: &windows_core::HSTRING,
418+
) -> windows_core::Result<windows_future::IAsyncOperation<IsoSessionUserResult>>;
419+
}
420+
impl IIsoSessionOpsPreview2_Vtbl {
421+
pub const fn new<Identity: IIsoSessionOpsPreview2_Impl, const OFFSET: isize>() -> Self {
422+
unsafe extern "system" fn AddUserAsync2<
423+
Identity: IIsoSessionOpsPreview2_Impl,
424+
const OFFSET: isize,
425+
>(
426+
this: *mut core::ffi::c_void,
427+
optappid: *mut core::ffi::c_void,
428+
optentraaccountname: *mut core::ffi::c_void,
429+
optwamtoken: *mut core::ffi::c_void,
430+
result__: *mut *mut core::ffi::c_void,
431+
) -> windows_core::HRESULT {
432+
unsafe {
433+
let this: &Identity =
434+
&*((this as *const *const ()).offset(OFFSET) as *const Identity);
435+
match IIsoSessionOpsPreview2_Impl::AddUserAsync2(
436+
this,
437+
core::mem::transmute(&optappid),
438+
core::mem::transmute(&optentraaccountname),
439+
core::mem::transmute(&optwamtoken),
440+
) {
441+
Ok(ok__) => {
442+
result__.write(core::mem::transmute_copy(&ok__));
443+
core::mem::forget(ok__);
444+
windows_core::HRESULT(0)
445+
}
446+
Err(err) => err.into(),
447+
}
448+
}
449+
}
450+
Self {
451+
base__: windows_core::IInspectable_Vtbl::new::<Identity, IIsoSessionOpsPreview2, OFFSET>(
452+
),
453+
AddUserAsync2: AddUserAsync2::<Identity, OFFSET>,
454+
}
455+
}
456+
pub fn matches(iid: &windows_core::GUID) -> bool {
457+
iid == &<IIsoSessionOpsPreview2 as windows_core::Interface>::IID
458+
}
459+
}
460+
#[repr(C)]
461+
#[doc(hidden)]
462+
pub struct IIsoSessionOpsPreview2_Vtbl {
463+
pub base__: windows_core::IInspectable_Vtbl,
464+
pub AddUserAsync2: unsafe extern "system" fn(
465+
*mut core::ffi::c_void,
466+
*mut core::ffi::c_void,
467+
*mut core::ffi::c_void,
468+
*mut core::ffi::c_void,
469+
*mut *mut core::ffi::c_void,
470+
) -> windows_core::HRESULT,
471+
}
400472
windows_core::imp::define_interface!(
401473
IIsoSessionProcess,
402474
IIsoSessionProcess_Vtbl,
@@ -1420,6 +1492,25 @@ impl IsoSessionOps {
14201492
.and_then(|| windows_core::Type::from_abi(result__))
14211493
}
14221494
}
1495+
pub fn AddUserAsync2(
1496+
&self,
1497+
optappid: &windows_core::HSTRING,
1498+
optentraaccountname: &windows_core::HSTRING,
1499+
optwamtoken: &windows_core::HSTRING,
1500+
) -> windows_core::Result<windows_future::IAsyncOperation<IsoSessionUserResult>> {
1501+
let this = &windows_core::Interface::cast::<IIsoSessionOpsPreview2>(self)?;
1502+
unsafe {
1503+
let mut result__ = core::mem::zeroed();
1504+
(windows_core::Interface::vtable(this).AddUserAsync2)(
1505+
windows_core::Interface::as_raw(this),
1506+
core::mem::transmute_copy(optappid),
1507+
core::mem::transmute_copy(optentraaccountname),
1508+
core::mem::transmute_copy(optwamtoken),
1509+
&mut result__,
1510+
)
1511+
.and_then(|| windows_core::Type::from_abi(result__))
1512+
}
1513+
}
14231514
}
14241515
impl windows_core::RuntimeType for IsoSessionOps {
14251516
const SIGNATURE: windows_core::imp::ConstBuffer =
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! App-id resolution for the isolation session backend.
5+
//!
6+
//! The OS Preview `AddUserAsync2` takes an app-scoped registration id
7+
//! ("appId"). When the caller supplies one it is used verbatim; otherwise we
8+
//! detect the Package Family Name of wxc-exec's *immediate parent* process —
9+
//! the app that invoked MXC — and pass "PFN:<pfn>". This mirrors the OS-side
10+
//! `ResolveRegistrationId` format so the resulting registration is identical
11+
//! whether formed here or in the OS client DLL. An unpackaged parent (or any
12+
//! detection failure) yields an empty appId, which lets the OS fall back to
13+
//! its default registration.
14+
//!
15+
//! Note: the OS `ResolveRegistrationId` runs in-proc inside wxc-exec, so an
16+
//! empty appId there would resolve to *wxc-exec's* own PFN — not the invoking
17+
//! app's. Detecting the parent here, and passing a non-empty "PFN:<pfn>" that
18+
//! the OS then uses verbatim, is what scopes the registration to the caller.
19+
20+
use wxc_common::process_util::OwnedHandle;
21+
22+
use windows::Win32::Foundation::{ERROR_SUCCESS, HANDLE};
23+
use windows::Win32::Storage::Packaging::Appx::GetPackageFamilyName;
24+
use windows::Win32::System::Diagnostics::ToolHelp::{
25+
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS,
26+
};
27+
use windows::Win32::System::Threading::{
28+
GetCurrentProcessId, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
29+
};
30+
use windows_core::PWSTR;
31+
32+
/// The prefix the OS uses for package-family-scoped registration ids.
33+
const PFN_PREFIX: &str = "PFN:";
34+
35+
/// Resolves the appId to pass to `AddUserAsync2`.
36+
///
37+
/// - Non-empty `explicit` -> returned unchanged (caller opted in).
38+
/// - Empty `explicit` -> detect the immediate parent process's Package Family
39+
/// Name and return "PFN:<pfn>".
40+
/// - Detection failure (unpackaged parent, PID lookup miss, OS error) ->
41+
/// empty string (OS default registration).
42+
pub(super) fn resolve_app_id(explicit: &str) -> String {
43+
if !explicit.is_empty() {
44+
return explicit.to_string();
45+
}
46+
match detect_parent_pfn() {
47+
Some(pfn) => format!("{PFN_PREFIX}{pfn}"),
48+
None => String::new(),
49+
}
50+
}
51+
52+
/// Returns the Package Family Name of wxc-exec's immediate parent process, or
53+
/// `None` if the parent is unpackaged or cannot be inspected.
54+
fn detect_parent_pfn() -> Option<String> {
55+
let parent_pid = parent_process_id()?;
56+
// PROCESS_QUERY_LIMITED_INFORMATION is sufficient for GetPackageFamilyName
57+
// and is grantable across integrity levels.
58+
let handle =
59+
unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, parent_pid) }.ok()?;
60+
let owned = OwnedHandle::new(handle);
61+
package_family_name(owned.get())
62+
}
63+
64+
/// Walks a ToolHelp process snapshot to find the current process's
65+
/// `th32ParentProcessID`. Best-effort: PID reuse can race, but this is only an
66+
/// identity hint for registration scoping.
67+
fn parent_process_id() -> Option<u32> {
68+
let current = unsafe { GetCurrentProcessId() };
69+
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?;
70+
let owned = OwnedHandle::new(snapshot);
71+
72+
let mut entry = PROCESSENTRY32W {
73+
dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
74+
..Default::default()
75+
};
76+
77+
if unsafe { Process32FirstW(owned.get(), &mut entry) }.is_err() {
78+
return None;
79+
}
80+
loop {
81+
if entry.th32ProcessID == current {
82+
return Some(entry.th32ParentProcessID);
83+
}
84+
if unsafe { Process32NextW(owned.get(), &mut entry) }.is_err() {
85+
return None;
86+
}
87+
}
88+
}
89+
90+
/// Queries the Package Family Name for a process handle. `None` when the
91+
/// process is unpackaged (`APPMODEL_ERROR_NO_PACKAGE`) or on any error.
92+
fn package_family_name(process: HANDLE) -> Option<String> {
93+
// First call with a null buffer to obtain the required length. A packaged
94+
// process returns ERROR_INSUFFICIENT_BUFFER with `length` set; an
95+
// unpackaged one returns APPMODEL_ERROR_NO_PACKAGE and leaves length 0.
96+
let mut length: u32 = 0;
97+
let _ = unsafe { GetPackageFamilyName(process, &mut length, None) };
98+
if length == 0 {
99+
return None;
100+
}
101+
102+
let mut buffer = vec![0u16; length as usize];
103+
let rc =
104+
unsafe { GetPackageFamilyName(process, &mut length, Some(PWSTR(buffer.as_mut_ptr()))) };
105+
if rc != ERROR_SUCCESS {
106+
return None;
107+
}
108+
109+
// `length` includes the terminating null; trim at the first null.
110+
let end = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
111+
Some(String::from_utf16_lossy(&buffer[..end]))
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
118+
#[test]
119+
fn explicit_app_id_is_passed_through_unchanged() {
120+
assert_eq!(resolve_app_id("PFN:Contoso.App_8wekyb3d8bbwe"), "PFN:Contoso.App_8wekyb3d8bbwe");
121+
assert_eq!(resolve_app_id("my-literal-id"), "my-literal-id");
122+
}
123+
124+
#[test]
125+
fn empty_explicit_resolves_via_parent_detection() {
126+
// Environment-dependent: the test runner's parent is typically an
127+
// unpackaged shell/cargo process, so this yields "". When the parent
128+
// *is* packaged, the value must carry the "PFN:" prefix. Either way it
129+
// must never panic and must be a valid (possibly empty) appId.
130+
let resolved = resolve_app_id("");
131+
if !resolved.is_empty() {
132+
assert!(
133+
resolved.starts_with(PFN_PREFIX),
134+
"detected appId must be PFN-prefixed, got {resolved:?}"
135+
);
136+
}
137+
}
138+
}

src/backends/isolation_session/common/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
//! - `state_aware`: `StatefulSandboxBackend` — per-phase methods called
1414
//! across multiple `wxc-exec` invocations by an external orchestrator.
1515
16+
#[cfg(target_os = "windows")]
17+
mod app_id;
1618
#[cfg(target_os = "windows")]
1719
mod console_mode;
1820
#[cfg(target_os = "windows")]

src/backends/isolation_session/common/src/manager.rs

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use windows::Win32::System::Console::{
1818
use windows::Win32::System::Threading::{CreateEventW, SetEvent, WaitForSingleObject};
1919
use windows_core::{HSTRING, PCWSTR};
2020

21+
use super::app_id::resolve_app_id;
2122
use super::console_mode::{get_local_console_size, ConsoleModeRestorer, CtrlHandlerGuard};
2223
use super::console_relay::{create_console_relay_thread, ConsoleRelayParams};
2324
use super::error::{check_result, format_iso_error, lifecycle_err, IsolationSessionError};
@@ -99,48 +100,58 @@ impl IsolationSessionManager {
99100
/// OS-assigned account name — which addresses every subsequent lifecycle
100101
/// op — plus the agent SID and the shared ephemeral workspace path).
101102
///
102-
/// Pass empty strings for a local agent user, or the Entra account name
103-
/// and its WAM token for an Entra-backed agent; the OS validates
104-
/// token/identity consistency. Because the account name is not known
105-
/// until this returns, the caller constructs the manager via `new`
106-
/// afterward — hence an associated function rather than a method.
103+
/// `opt_app_id` is the app-scoped registration id. Pass an explicit value
104+
/// to use it verbatim, or an empty string to auto-detect the invoking
105+
/// (parent) process's Package Family Name as "PFN:<pfn>"; an unpackaged
106+
/// parent falls back to the OS default registration. Pass empty strings
107+
/// for `opt_entra_account_name` / `opt_wam_token` for a local agent user,
108+
/// or the Entra account name and its WAM token for an Entra-backed agent;
109+
/// the OS validates token/identity consistency. Because the account name
110+
/// is not known until this returns, the caller constructs the manager via
111+
/// `new` afterward — hence an associated function rather than a method.
107112
///
108113
/// Note: `lifecycle.destroyOnExit` is silently ignored on this backend.
109114
/// The in-proc API hardcodes `Indefinite` lifetime.
110115
pub(super) fn add_user(
116+
opt_app_id: &str,
111117
opt_entra_account_name: &str,
112118
opt_wam_token: &str,
113119
) -> Result<ProvisionedUser, IsolationSessionError> {
114120
let ops = check_service_available_and_activate()?;
121+
// Resolve the app-scoped registration id: an explicit value is used
122+
// verbatim; an empty value triggers parent-PFN detection ("PFN:<pfn>")
123+
// or falls back to the OS default registration.
124+
let app_id = resolve_app_id(opt_app_id);
115125
let async_op = ops
116-
.AddUserAsync(
126+
.AddUserAsync2(
127+
&HSTRING::from(app_id.as_str()),
117128
&HSTRING::from(opt_entra_account_name),
118129
&HSTRING::from(opt_wam_token),
119130
)
120-
.map_err(|e| lifecycle_err(format!("AddUserAsync call failed: {}", e)))?;
131+
.map_err(|e| lifecycle_err(format!("AddUserAsync2 call failed: {}", e)))?;
121132
let user_result: IsoSessionUserResult = async_op
122133
.join()
123-
.map_err(|e| lifecycle_err(format!("AddUserAsync wait failed: {}", e)))?;
134+
.map_err(|e| lifecycle_err(format!("AddUserAsync2 wait failed: {}", e)))?;
124135

125136
let err = user_result
126137
.Error()
127-
.map_err(|e| lifecycle_err(format!("AddUserAsync: get Error failed: {}", e)))?;
138+
.map_err(|e| lifecycle_err(format!("AddUserAsync2: get Error failed: {}", e)))?;
128139
let is_error = err
129140
.IsError()
130-
.map_err(|e| lifecycle_err(format!("AddUserAsync: get IsError failed: {}", e)))?;
141+
.map_err(|e| lifecycle_err(format!("AddUserAsync2: get IsError failed: {}", e)))?;
131142
if is_error {
132-
return Err(format_iso_error("AddUserAsync", &err));
143+
return Err(format_iso_error("AddUserAsync2", &err));
133144
}
134145

135146
let agent_user_name = user_result
136147
.AgentUserName()
137-
.map_err(|e| lifecycle_err(format!("AddUserAsync: get AgentUserName failed: {}", e)))?;
148+
.map_err(|e| lifecycle_err(format!("AddUserAsync2: get AgentUserName failed: {}", e)))?;
138149
let agent_user_sid = user_result
139150
.AgentUserSid()
140-
.map_err(|e| lifecycle_err(format!("AddUserAsync: get AgentUserSid failed: {}", e)))?;
151+
.map_err(|e| lifecycle_err(format!("AddUserAsync2: get AgentUserSid failed: {}", e)))?;
141152
let ephemeral_workspace_path = user_result.EphemeralWorkspacePath().map_err(|e| {
142153
lifecycle_err(format!(
143-
"AddUserAsync: get EphemeralWorkspacePath failed: {}",
154+
"AddUserAsync2: get EphemeralWorkspacePath failed: {}",
144155
e
145156
))
146157
})?;

0 commit comments

Comments
 (0)