Part of #1471
Dependencies
Summary
Launch the real packaged copilot.exe through MXC whenever the effective trust profile requires containment. GitHub.Copilot.SDK 1.0.11 has no process-launch callback, so implement a separate phantom-copilot-wrapper.exe and select it with RuntimeConnection.ForStdio(wrapperPath, args). The arguments identify the policy file path and the absolute real CLI path. The wrapper launches that CLI through #1474 and transparently relays stdio. Unconstrained profiles continue to use the direct SDK runtime.
Root Cause
Phantom.Workspaces.Llm.Core/CopilotSdkChatClient.cs:527-546,1298-1325 constructs CopilotClientOptions and lets the SDK own process creation. Copilot/ICopilotClientFactory.cs only constructs the SDK client and cannot intercept that child launch. The SDK exposes RuntimeConnection.ForStdio(path, args), working directory, and environment, but no launcher factory. The unmodified licensed CLI remains a loose runtimes\<rid>\native\copilot.exe payload.
Affected Files
| Area / File |
Required Change |
Phantom.Workspaces.Llm.Core/CopilotSdkChatClient.cs |
Build and cache one direct-or-wrapper connection selection used by both connection paths. |
New Copilot/CopilotRuntimeConnectionFactory.cs |
Resolve real/wrapper paths, compile policy locally, create the policy file, and return RuntimeConnection plus cleanup ownership. |
New Phantom.Workspaces.Copilot.Cli.Wrapper win-x64 executable |
Parse the fixed --policy and --copilot arguments, consume the policy envelope, invoke #1474, relay byte streams, and return the child exit code. |
| Shared execution contracts from #1474/#1475 |
Supply IProcessExecutor, ProcessExecutionRequest, and serializable MxcProcessPolicy. |
| App/solution packaging and validation |
Publish wrapper, real CLI, MXC files, and licenses as loose runtime assets. |
| Copilot and wrapper tests |
Cover selection, handoff security, stdio, lifecycle, errors, and packaging. |
Design / Fix
SDK connection selection
For a constrained local launch, construct:
var connection = RuntimeConnection.ForStdio(
wrapperPath,
["--policy", policyFilePath, "--copilot", realCliPath]);
Use absolute canonical paths. realCliPath is the explicit cliPath override when supplied; otherwise it is AppContext.BaseDirectory\runtimes\<rid>\native\copilot.exe. wrapperPath is the separately named phantom-copilot-wrapper.exe in that runtime directory. Reject missing files, directories, reparse-point surprises, and canonical path equality between wrapper and real CLI.
Do not embed policy JSON inline. The --policy argument is only a file path. The wrapper accepts no implicit CLI discovery and no alternate option aliases. GitHub.Copilot.SDK prepends the configured ForStdio arguments, then appends its own CLI arguments: --headless, --no-auto-update, optional --log-level <value>, --stdio, optional --auth-token-env COPILOT_SDK_AUTH_TOKEN, optional --no-auto-login, optional --session-idle-timeout <seconds>, and optional --remote. The wrapper consumes exactly the fixed leading --policy <path> --copilot <path> prefix and forwards every remaining argument to the real CLI in the original order without parsing, dropping, rewriting, or requiring a -- delimiter.
Add one async, lock-protected CopilotRuntimeConnectionFactory result per CopilotSdkChatClient. EnsureConnectedAsync and EnsureSessionAsync both call the same helper so they cannot compile twice, create different files, or select divergent direct/contained modes. The result owns policy-file cleanup until the wrapper consumes it or client startup fails.
One-use policy file
The local launch host compiles #1475's versioned MxcProcessPolicy, then writes a CopilotLaunchPolicyEnvelope containing:
schemaVersion (initially 1);
createdUtc and expiresUtc (maximum five-minute startup window);
- parent process ID;
- 128-bit random nonce;
- the compiled
MxcProcessPolicy.
Create the file with CreateNew under a dedicated random-name launch directory owned by the current user, with inherited access disabled and access limited to the current user and SYSTEM. Cap UTF-8 JSON at 1 MiB. Never include tokens, environment values, command arguments, or the real CLI path in the file.
The wrapper canonicalizes the supplied policy path and requires it to be a normal file beneath the configured launch directory. It opens it once with no read/write sharing and delete-on-close, rejects reparse points, validates size/schema/expiry/parent PID, deserializes with strict unknown-member handling, and closes/deletes it before launching the CLI. The parent also deletes stale/unconsumed files after startup failure and removes expired files during later launches.
This is local IPC only. For #1443 remote model execution, the remote model host resolves the trust-profile reference, compiles there, and creates its own local file. Policy files and compiled policies never cross the transport boundary.
Wrapper execution and stdio
The wrapper creates ProcessExecutionRequest with:
- executable = canonical
--copilot path;
- argv = every value after the four fixed wrapper-prefix arguments;
- working directory = wrapper's current directory;
- environment = wrapper's inherited environment snapshot;
- policy = envelope
MxcProcessPolicy.
It adds the exact packaged CLI/runtime directory as a documented read-only bootstrap grant required to load the executable and adjacent runtime assets. It then invokes #1474. MXC failure is terminal; it never starts the CLI directly.
Relay raw streams concurrently:
- wrapper stdin → child stdin; EOF closes child stdin;
- child stdout → wrapper stdout byte-for-byte; no diagnostics ever go to stdout;
- child stderr → wrapper stderr byte-for-byte.
Use Console.OpenStandardInput/Output/Error and asynchronous byte copying, not line-oriented text APIs. Drain stdout and stderr concurrently, await child exit and pump completion, flush outputs, and return the real CLI exit code. If the parent closes stdin or the wrapper receives cancellation/termination, dispose the process handle; #1474 must kill the complete child tree. The executor's kill-on-owner-close primitive protects against abrupt wrapper termination.
Wrapper startup failures write a concise sanitized diagnostic to stderr and return reserved codes: 64 invalid arguments, 65 invalid/expired envelope, 66 unsafe CLI/policy path, 70 internal wrapper failure, 71 MXC/executor launch failure. Once the child starts, return its exit code unchanged.
Packaging and fail-closed behavior
Publish phantom-copilot-wrapper.exe beside the unmodified copilot.exe, mxc_ffi.dll, plm.exe, and required licenses for win-x64. Update release payload validation. If containment is required, any compile, policy-file, wrapper, or MXC error fails client startup without retrying direct copilot.exe.
DACL mutation by MXC is permitted. Surface MXC warnings through stderr/host diagnostics without touching protocol stdout.
Considered / Background
- Extending
ICopilotClientFactory was rejected because it cannot intercept SDK process creation.
- Passing policy JSON directly in argv was rejected because of command-line visibility, quoting, and length limits.
- Environment-variable policy delivery was considered, but command-line arguments are simpler and avoid environment inheritance surprises.
- Named-pipe policy delivery was considered, but a secured one-use file with path args is simpler to diagnose and package.
- Modifying/replacing
copilot.exe was rejected; the licensed CLI remains unmodified.
- A future upstream SDK launcher hook may replace the wrapper only after equivalent enforcement is demonstrated.
Expected Tests
| Test Name |
Class |
What It Verifies |
CreateConnection_UnconstrainedProfile_UsesDirectSdkRuntime |
CopilotRuntimeConnectionFactoryTests |
Unconstrained execution preserves the direct CLI path. |
CreateConnection_ConstrainedProfile_UsesWrapperWithPolicyAndCopilotArguments |
CopilotRuntimeConnectionFactoryTests |
ForStdio receives the wrapper and exact --policy <path> --copilot <path> arguments. |
CreateConnection_EnsureConnectedAndEnsureSession_ReusesSelection |
CopilotSdkChatClientTests |
Both lifecycle paths reuse one connection decision and one handoff. |
PolicyEnvelope_OwnerRestrictedFile_RoundTripsAndDeletesOnRead |
CopilotLaunchPolicyEnvelopeTests |
Secure one-use handoff validates and is deleted after consumption. |
PolicyEnvelope_ExpiredOrOversized_RejectsBeforeLaunch |
CopilotLaunchPolicyEnvelopeTests |
Invalid envelopes cannot start the CLI. |
Wrapper_CopilotPathEqualsWrapper_RejectsRecursion |
CopilotCliWrapperTests |
Canonical path equality prevents recursive launch. |
Wrapper_StdioTraffic_RelaysBytesWithoutProtocolContamination |
CopilotCliWrapperTests |
Binary stdin/stdout/stderr relay is exact and diagnostics never enter stdout. |
Wrapper_SdkArguments_ForwardsAllRemainingArgumentsUnchanged |
CopilotCliWrapperTests |
SDK-added headless, stdio, authentication, timeout, logging, and remote flags reach the real CLI in order. |
Wrapper_MxcLaunchFails_DoesNotLaunchDirectCli |
CopilotCliWrapperTests |
Required containment fails closed. |
Wrapper_ParentTerminates_KillsContainedProcessTree |
CopilotCliWrapperTests |
Wrapper ownership termination cleans up the real CLI tree. |
RemoteModel_ConstrainedProfile_CompilesAndCreatesEnvelopeOnRemoteHost |
RemoteModelHostTests |
No compiled policy or policy file crosses the remote boundary. |
RuntimePayload_WinX64_IncludesWrapperCliMxcAndLicenses |
ReleasePackagingTests |
The complete loose runtime payload is packaged. |
Part of #1471
Dependencies
MxcProcessPolicycompiler)Summary
Launch the real packaged
copilot.exethrough MXC whenever the effective trust profile requires containment. GitHub.Copilot.SDK 1.0.11 has no process-launch callback, so implement a separatephantom-copilot-wrapper.exeand select it withRuntimeConnection.ForStdio(wrapperPath, args). The arguments identify the policy file path and the absolute real CLI path. The wrapper launches that CLI through #1474 and transparently relays stdio. Unconstrained profiles continue to use the direct SDK runtime.Root Cause
Phantom.Workspaces.Llm.Core/CopilotSdkChatClient.cs:527-546,1298-1325constructsCopilotClientOptionsand lets the SDK own process creation.Copilot/ICopilotClientFactory.csonly constructs the SDK client and cannot intercept that child launch. The SDK exposesRuntimeConnection.ForStdio(path, args), working directory, and environment, but no launcher factory. The unmodified licensed CLI remains a looseruntimes\<rid>\native\copilot.exepayload.Affected Files
Phantom.Workspaces.Llm.Core/CopilotSdkChatClient.csCopilot/CopilotRuntimeConnectionFactory.csRuntimeConnectionplus cleanup ownership.Phantom.Workspaces.Copilot.Cli.Wrapperwin-x64 executable--policyand--copilotarguments, consume the policy envelope, invoke #1474, relay byte streams, and return the child exit code.IProcessExecutor,ProcessExecutionRequest, and serializableMxcProcessPolicy.Design / Fix
SDK connection selection
For a constrained local launch, construct:
Use absolute canonical paths.
realCliPathis the explicitcliPathoverride when supplied; otherwise it isAppContext.BaseDirectory\runtimes\<rid>\native\copilot.exe.wrapperPathis the separately namedphantom-copilot-wrapper.exein that runtime directory. Reject missing files, directories, reparse-point surprises, and canonical path equality between wrapper and real CLI.Do not embed policy JSON inline. The
--policyargument is only a file path. The wrapper accepts no implicit CLI discovery and no alternate option aliases. GitHub.Copilot.SDK prepends the configuredForStdioarguments, then appends its own CLI arguments:--headless,--no-auto-update, optional--log-level <value>,--stdio, optional--auth-token-env COPILOT_SDK_AUTH_TOKEN, optional--no-auto-login, optional--session-idle-timeout <seconds>, and optional--remote. The wrapper consumes exactly the fixed leading--policy <path> --copilot <path>prefix and forwards every remaining argument to the real CLI in the original order without parsing, dropping, rewriting, or requiring a--delimiter.Add one async, lock-protected
CopilotRuntimeConnectionFactoryresult perCopilotSdkChatClient.EnsureConnectedAsyncandEnsureSessionAsyncboth call the same helper so they cannot compile twice, create different files, or select divergent direct/contained modes. The result owns policy-file cleanup until the wrapper consumes it or client startup fails.One-use policy file
The local launch host compiles #1475's versioned
MxcProcessPolicy, then writes aCopilotLaunchPolicyEnvelopecontaining:schemaVersion(initially1);createdUtcandexpiresUtc(maximum five-minute startup window);MxcProcessPolicy.Create the file with
CreateNewunder a dedicated random-name launch directory owned by the current user, with inherited access disabled and access limited to the current user and SYSTEM. Cap UTF-8 JSON at 1 MiB. Never include tokens, environment values, command arguments, or the real CLI path in the file.The wrapper canonicalizes the supplied policy path and requires it to be a normal file beneath the configured launch directory. It opens it once with no read/write sharing and delete-on-close, rejects reparse points, validates size/schema/expiry/parent PID, deserializes with strict unknown-member handling, and closes/deletes it before launching the CLI. The parent also deletes stale/unconsumed files after startup failure and removes expired files during later launches.
This is local IPC only. For #1443 remote model execution, the remote model host resolves the trust-profile reference, compiles there, and creates its own local file. Policy files and compiled policies never cross the transport boundary.
Wrapper execution and stdio
The wrapper creates
ProcessExecutionRequestwith:--copilotpath;MxcProcessPolicy.It adds the exact packaged CLI/runtime directory as a documented read-only bootstrap grant required to load the executable and adjacent runtime assets. It then invokes #1474. MXC failure is terminal; it never starts the CLI directly.
Relay raw streams concurrently:
Use
Console.OpenStandardInput/Output/Errorand asynchronous byte copying, not line-oriented text APIs. Drain stdout and stderr concurrently, await child exit and pump completion, flush outputs, and return the real CLI exit code. If the parent closes stdin or the wrapper receives cancellation/termination, dispose the process handle; #1474 must kill the complete child tree. The executor's kill-on-owner-close primitive protects against abrupt wrapper termination.Wrapper startup failures write a concise sanitized diagnostic to stderr and return reserved codes:
64invalid arguments,65invalid/expired envelope,66unsafe CLI/policy path,70internal wrapper failure,71MXC/executor launch failure. Once the child starts, return its exit code unchanged.Packaging and fail-closed behavior
Publish
phantom-copilot-wrapper.exebeside the unmodifiedcopilot.exe,mxc_ffi.dll,plm.exe, and required licenses for win-x64. Update release payload validation. If containment is required, any compile, policy-file, wrapper, or MXC error fails client startup without retrying directcopilot.exe.DACL mutation by MXC is permitted. Surface MXC warnings through stderr/host diagnostics without touching protocol stdout.
Considered / Background
ICopilotClientFactorywas rejected because it cannot intercept SDK process creation.copilot.exewas rejected; the licensed CLI remains unmodified.Expected Tests
CreateConnection_UnconstrainedProfile_UsesDirectSdkRuntimeCopilotRuntimeConnectionFactoryTestsCreateConnection_ConstrainedProfile_UsesWrapperWithPolicyAndCopilotArgumentsCopilotRuntimeConnectionFactoryTestsForStdioreceives the wrapper and exact--policy <path> --copilot <path>arguments.CreateConnection_EnsureConnectedAndEnsureSession_ReusesSelectionCopilotSdkChatClientTestsPolicyEnvelope_OwnerRestrictedFile_RoundTripsAndDeletesOnReadCopilotLaunchPolicyEnvelopeTestsPolicyEnvelope_ExpiredOrOversized_RejectsBeforeLaunchCopilotLaunchPolicyEnvelopeTestsWrapper_CopilotPathEqualsWrapper_RejectsRecursionCopilotCliWrapperTestsWrapper_StdioTraffic_RelaysBytesWithoutProtocolContaminationCopilotCliWrapperTestsWrapper_SdkArguments_ForwardsAllRemainingArgumentsUnchangedCopilotCliWrapperTestsWrapper_MxcLaunchFails_DoesNotLaunchDirectCliCopilotCliWrapperTestsWrapper_ParentTerminates_KillsContainedProcessTreeCopilotCliWrapperTestsRemoteModel_ConstrainedProfile_CompilesAndCreatesEnvelopeOnRemoteHostRemoteModelHostTestsRuntimePayload_WinX64_IncludesWrapperCliMxcAndLicensesReleasePackagingTests