Skip to content

[mxc] - Launch copilot.exe through the MXC process executor #1476

Description

@JoshuaRowePhantom

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingneeds-slow-testsRequires full test suite including slow Git tests at checkinverified-locallyImplementation has been verified locally

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions