From bdcc04440c827d78365de196fb013f0335fc1743 Mon Sep 17 00:00:00 2001 From: Hector Flores Date: Thu, 19 Mar 2026 12:26:15 -0500 Subject: [PATCH 1/4] feat(providers): add GitHub Copilot CLI agent provider Add copilot as a new supported agent provider, following the same pattern as existing providers (claude, codex, opencode). Copilot CLI manages its own API communication so no inference routing changes are needed. - New CopilotProvider with COPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN credential discovery - Normalization aliases: copilot, gh-copilot, github-copilot - Two-token command detection for `gh copilot` wrapper invocation - Network policy with GitHub and Copilot API endpoints - Updated docs and README with Copilot agent references --- README.md | 7 +-- crates/openshell-providers/src/lib.rs | 36 +++++++++++++++ .../src/providers/copilot.rs | 46 +++++++++++++++++++ .../openshell-providers/src/providers/mod.rs | 1 + .../testdata/sandbox-policy.yaml | 16 +++++++ docs/about/supported-agents.md | 1 + 6 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 crates/openshell-providers/src/providers/copilot.rs diff --git a/README.md b/README.md index 1800a468..cbdcc6cb 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ uv tool install -U openshell ### Create a sandbox ```bash -openshell sandbox create -- claude # or opencode, codex, ollama +openshell sandbox create -- claude # or opencode, codex, copilot, ollama ``` A gateway is created automatically on first use. To deploy on a remote host instead, pass `--remote user@host` to the create command. @@ -45,7 +45,7 @@ The sandbox container includes the following tools by default: | Category | Tools | | ---------- | -------------------------------------------------------- | -| Agent | `claude`, `opencode`, `codex` | +| Agent | `claude`, `opencode`, `codex`, `copilot` | | Language | `python` (3.13), `node` (22) | | Developer | `gh`, `git`, `vim`, `nano` | | Networking | `ping`, `dig`, `nslookup`, `nc`, `traceroute`, `netstat` | @@ -115,7 +115,7 @@ Policies are declarative YAML files. Static sections (filesystem, process) are l ## Providers -Agents need credentials — API keys, tokens, service accounts. OpenShell manages these as **providers**: named credential bundles that are injected into sandboxes at creation. The CLI auto-discovers credentials for recognized agents (Claude, Codex, OpenCode) from your shell environment, or you can create providers explicitly with `openshell provider create`. Credentials never leak into the sandbox filesystem; they are injected as environment variables at runtime. +Agents need credentials — API keys, tokens, service accounts. OpenShell manages these as **providers**: named credential bundles that are injected into sandboxes at creation. The CLI auto-discovers credentials for recognized agents (Claude, Codex, OpenCode, Copilot) from your shell environment, or you can create providers explicitly with `openshell provider create`. Credentials never leak into the sandbox filesystem; they are injected as environment variables at runtime. ## GPU Support @@ -136,6 +136,7 @@ The CLI auto-bootstraps a GPU-enabled gateway on first use. GPU intent is also i | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Works out of the box. Provider uses `ANTHROPIC_API_KEY`. | | [OpenCode](https://opencode.ai/) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Works out of the box. Provider uses `OPENAI_API_KEY` or `OPENROUTER_API_KEY`. | | [Codex](https://developers.openai.com/codex) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Works out of the box. Provider uses `OPENAI_API_KEY`. | +| [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Works out of the box. Provider uses `GITHUB_TOKEN` or `COPILOT_GITHUB_TOKEN`. | | [OpenClaw](https://openclaw.ai/) | [Community](https://github.com/NVIDIA/OpenShell-Community) | Launch with `openshell sandbox create --from openclaw`. | | [Ollama](https://ollama.com/) | [Community](https://github.com/NVIDIA/OpenShell-Community) | Launch with `openshell sandbox create --from ollama`. | diff --git a/crates/openshell-providers/src/lib.rs b/crates/openshell-providers/src/lib.rs index 143466d2..3e45680d 100644 --- a/crates/openshell-providers/src/lib.rs +++ b/crates/openshell-providers/src/lib.rs @@ -77,6 +77,7 @@ impl ProviderRegistry { let mut registry = Self::default(); registry.register(providers::claude::ClaudeProvider); registry.register(providers::codex::CodexProvider); + registry.register(providers::copilot::CopilotProvider); registry.register(providers::opencode::OpencodeProvider); registry.register(providers::generic::GenericProvider); registry.register(providers::openai::OpenaiProvider); @@ -128,6 +129,7 @@ pub fn normalize_provider_type(input: &str) -> Option<&'static str> { match normalized.as_str() { "claude" => Some("claude"), "codex" => Some("codex"), + "copilot" | "gh-copilot" | "github-copilot" => Some("copilot"), "opencode" => Some("opencode"), "generic" => Some("generic"), "openai" => Some("openai"), @@ -142,6 +144,18 @@ pub fn normalize_provider_type(input: &str) -> Option<&'static str> { #[must_use] pub fn detect_provider_from_command(command: &[String]) -> Option<&'static str> { + // Two-token subcommand patterns (e.g., `gh copilot`) + if command.len() >= 2 { + let first = &command[0]; + let basename = Path::new(first) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(first); + if basename.eq_ignore_ascii_case("gh") && command[1].eq_ignore_ascii_case("copilot") { + return Some("copilot"); + } + } + // Single-token detection let first = command.first()?; let basename = Path::new(first) .file_name() @@ -164,6 +178,9 @@ mod tests { assert_eq!(normalize_provider_type("openai"), Some("openai")); assert_eq!(normalize_provider_type("anthropic"), Some("anthropic")); assert_eq!(normalize_provider_type("nvidia"), Some("nvidia")); + assert_eq!(normalize_provider_type("copilot"), Some("copilot")); + assert_eq!(normalize_provider_type("gh-copilot"), Some("copilot")); + assert_eq!(normalize_provider_type("github-copilot"), Some("copilot")); assert_eq!(normalize_provider_type("unknown"), None); } @@ -181,5 +198,24 @@ mod tests { detect_provider_from_command(&["/usr/bin/bash".to_string()]), None ); + // Copilot standalone binary + assert_eq!( + detect_provider_from_command(&["copilot".to_string()]), + Some("copilot") + ); + assert_eq!( + detect_provider_from_command(&["/usr/local/bin/copilot".to_string()]), + Some("copilot") + ); + // gh copilot wrapper + assert_eq!( + detect_provider_from_command(&["gh".to_string(), "copilot".to_string()]), + Some("copilot") + ); + // gh alone still maps to github + assert_eq!( + detect_provider_from_command(&["gh".to_string()]), + Some("github") + ); } } diff --git a/crates/openshell-providers/src/providers/copilot.rs b/crates/openshell-providers/src/providers/copilot.rs new file mode 100644 index 00000000..fff74cc3 --- /dev/null +++ b/crates/openshell-providers/src/providers/copilot.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + ProviderDiscoverySpec, ProviderError, ProviderPlugin, RealDiscoveryContext, discover_with_spec, +}; + +pub struct CopilotProvider; + +pub const SPEC: ProviderDiscoverySpec = ProviderDiscoverySpec { + id: "copilot", + credential_env_vars: &["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"], +}; + +impl ProviderPlugin for CopilotProvider { + fn id(&self) -> &'static str { + SPEC.id + } + + fn discover_existing(&self) -> Result, ProviderError> { + discover_with_spec(&SPEC, &RealDiscoveryContext) + } + + fn credential_env_vars(&self) -> &'static [&'static str] { + SPEC.credential_env_vars + } +} + +#[cfg(test)] +mod tests { + use super::SPEC; + use crate::discover_with_spec; + use crate::test_helpers::MockDiscoveryContext; + + #[test] + fn discovers_copilot_env_credentials() { + let ctx = MockDiscoveryContext::new().with_env("COPILOT_GITHUB_TOKEN", "ghp-copilot-token"); + let discovered = discover_with_spec(&SPEC, &ctx) + .expect("discovery") + .expect("provider"); + assert_eq!( + discovered.credentials.get("COPILOT_GITHUB_TOKEN"), + Some(&"ghp-copilot-token".to_string()) + ); + } +} diff --git a/crates/openshell-providers/src/providers/mod.rs b/crates/openshell-providers/src/providers/mod.rs index 8ab52ed9..6fe39513 100644 --- a/crates/openshell-providers/src/providers/mod.rs +++ b/crates/openshell-providers/src/providers/mod.rs @@ -4,6 +4,7 @@ pub mod anthropic; pub mod claude; pub mod codex; +pub mod copilot; pub mod generic; pub mod github; pub mod gitlab; diff --git a/crates/openshell-sandbox/testdata/sandbox-policy.yaml b/crates/openshell-sandbox/testdata/sandbox-policy.yaml index 6f0011ce..ec6c7f78 100644 --- a/crates/openshell-sandbox/testdata/sandbox-policy.yaml +++ b/crates/openshell-sandbox/testdata/sandbox-policy.yaml @@ -57,6 +57,22 @@ network_policies: binaries: - { path: /usr/bin/git } + copilot: + name: copilot + endpoints: + - { host: github.com, port: 443 } + - { host: api.github.com, port: 443 } + - { host: api.githubcopilot.com, port: 443 } + - { host: api.enterprise.githubcopilot.com, port: 443 } + - { host: release-assets.githubusercontent.com, port: 443 } + binaries: + - { path: "/usr/lib/node_modules/@github/copilot/node_modules/@github/**/copilot" } + - { path: /usr/local/bin/copilot } + - { path: "/home/*/.local/bin/copilot" } + - { path: /usr/bin/node } + - { path: /usr/bin/gh } + - { path: /usr/local/bin/gh } + gitlab: name: gitlab endpoints: diff --git a/docs/about/supported-agents.md b/docs/about/supported-agents.md index 6fd313dc..309139f0 100644 --- a/docs/about/supported-agents.md +++ b/docs/about/supported-agents.md @@ -7,6 +7,7 @@ The following table summarizes the agents that run in OpenShell sandboxes. All a | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Works out of the box. Requires `ANTHROPIC_API_KEY`. | | [OpenCode](https://opencode.ai/) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Partial coverage | Pre-installed. Add `opencode.ai` endpoint and OpenCode binary paths to the policy for full functionality. | | [Codex](https://developers.openai.com/codex) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | No coverage | Pre-installed. Requires a custom policy with OpenAI endpoints and Codex binary paths. Requires `OPENAI_API_KEY`. | +| [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Pre-installed. Works out of the box. Requires `GITHUB_TOKEN` or `COPILOT_GITHUB_TOKEN`. | | [OpenClaw](https://openclaw.ai/) | [`openclaw`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/openclaw) | Bundled | Agent orchestration layer. Launch with `openshell sandbox create --from openclaw`. | | [Ollama](https://ollama.com/) | [`ollama`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/ollama) | Bundled | Run cloud and local models. Includes Claude Code, Codex, and OpenClaw. Launch with `openshell sandbox create --from ollama`. | From e305da7f1a651a97f257e821e6c7b31451499dc2 Mon Sep 17 00:00:00 2001 From: Hector Flores Date: Thu, 19 Mar 2026 16:12:21 -0500 Subject: [PATCH 2/4] fix(providers): remove unnecessary gh copilot two-token detection The copilot provider uses a standalone copilot binary, not gh copilot. Removes the two-token subcommand detection block and its corresponding test. --- crates/openshell-providers/src/lib.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/openshell-providers/src/lib.rs b/crates/openshell-providers/src/lib.rs index 3e45680d..118eb46d 100644 --- a/crates/openshell-providers/src/lib.rs +++ b/crates/openshell-providers/src/lib.rs @@ -144,18 +144,6 @@ pub fn normalize_provider_type(input: &str) -> Option<&'static str> { #[must_use] pub fn detect_provider_from_command(command: &[String]) -> Option<&'static str> { - // Two-token subcommand patterns (e.g., `gh copilot`) - if command.len() >= 2 { - let first = &command[0]; - let basename = Path::new(first) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(first); - if basename.eq_ignore_ascii_case("gh") && command[1].eq_ignore_ascii_case("copilot") { - return Some("copilot"); - } - } - // Single-token detection let first = command.first()?; let basename = Path::new(first) .file_name() @@ -207,11 +195,6 @@ mod tests { detect_provider_from_command(&["/usr/local/bin/copilot".to_string()]), Some("copilot") ); - // gh copilot wrapper - assert_eq!( - detect_provider_from_command(&["gh".to_string(), "copilot".to_string()]), - Some("copilot") - ); // gh alone still maps to github assert_eq!( detect_provider_from_command(&["gh".to_string()]), From 89833d4dd1f7ebefb838692d60813346fef7623e Mon Sep 17 00:00:00 2001 From: Hector Flores Date: Thu, 19 Mar 2026 16:15:45 -0500 Subject: [PATCH 3/4] fix(providers): remove gh-copilot and github-copilot aliases The copilot provider is just 'copilot', not 'gh copilot' or 'github-copilot'. Simplifies the normalize_provider_type match to only accept 'copilot'. --- crates/openshell-providers/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/openshell-providers/src/lib.rs b/crates/openshell-providers/src/lib.rs index 118eb46d..e2bcc0c0 100644 --- a/crates/openshell-providers/src/lib.rs +++ b/crates/openshell-providers/src/lib.rs @@ -129,7 +129,7 @@ pub fn normalize_provider_type(input: &str) -> Option<&'static str> { match normalized.as_str() { "claude" => Some("claude"), "codex" => Some("codex"), - "copilot" | "gh-copilot" | "github-copilot" => Some("copilot"), + "copilot" => Some("copilot"), "opencode" => Some("opencode"), "generic" => Some("generic"), "openai" => Some("openai"), @@ -167,8 +167,6 @@ mod tests { assert_eq!(normalize_provider_type("anthropic"), Some("anthropic")); assert_eq!(normalize_provider_type("nvidia"), Some("nvidia")); assert_eq!(normalize_provider_type("copilot"), Some("copilot")); - assert_eq!(normalize_provider_type("gh-copilot"), Some("copilot")); - assert_eq!(normalize_provider_type("github-copilot"), Some("copilot")); assert_eq!(normalize_provider_type("unknown"), None); } From 52f8dae14bdbc28aafad8ed654141998341c6c6e Mon Sep 17 00:00:00 2001 From: Hector Flores Date: Thu, 19 Mar 2026 19:41:36 -0500 Subject: [PATCH 4/4] fix(sandbox): complete copilot network policy from official allowlist Adds missing endpoints from GitHub's official Copilot allowlist reference: - api.individual.githubcopilot.com (Pro/Pro+ plan routing) - api.business.githubcopilot.com (Business plan routing) - copilot-proxy.githubusercontent.com (model proxy) - copilot-telemetry.githubusercontent.com (telemetry) - default.exp-tas.com (feature flags/experimentation) - origin-tracker.githubusercontent.com (API service) Removes gh binaries since copilot is a standalone binary, not gh copilot. Source: https://docs.github.com/en/copilot/reference/copilot-allowlist-reference --- crates/openshell-sandbox/testdata/sandbox-policy.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/openshell-sandbox/testdata/sandbox-policy.yaml b/crates/openshell-sandbox/testdata/sandbox-policy.yaml index ec6c7f78..76ad39b2 100644 --- a/crates/openshell-sandbox/testdata/sandbox-policy.yaml +++ b/crates/openshell-sandbox/testdata/sandbox-policy.yaml @@ -63,15 +63,19 @@ network_policies: - { host: github.com, port: 443 } - { host: api.github.com, port: 443 } - { host: api.githubcopilot.com, port: 443 } + - { host: api.individual.githubcopilot.com, port: 443 } + - { host: api.business.githubcopilot.com, port: 443 } - { host: api.enterprise.githubcopilot.com, port: 443 } + - { host: copilot-proxy.githubusercontent.com, port: 443 } + - { host: copilot-telemetry.githubusercontent.com, port: 443 } + - { host: default.exp-tas.com, port: 443 } + - { host: origin-tracker.githubusercontent.com, port: 443 } - { host: release-assets.githubusercontent.com, port: 443 } binaries: - { path: "/usr/lib/node_modules/@github/copilot/node_modules/@github/**/copilot" } - { path: /usr/local/bin/copilot } - { path: "/home/*/.local/bin/copilot" } - { path: /usr/bin/node } - - { path: /usr/bin/gh } - - { path: /usr/local/bin/gh } gitlab: name: gitlab