diff --git a/cmd/enclave/tool_imports.go b/cmd/enclave/tool_imports.go index aa046599..27028c96 100644 --- a/cmd/enclave/tool_imports.go +++ b/cmd/enclave/tool_imports.go @@ -13,5 +13,6 @@ package main import ( _ "enclave/extensions/tools/claude/go" _ "enclave/extensions/tools/codex/go" + _ "enclave/extensions/tools/copilot/go" _ "enclave/extensions/tools/opencode/go" ) diff --git a/docs/configuration.md b/docs/configuration.md index e9c61c9e..c1e066a1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -295,6 +295,7 @@ Common settings paths: |------|--------------------| | claude | `settings.json` | | codex | `config.toml` | +| copilot | `settings.json` | | mistral-vibe | `config.toml` | | opencode | `opencode.json` | | pi | `agent/settings.json` | diff --git a/docs/tools.md b/docs/tools.md index 9730a9c1..08811407 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -13,6 +13,7 @@ Built-in tool profiles: |------|-------------| | `claude` | [Claude Code](https://www.anthropic.com/claude-code) (Anthropic) | | `codex` | [Codex CLI](https://github.com/openai/codex) (OpenAI) | +| `copilot` | [GitHub Copilot CLI](https://github.com/features/copilot/cli) (GitHub) | | `mistral-vibe` | [Mistral Vibe CLI](https://github.com/mistralai/mistral-vibe) (opt-in/experimental) | | `opencode` | [OpenCode](https://opencode.ai/) | | `pi` | [Pi](https://github.com/earendil-works/pi) | diff --git a/extensions/tools/copilot/README.md b/extensions/tools/copilot/README.md new file mode 100644 index 00000000..b77c3da0 --- /dev/null +++ b/extensions/tools/copilot/README.md @@ -0,0 +1,112 @@ +# Copilot CLI + +GitHub Copilot CLI is the command-line coding assistant for GitHub Copilot. + +## Configuration + +- **Command**: `copilot --disable-builtin-mcps` +- **YOLO flag**: `--yolo` (enabled by default) +- **Config directory**: `~/.copilot` +- **Settings file**: `~/.copilot/settings.json` +- **Skills directory**: `~/.copilot/skills` + +The extension also passes through selected project-level Copilot configuration: +`agents/`, `copilot-instructions.md`, `extensions/`, `hooks/`, `instructions/`, +`lsp-config.json`, `mcp-config.json`, `providers.json`, `settings.json`, and +`skills/`. + +## Authentication + +| Variable | Purpose | +|----------|---------| +| `COPILOT_GITHUB_TOKEN` | GitHub token injected by the network gateway | + +The provider declares `~/.copilot/config.json` as its auth file. Enclave uses +its presence to detect an existing auth session and includes it in auth +import/export. See [Authentication & Secrets](../../../docs/auth.md). + +The gateway sends the declared token as a Bearer authorization header to +`api.github.com` and `*.githubcopilot.com`. + +### Recommended: fine-grained PAT via `COPILOT_GITHUB_TOKEN` + +Prefer `COPILOT_GITHUB_TOKEN` with a fine-grained personal access token that +only grants the **Copilot Requests** permission. This is the minimum Copilot CLI +needs and the approach recommended by the Copilot CLI maintainers +([github/copilot-cli#953](https://github.com/github/copilot-cli/issues/953)). + +### OAuth login (`/login`) grants broad scopes + +> **Warning:** The device-code and browser OAuth login (`/login`) issues a token +> with far more scopes than Copilot needs. Observed token scopes: +> `codespace`, `gist`, `read:org`, `read:user`, `repo` (read and write), and +> `write:plugin_gateway_connections`. + +A token with `repo` write access can read and modify private repositories, so +anyone who obtains the stored OAuth credentials (under +`~/.local/state/enclave/tools/copilot/`) can act on your GitHub account beyond +Copilot. Use OAuth only when you accept this risk; otherwise use a fine-grained +PAT as described above. + +## Memory and Skills + +Copilot Memory is **disabled by default** (`"memory": false` in the settings +template). Copilot stores memories on GitHub's servers, not in a local memory +directory. Account-scoped memories can be retrieved by later sessions in other +projects, other Enclave state roots, or Copilot outside Enclave. This can expose +project information across those boundaries and let one project's agent +influence another through shared memories. In YOLO mode, an agent can store +memories without a confirmation prompt. + +Enclave's per-project memory isolation applies to local files and cannot isolate +this server-side memory. The extension therefore does not declare a native +memory directory or memory-disable arguments; `--no-memory` is not a control for +Copilot's server-side memory. + +If you accept this cross-project sharing, run `/memory on` inside a Copilot +session to enable it, or `/memory off` to disable it again. When an Enclave config +override or patch is active, the changed setting carries forward in that +project's config store. See [Tool Config Patches](../../../docs/configuration.md#tool-config-patches) +for settings persistence and precedence. + +For a persistent opt-in, add `"memory": true` to a +[settings patch](../../../docs/configuration.md#tool-config-patches), for example +`~/.config/enclave/patches/copilot/settings.json` on Linux: + +```json +{ + "memory": true +} +``` + +This global patch enables memory for all projects unless overridden by a +project-specific setting. It does not make the stored memories project-scoped. + +The extension's `enclave-help` skill is installed alongside managed skills in +`~/.copilot/skills`. + +## Network Access + +The Copilot service domains are `api.github.com` and `*.githubcopilot.com`. +The DNS allowlist also includes the common GitHub, npm, PyPI, Go, CDN, and +TLS/OCSP infrastructure fragments used by Enclave. + +## Settings + +The default settings template sets `remoteExport` and `autoUpdate` to `false`, +`includeCoAuthoredBy`, `storeTokenPlaintext`, and `memory` to `false`, +`logLevel` to `default`, and `model` to `auto`. See [Memory and Skills](#memory-and-skills) +before enabling memory. + +## Files + +| File | Purpose | +|------|---------| +| `spec.yaml` | Extension manifest (launch behavior, settings, credentials, network, and provider auth) | +| `install.sh` | Installs `@github/copilot` and its `copilot` command | +| `check-update.sh` | Returns the latest npm package version for update probes | +| `gateway-allowlist.conf` | DNS allowlist for network isolation | +| `templates/settings.json` | Default Copilot settings template | +| `entrypoint.d/setup.sh` | Creates `~/.copilot` and sets `COPILOT_HOME` | +| `skills/enclave-help/SKILL.md` | Bundled skill for answering questions from Enclave's documentation | +| `go/handler.go` | Registers the Copilot tool handler | \ No newline at end of file diff --git a/extensions/tools/copilot/check-update.sh b/extensions/tools/copilot/check-update.sh new file mode 100644 index 00000000..22562746 --- /dev/null +++ b/extensions/tools/copilot/check-update.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Dirk Fauth and others. +# +# This program and the accompanying materials are made available under the +# terms of the MIT License, which is available in the project root. +# +# SPDX-License-Identifier: MIT + +set -euo pipefail + +npm view @github/copilot@latest version diff --git a/extensions/tools/copilot/entrypoint.d/setup.sh b/extensions/tools/copilot/entrypoint.d/setup.sh new file mode 100644 index 00000000..523c6abc --- /dev/null +++ b/extensions/tools/copilot/entrypoint.d/setup.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Copyright (C) 2026 Dirk Fauth and others. +# +# This program and the accompanying materials are made available under the +# terms of the MIT License, which is available in the project root. +# +# SPDX-License-Identifier: MIT +# shellcheck shell=bash + +# Copilot extension setup +export COPILOT_HOME="$HOME/.copilot" +mkdir -p "$COPILOT_HOME" + +# In yolo mode, remember the project workspace as trusted for future sessions. +if [ "${ENCLAVE_YOLO:-}" = "1" ] && [ -n "${PROJECT_DIR:-}" ] && command -v jq >/dev/null 2>&1; then + _config="$COPILOT_HOME/config.json" + if [ -f "$_config" ]; then + # Copilot rewrites config.json as JSONC (leading // comments); strip them for jq. + _updated="$(grep -v '^[[:space:]]*//' "$_config" | jq --arg dir "$PROJECT_DIR" \ + '.trustedFolders = (((.trustedFolders // []) + [$dir]) | unique)' \ + 2>/dev/null)" && printf '%s\n' "$_updated" > "$_config" + else + jq -n --arg dir "$PROJECT_DIR" '{trustedFolders:[$dir]}' > "$_config" + fi + unset _config _updated +fi diff --git a/extensions/tools/copilot/gateway-allowlist.conf b/extensions/tools/copilot/gateway-allowlist.conf new file mode 100644 index 00000000..12f4c99c --- /dev/null +++ b/extensions/tools/copilot/gateway-allowlist.conf @@ -0,0 +1,19 @@ +# Copyright (C) 2026 Dirk Fauth and others. +# +# This program and the accompanying materials are made available under the +# terms of the MIT License, which is available in the project root. +# +# SPDX-License-Identifier: MIT + +# Enclave DNS Allowlist Configuration - Copilot +no-resolv +no-poll +address=/#/ + +# Common Infrastructure +conf-file=/etc/dnsmasq.allowlists/fragments/github.conf +conf-file=/etc/dnsmasq.allowlists/fragments/npm.conf +conf-file=/etc/dnsmasq.allowlists/fragments/pypi.conf +conf-file=/etc/dnsmasq.allowlists/fragments/go.conf +conf-file=/etc/dnsmasq.allowlists/fragments/cdn.conf +conf-file=/etc/dnsmasq.allowlists/fragments/tls.conf diff --git a/extensions/tools/copilot/go/handler.go b/extensions/tools/copilot/go/handler.go new file mode 100644 index 00000000..e0c7b8e4 --- /dev/null +++ b/extensions/tools/copilot/go/handler.go @@ -0,0 +1,18 @@ +// Copyright (C) 2026 Dirk Fauth and others. +// +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. +// +// SPDX-License-Identifier: MIT + +package copilot + +import "enclave/internal/tools" + +func init() { + tools.RegisterHandler("copilot", Handler{}) +} + +type Handler struct { + tools.BaseHandler +} diff --git a/extensions/tools/copilot/install.sh b/extensions/tools/copilot/install.sh new file mode 100644 index 00000000..3966efc4 --- /dev/null +++ b/extensions/tools/copilot/install.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copyright (C) 2026 Dirk Fauth and others. +# +# This program and the accompanying materials are made available under the +# terms of the MIT License, which is available in the project root. +# +# SPDX-License-Identifier: MIT + +set -e + +enclave-install-npm-tool @github/copilot@latest copilot Copilot diff --git a/extensions/tools/copilot/skills/enclave-help/SKILL.md b/extensions/tools/copilot/skills/enclave-help/SKILL.md new file mode 100644 index 00000000..6f350d92 --- /dev/null +++ b/extensions/tools/copilot/skills/enclave-help/SKILL.md @@ -0,0 +1,44 @@ +--- +name: enclave-help +description: Answer questions about Enclave by reading the bundled documentation +--- + +# Enclave Help + +Answer the user's question about Enclave by reading the relevant documentation +files bundled at `/usr/share/doc/enclave/`. + +## Important context + +You are running inside an Enclave container. Host-global configuration +(e.g. `~/.config/enclave/config.json`, network policies, secrets) lives on the +host filesystem and is not directly editable from inside the container. +When a question involves host-side configuration changes, explain what the +user needs to do on the host or via the `enclave` CLI rather than +attempting in-container file edits. + +## Documentation index + +| Path | Topic | +|------|-------| +| `docs/README.md` | Documentation index | +| `docs/ARCHITECTURE.md` | Detailed architecture with diagrams | +| `docs/cli-reference.md` | CLI flags and options | +| `docs/configuration.md` | Config files and settings | +| `docs/networking.md` | Network isolation and gateway | +| `docs/auth.md` | Authentication and API keys | +| `docs/persistence.md` | Persistent stores and session data | +| `docs/tools.md` | Supported agent tools | +| `docs/extensions/` | Extension system (adding tools, features) | +| `docs/runtime/` | Runtime behavior (stores, auth sync, network flow) | +| `docs/security/` | Security model and host hardening | +| `extensions/tools//README.md` | Per-tool documentation | +| `extensions/features//README.md` | Per-feature documentation | + +All paths are relative to `/usr/share/doc/enclave/`. + +## Steps + +1. Read the doc(s) most relevant to the user's question. +2. Synthesize a clear, concise answer from what you find. +3. If the docs don't cover the topic, say so explicitly. \ No newline at end of file diff --git a/extensions/tools/copilot/spec.yaml b/extensions/tools/copilot/spec.yaml new file mode 100644 index 00000000..b831668e --- /dev/null +++ b/extensions/tools/copilot/spec.yaml @@ -0,0 +1,71 @@ +# Copyright (C) 2026 Dirk Fauth and others. +# +# This program and the accompanying materials are made available under the +# terms of the MIT License, which is available in the project root. +# +# SPDX-License-Identifier: MIT + +schemaVersion: "1" +kind: sandbox +name: copilot +displayName: Copilot CLI +description: Copilot CLI + +sandbox: + entrypoint: { run: [copilot, --disable-builtin-mcps, --disable-client-telemetry] } + configDir: .copilot + statePaths: [ + config.json, + session-state/, + sidebar-sessions-state/, + command-history-state*, + session-store.db*, + permissions-config.json, + mcp-oauth-config/, + mcp-secrets/, + installed-plugins/, + plugin-data/ + ] + qemuMinMemoryMiB: 4096 + skillsDir: .copilot/skills + settingsFile: copilot-settings.json + settingsTarget: .copilot/settings.json + yoloFlag: --yolo + yoloEnabled: true + continueArgs: [--continue] + resumeArgs: [--resume] + passthroughPaths: [ + agents/, + copilot-instructions.md, + extensions/, + hooks/, + instructions/, + lsp-config.json, + mcp-config.json, + providers.json, + settings.json, + skills/] + +credentials: + sources: + copilot-github-token: + env: [COPILOT_GITHUB_TOKEN] + placeholder: + prefix: github_pat_ + random: { encoding: alphanumeric, length: 82 } + +network: + serviceDomains: + api.github.com: copilot-github-token + "*.githubcopilot.com": copilot-github-token + serviceAuth: + copilot-github-token: { headerName: Authorization, valueFormat: "Bearer %s" } + +providers: + - name: github + credentials: [copilot-github-token] + authFiles: [config.json] + authSession: + mode: any + checks: + - { file: config.json, type: file_exists } diff --git a/extensions/tools/copilot/templates/settings.json b/extensions/tools/copilot/templates/settings.json new file mode 100644 index 00000000..5c6e8545 --- /dev/null +++ b/extensions/tools/copilot/templates/settings.json @@ -0,0 +1,9 @@ +{ + "remoteExport": false, + "autoUpdate": false, + "logLevel": "default", + "includeCoAuthoredBy": false, + "storeTokenPlaintext": false, + "model": "auto", + "memory": false +} diff --git a/internal/config/testdata/golden/tool-copilot.json b/internal/config/testdata/golden/tool-copilot.json new file mode 100644 index 00000000..3ac1f482 --- /dev/null +++ b/internal/config/testdata/golden/tool-copilot.json @@ -0,0 +1,88 @@ +{ + "name": "copilot", + "command": "copilot --disable-builtin-mcps --disable-client-telemetry", + "continue_args": [ + "--continue" + ], + "resume_args": [ + "--resume" + ], + "yolo_flag": "--yolo", + "yolo_enabled": true, + "config_dir": ".copilot", + "skills_dir": ".copilot/skills", + "state_paths": [ + "config.json", + "session-state/", + "sidebar-sessions-state/", + "command-history-state*", + "session-store.db*", + "permissions-config.json", + "mcp-oauth-config/", + "mcp-secrets/", + "installed-plugins/", + "plugin-data/" + ], + "settings_file": "copilot-settings.json", + "settings_target": ".copilot/settings.json", + "passthrough_paths": [ + "agents/", + "copilot-instructions.md", + "extensions/", + "hooks/", + "instructions/", + "lsp-config.json", + "mcp-config.json", + "providers.json", + "settings.json", + "skills/" + ], + "qemu_min_memory_mib": 4096, + "providers": [ + { + "name": "github", + "credential_secrets": [ + "copilot-github-token" + ], + "auth_files": [ + "config.json" + ], + "auth_session": { + "mode": "any", + "checks": [ + { + "file": "config.json", + "type": "file_exists" + } + ] + } + } + ], + "secrets": { + "copilot-github-token": { + "env_vars": [ + "COPILOT_GITHUB_TOKEN" + ], + "release": { + "http": { + "hosts": [ + "*.githubcopilot.com", + "api.github.com" + ], + "header": "authorization", + "format": "Bearer %s" + } + }, + "placeholder": { + "prefix": "github_pat_", + "random": { + "encoding": "alphanumeric", + "length": 82 + } + } + } + }, + "host_config_dir": "", + "host_credentials_file": "", + "host_oauth_json": "" +} diff --git a/internal/config/testdata/golden/tool-ext-copilot.json b/internal/config/testdata/golden/tool-ext-copilot.json new file mode 100644 index 00000000..9f04d45c --- /dev/null +++ b/internal/config/testdata/golden/tool-ext-copilot.json @@ -0,0 +1,32 @@ +{ + "type": "sandbox", + "name": "copilot", + "display_name": "Copilot CLI", + "description": "Copilot CLI", + "default_included": true, + "priority": 100, + "secrets": { + "copilot-github-token": { + "env_vars": [ + "COPILOT_GITHUB_TOKEN" + ], + "release": { + "http": { + "hosts": [ + "*.githubcopilot.com", + "api.github.com" + ], + "header": "authorization", + "format": "Bearer %s" + } + }, + "placeholder": { + "prefix": "github_pat_", + "random": { + "encoding": "alphanumeric", + "length": 82 + } + } + } + } +}