Skip to content

feat: add remote session support across all SDKs#1192

Open
patniko wants to merge 3 commits intomainfrom
feat/remote-sessions
Open

feat: add remote session support across all SDKs#1192
patniko wants to merge 3 commits intomainfrom
feat/remote-sessions

Conversation

@patniko
Copy link
Copy Markdown
Contributor

@patniko patniko commented May 4, 2026

Summary

Adds remote session support (Mission Control integration) to all 4 language SDKs, providing parity with the CLI's --remote flag and /remote on//remote off commands.

Two complementary mechanisms

1. Client-level remote option (always-on)

Set remote: true on CopilotClientOptions to pass --remote to the CLI process. All sessions in a GitHub repo working directory automatically get a remote URL via session.info events.

const client = new CopilotClient({ remote: true });
session.on("session.info", (event) => {
  if (event.data.infoType === "remote") {
    console.log("Remote URL:", event.data.url);
  }
});

2. Per-session session.rpc.remote.enable() / .disable() (on-demand)

Toggle remote mid-session — equivalent to /remote on and /remote off in the CLI.

const result = await session.rpc.remote.enable();
console.log("Remote URL:", result.url);
await session.rpc.remote.disable();

Changes

Client option (all 4 SDKs)

SDK File Field
Node types.ts, client.ts remote?: boolean
Python client.py remote: bool = False on SubprocessConfig
Go types.go, client.go Remote bool on ClientOptions
.NET Types.cs, Client.cs bool Remote on CopilotClientOptions + clone

RPC types (generated + manual)

SDK Method Notes
Node session.rpc.remote.enable() / .disable() Auto-generated from schema
C# session.Rpc.Remote.EnableAsync() / .DisableAsync() Auto-generated from schema
Go session.RPC.Remote.Enable(ctx) / .Disable(ctx) Manually added (quicktype bug)
Python session.rpc.remote.enable() / .disable() Manually added (quicktype bug)

Documentation

  • New docs/features/remote-sessions.md — covers both always-on and on-demand patterns with all-language examples, QR code library recommendations
  • Updated docs/features/index.md and docs/index.md

Verification

  • ✅ Node TypeScript compiles (tsc --noEmit)
  • ✅ Go compiles (go build ./...)
  • ✅ .NET compiles (dotnet build)
  • ✅ Python import works + 50/50 unit tests pass

Related

@patniko patniko requested a review from a team as a code owner May 4, 2026 04:47
Copilot AI review requested due to automatic review settings May 4, 2026 04:47
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1192 · ● 822K

Assert.Equal(original.Environment, clone.Environment);
Assert.Equal(original.GitHubToken, clone.GitHubToken);
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
Assert.Equal(original.CopilotHome, clone.CopilotHome);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CopilotHome property is now covered by the clone test — great! However the Remote property added in this PR is not tested here. Since Remote = other.Remote was added to the clone constructor, it would be worth adding coverage:

// In the original initializer (around line 29):
Remote = true,

// Assertion to add after line 47:
Assert.Equal(original.Remote, clone.Remote);

Without this, a regression that accidentally drops the Remote copy in the future won't be caught.

ping: async (params: PingRequest): Promise<PingResult> =>
connection.sendRequest("ping", params),
connect: async (params: ConnectRequest): Promise<ConnectResult> =>
connection.sendRequest("connect", params),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The connect() server-level RPC method was added here (and in the .NET ServerRpc.ConnectAsync()), but the equivalent is not present in the Go (go/rpc/generated_rpc.go) or Python (python/copilot/generated/rpc.py) server RPC. The PR description notes that Go and Python needed manual additions due to a quicktype bug — connect() appears to have been missed.

For Go, this would look like:

func (c *ServerRpc) Connect(ctx context.Context, token *string) (*ConnectResult, error) {
    req := map[string]any{}
    if token != nil {
        req["token"] = *token
    }
    raw, err := c.client.Request("connect", req)
    if err != nil {
        return nil, err
    }
    var result ConnectResult
    if err := json.Unmarshal(raw, &result); err != nil {
        return nil, err
    }
    return &result, nil
}

For Python:

async def connect(self, *, token: str | None = None, timeout: float | None = None) -> ConnectResult:
    params: dict = {}
    if token is not None:
        params["token"] = token
    return ConnectResult.from_dict(await self._client.request("connect", params, **_timeout_kwargs(timeout)))

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to bring the four language SDKs into parity with the Copilot CLI’s remote-session capabilities, so SDK clients can either start the CLI in remote mode or toggle remote access on an existing session. It also includes a second, separate cross-SDK API addition for configuring COPILOT_HOME/copilotHome.

Changes:

  • Adds a client-level remote option in Node, Python, Go, and .NET so spawned CLI processes can start with --remote.
  • Adds session-scoped remote RPC bindings (enable/disable) in the generated or hand-maintained RPC clients for each SDK.
  • Updates shared docs and language docs, and extends some option/clone tests around the new configuration surface.
Show a summary per file
File Description
python/test_client.py Adds unit tests for Python copilot_home option storage.
python/copilot/generated/rpc.py Adds Python typed RPC bindings for remote enable/disable.
python/copilot/client.py Adds Python subprocess config fields for copilot_home and remote, and wires them into CLI startup.
python/README.md Documents Python copilot_home option.
nodejs/test/client.test.ts Adds Node unit tests for copilotHome option storage.
nodejs/src/types.ts Adds Node public client options for copilotHome and remote.
nodejs/src/generated/rpc.ts Adds Node RPC types for remote APIs and a generated connect server RPC.
nodejs/src/client.ts Wires Node copilotHome env handling and --remote CLI arg into startup.
nodejs/README.md Documents Node copilotHome option.
go/types.go Adds Go ClientOptions fields for CopilotHome and Remote.
go/rpc/generated_rpc.go Adds Go RPC types and methods for remote enable/disable.
go/internal/embeddedcli/embeddedcli.go Changes embedded CLI install path selection to honor COPILOT_HOME.
go/client_test.go Adds Go unit tests for CopilotHome option storage.
go/client.go Wires Go CopilotHome env handling and --remote CLI arg into startup.
go/README.md Documents Go CopilotHome behavior and cache-path nuance.
dotnet/test/Unit/CloneTests.cs Extends clone coverage for the new .NET options.
dotnet/src/Types.cs Adds .NET CopilotHome and Remote options and updates cloning.
dotnet/src/Generated/Rpc.cs Adds .NET RPC types and methods for remote APIs plus a generated connect RPC.
dotnet/src/Client.cs Wires .NET CopilotHome env handling and --remote CLI arg into startup.
dotnet/README.md Documents .NET CopilotHome option.
docs/index.md Adds Remote Sessions guide to docs index.
docs/features/remote-sessions.md Introduces shared remote-session feature documentation and examples.
docs/features/index.md Adds Remote Sessions to the feature guide index.

Copilot's findings

  • Files reviewed: 20/23 changed files
  • Comments generated: 13

Comment on lines +121 to +123
```go
result, err := session.RPC.Remote.Enable(ctx)
fmt.Println("Remote URL:", *result.URL)
Comment thread nodejs/src/client.ts
Comment on lines +1437 to +1439
if (this.options.remote) {
args.push("--remote");
}
Comment thread dotnet/src/Client.cs
Comment on lines +1195 to +1198
if (options.Remote)
{
args.Add("--remote");
}
Comment thread go/client_test.go
Comment on lines +347 to +364
func TestClient_CopilotHome(t *testing.T) {
t.Run("should accept CopilotHome option", func(t *testing.T) {
client := NewClient(&ClientOptions{
CopilotHome: "/custom/copilot/home",
})

if client.options.CopilotHome != "/custom/copilot/home" {
t.Errorf("Expected CopilotHome to be '/custom/copilot/home', got %q", client.options.CopilotHome)
}
})

t.Run("should default CopilotHome to empty string", func(t *testing.T) {
client := NewClient(&ClientOptions{})

if client.options.CopilotHome != "" {
t.Errorf("Expected CopilotHome to be empty, got %q", client.options.CopilotHome)
}
})
Comment on lines +29 to 48
CopilotHome = "/custom/copilot/home",
SessionIdleTimeoutSeconds = 600,
};

var clone = original.Clone();

Assert.Equal(original.CliPath, clone.CliPath);
Assert.Equal(original.CliArgs, clone.CliArgs);
Assert.Equal(original.Cwd, clone.Cwd);
Assert.Equal(original.Port, clone.Port);
Assert.Equal(original.UseStdio, clone.UseStdio);
Assert.Equal(original.CliUrl, clone.CliUrl);
Assert.Equal(original.LogLevel, clone.LogLevel);
Assert.Equal(original.AutoStart, clone.AutoStart);

Assert.Equal(original.Environment, clone.Environment);
Assert.Equal(original.GitHubToken, clone.GitHubToken);
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
Assert.Equal(original.CopilotHome, clone.CopilotHome);
Assert.Equal(original.SessionIdleTimeoutSeconds, clone.SessionIdleTimeoutSeconds);
Comment thread python/test_client.py
Comment on lines +225 to +245
class TestCopilotHome:
def test_accepts_copilot_home(self):
client = CopilotClient(
SubprocessConfig(
cli_path=CLI_PATH,
copilot_home="/custom/copilot/home",
log_level="error",
)
)
assert isinstance(client._config, SubprocessConfig)
assert client._config.copilot_home == "/custom/copilot/home"

def test_default_copilot_home_is_none(self):
client = CopilotClient(
SubprocessConfig(
cli_path=CLI_PATH,
log_level="error",
)
)
assert isinstance(client._config, SubprocessConfig)
assert client._config.copilot_home is None
Comment thread python/copilot/client.py
Comment on lines +2335 to +2336
if cfg.remote:
args.append("--remote")
Comment thread go/client.go
Comment on lines +1405 to +1406
if c.options.Remote {
args = append(args, "--remote")
Comment on lines +87 to +95
if copilotHome := os.Getenv("COPILOT_HOME"); copilotHome != "" {
installDir = filepath.Join(copilotHome, "cache", "copilot-sdk")
} else {
var err error
if installDir, err = os.UserCacheDir(); err != nil {
// Fall back to temp dir if UserCacheDir is unavailable
installDir = os.TempDir()
}
installDir = filepath.Join(installDir, "copilot-sdk")
Comment on lines +665 to +680
it("should accept copilotHome option", () => {
const client = new CopilotClient({
copilotHome: "/custom/copilot/home",
logLevel: "error",
});

expect((client as any).options.copilotHome).toBe("/custom/copilot/home");
});

it("should leave copilotHome undefined when not provided", () => {
const client = new CopilotClient({
logLevel: "error",
});

expect((client as any).options.copilotHome).toBeUndefined();
});
@patniko
Copy link
Copy Markdown
Contributor Author

patniko commented May 4, 2026

CI Notes

Expected failures:

  • Codegen Check: The Go and Python generated files include manually-added RemoteApi/RemoteEnableResult types for the new session.remote.enable/disable RPC methods. These will be auto-generated once the runtime PR (github/copilot-agent-runtime#7365) merges and a new @github/copilot package is published with the updated api.schema.json.

Merge order: Runtime PR → publish @github/copilot → re-run codegen → this SDK PR.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1192 · ● 915.2K

Comment thread go/rpc/generated_rpc.go
return &result, nil
}

type RemoteApi sessionApi
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency gap: connect RPC missing from Go ServerRpc

This PR adds a connect server-level RPC method to Node.js (createServerRpc) and .NET (ServerRpc.ConnectAsync), but the equivalent method is absent from Go's ServerRpc. Currently Go only has Ping at the server level.

Suggested addition to ServerRpc (alongside Ping):

func (a *ServerRpc) Connect(ctx context.Context, token *string) (*ConnectResult, error) {
    req := map[string]any{}
    if token != nil {
        req["token"] = *token
    }
    raw, err := a.common.client.Request("connect", req)
    if err != nil {
        return nil, err
    }
    var result ConnectResult
    if err := json.Unmarshal(raw, &result); err != nil {
        return nil, err
    }
    return &result, nil
}

Along with the corresponding ConnectResult and ConnectRequest types.

Assert.Equal(original.Environment, clone.Environment);
Assert.Equal(original.GitHubToken, clone.GitHubToken);
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
Assert.Equal(original.CopilotHome, clone.CopilotHome);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Remote property in clone test

The CopilotClientOptions_Clone_CopiesAllProperties test was updated to cover CopilotHome but the new Remote property added in this same PR is not verified. Since Remote is a bool with a non-obvious default (false), setting it to true in the test would exercise the clone path.

Suggested additions:

// In the original object initializer:
Remote = true,

// In the assertions:
Assert.Equal(original.Remote, clone.Remote);

return UsageGetMetricsResult.from_dict(await self._client.request("session.usage.getMetrics", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))


class RemoteApi:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency gap: connect RPC missing from Python ServerRpc

This PR adds a server-level connect RPC to Node.js and .NET, but Python's ServerRpc only has ping. The equivalent connect method and its result type should be added here for parity.

Suggested additions:

`@dataclass`
class ConnectResult:
    """Result of the connect RPC call."""
    ok: bool
    protocol_version: int
    version: str

    `@staticmethod`
    def from_dict(obj: Any) -> 'ConnectResult':
        assert isinstance(obj, dict)
        return ConnectResult(
            ok=from_bool(obj.get("ok")),
            protocol_version=from_int(obj.get("protocolVersion")),
            version=from_str(obj.get("version")),
        )

And in ServerRpc:

async def connect(self, token: str | None = None, *, timeout: float | None = None) -> ConnectResult:
    params: dict[str, Any] = {}
    if token is not None:
        params["token"] = token
    return ConnectResult.from_dict(await self._client.request("connect", params, **_timeout_kwargs(timeout)))

patniko and others added 3 commits May 4, 2026 13:33
Add a first-class option to CopilotClientOptions in all four SDKs
(Node/TS, Python, Go, .NET) that sets the COPILOT_HOME environment
variable on the spawned CLI process, allowing users to control where
the CLI stores session state, config, and other data files.

This addresses environments with restricted write access (e.g., M365 B2
service where only specific directories like D:\data are writable).

- Node/TS: copilotHome?: string
- Python: copilot_home: str | None
- Go: CopilotHome string (also used as fallback for embedded CLI cache)
- .NET: CopilotHome: string?

The explicit option takes priority over any COPILOT_HOME set in the raw
env option. The option is ignored when connecting to an external server
via cliUrl.

Closes github/copilot-sdk-partners#29

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a `remote` option to CopilotClientOptions in all 4 language SDKs
(Node, Python, Go, .NET) that passes `--remote` to the CLI process,
enabling Mission Control integration for GitHub web and mobile access.

Also adds `session.rpc.remote.enable()` and `session.rpc.remote.disable()`
RPC methods for on-demand per-session toggling, providing parity with
the CLI's `/remote on` and `/remote off` commands.

Changes:
- Node: remote option in CopilotClientOptions, generated RPC types
- Python: remote field on SubprocessConfig, manual RPC types
- Go: Remote field on ClientOptions, manual RPC types
- .NET: Remote property on CopilotClientOptions + clone, generated RPC
- Docs: new docs/features/remote-sessions.md with all-language examples
- Updated docs/features/index.md and docs/index.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  option not in published SDK yet, fragments lack standalone context)
- Fix C# permission handler: use Kind=Approved pattern
- Fix Go permission handler: use correct 2-param signature

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS
Copy link
Copy Markdown
Contributor

@patniko I looked into merging this but I see we're still waiting for runtime-side changes to land, so will hold off for a bit.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

Cross-SDK Consistency Review

The remote session feature is added consistently to all four SDKs — the remote client option, the RemoteEnableResult type, and the session.rpc.remote.enable() / .disable() RPC calls are all present in Node.js, Python, Go, and .NET. 👍

However, I found two issues in this PR:

🐛 Bug — Go: COPILOT_HOME set twice (go/client.go)

The PR adds a new append(... "COPILOT_HOME="+...) block, but the pre-existing code already sets COPILOT_HOME via setEnvValue just a few lines earlier. The result is the env var appearing twice in the spawned process's environment. The new block should be removed. (Inline comment added.)

⚠️ Inconsistency — Node.js: connect added to the public createServerRpc() (nodejs/src/generated/rpc.ts)

The PR adds a connect entry to the public createServerRpc() return object, but connect is intentionally kept internal in every other SDK:

  • Go → InternalServerRpc.Connect() (explicitly non-public)
  • Python → _InternalServerRpc.connect() (private class)
  • .NET → internal async Task<ConnectResult> ConnectAsync(...)

In the Node.js file itself, connect is already exposed through the @internal-annotated createInternalServerRpc() function. Adding it to the public function leaks an implementation detail that callers shouldn't use directly. (Inline comment added.)


Everything else looks consistent and well-implemented across all SDKs.

Generated by SDK Consistency Review Agent for issue #1192 · ● 1.4M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1192 · ● 1.4M

ping: async (params: PingRequest): Promise<PingResult> =>
connection.sendRequest("ping", params),
connect: async (params: ConnectRequest): Promise<ConnectResult> =>
connection.sendRequest("connect", params),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistency: connect exposed in public createServerRpc but is internal in all other SDKs

This PR adds connect to the public createServerRpc() function, but in every other SDK this method is explicitly kept internal:

  • Go: Lives on InternalServerRpc.Connect() with the doc comment "Internal: Connect is part of the SDK's internal handshake/plumbing; external callers should not use it."
  • Python: Lives on _InternalServerRpc.connect() (underscore-prefixed, private class) with :meta private:.
  • .NET: ServerRpc.ConnectAsync() is marked internal.

The connect RPC is already available via the correct internal helper in this file at createInternalServerRpc() (marked @internal). Adding it to the public createServerRpc() object leaks an implementation detail that consumers shouldn't call directly.

Suggestion: Remove the connect entry from createServerRpc(). It's already exposed through createInternalServerRpc() for internal SDK use.

// Remove these two lines from createServerRpc():
connect: async (params: ConnectRequest): Promise<ConnectResult> =>
    connection.sendRequest("connect", params),

Comment thread go/client.go

// Set COPILOT_HOME if configured
if c.options.CopilotHome != "" {
c.process.Env = append(c.process.Env, "COPILOT_HOME="+c.options.CopilotHome)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: COPILOT_HOME is set twice in the same environment array

This PR adds a second block to set COPILOT_HOME, but the pre-existing code at line 1496 already handles it via setEnvValue:

// Already present (line ~1494-1497):
if c.options.CopilotHome != "" {
    c.process.Env = setEnvValue(c.process.Env, "COPILOT_HOME", c.options.CopilotHome)
}

// This PR adds (redundant + incorrect):
// Set COPILOT_HOME if configured
if c.options.CopilotHome != "" {
    c.process.Env = append(c.process.Env, "COPILOT_HOME="+c.options.CopilotHome)  // ← duplicate!
}

setEnvValue replaces an existing entry if it's already in the slice; append just adds another entry. The result is COPILOT_HOME appearing twice in the spawned process's environment. On most systems the first value wins, so the second assignment has no effect — but it's still incorrect and confusing.

Suggestion: Remove the newly added block entirely. The existing setEnvValue call at line 1496 is sufficient and was already there before this PR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants