Skip to content

[RemoteChat] - Integrate Copilot wrapper and stdio MCP ownership #1491

Description

@JoshuaRowePhantom

Part of #1483

Summary

Integrate the existing Copilot wrapper handoff and executor-backed stdio MCP ownership without replacement APIs or cross-machine compiled policy.

Scope

Implement only commit 8 of the approved design. Preserve the contracts below exactly; local and remote behavior must remain compatible.

Implementation Commit Mapping

This issue implements Commit 8 — Integrate Copilot wrapper and stdio MCP ownership from exact design commit 0c6578641befad31d5a38003baf6c0f0fe64748a.

Commit 8 - Integrate Copilot wrapper and stdio MCP ownership

Files: existing #1476 CopilotRuntimeConnectionFactory handoff and #1477
ProcessExecutorBackedClientTransport/ProcessOwnedMcpTransport integration only; no replacement
types.
Tests: direct versus wrapper selection, stdio ownership, constrained HTTP/SSE rejection,
fail-closed launch, sanitized projection, and no compiled policy on a machine frame.
Dependencies: Commit 7 and #1473-#1477.

Applicable product requirements

Full-remote AgentChat

  • The owning host constructs the real AgentChat, its AgentServices, persistence services, tool providers, and current-session identity from that host's WorkspaceEntitySession / CurrentSessionContext.
  • The owning host resolves executor bindings and routes each component to its selected execution host.
  • The host that actually launches a process resolves the referenced effective TrustProfile, checks the expected revision, compiles MXC locally, and launches through the shared process executor.
  • A local proxy mirrors history, queue state, streaming state, busy state, tools, subagents, modals, and errors. It submits user input through the common queue API and forwards modal responses and interrupt/terminate requests.
  • The proxy does not launch remote Copilot, stdio MCP, or tool subprocesses and must never claim to enforce their containment.
  • Opening a remote subagent creates another authorized proxy to the remote subagent; it does not re-parent the subagent locally.

Split-client topology

  • The topology in remote-chat-client-session.md remains distinct: the local host owns AgentChat, while a remote model host owns the Copilot client/process.
  • In split-client mode, the remote model host resolves and compiles MXC for the Copilot process. Each tool's selected executor host independently resolves and compiles MXC for subprocesses it launches.
  • Component executor bindings from per-component-executor-binding.md remain the source of execution placement. Agent ownership does not override an explicit component binding.

Process and non-process tools

Security and failure semantics

  • attach-agent-session authenticates the transport peer and authorizes that peer for the requested owning profile and session before revealing whether the session exists.
  • Attach authorization is evaluated on every initial attach, reconnect, child-subagent attach, mutation verb, and takeover request. Existing execution-target reachability alone is not sufficient proof of session access.
  • MXC does not authorize attach, and transport authorization does not sandbox processes.
  • A required containment compile, handoff, wrapper, or launch failure fails closed. No layer retries with a null policy or direct uncontained launch.
  • Host-local details remain in protected logs. Wire errors contain only a stable error code, safe operation category, retryability, user-safe message, and correlation id. They exclude policy JSON, grants, local paths, environment values, command arguments, stderr containing secrets, native handles, and credentials.
  • operation-error terminates the affected operation without necessarily terminating the owning session. A fatal runtime error additionally emits session-terminal.
  • MXC is a preview dependency and must not be described as a production-grade security boundary.

Non-goals

  • Discovering every running session on every profile.
  • Migrating live AgentChat, process handles, wrapper handoffs, or compiled MXC state between hosts.
  • Sending compiled MXC policy across transport.
  • Replacing transport authentication with containment or replacing application authorization with MXC.
  • Sandboxing in-process GUI/entity operations.
  • Merging full-remote and split-client topologies into one ambiguous mode.
  • Replacing ConPTY or containing processes Phantom does not launch.

API and protocol guardrails

API shape convention

  • Every method and constructor in this design is audited for call-site clarity. A method with four or
    more independent arguments (including similarly typed identifiers), or any method whose positional
    arguments are easy to transpose, takes one property-based *Request or *Options value plus an
    optional CancellationToken. Small cohesive operations remain direct, for example
    ConnectAsync(AgentSessionOpenRequest request, CancellationToken ct = default).
  • Request/options and data records use object initializers. Semantically required members are
    required init; optional members have explicit defaults. Constructors are reserved for enforcing a
    scalar value invariant or receiving a small cohesive set of services.
  • Protocol DTOs are property-based records with required init payload members. Their fixed Type
    discriminator is initialized by the concrete DTO and is not caller-selectable. This changes only
    the C# construction shape: version-1 kebab-case JSON names, required/optional wire members,
    discriminators, strict unknown-member rejection, and semantics remain unchanged.
  • Existing framework/base-class overrides retain their inherited signatures. APIs consumed unchanged
    from [mxc] - Streaming process executor backed by MXC #1474-[mxc] - Execute MCP tools through MXC-constrained process executor #1477 retain the signatures owned and tested by those designs. Neither case introduces a
    new positional API in this design.

The complete design contains exactly 28 request/options types: 22 public and 6 internal. This issue introduces no replacement request/options type and does not change that count. In particular, there is no InterruptAgentSessionRequest; the client API remains Task InterruptAsync(Guid commandId, CancellationToken ct = default).

The shared snapshot state used by reconnect and wire projections retains these exact public property-based definitions and semantics:

The two state values are New public readonly record structs in
Phantom.Workspaces.Llm.Core/IAgentChat.cs, with exactly these public names, fields, types, and
semantics:

public readonly record struct Usage
{
    public Usage() { }
    public long? TotalInputTokenCount { get; init; } = null;
    public long? TotalOutputTokenCount { get; init; } = null;
    public long? TotalCacheReadTokenCount { get; init; } = null;
    public long? TotalCacheWriteTokenCount { get; init; } = null;
    public long? TotalReasoningTokenCount { get; init; } = null;
    public double? TotalSessionCostUsd { get; init; } = null;
}

public readonly record struct AgentInformation
{
    public AgentInformation() { }
    public required string AgentSessionId { get; init; }
    public required string AgentId { get; init; }
    public required string Name { get; init; }
    public required string DisplayName { get; init; }
    public required string Description { get; init; }
    public required bool AcceptsUserInput { get; init; }
    public string? CurrentModelId { get; init; } = null;
    public required AgentDefinition AgentDefinition { get; init; }
}

Usage permits null for a metric the provider did not report; counts must otherwise be nonnegative
and cost remains a nonnegative double measured in USD. AgentInformation requires non-null,
nonblank values for its first five strings and a non-null complete AgentDefinition;
CurrentModelId is null or nonblank. Required members provide compile-time construction checks;
local publishers and the protocol codec validate value invariants before accepting,
serializing, or publishing them. Local implementations build a complete replacement value before
publishing it. Proxy implementations deserialize and validate a complete replacement value before
one foreground assignment. UsageChanged and InformationChanged are raised only after that atomic
assignment, once per applied session sequence; observers never see fields from different versions.
Record equality is the intended value equality.

Both records use the existing JSON options and kebab-case protocol naming. After authorization, the
owner serializes the full definition with AgentDefinition.ToJson() and the client deserializes it
with PhantomAgentSchema.AgentDefinitionFromJson(string). Every authorized attached GUI receives
the same complete definition in AgentInformation; this is mandatory protocol state, not an
optional capability. Authorization completes before runtime lookup, snapshot construction, or
definition serialization. An unauthorized peer receives only the indistinguishable sanitized
denial and no session metadata, definition bytes, existence signal, or redacted AgentInformation.
No field-level redaction is applied after authorization.

Containment relationship and background

Relationship to MXC process containment

Remote ownership and containment compose as three axes:

Axis Decision Authority
Agent placement Which host owns AgentChat and publishes its event stream? owning-profile binding + attach-agent-session host
Component placement Which host executes the model/tool component? persisted ExecutorBindings / connection descriptor
Process confinement How is a process restricted on that host? launch-host effective TrustProfile + #1475 compiler + #1474 executor

For full-remote operation, the owning host constructs AgentChat and routes components. If it launches Copilot or stdio MCP locally, it resolves and compiles there. If a binding sends a component to another host, that final host resolves and compiles there. The viewer never supplies a compiled policy.

For split-client operation, local AgentChat owns routing and persistence, the remote model host confines Copilot, and each tool executor confines its own subprocesses. The same launch-host rule applies even though no full-session proxy is involved.

The only approved serialization of MxcProcessPolicy is the protected, one-use, same-machine Copilot wrapper handoff in #1476. Machine boundaries carry stable trust-profile identity/revision and component binding intent, never compiled policy or runtime handles.

Overlap matrix

Concern Remote-agent design owns MXC / executor design owns Integration rule
Remote AgentChat protocol Start/attach, snapshots, ordered deltas, proxy commands None Protocol selects/observes owner; it does not launch or sandbox.
Executor routing Persists and restores session/component intent ExecutorBindings resolves component connection descriptors Owner routes; final executor host enforces.
Trust-profile resolution Carries stable profile reference + expected revision #1472 schema/composition and #1475 compiler input Resolve effective profile again on the launch host; reject stale/missing revision.
Process execution Owns runtime/component lease boundaries #1474 ordinary/MXC streaming executor A non-final detach does not dispose executor handles; final detach disposes them unless background continuation is enabled.
Copilot wrapper Reports lifecycle/error events #1476 wrapper, one-use policy file, direct path when uncontained Wrapper and handoff are created only on the Copilot launch host.
stdio MCP Routes tool to selected host #1477 executor-backed MCP transport Selected host compiles and launches; channel caller sends no policy.
Persistence Owner/generation, bindings, trust intent, history No runtime MXC persistence Reconstruct and recompile after restart/takeover.
Lifecycle Owning lease, attachment leases, reconnect grace, background preference, terminate/takeover Process handle/tree disposal Session lease owns children; the final released attachment stops it by default.
Errors Sanitized ordered protocol events Structured local compiler/executor diagnostics Map detailed host error to safe wire DTO with correlation id.
Security Peer/session attach authorization Filesystem/network process confinement Both checks are required and neither substitutes for the other.

The relevant resolved impedance mismatches remain:

  1. Connection descriptors select a host, while MXC policy is host-specific.
    Resolution: persist descriptors and trust references/revisions. The selected launch host resolves, canonicalizes, compiles, and applies policy locally.

  2. [mxc] - Compile effective trust profiles into MXC sandbox policies #1475 defines a serializable MxcProcessPolicy, which could be mistaken for a network DTO.
    Resolution: permit it only in-process and in [mxc] - Launch copilot.exe through the MXC process executor #1476's protected same-machine wrapper envelope. Remote contracts have no compiled-policy property and reject unknown attempts to add one.

  3. Viewer/channel disposal must normally stop an unobserved remote session without making transient network loss destructive.
    Resolution: separate RemoteAgentSessionLease from RemoteAgentAttachmentLease, count logical viewers under the runtime lifecycle gate, and reserve an unexpectedly lost attachment for five seconds. Final explicit detach, or grace expiry with continue-in-background == false, gracefully stops the runtime; the persisted opt-in is the only ordinary zero-viewer retention path.

  4. Takeover wants continuity, while confinement contains host-local paths, capabilities, temp directories, and handles.
    Resolution: transfer persisted history and stable intent only. Terminate the old runtime, advance ownership generation, rebuild services, and compile fresh policy on the new host.

  5. Transport errors need useful UI messages, while MXC diagnostics may expose paths or policy details.
    Resolution: map local diagnostics to RemoteAgentOperationError; retain details under a correlation id in host logs.

  6. Full-remote routing may include GUI/entity tools that are not child processes.
    Resolution: route them according to the application/component binding and enforce normal application authorization. Do not fabricate an MXC guarantee for in-process code.

  7. Interrupt and shutdown have different child-process consequences.
    Resolution: interrupt cancels the active turn while retaining the runtime and reusable clients; terminate-session releases the runtime lease and disposes all process-backed resources.

Detailed design

MXC/executor-facing types - Existing plans, integrated without parallel APIs

  • ExecutorBindings - Existing from the component-binding design, public sealed record in
    Phantom.Workspaces.Llm.Core.Manifest,
    Phantom.Workspaces.Llm.Core/Manifest/ExecutorBindings.cs. Remote integration adds no member and
    calls existing
    JsonElement ResolveComponent(string? executorName), ExecutorTopology ToTopology(), and
    JsonElement ToPersistableMap(). Resolution is deterministic and local; unknown names throw and
    no transport is opened.
  • AgentExecutionTrustContext - New in [mxc] - Execute MCP tools through MXC-constrained process executor #1477, public sealed class in
    Phantom.Workspaces.Llm.Trust,
    Phantom.Workspaces.Llm.Core/Trust/AgentExecutionTrustContext.cs. Remote integration adds no
    parallel context. Its consumed operation is
    ValueTask<TrustProfileProcessPolicyCompilation> GetCompilationAsync(CancellationToken ct = default). The first caller resolves the expected revision and compiles on the launch host;
    concurrent callers share the result. Caller cancellation stops only that wait after work starts.
    Missing/stale profiles and compiler errors are cached failures for the runtime epoch.
  • ITrustProfileProcessPolicyCompiler,
    TrustProfileProcessPolicyCompilation, and MxcProcessPolicy - New in [mxc] - Compile effective trust profiles into MXC sandbox policies #1475, public interface
    and immutable records in Phantom.Workspaces.Llm.Trust,
    Phantom.Workspaces.Llm.Core/Trust/ITrustProfileProcessPolicyCompiler.cs and
    MxcProcessPolicy.cs. Exact consumed signature:
    TrustProfileProcessPolicyCompilation Compile(TrustProfile effectiveProfile). It validates a
    nonnull composed profile, performs no launch, and returns either uncontained, a complete policy,
    or diagnostics; required-policy failure is never interpreted as uncontained.
  • IProcessExecutor, ProcessExecutionRequest, and IProcessHandle - New in [mxc] - Streaming process executor backed by MXC #1474, public
    interface/records in Phantom.Workspaces.Processes,
    Phantom.Workspaces.Processes/IProcessExecutor.cs. Exact consumed signatures are
    Task<IProcessHandle> StartAsync(ProcessExecutionRequest request, CancellationToken ct = default),
    Task<ProcessExitResult> WaitAsync(CancellationToken ct = default),
    Task TerminateAsync(CancellationToken ct = default), and ValueTask DisposeAsync().
    Start cancellation owns and cleans any partial process; wait cancellation does not transfer
    ownership; terminate and disposal are idempotent and kill the tree. An MXC launch failure never
    retries ordinary execution.
  • CopilotRuntimeConnectionFactory and CopilotLaunchPolicyEnvelope - New in [mxc] - Launch copilot.exe through the MXC process executor #1476, public
    sealed class/internal strict handoff record in Phantom.Workspaces.Llm.Copilot,
    Phantom.Workspaces.Llm.Core/Copilot/CopilotRuntimeConnectionFactory.cs. Exact integration method:
    Task<CopilotRuntimeConnectionLease> CreateConnectionAsync(AgentExecutionTrustContext trustContext, string? cliPath, CancellationToken ct = default). It returns one async-disposable direct-or-wrapper
    connection lease per CopilotSdkChatClient; constrained mode writes a protected one-use local
    envelope. Cancellation/failure deletes unconsumed files. The lease, envelope, policy, and path
    never cross machine transport.
    CopilotRuntimeConnectionLease is the New in [mxc] - Launch copilot.exe through the MXC process executor #1476 public sealed async-disposable result in
    the same file; it exposes RuntimeConnection Connection { get; } and idempotent
    ValueTask DisposeAsync(). Disposal deletes only an unconsumed handoff and never kills an
    SDK-owned runtime after ownership has transferred.
  • ProcessExecutorBackedClientTransport and ProcessOwnedMcpTransport - New in [mxc] - Execute MCP tools through MXC-constrained process executor #1477, public
    sealed IClientTransport and internal sealed ITransport decorator in
    Phantom.Workspaces.Llm.Core/Mcp/ProcessExecutorBackedClientTransport.cs. Exact public members are
    string Name { get; } and
    Task<ModelContextProtocol.Protocol.ITransport> ConnectAsync(CancellationToken cancellationToken = default). Connect is single-use, always launches via IProcessExecutor, separates stderr, and
    returns the process-owning transport. Its DisposeAsync() closes MCP/stdin, kills a still-running
    tree, and drains exit/stderr tasks. A non-final viewer detach does not dispose it; final detach
    reaches it only through graceful runtime stop when background continuation is disabled.
  • Effective trust resolution is host-local: the persisted reference/revision is resolved/composed,
    then Compile is called. Missing/stale references, unavailable MXC, compiler failure, wrapper
    failure, or executor failure never downgrade to null policy or direct launch.

Data flow

  1. Create/persist. The creator persists owner, generation, executor bindings, and trust
    reference/revision. No runtime policy is persisted.
  2. Hydrate. RunningAgentChatTable.AcquireAsync calls the runtime-context factory once at first
    materialization and merges the result into a new AgentServices record. The owning host creates
    CurrentSessionContext from its own profile/user/computer, never from viewer claims.
  3. Open. The client opens a message channel with the strict open descriptor. The listener obtains
    authenticated peer identity and the host authorizes before runtime lookup.
  4. Start/attach. The registry single-flights runtime creation. An attachment subscribes before
    snapshot capture. The host sends a retained replay only when the supplied cursor is fully covered;
    otherwise it sends one authoritative snapshot.
  5. Mutate. Each command is reauthorized and deduplicated, then checked against current
    generation+epoch. The host mutates the real AgentChat, appends the resulting event under the
    runtime scheduler, and broadcasts it in sequence.
  6. Queue and steer. Every user input, including one intended to affect an active run, is an
    enqueue-input command against a stable target queue and expected aggregate revision. The owner assigns the
    item id, applies once, acknowledges, and broadcasts queue-changed. Based on current-run state,
    queue immediacy, and mode, owning AgentChat may consume the item immediately through the
    internal Copilot path; that consumption is another ordered queue delta. Otherwise the item
    remains for a tool boundary or future turn. The remote GUI never invokes Copilot and there is no
    steering command.
  7. Route/contain. ExecutorBindings chooses the final component host. That host resolves the
    persisted trust reference/revision, composes the effective profile, compiles once through [mxc] - Compile effective trust profiles into MXC sandbox policies #1475,
    and launches through [mxc] - Streaming process executor backed by MXC #1474/[mxc] - Launch copilot.exe through the MXC process executor #1476/[mxc] - Execute MCP tools through MXC-constrained process executor #1477. Machine transports serialize trust intent, never compiled
    policy.
  8. Reconnect. The proxy keeps only its cursor and persisted open request. Reconnect reauthorizes
    and reuses the runtime. A snapshot includes Usage, AgentInformation, and all queue snapshots;
    retained replay applies queue deltas by epoch/global sequence. It neither reconstructs
    AgentChat nor relaunches children.
  9. Take over. The old host fences mutations and confirms complete runtime disposal. Persistence
    compare/exchanges owner and increments generation. The new host hydrates and starts a fresh epoch.
    Failure to confirm an unexpired old lease blocks takeover.
  10. Detach/stop. Explicit detach, proxy disposal, UI tab close, and viewer-application shutdown
    release immediately; transport loss reserves the attachment token for five seconds. On final
    release, continue-in-background == false fences and gracefully stops the runtime. Explicit
    terminate, takeover, owner-host shutdown, or runtime-lease expiry always fences and stops it.
    Stop rejects mutations, interrupts the active turn if required, disposes the chat, component
    transports, MXC/process leases, wrappers, process-owned MCP transports and child trees, persists
    terminal/stopped state, emits terminal where the channel remains writable, and closes channels.

Tests

RemoteExecutionContainmentMatrixTests:

  • ExecutionMatrix_AllEightPlacementContainmentCases_UseExpectedAgentAndLaunchHosts.
  • ExecutionMatrix_AllEightCases_ResolveTrustOnlyOnFinalLaunchHost.
  • ExecutionMatrix_ContainmentNotRequired_UsesOrdinaryExecutorBranch.
  • ExecutionMatrix_ContainmentRequired_UsesMxcExecutorBranch.
  • ExecutionMatrix_RemoteBoundary_ContainsNoCompiledPolicyOrPolicyPath.
  • ExecutionMatrix_GuiAndEntityTools_UseApplicationAuthorizationNotMxcClaim.
  • SplitClient_RemoteCopilot_LocalAgentChat_PreservesDistinctTopology.
  • ContainedCopilot_RemoteOwner_CreatesWrapperEnvelopeOnlyOnCopilotHost.
  • ContainedStdioMcp_RemoteExecutor_OwnsProcessTransportOnToolHost.
  • ContainmentCompileFails_ReturnsSanitizedErrorAndDoesNotLaunch.
  • MxcLaunchFails_DoesNotFallbackUncontained.
  • ConstrainedHttpOrSse_ReturnsUnsupportedPolicy.
  • RemoteError_PolicyPathEnvironmentArgvAndStderr_AreAbsent.
  • Takeover_NewHost_RehydratesIntentAndRecompilesPolicy.

This matrix also consumes the public-method tests owned by #1474-#1477:
Compile_* in MxcTrustProfilePolicyCompilerTests,
ProcessExecutor_*/ProcessHandle_*,
CreateConnection_* in CopilotRuntimeConnectionFactoryTests,
ConnectAsync_*/DisposeAsync_* in ProcessExecutorBackedClientTransportTests, and
RemoteStdio_* in RemoteMcpHostHandlerTests. Those APIs are not duplicated here.
Remote integration additionally requires:

  • GetCompilationAsync_ConcurrentCallers_ResolveAndCompileOnce in
    AgentExecutionTrustContextTests.
  • GetCompilationAsync_StaleRevision_CachesFailClosedResult in
    AgentExecutionTrustContextTests.
  • StartAsync_CancelledDuringLaunch_CleansPartialProcess and
    TerminateAsync_RepeatedCall_KillsTreeOnce in ProcessExecutorTests.
  • WaitAsync_Cancelled_DoesNotReleaseProcessOwnership in ProcessHandleTests.
  • CreateConnectionAsync_ConcurrentLifecyclePaths_ReturnsOneSelection and
    CreateConnectionAsync_Cancelled_RemovesUnconsumedEnvelope in
    CopilotRuntimeConnectionFactoryTests.
  • DisposeAsync_UnconsumedHandoff_DeletesFileOnce and
    DisposeAsync_TransferredConnection_DoesNotKillSdkRuntime in
    CopilotRuntimeConnectionFactoryTests.
  • Name_ConstructedTransport_ReturnsConfiguredName in
    ProcessExecutorBackedClientTransportTests.

Dependencies

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 workingverified-locallyImplementation has been verified locally

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions