Skip to content

feat: GitHub Copilot CLI over ACP + TCP transport [INT-945]#415

Open
AlexanderZ-Band wants to merge 5 commits into
devfrom
int-945-add-github-copilot-cli-integration-via-acp
Open

feat: GitHub Copilot CLI over ACP + TCP transport [INT-945]#415
AlexanderZ-Band wants to merge 5 commits into
devfrom
int-945-add-github-copilot-cli-integration-via-acp

Conversation

@AlexanderZ-Band

@AlexanderZ-Band AlexanderZ-Band commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

GitHub Copilot CLI as an ACP client backend, plus a TCP transport on the generic
ACP client layer. Copilot speaks vanilla ACP, so it rides the existing ACPClientAdapter
via a thin CopilotACPAdapter — the only net-new SDK capability is TCP. Verified live
against copilot 1.0.69 over both stdio and TCP.

Linear: https://linear.app/thenvoi/issue/INT-945/add-github-copilot-cli-integration-via-acp (Implements INT-945)

Stacked on #410 (copilot_sdk) for its GITHUB_TOKEN plumbing — rebase onto dev after #410 merges.

Changes

  • TCP transporttcp_spawn_process connect-only seam + host/port on
    ACPClientAdapter (keyword-only; exactly-one-of command/host+port). Zero
    ACPRuntime core change (uses the injectable spawn_process seam).
  • CopilotACPAdapter (+ config) — thin ACPClientAdapter; flexible auth via env
    (env token / stored login / gh / BYOK; github_token is a convenience). Registered in
    the baseline matrix (backends lane, gated on the CLI only like codex); excluded from
    framework-conformance as a bridge.
  • Tests — DI (FakeSpawn) instead of monkeypatching; new acp_toolkit (a fake ACP
    agent + acp_adapter driver + Reply view) drives the adapter over a real in-process
    socketpair — real JSON-RPC, LLM faked: text/thought/tool_call+result/plan relay,
    permission auto-approve, room isolation.
  • Examples — reorganized into examples/acp/{servers,clients,copilot_docker,copilot_sandbox}/;
    two Copilot-in-a-container Docker examples (compose + colocated) over TCP with Band tools via a
    band-mcp SSE server (socat fronts copilot --acp on 0.0.0.0, since --port binds loopback
    only; images build), plus a Docker-sandbox (sbx) example over stdio — validated live
    (sbx exec -i … copilot --acp is byte-clean; host secret-proxy auth, token never enters the VM).
  • Docs — AGENTS.md ACP section (transports, Copilot backend, auth, Docker examples).

Testing

  • Unit suite (3582 passed); ruff / ruff format / pyrefly clean
  • Live: copilot --acp round-trips over stdio and TCP (copilot 1.0.69, stored login, no token)
  • Docker images build (compose + colocated)
  • Full platform E2E (adapter through a live Band room) — needs a Band agent + entitled token; not yet run

🤖 Generated with Claude Code

@AlexanderZ-Band AlexanderZ-Band requested a review from a team July 8, 2026 09:36
@AlexanderZ-Band AlexanderZ-Band force-pushed the int-921-sdk-copilot_sdk-adapter-python-implement-test branch from dd08372 to 9801c4b Compare July 8, 2026 18:31
@AlexanderZ-Band AlexanderZ-Band force-pushed the int-945-add-github-copilot-cli-integration-via-acp branch 2 times, most recently from e9563a3 to bd5035f Compare July 8, 2026 19:25
@AlexanderZ-Band AlexanderZ-Band force-pushed the int-921-sdk-copilot_sdk-adapter-python-implement-test branch from 9801c4b to 0477e81 Compare July 9, 2026 09:52
Base automatically changed from int-921-sdk-copilot_sdk-adapter-python-implement-test to dev July 9, 2026 20:09
@linear-code

linear-code Bot commented Jul 9, 2026

Copy link
Copy Markdown

INT-945

@AlexanderZ-Band AlexanderZ-Band force-pushed the int-945-add-github-copilot-cli-integration-via-acp branch from bd5035f to dd1ddeb Compare July 10, 2026 05:46
AlexanderZ-Band and others added 3 commits July 10, 2026 08:56
pyrefly runs on Linux CI where msvcrt's locking/LK_NBLCK/LK_UNLCK
attributes are absent, failing the lint gate. Suppress inline, matching
the existing pyrefly-ignore convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(e2e): baseline scorecard — N/A reasons on exclude + CI artifact [INT-820]

Excluded adapters vanished from baseline results with no row and the reason
buried in a comment. Make the full adapter×test grid observable:

- exclude carries a reason: @per_adapter(exclude=...) takes ExcludedAdapter(
  adapter, reason) records; a non-empty reason is required at decoration time
  (ExcludedAdapter.__post_init__), mirroring AdapterSpec.e2e_pending.
- the PerAdapter marker carries the exclusions so collection can read them
  back; specs()/adapter_params() stay id-based (clean layering).
- scorecard.py: pure functions (na_rows / outcome_row / merge / to_markdown)
  build the grid from the run reports (exact nodeid keys, no junit scraping)
  plus the marker exclusions; a settings-driven conftest hook writes per-run
  JSON, and a merge CLI folds the per-lane files into one artifact.
- CI: each e2e lane emits artifacts/scorecard-<lane>-<os>.json; a final
  scorecard job merges them into artifacts/scorecard.json (+ markdown). It
  reports the matrix, it does not gate on it.
- migrate all 13 exclude call sites to ExcludedAdapter with reasons.

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

* fix(e2e): scorecard counts only registered-adapter cells [INT-820]

outcome_row treated any parametrized nodeid as a matrix cell, so non-adapter
parametrizations (e.g. test_send_event[thought]) leaked "error"/"task"/
"thought" into the grid as phantom adapters. Filter cell params to the
registered Adapter ids; add a regression test.

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

* refactor(e2e): scorecard collector as a registered plugin, no globals [INT-820]

The conftest routed session-wide hooks (pytest_runtest_logreport /
pytest_sessionfinish) through module globals because logreport gets no
config/session. Make ScorecardCollector a plugin object that owns its state
and output path; the conftest registers one only when a path is set. Removes
the globals and the `global` statement — the idiomatic pytest pattern for
stateful hook collection.

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

* test(e2e): exclude letta from partial-reboot scenario [INT-985]

Two Letta agents in one org can't keep separate identities: Letta stores
MCP tools by name at org scope and re-points the shared band_send_message
row to the most-recently-registered adapter's server, so the rebooted
agent's reply routes through the peer's MCP server and posts under the
peer's identity. Confirmed with a live probe. The cascade-delete fix
(no deregister on release) removed the 404s that previously masked this,
exposing the identity cross-wire. Excluded with a plain-language reason
until per-instance tool-name isolation lands (INT-985).

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

* ci: add ci-required aggregate gate job [INT-975]

Branch protection can't reliably require the unit/conformance CI today: the
`test`/`test-crewai` jobs are matrices, so GitHub emits per-cell contexts
(`test (ubuntu-latest, 3.11)`, …) and the bare `test (3.11)`/`test (3.12)`
names in the ruleset never report — they sit "Expected" forever. dev requires
no test context at all.

Add one aggregate `ci-required` job that `needs` every gating job and fails
unless all resolved to success. A matrix dependency is green only when all its
cells passed, so a single required `ci-required` context covers the whole
OS/Python matrix and survives future matrix changes without another ruleset
edit. `if: always()` so a failed dependency still runs the gate (a skipped job
reports nothing and would leave the required check stuck).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant