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..e2bcc0c0 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" => Some("copilot"), "opencode" => Some("opencode"), "generic" => Some("generic"), "openai" => Some("openai"), @@ -164,6 +166,7 @@ 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("unknown"), None); } @@ -181,5 +184,19 @@ 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 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..76ad39b2 100644 --- a/crates/openshell-sandbox/testdata/sandbox-policy.yaml +++ b/crates/openshell-sandbox/testdata/sandbox-policy.yaml @@ -57,6 +57,26 @@ 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.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 } + 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`. |