Skip to content

feat(cli): add GitHub Copilot CLI as fourth supported agent - #94

Open
shejnowicz wants to merge 15 commits into
masterfrom
feat/copilot-agent
Open

feat(cli): add GitHub Copilot CLI as fourth supported agent#94
shejnowicz wants to merge 15 commits into
masterfrom
feat/copilot-agent

Conversation

@shejnowicz

Copy link
Copy Markdown
Collaborator

Fixes #62.

Summary

Adds `copilot` alongside `claude`, `cursor`, and `codex` as a first-class sandcat agent. `sandcat init --agent copilot ...` produces a container running the `@github/copilot` CLI with the same network isolation, secret substitution, mitmproxy interception, no-new-privileges hardening, and IDE integration as the existing agents.

Follows the Codex integration (PR #87) pattern — every per-agent case statement in `cli/lib/agents.bash` gains a `copilot` branch; new user-settings template + mitmproxy addon file; VS Code extension `GitHub.copilot` added to devcontainer.json when `ide=vscode`; `copilot-yolo` shell alias mirroring `claude-yolo`/`codex-yolo`.

Auth model

Copilot CLI accepts these env vars in precedence order: `COPILOT_GITHUB_TOKEN` → `GH_TOKEN` → `GITHUB_TOKEN`. Sandcat's template defines `COPILOT_GITHUB_TOKEN` as a dedicated secret (least privilege — Copilot API scope only) and also keeps a separate `GITHUB_TOKEN` for regular git/gh operations. Users supply a fine-grained PAT with the "Copilot Requests" permission, or reuse `gh auth token` for a quick start.

Custom placeholder for Copilot's client-side format check: Copilot CLI validates that the token starts with `gho_` or `github_pat_` and rejects the request locally otherwise — sandcat's default `SANDCAT_PLACEHOLDER_` shape fails this check. Extended `mitmproxy_addon_common.py` `load_secrets` to honor a per-secret `placeholder` field in settings.json (fallback to the default when absent). The copilot template sets `"placeholder": "gho_SANDCAT_PLACEHOLDER_COPILOT_GITHUB_TOKEN"` — the CLI accepts the `gho` prefix, and mitmproxy's on-the-wire `.replace(placeholder, value)` still swaps the entire string for a well-formed real token. Backward-compatible: existing agents don't set the field and keep the default.

Install

Node.js 22 (via NodeSource setup script) + `npm install -g @github/copilot` under `USER root`, restored to `USER vscode` at end of block. Adds ~120MB to image (base image doesn't ship Node.js).

Config layout

  • `~/.copilot/mcp-config.json` bind-mounted read-only from host (optional, gated by `SANDCAT_MOUNT_COPILOT_CONFIG=true` — matches other agents)
  • `~/.copilot/session-state` bind-mounted read-write — Copilot appends `events.jsonl` per session UUID; read-only mount fails with EROFS on every prompt
  • macOS Keychain is untouched — sandcat uses the env-var auth path

Network allowlist

Copilot CLI hits `api.github.com`, `api.individual.githubcopilot.com` (or `api.business/enterprise.githubcopilot.com` per tier), and `telemetry.individual.githubcopilot.com`. Allowlist in the template: `.githubcopilot.com`, `api.github.com`, `.github.com`, `*.githubusercontent.com`.

Not included in this PR

  • RTK integration — Copilot doesn't have a PreToolUse-hook mechanism like Claude/Cursor. `rtk` binary is still installed in the container (unless `--features no-rtk`) but there's no auto-init; falls through to the `*)` no-op in `sct_rtk_user_init_block`, same as codex. Follow-up when either rtk adds `--copilot` or Copilot exposes a hook API.
  • `GitHub.copilot-chat` VS Code extension — sandcat's `sct_agent_vscode_extension` dispatcher captures output as a single JSON-safe token (see `devcontainer.bash:260`, `printf "%s"` formatting). Multi-token output would produce malformed JSON. Users can add the chat extension manually in `.devcontainer/devcontainer.json` if wanted. Making the dispatcher multi-token-friendly is a separate refactor.

Test plan

  • 87 agents-bats + 38 composefile-bats + full init suite pass on `feat/copilot-agent` locally
  • Full bats test surface (14 subdirs) green
  • Python unit tests added for the new per-secret `placeholder` fallback: default placeholder set when field absent, custom placeholder honored when specified
  • Hands-on end-to-end verified:
    • Node.js 22 + `@github/copilot` install produces a working `copilot --version` inside the agent container
    • `COPILOT_GITHUB_TOKEN` env in a login shell shows the `gho_SANDCAT_PLACEHOLDER_COPILOT_GITHUB_TOKEN` placeholder
    • `copilot -p "reply PONG" --allow-all-tools` returns `PONG` with AI credits billed → proves the substituted token reached the real Copilot API
    • `copilot-yolo` alias resolves inside an interactive login shell after a fresh volume (`down -v && up --build`)
    • Non-allowlisted host (`example.com`) returns `403` from mitmproxy's network filter
    • Session events persist to the host bind-mount, resume IDs work

🤖 Generated with Claude Code

shejnowicz and others added 12 commits August 11, 2026 12:46
Design: add `copilot` alongside claude/cursor/codex. Auth via
COPILOT_GITHUB_TOKEN env with placeholder substitution (identical to
Codex Bearer-token mechanism). Install as npm package under NodeSource
Node.js 22 in the Dockerfile. Config directory ~/.copilot/ with
optional bind-mount (mcp-config.json + session-state/). VS Code
extensions GitHub.copilot + GitHub.copilot-chat. Network allowlist
covers *.githubcopilot.com + api.github.com.

Plan: 7 tasks fork the Codex integration pattern (PR#87) with
per-agent dispatcher hooks in agents.bash, an add_copilot_config_volumes
in composefile.bash, new settings template + mitmproxy addon files,
and a hands-on end-to-end verification that exercises real auth and
network traffic. Streaming flags kept empty by default (Copilot's SSE
looks Codex-shaped, not Cursor-shaped) with a Task 6 fallback if
verification finds otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds copilot case branches to four dispatcher functions:
- sct_agent_vscode_extension: emits GitHub.copilot
- sct_agent_devcontainer_settings_block: no forced settings (empty)
- sct_agent_compose_environment_entries: no env vars (empty)
- sct_agent_mitm_streaming_flags: buffered (empty, verified in Task 6)

Also adds four test cases that verify each dispatcher returns expected values
for copilot agent. Step 1 verification confirmed sct_agent_vscode_extension
callers treat output as single token; chat extension omitted, user adds
manually if desired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add sct_agent_docker_install_block, sct_agent_docker_home_prep_block,
and sct_agent_user_init_block cases for copilot agent. Installs Node.js
22 via NodeSource, @github/copilot npm package, pre-creates ~/.copilot,
and runs copilot --version health check on startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds add_copilot_config_volumes to composefile.bash — mounts
~/.copilot/mcp-config.json and ~/.copilot/session-state into the agent
container — and wires it into customize_compose_file's copilot) branch
via SANDCAT_MOUNT_COPILOT_CONFIG (default true). Header comment updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds settings-user-copilot.json with COPILOT_GITHUB_TOKEN secret and
GitHub network rules; adds mitmproxy_addon_copilot.py (thin SandcatAddon
wrapper, no streaming flags); wires copilot) case into the mitm_addon_file
switch in devcontainer.bash; adds bats tests for template resolution and
addon rendering; fixes pre-existing interactive stub failures caused by
copilot being added to sct_available_agents without updating the stubs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two runtime issues surfaced by the hands-on integration test that the
task-1..5 plan didn't anticipate:

1. Copilot CLI validates the token format client-side — anything not
   prefixed with `gho_` or `github_pat_` triggers "No authentication
   information found" and the request never reaches mitmproxy. The
   default sandcat placeholder `SANDCAT_PLACEHOLDER_COPILOT_GITHUB_TOKEN`
   is rejected on that check. Fix: allow a per-secret `placeholder`
   override in settings.json (mitmproxy_addon_common falls back to the
   default when not set), and set the copilot template's placeholder to
   `gho_SANDCAT_PLACEHOLDER_COPILOT_GITHUB_TOKEN`. Mitmproxy's
   `.replace(placeholder, value)` still swaps the ENTIRE string with the
   real token, so the on-the-wire request is well-formed. Backward-
   compatible: existing secrets without a `placeholder` field keep the
   old default.

2. `~/.copilot/session-state` was mounted read-only. Copilot appends to
   `events.jsonl` under a session UUID directory on every prompt and
   fails with EROFS otherwise. Change the bind-mount to rw — same trust
   posture as other agent config mounts.

Verified end-to-end: `copilot -p "reply PONG" --allow-all-tools` from
inside the sandbox agent returned `PONG` and 2.07 AI credits were
billed against the real Copilot subscription — proof the substituted
token reached the real API. `curl https://example.com/` still returns
403 (non-allowlisted host rejected as expected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add GitHub Copilot CLI to the supported agents in README.md and cli/README.md.
Includes setup instructions, authentication options (fine-grained PAT and gh OAuth),
notes on image size delta from Node.js 22, and VS Code extension integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove claim that GitHub.copilot-chat is shipped (only GitHub.copilot is set)
- Add gho_ prefix to placeholder (gho_SANDCAT_PLACEHOLDER_COPILOT_GITHUB_TOKEN)
- Remove copilot-yolo alias claim (not actually implemented in agents.bash)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two unit tests to TestOpSecretResolution:
- test_secret_uses_default_placeholder_when_not_specified: verifies that
  an entry without a `placeholder` field resolves to SANDCAT_PLACEHOLDER_<NAME>
- test_secret_honors_custom_placeholder_field: verifies that a custom
  `placeholder` value (e.g. gho_custom_placeholder) is stored verbatim,
  covering the Copilot client-side token-format check regression.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove docs/superpowers/specs/2026-08-11-copilot-agent-design.md and
docs/superpowers/plans/2026-08-11-copilot-agent.md — SDD artifacts are
not part of the shipped deliverable and are excluded from PRs per standing
project convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot CLI natively accepts --yolo as a shortcut for
--allow-all-tools --allow-all-paths --allow-all-urls. Add a shell
alias in .bashrc so `copilot-yolo` works out of the box, mirroring
the existing `claude-yolo` and `codex-yolo` aliases.

The sandcat network isolation is the security boundary, so bypassing
in-container permission prompts is the intended workflow — same
rationale as the sibling aliases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 12, 2026 09:23

Copilot AI left a comment

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.

Pull request overview

Adds copilot as a fourth first-class sandcat agent alongside claude, cursor, and codex, including templates, devcontainer wiring, mitmproxy integration, and test coverage to support initializing and running a Copilot CLI-based sandbox.

Changes:

  • Introduces a Copilot agent path across the CLI dispatchers (agent listing/validation, devcontainer template selection, compose mounts, Docker install/home prep, and user-init health check).
  • Adds a Copilot-specific settings template and mitmproxy addon, plus extends secret loading to support per-secret placeholder overrides.
  • Expands bats + Python tests to cover the new agent and the placeholder override behavior.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Documents Copilot as a supported agent and adds setup/auth/integration guidance.
cli/README.md Updates CLI docs to list copilot as a valid --agent option.
cli/lib/agents.bash Adds copilot to agent dispatchers (mount env var, host paths, help text, VS Code extension, install/home prep, user init).
cli/lib/devcontainer.bash Wires Copilot’s mitmproxy addon selection for template customization.
cli/lib/composefile.bash Adds Copilot config volume mounts and hooks them into compose customization.
cli/templates/settings-user-copilot.json New user settings template defining token secret, placeholder, and default network allowlist.
cli/templates/devcontainer/sandcat/scripts/mitmproxy_addon_copilot.py New Copilot-focused mitmproxy addon wrapper using the shared common addon.
cli/templates/devcontainer/sandcat/scripts/mitmproxy_addon_common.py Extends secret loading to honor an optional per-secret placeholder field.
cli/test/agents/agents.bats Adds/updates tests covering Copilot agent dispatcher behavior and Docker/init blocks.
cli/test/composefile/composefile.bats Adds tests for Copilot config mounts and compose wiring.
cli/test/init/init.bats Adds tests ensuring init accepts copilot and creates COPILOT_GITHUB_TOKEN defaults.
cli/test/init/extensions.bats Adds tests ensuring Copilot mitmproxy defaults are applied and Cursor-only streaming flags are not.
cli/test/mitmproxy/test_mitmproxy_addon.py Adds unit tests for default vs custom placeholder behavior in secret loading.
Suppressed comments (1)

README.md:579

  • This claims the VS Code Copilot extension is authenticated via COPILOT_GITHUB_TOKEN and needs no separate sign-in step, but the devcontainer wiring here only installs the extension. The VS Code extension generally relies on VS Code/GitHub authentication, not an env var, so this statement is likely misleading.
**VS Code integration:** When the IDE is `vscode`, the bundled `devcontainer.json`
includes the `GitHub.copilot` extension. It uses the same GitHub account as the
Copilot CLI (authenticated via the `COPILOT_GITHUB_TOKEN` environment variable) —
no separate sign-in step is needed.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread README.md Outdated
Comment on lines +566 to +570
2. **GitHub CLI OAuth token (quick setup):** If you already have `gh` CLI logged in:
```bash
# Edit ~/.config/sandcat/settings.json and set:
COPILOT_GITHUB_TOKEN=$(gh auth token)
```
Comment on lines +374 to +384
# Per-secret placeholder override (rare). Needed when the consumer
# tool validates token format client-side and would reject the
# default `SANDCAT_PLACEHOLDER_<NAME>` shape before ever hitting
# mitmproxy — GitHub Copilot CLI checks the `gho_`/`github_pat_`
# prefix on `COPILOT_GITHUB_TOKEN` and errors out with "no auth
# information found" for anything else. A custom placeholder in
# settings.json (e.g. `gho_SANDCAT_PLACEHOLDER_COPILOT_GITHUB_TOKEN`)
# keeps the CLI happy on the env-var side; mitmproxy's on-the-wire
# `.replace(placeholder, value)` still swaps the ENTIRE string with
# the real token, so what actually leaves the sandbox is well-formed.
placeholder = entry.get("placeholder") or f"SANDCAT_PLACEHOLDER_{name}"
shejnowicz and others added 3 commits August 12, 2026 12:59
Guard against non-string and marker-free placeholder values that could
cause type errors or false-positive substitutions in request traffic.
Warn via ctx.log.warn and fall back to the default when invalid.
Update existing placeholder test to use a marker-containing value;
add three new unit tests covering the rejection paths and the valid
prefixed case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Option 2 auth setup now shows a yq command that mutates settings.json
rather than a bare shell assignment floating in a JSON comment context.
VS Code extension auth note corrected: the extension uses VS Code's own
GitHub sign-in, not the COPILOT_GITHUB_TOKEN env var.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot users need a separate GITHUB_TOKEN for regular git/gh
operations alongside COPILOT_GITHUB_TOKEN (which is scoped to the
Copilot API). Matching the codex template — two secrets, one per
concern, least-privilege by default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shejnowicz

Copy link
Copy Markdown
Collaborator Author

Copilot review comments addressed in the three follow-up commits pushed to the branch:

  • README shell/JSON confusion (comment on line 570) → ee26f8f — restructured the quick-setup section so the gh auth token shell command clearly writes into the JSON via yq -i, no floating shell-looking lines mixed with JSON.
  • README VS Code extension auth claim (suppressed comment on line 579) → ee26f8f — softened the wording; the VS Code Copilot extension authenticates through VS Code's own GitHub sign-in flow, not the COPILOT_GITHUB_TOKEN env var.
  • Custom placeholder validation (comment on mitmproxy_addon_common.py:384) → f9367c6 — validate that a supplied placeholder is a string AND contains SANDCAT_PLACEHOLDER_ (defensive against typos like "GET" that would false-positive-match request substrings). On invalid input, log a warning via ctx.log.warn and fall back to the default. Added three unit tests covering non-string, missing-marker, and honored-custom cases.

Bonus commit 467b603 adds GITHUB_TOKEN alongside COPILOT_GITHUB_TOKEN in the settings template — Copilot users still need a plain-git token for git clone/gh operations, mirroring the codex template's two-secret shape.

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.

Support for github copilot

2 participants