Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ the matching provider, and passes everything else through to GitHub unchanged. T
interception. It never modifies the user's shell profile, and it writes credentials to disk only
through the opt-in key store (`copilot-byok keys set` or `/byok`), never on its own initiative.

`--native-byok` publishes the same models through the CLI's supported `providers.json` registry
instead: discovery runs as usual, a private temporary registry is written, `COPILOT_PROVIDERS_CONFIG`
points at it for the child process, and it is deleted when the session ends. That file contains the
provider credentials for the session, which is why the mode stays opt-in.

`COPILOT_API_URL` is an internal Copilot CLI variable, absent from `copilot help environment`.
Treat it as the project's main external risk: if a CLI release changes it, the router breaks and
`--native` / `--legacy` are the fallbacks. Do not assume other undocumented variables exist —
Expand Down Expand Up @@ -43,19 +48,27 @@ There is no VS Code extension, no bundler, no compiled output, and no hosted dep
```text
bin/copilot-byok.mjs Executable entry point; only wires main() to process.exitCode
src/args.mjs Pure argument parser; no I/O
src/cli.mjs Orchestration: router mode (default), legacy mode, native mode
src/argv-config.mjs Pure --config extraction for subcommands
src/cli.mjs Orchestration: router mode (default), native registry mode, legacy mode
src/router.mjs The local proxy: model injection, provider routing, passthrough
src/catalog.mjs Turns providers into the model entries Copilot expects from GET /models
src/launcher.mjs Starts the router on an ephemeral port and spawns Copilot against it
src/upstream.mjs Resolves the Copilot API tier (individual / business / enterprise)
src/upstream.mjs Resolves the Copilot API tier (individual / business / enterprise, data residency)
src/discovery.mjs Asks each provider for its live model list; falls back to the remembered list
src/model-cache.mjs Remembers the last model list each provider returned
src/models-auth.mjs Scopes provider credentials to the catalog origin (pure)
src/config.mjs Provider config loading and validation; loads providers.default.json
src/providers.default.json Generated catalog — never edit by hand, run npm run catalog:update
src/native-registry.mjs Pure translation of providers into the CLI's providers.json registry
src/registry-file.mjs Reads/writes the native registry; private temp file for one session
src/keystore.mjs Optional on-disk key store (0600 / owner-only ACL)
src/keys-command.mjs `copilot-byok keys ...`
src/doctor.mjs `copilot-byok doctor`; installation diagnostics
src/models-command.mjs `copilot-byok models`; discovery output without a session
src/extension-install.mjs `copilot-byok extension ...`; rewrites the template's package root
src/shim-install.mjs `copilot-byok shim ...`; writes the per-platform `copilot` shim
src/session-log.mjs Router diagnostics to a file, never over Copilot's TUI
src/progress.mjs Startup activity line (TTY only)
src/copilot-bin.mjs Resolves the Copilot executable, skipping the stale VS Code shim
src/model-ranking.mjs Pure ranking of provider model catalogs (legacy mode)
src/process-env.mjs Strips stale/secret variables from the child environment
Expand All @@ -68,8 +81,8 @@ schemas/providers.schema.json Published JSON Schema for providers.json
examples/providers.example.json Documented example configuration
docs/provider-verification.md Evidence matrix for provider claims
docs/providers.md Provider table, keys, model discovery, the /byok command
docs/configuration.md Custom providers.json, commands, environment variables, shim, reasoning effort
docs/how-it-works.md Router design, COPILOT_API_URL dependency, picker behavior
docs/configuration.md Custom providers.json, native registry mode, commands, environment variables, shim, reasoning effort
docs/how-it-works.md Router design, native registry mode, COPILOT_API_URL dependency, picker behavior
docs/troubleshooting.md Failure modes and diagnostics
```

Expand All @@ -96,8 +109,9 @@ There is no `dev`, `build`, `format`, or `type-check` script. Do not invent one

- Two-space indentation, single quotes, semicolons, trailing commas in multiline literals.
- Named exports only; no default exports outside `bin/`.
- Modules stay small and single-purpose. `args.mjs`, `model-ranking.mjs`, `provider-env.mjs`, and
`process-env.mjs` must remain **pure** (no file, network, or process access) so they stay trivially testable.
- Modules stay small and single-purpose. `args.mjs`, `argv-config.mjs`, `catalog.mjs`,
`model-ranking.mjs`, `models-auth.mjs`, `native-registry.mjs`, `process-env.mjs`, and
`provider-env.mjs` must remain **pure** (no file, network, or process access) so they stay trivially testable.
- All I/O is injected through the `io` object (`{ stdin, stdout, stderr, env }`) passed to `main()`.
Never read `process.env` or write to `process.stdout` directly from `src/` outside the documented defaults.
- Errors are thrown as `Error` with actionable messages; `bin/copilot-byok.mjs` prints them prefixed with
Expand Down Expand Up @@ -130,6 +144,12 @@ This project handles third-party provider API keys. Every change must preserve t
- Catalog requests must keep a bounded timeout (`AbortSignal.timeout`, default 10 s, configurable 10–300000 ms).
- No `.env` file is used or expected; there is no `.env.example`. Do not add one.
- Never commit real keys, tokens, or a `providers.json` containing credentials.
- `--native-byok` writes provider credentials to a temporary registry because the CLI's supported
path requires them inline. That file must keep `0600` (owner-only ACL on Windows), live in the
system temp directory, and be removed on exit, including on failure. It must never be written to
the repository or to the user's Copilot config directory.
- `--legacy` must keep isolating itself from a user `providers.json` when one is present: that file
takes precedence over `COPILOT_PROVIDER_*`, and silently using it would break the mode.

## Adding or changing a built-in provider

Expand Down Expand Up @@ -173,7 +193,8 @@ that had already been verified working.
## Compatibility and anti-breaking-change rules

- The CLI contract is public: existing flags, aliases, provider ids, and the `--dry-run` JSON shape must keep
working. Add options; do not rename or remove them.
working. Add options; do not rename or remove them. `--native-byok`, `doctor`, and `models` are additive;
the existing `--dry-run` shapes keep their keys, and the new native mode reports `"mode": "native-byok"`.
- Unrecognized arguments and everything after `--` are forwarded verbatim to Copilot CLI. Do not start
consuming new argument names without documenting the change in the README, the relevant `docs/` page, and
`CHANGELOG.md`.
Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@

All notable changes to this project are documented in this file.

## [Unreleased]

### Added

- **`--native-byok`** publishes the configured models through the Copilot CLI's supported
`providers.json` registry instead of the local router. Discovery runs as usual, a private
temporary registry is written and passed via `COPILOT_PROVIDERS_CONFIG`, and the file is removed
when the session ends. The mode needs a CLI that reads `providers.json` (verified on 1.0.105).
Model ids in `/model` are `provider/model`; a `--model byok-...` value is translated.
- **`copilot-byok doctor`** diagnoses the installation in one pass: Node, the Copilot binary and
its version, the provider config, credentials, key store, native registry, cache and log paths,
shim and extension. `--json` for scripts, `--online` to query keyed providers.
- **`copilot-byok models [provider]`** lists what each provider currently serves without starting
a session. `--json` and `--offline` are supported.
- Data residency: when `COPILOT_GH_HOST` or `GH_HOST` names a `*.ghe.com` tenant, the router derives
`copilot-api.<tenant>.ghe.com` instead of the public Copilot hosts.
- Legacy mode now passes `COPILOT_PROVIDER_HEADERS` (from `headers`) and
`COPILOT_PROVIDER_API_KEY_COMMAND` (from `apiKeyCommand`), and sets `COPILOT_PROVIDER_MODEL_ID`
when `catalogModelId` is configured.

### Changed

- The reasoning-effort selector is declared for models that report reasoning support, instead of
being opt-in: current CLIs forward the level to BYOK providers. It can still be hidden with
`reasoningEffortPicker: false`. Docs and verification evidence were updated for CLI 1.0.105.
- Legacy mode detects a user `providers.json` that would take precedence over `COPILOT_PROVIDER_*`,
points the child at an empty temporary registry, and says so on stderr.

### Fixed

- A reasoning-effort level sent by the CLI and rejected by the provider no longer loops: the retry
now carries the stepped-down level instead of replaying the original request. The close listener
is also registered once per client request instead of once per retry.
- `docs/troubleshooting.md` had a broken router log path.

## [2.0.1] - 2026-10-21

### Security
Expand Down
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ copilot-byok --list-providers

# 3. Start Copilot
copilot-byok

# 4. Or publish the same models through the CLI's own BYOK registry
copilot-byok --native-byok
```

Open `/model` inside the session and pick one of your models. Arguments after `--` go to Copilot
Expand All @@ -49,19 +52,24 @@ copilot-byok -- -p "fix the failing test" --allow-all-tools
- Use as many providers per session as you configure.
- GitHub models stay available while BYOK is active, and models can be switched with `/model`
without restarting.
- Two ways to publish them: the default local router, or `--native-byok`, which feeds the CLI's
own supported `providers.json` registry from the same discovery. The native path has no local
proxy; it writes a private temporary registry for the session and deletes it on exit.
- Live discovery: each configured provider is asked what it serves at startup, with a remembered
list as the fallback when a request fails.
- Zero AI credits for BYOK turns; you pay your provider directly.
- Credentials come from environment variables or an opt-in local key store, and never reach the
Copilot process in router mode.
- Credentials come from environment variables or an opt-in local key store. In router mode they
never reach the Copilot process; in native mode they are written only to a temporary file with
owner-only permissions and removed when the session ends.
- No TLS interception: the router binds to loopback and exits with the session.
- Windows, macOS and Linux are supported with Node.js only, with no build step.

## Requirements

- Node.js 22.13 or newer.
- GitHub Copilot CLI 1.0.79 or newer, available as `copilot` on `PATH` (set `COPILOT_BIN` to
override).
override). The current release line, 1.0.105, is the version this project is verified against;
`--native-byok` needs a CLI that reads `providers.json`, which 1.0.105 does.
- A GitHub Copilot account, on any plan including free.

## Configuration
Expand All @@ -78,25 +86,29 @@ the custom `providers.json` format, the command list and single-provider mode ar
| `COPILOT_BYOK_PROGRESS=off` | Turns off the startup progress line | Progress line on |
| `COPILOT_BYOK_DEBUG=1` | Streams router events live instead of only to the log | Log only |
| `COPILOT_BYOK_PROVIDER_TIMEOUT_MS` | Idle timeout on a provider request | 10 minutes |
| `COPILOT_GH_HOST`, `GH_HOST` | Data residency tenant host; the router derives `copilot-api.<tenant>.ghe.com` from it | Public GitHub hosts |

All 28 built-in providers, the environment variable for each key, and the model-discovery
behavior are documented in the [providers guide](docs/providers.md).

## Privacy and security

No TLS is intercepted, no certificate authority is installed, the router listens on loopback only
and exits with the session, provider keys never reach Copilot or GitHub, and your GitHub token
passes through the router unlogged because it has to. Provider keys come from environment
variables or the opt-in key store; keys in the store are plain text on disk protected by file
permissions, so environment variables remain the safer default. `SECURITY.md` covers the full
model, including what the undocumented `COPILOT_API_URL` dependency means for you.
and exits with the session, provider keys never reach GitHub, and your GitHub token passes through
the router unlogged because it has to. In router mode the keys also stay out of the Copilot
process; `--native-byok` writes them to a temporary registry with owner-only permissions for the
duration of the session because that is how the CLI's supported BYOK registry works, and removes it
on exit. Provider keys come from environment variables or the opt-in key store; keys in the store
are plain text on disk protected by file permissions, so environment variables remain the safer
default. `SECURITY.md` covers the full model, including what the undocumented `COPILOT_API_URL`
dependency means for you.

## Troubleshooting

### No BYOK models appear in `/model`

No provider is active. Run `copilot-byok --list-providers` to see which provider is unlocked by
which environment variable or stored key.
which environment variable or stored key, and `copilot-byok doctor` for a full diagnosis.

### `Unknown BYOK model`

Expand All @@ -109,7 +121,9 @@ The key for that provider is wrong or expired. This is not a GitHub authenticati
### Everything is a GitHub model again

`COPILOT_API_URL` may no longer work on your CLI version. Run `copilot-byok --native` for a stock
session and check the router notes in [how it works](docs/how-it-works.md).
session, `copilot-byok --native-byok` to use the CLI's supported registry instead of the router, and
check the router notes in [how it works](docs/how-it-works.md). `copilot-byok doctor` reports the CLI
version it found.

### Startup pauses before the picker opens

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Out of scope:

## How credentials are handled

**Provider keys.** They are read from environment variables or, if you opt in, from the local key store. In router mode the keys stay in the router's own process: they are attached to that provider's outgoing request and stripped from the environment handed to Copilot CLI, so the agent process never sees them. In `--legacy` mode the selected credential is passed to Copilot itself, because that mode uses Copilot's built-in BYOK variables.
**Provider keys.** They are read from environment variables or, if you opt in, from the local key store. In router mode the keys stay in the router's own process: they are attached to that provider's outgoing request and stripped from the environment handed to Copilot CLI, so the agent process never sees them. In `--native-byok` mode the CLI reads them from a temporary `providers.json` written by this tool: the file is created in the system temp directory with `0600` and an owner-only ACL on Windows, passed to the CLI through `COPILOT_PROVIDERS_CONFIG`, and deleted when the session ends. In `--legacy` mode the selected credential is passed to Copilot itself, because that mode uses Copilot's built-in BYOK variables.

**The local key store.** `copilot-byok keys set` and the `/byok` command write to `~/.config/copilot-byok/keys.json` (`%APPDATA%\copilot-byok\keys.json` on Windows), created with mode `0600` and an owner-only ACL on Windows. **Keys stored there are plain text on disk**, protected by file permissions alone; if that does not fit your threat model, use environment variables, which always take precedence and are the default. The store is never created unless you use it. Keys are only accepted from an interactive prompt, never from command-line arguments or pipes, so they do not reach shell history or process listings.

Expand All @@ -45,7 +45,7 @@ Router mode relies on `COPILOT_API_URL`, an environment variable that Copilot CL

- Nothing is intercepted at the TLS level, no certificate authority is installed, and no system trust store is modified. Copilot talks in the clear to loopback, and the router opens its own ordinary HTTPS connection to GitHub.
- Requests that are not for a BYOK model are forwarded to GitHub unchanged.
- Because the variable is undocumented, a Copilot CLI update may change or remove it. If that happens the router stops receiving traffic; `--native` and `--legacy` remain available.
- Because the variable is undocumented, a Copilot CLI update may change or remove it. If that happens the router stops receiving traffic; `--native-byok` uses the CLI's supported `providers.json` registry instead, and `--native` and `--legacy` also remain available.

## The shim

Expand Down
Loading
Loading