Skip to content

Commit 6ccfb4b

Browse files
danielschollDaniel SchollWirasm
authored
feat(providers): add GitHub Copilot community provider (#1505)
* feat(providers): add GitHub Copilot provider configuration types Define CopilotProviderDefaults with model, reasoning effort, and auth options Include system message injection and CLI path configuration support * feat(providers): add GitHub Copilot community provider integration Implement full provider with session management, streaming, and binary resolution Include comprehensive test coverage and lazy-load SDK pattern * feat(providers): add Copilot provider registration and exports Export CopilotProvider, config parser, and binary resolver utilities Register Copilot provider in community providers initialization * test(e2e): add GitHub Copilot provider smoke and abort tests Include streaming verification, token validation, and interrupt handling Verify connectivity, output plumbing, and session management * feat(copilot): add reasoning effort alias and session timeout improvements Map Archon `max` effort to SDK `xhigh` and extend sendAndWait timeout to 60min Handle fork-session requests with fresh session creation fallback * feat(copilot): add environment variable override support and auto model default Add COPILOT_MODEL env var with envOverrides tracking across config system Update provider to default model to 'auto' and enhance settings UI * docs(copilot): clarify session option handling comment * feat(copilot): add MCP, skills, agents, and structured output support Implement full Copilot SDK feature translation including tool restrictions, session config assembly, and best-effort JSON parsing for structured output * feat(copilot): respect useLoggedInUser to override env token test(copilot): cover env token precedence and override behavior * refactor(copilot): remove isCopilotModelCompatible and model-ref delete model-ref.ts and model-ref.test.ts update copilot index and registration to drop isCopilotModelCompatible export * fix(struct-out): enforce object requirement for structured output parsing return undefined if parsed JSON is not an object add tests covering non-object JSON in structured output parsing * feat(copilot): add isExecutableFile check for Copilot binary implement isExecutableFile using stat/access and use it in path resolution update errors to reference executable file and chmod guidance * feat(copilot): add PATH lookup for copilot binary resolution export resolveFromPath and prefer PATH result when executable * ci(workflows): migrate and add Copilot CI workflows - rename e2e-copilot-abort.yaml to test-workflows/e2e-copilot-abort.yaml - add e2e-copilot-all-features.yaml and relocate smoke workflow to test-workflows * refactor(shared): centralize structured-output parsing and skills update providers to re-export shared implementations expose shared utilities: tryParseStructuredOutput, augmentPromptForJsonSchema * feat(registry): register Copilot community provider update registry tests to cover copilot provider registration verify no collision with built-ins and copilot appears in lists * feat(copilot): defer session error warning and harden abort flow update event-bridge to emit no system chunk on session.error add provider-hardening tests for abort, trim model config and cleanup * ci(workflow): simplify output capture in e2e-copilot-smoke workflow * ci(workflows): restructure Copilot e2e workflows for clarity refactor multiple files into sections for fixtures, demos, and checks * ci(workflow): remove e2e-copilot-all-features workflow * feat(workflows): add e2e-copilot-all-nodes-smoke workflow delete old e2e-copilot-smoke workflow extend Copilot smoke tests to cover all node types and structured outputs * refactor(config): remove envOverrides support and COPILOT_MODEL usage use DEFAULT_AI_ASSISTANT env var to select default ai assistant update tests and docs to reflect new default and env var usage * docs: update Copilot docs and env sample * feat(copilot): implement token precedence for Copilot auth introduce COPILOT_GITHUB_TOKEN and generic GH tokens; track tokenSource reorder provider registration to register Pi before Copilot * fe​at(copilot): improve binary resolution and skill dir validation use isExecutableFile for vendor and autodetect checks validate skill names to reject absolute or traversal paths * fix: address review feedback on Copilot community provider - Add packages/providers/src/shared/structured-output.test.ts covering augmentPromptForJsonSchema, the happy-path clean parse, fence stripping (both ```json and bare ```), the forward-brace scan recovery for reasoning-model prose preamble, fence + preamble combo, whitespace trimming, invalid JSON, empty input, and the bare-primitive rejection contract (null/number/string/boolean). - Add packages/providers/src/shared/skills.test.ts covering empty/null inputs, non-string and empty-string skipping, missing skills, cwd vs home resolution order, cwd-shadows-home semantics, deduplication, and the name-only contract (rejection of absolute paths, nested paths, and parent traversal). Uses a staged temp HOME so reads are isolated. - Wire both new test files into packages/providers/package.json so they run in CI as separate bun test invocations. - Add `copilot` to the registered-providers list in the validation error example at guides/authoring-workflows.md, add a Copilot bullet to the Model strings section, and add an AI Providers -- Copilot env-var subsection plus DEFAULT_AI_ASSISTANT enumeration to reference/configuration.md. The two duplicate-import HIGH findings from the May 14 review were hallucinations — the imports don't exist in the current branch — so they need no fix. * chore(rebase): resolve semantic conflicts from dev - Update loadMcpConfig import to ../../mcp/config — #1459 (Codex MCP nodes) extracted it out of claude/provider.ts into its own module. - Regenerate bun.lock from current dev (configVersion: 1). Old commits on this branch carried configVersion: 0; rebased forward unchanged but produced different transitive resolution on install (telegram markdown tests fail locally despite identical telegramify-markdown pin). bun install re-adds @github/copilot-sdk on top of the fresh lockfile. * test(copilot): address CodeRabbit feedback on shared/skills tests - Stage the home copy of `delta` in `.agents` (not `.claude`) so the "prefers cwd over home" precedence test actually verifies precedence within `.agents`. Previously the home copy was in `.claude`, which could not have beaten the cwd `.agents` copy regardless of the resolver's behavior. - Add explicit return types on `makeFakeWorld` and the inner `stageSkill` to satisfy the project's strict TS annotation rule. * fix(providers): address remaining Wirasm review items - pi/event-bridge.ts: consolidate the `export-from` + `import-from` pair on shared/structured-output into the idiomatic `import { X }; export { X };` form. The preceding comment already promised "import once for local use and re-export" but the prior order said the opposite. - authoring-workflows.md: add `copilot` to the prose listing of registered providers (the example validation error string below it already includes copilot). * chore(copilot): drop stale "Claude's loadMcpConfig" attribution #1459 (Codex MCP nodes) extracted loadMcpConfig out of claude/provider.ts into a shared mcp/config.ts module. Update the applyMcpServers docblock to reflect that the helper is shared, not Claude-specific. --------- Co-authored-by: Daniel Scholl <daniel.scholl@microsoft.com> Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
1 parent 4301075 commit 6ccfb4b

36 files changed

Lines changed: 3802 additions & 161 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# E2E manual abort test — GitHub Copilot community provider
2+
# Verifies: Ctrl-C propagates through the bridge to session.abort() and
3+
# sendAndWait unwinds cleanly without dangling listeners.
4+
# Manual: start, wait for streaming to begin, press Ctrl-C. Not for CI.
5+
name: e2e-copilot-abort
6+
description: 'Manual test: start, then Ctrl-C. Verifies abort wiring.'
7+
provider: copilot
8+
model: gpt-5-mini
9+
10+
nodes:
11+
- id: long
12+
prompt: 'Count slowly from 1 to 200, one number per line, with a brief phrase after each number explaining its mathematical significance. Do not skip any numbers.'
13+
effort: low
14+
idle_timeout: 120000
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# E2E smoke test — Copilot provider, every CI-compatible node type
2+
# Covers: prompt, command, loop (AI node types) + bash, script bun/uv
3+
# (deterministic node types) + depends_on / when / trigger_rule / $nodeId.output
4+
# (DAG features) + Copilot-specific options: effort, allowed_tools,
5+
# output_format (best-effort JSON via prompt augment + 2-tier parser).
6+
# Skipped: `approval:` — pauses for human input, incompatible with CI.
7+
# Auth: `gh auth login` OR `COPILOT_GITHUB_TOKEN`.
8+
# To use `GH_TOKEN` / `GITHUB_TOKEN`, also set `assistantConfig.useLoggedInUser: false`.
9+
# Requires an active GitHub Copilot subscription.
10+
name: e2e-copilot-all-nodes-smoke
11+
description: 'Copilot provider smoke across every CI-compatible node type plus Copilot-specific options.'
12+
provider: copilot
13+
model: gpt-5-mini
14+
15+
nodes:
16+
# ─── AI node types ──────────────────────────────────────────────────────
17+
18+
# 1. prompt: inline prompt + effort + allowed_tools (no tool calls).
19+
# Verifies reasoningEffort and availableTools=[] reach the SDK.
20+
- id: prompt-node
21+
prompt: "Reply with exactly the single word 'ok' and nothing else."
22+
allowed_tools: []
23+
effort: low
24+
idle_timeout: 30000
25+
26+
# 2. command: named command file (.archon/commands/e2e-echo-command.md).
27+
# The command echoes back $ARGUMENTS (the workflow invocation message).
28+
- id: command-node
29+
command: e2e-echo-command
30+
allowed_tools: []
31+
idle_timeout: 30000
32+
33+
# 3. loop: iterative AI prompt until completion signal.
34+
# Bounded by max_iterations: 2 so a misbehaving model can't hang CI.
35+
- id: loop-node
36+
loop:
37+
prompt: "Reply with exactly 'DONE' and nothing else."
38+
until: 'DONE'
39+
max_iterations: 2
40+
allowed_tools: []
41+
effort: low
42+
idle_timeout: 60000
43+
44+
# 4. output_format: Copilot's best-effort structured output path
45+
# (prompt augmented with schema + 2-tier JSON parser on result text).
46+
# Unique to Copilot/Pi vs. Claude/Codex native JSON mode — only an
47+
# E2E test catches "real model drifted around the schema".
48+
- id: structured-node
49+
prompt: |
50+
Return a JSON object with two fields, no fences and no prose:
51+
- "status": always "ok" (string)
52+
- "value": always 42 (number)
53+
allowed_tools: []
54+
effort: low
55+
idle_timeout: 30000
56+
output_format:
57+
type: object
58+
properties:
59+
status:
60+
type: string
61+
value:
62+
type: number
63+
required: [status, value]
64+
65+
# ─── Deterministic node types (no AI) ───────────────────────────────────
66+
67+
# 5. bash: shell script with JSON output (enables $nodeId.output.status
68+
# dot-access downstream).
69+
- id: bash-json-node
70+
bash: 'echo ''{"status":"ok"}'''
71+
72+
# 6. script: bun (TypeScript/JavaScript runtime)
73+
- id: script-bun-node
74+
script: echo-args
75+
runtime: bun
76+
timeout: 30000
77+
78+
# 7. script: uv (Python runtime)
79+
- id: script-python-node
80+
script: echo-py
81+
runtime: uv
82+
timeout: 30000
83+
84+
# ─── DAG features ───────────────────────────────────────────────────────
85+
86+
# 8. depends_on + $nodeId.output substitution
87+
- id: downstream
88+
bash: "echo 'downstream got: $prompt-node.output'"
89+
depends_on: [prompt-node]
90+
91+
# 9. when: conditional (JSON dot-access on bash JSON output)
92+
- id: gated
93+
bash: "echo 'gated-ok'"
94+
depends_on: [bash-json-node]
95+
when: "$bash-json-node.output.status == 'ok'"
96+
97+
# 10. when: conditional on AI structured output (proves output_format
98+
# parsed and dot-access works on the resulting object).
99+
- id: structured-check
100+
bash: "echo \"structured.status=$structured-node.output.status\""
101+
depends_on: [structured-node]
102+
when: "$structured-node.output.status == 'ok'"
103+
104+
# 11. trigger_rule: merge multiple deps (all_success semantics)
105+
- id: merge
106+
bash: "echo 'merge-ok'"
107+
depends_on:
108+
[downstream, gated, structured-check, script-bun-node, script-python-node]
109+
trigger_rule: all_success
110+
111+
# ─── Final assertion ────────────────────────────────────────────────────
112+
113+
# 12. Verify every upstream node produced non-empty output, including
114+
# dot-access on the structured-output node (proves output_format
115+
# parsed and downstream consumers can index into it).
116+
# Note: value-equality on string fields is avoided on purpose —
117+
# shellQuote() wraps strings in literal single quotes, so a literal
118+
# `[ "$x" != "ok" ]` would always fail. Non-emptiness is the right
119+
# bar for a smoke; the `when:` gate on structured-check already
120+
# proved the value matched 'ok' to reach this node.
121+
- id: assert
122+
bash: |
123+
fail=0
124+
check() {
125+
local name="$1"
126+
local value="$2"
127+
if [ -z "$value" ]; then
128+
echo "FAIL: $name produced empty output"
129+
fail=1
130+
fi
131+
}
132+
check prompt-node "$prompt-node.output"
133+
check command-node "$command-node.output"
134+
check loop-node "$loop-node.output"
135+
check bash-json-node "$bash-json-node.output"
136+
check script-bun-node "$script-bun-node.output"
137+
check script-python-node "$script-python-node.output"
138+
check downstream "$downstream.output"
139+
check gated "$gated.output"
140+
check merge "$merge.output"
141+
check structured.status "$structured-node.output.status"
142+
check structured.value "$structured-node.output.value"
143+
144+
if [ "$fail" -eq 1 ]; then exit 1; fi
145+
echo "PASS: all node types + structured output verified"
146+
depends_on: [merge, loop-node, command-node]
147+
trigger_rule: all_success

.env.example

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ CODEX_REFRESH_TOKEN=
3939
CODEX_ACCOUNT_ID=
4040
# CODEX_BIN_PATH= # Optional: path to Codex native binary (binary builds only)
4141

42+
# GitHub Copilot (community provider — @github/copilot-sdk)
43+
# Requires an active GitHub Copilot subscription. By default, Archon uses
44+
# the credentials you configured via the Copilot CLI (`copilot login`).
45+
# Generic GH_TOKEN / GITHUB_TOKEN (declared below) are intentionally NOT
46+
# picked up — classic PATs lack Copilot entitlement and would fail. To
47+
# opt back into env-token auth, set `useLoggedInUser: false` in
48+
# `.archon/config.yaml`. Setting COPILOT_GITHUB_TOKEN is treated as
49+
# explicit Copilot intent and always wins.
50+
#
51+
# COPILOT_GITHUB_TOKEN= # Copilot-scoped PAT (always wins when set)
52+
# COPILOT_BIN_PATH= # Optional: path to Copilot CLI binary (binary builds only)
53+
4254
# Pi (community provider — @mariozechner/pi-coding-agent)
4355
# One adapter, ~20 LLM backends. Archon's Pi adapter picks up credentials
4456
# you've already configured via the Pi CLI (`pi /login` writes to
@@ -64,7 +76,7 @@ CODEX_ACCOUNT_ID=
6476
# before the container starts (Pi reads it on each file path lookup).
6577
# PI_CODING_AGENT_DIR=/.archon/pi
6678

67-
# Default AI Assistant (must match a registered provider, e.g. claude, codex, pi)
79+
# Default AI Assistant (must match a registered provider, e.g. claude, codex, copilot, pi)
6880
# Used for new conversations when no codebase specified — errors on unknown values
6981
DEFAULT_AI_ASSISTANT=claude
7082

bun.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/config/config-loader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const SAFE_ASSISTANT_FIELDS: Record<string, readonly string[]> = {
9999
// community providers — list each field we're confident is safe to
100100
// show in the web UI. Unknown providers fall through with no fields.
101101
pi: ['model'],
102+
copilot: ['model'],
102103
};
103104

104105
function toSafeAssistantDefaults(assistants: AssistantDefaults): SafeConfig['assistants'] {

packages/core/src/config/config-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
import type {
1717
ClaudeProviderDefaults,
1818
CodexProviderDefaults,
19+
CopilotProviderDefaults,
1920
PiProviderDefaults,
2021
ProviderDefaultsMap,
2122
} from '@archon/providers/types';
2223

2324
export type {
2425
ClaudeProviderDefaults,
2526
CodexProviderDefaults,
27+
CopilotProviderDefaults,
2628
PiProviderDefaults,
2729
ProviderDefaultsMap,
2830
};

0 commit comments

Comments
 (0)