Repository navigation
Conversation
Copilot CLI holds its provider credential for the whole session, so the
Databricks token ug passed at launch expired mid-session (the 30-minute
refresher only rewrote ~/.copilot/ucode.env, which Copilot never reads).
The token was also persisted in that file and its backup, and `ug revert`
restored it.
`ug copilot` now starts a per-session proxy on 127.0.0.1. Copilot gets the
proxy URL and a random session secret; the proxy accepts the secret only on
gateway model routes (/ai-gateway/{anthropic,mlflow/v1,openai/v1}/), swaps
it for a fresh Databricks token (CLI profile or custom OAuth), and adds the
ucode/copilot User-Agent, request tags and managed http_headers. A rejected
token is refreshed and the request retried; when refresh itself fails the
proxy answers with a "Databricks sign-in expired" error in the request's
dialect and logs to ~/.ucode/logs/copilot-proxy.log instead of the TUI.
- Configure and launch write no credential; earlier tokens are scrubbed
from ucode.env, a token-bearing backup is dropped (also before revert),
and ug no longer backs up its own file on reconfigure.
- The refresher thread is gone, so a running session no longer resets
current_workspace or rewrites state.
- Ctrl-C no longer re-signals Copilot (send_signal(SIGINT) raised on
Windows); ug waits for Copilot to exit and then stops the proxy.
- Inherited COPILOT_PROVIDER_API_KEY(_COMMAND) / _TRANSPORT are dropped so
they can't bypass the proxy.
Tests: hermetic socket tests for the proxy (secret, path allowlist incl.
encoded traversal, header swap, refresh-and-retry, actionable 401), Copilot
unit tests, the Copilot e2e now runs through the proxy, and a new
non-blocking Copilot integration lane (Linux + Windows) with Claude and GPT
headless journeys that also assert no credential on disk.
Co-authored-by: Isaac <no-reply@databricks.com>
…fused connect Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
… revert Co-authored-by: Isaac <no-reply@databricks.com>
…oken Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
|
/integration-test d333475 |
|
/user-journey-check |
|
User-journey check success on d333475: https://github.com/databricks/unity-gateway/actions/runs/39415944301 |
|
Fork integration failure on d333475: https://github.com/databricks/unity-gateway/actions/runs/39415938650 |
….json Co-authored-by: Isaac <no-reply@databricks.com>
|
/integration-test 46fd13d |
|
/user-journey-check |
|
User-journey check success on 46fd13d: https://github.com/databricks/unity-gateway/actions/runs/39421346497 |
|
Fork integration failure on 46fd13d: https://github.com/databricks/unity-gateway/actions/runs/39421339572 |
Co-authored-by: Isaac <no-reply@databricks.com>
|
/integration-test 5d4bc4a |
|
/user-journey-check |
|
User-journey check success on 5d4bc4a: https://github.com/databricks/unity-gateway/actions/runs/39432394618 |
|
Fork integration failure on 5d4bc4a: https://github.com/databricks/unity-gateway/actions/runs/39432390414 |
Co-authored-by: Isaac <no-reply@databricks.com>
🥞 Stacked PR
Use this link to review incremental changes.
What did you change, and why?
Change:
ug copilotnow registers every Claude and GPT model the user can use on the workspace and lets them switch between them inside one Copilot session.COPILOT_PROVIDERS_CONFIG) to~/.copilot/ucode-providers-<id>.json(0600), deleted when the session ends. Each session holds a lock on a sibling.lockfile, and registries whose lock nobody holds (a killed session) are swept at the next launch and byug revert. The user's own~/.copilot/providers.jsonis never touched.dbx-anthropic(Anthropic Messages),dbx-responses(Responses, GPT-5+) anddbx-completions(Chat Completions, older GPT). All of them point at the existing loopback auth proxy with the session secret; only providers that have models are written.modelId, so Copilot sends Claude request settings, plus prompt/output limits. Without explicit limits Copilot assumes a 128K prompt for registry models.COPILOT_PROVIDERS_CONFIG,COPILOT_MODEL=<provider>/<id>andCOPILOT_OFFLINE=true, and drops inheritedCOPILOT_PROVIDER*settings. The single-provider env (COPILOT_PROVIDER_TYPE/BASE_URL/WIRE_API/MODEL_ID) and theCOPILOT_PROVIDER_WIRE_MODELoverride are gone.supported_api_types. When that listing is empty, it falls back to the gateway listings. No extra requests.claude-*(including baredatabricks-claude-*, GitHub Copilot does not work with Anthropic models #108) -> Anthropic;gpt-N-> Responses for N >= 5, else Completions. Advertised API types only exclude.stream_options), open-source models (not validated with Copilot yet), non-chat models,claude-opus-4-1(the gateway 400s Copilot's thinking settings).--model/-maccept a short name,system.ai.*or Copilot'sprovider/id. They're rewritten toprovider/id, because Copilot only matches that form; Copilot has no-m, so it becomes--model.--model, then the budget recommendation or managed default (now passed throughLaunchOptions.launch_model; it was ignored for Copilot before), then the managed list, then the newest Sonnet/Opus/Haiku, then GPT.model_serviceslist for Copilot now reaches state through the existing static-models seam and replaces discovery. Entries Copilot can't serve are skipped with a warning, and the wizard's Copilot families drop Gemini/OSS.ug statuslists Copilot's catalog and default, andug doctorreports the model counts.ug copilot -pkeeps stdout for Copilot by moving ug's banners to stderr.launch_tokenhelper is reused.Why: Copilot's env-var BYOK supports one provider per session, so
/modelcouldn't reach Databricks models across families, and switching between Claude and GPT meant relaunching. Copilot also only saw the newest model per Claude family plus the Codex catalog, which leakedgpt-oss. Copilot doesn't discover gateway models itself the way Claude Code and Codex do, so ug builds the catalog.BEFORE:
ug copilotpins one model and wire per launch, and/modelcan't switch to another Databricks model family.databricks-claude-*ids don't reach Copilot or take the OpenAI path.AFTER:
/modellists all authorized Claude and GPT models (on dogfood: 14 Claude + 18 GPT) and switches across providers mid-session, carrying tool history.--modelaccepts friendly names and fails fast with the list.How do you know it works?
Testing:
type/wireApi/baseUrl), file mode and cleanup, sweep of dead sessions, env, version gate;-pstdout, the listing's API types;tests/test_agent_copilot.pyruns the real proxy with a stubbed Copilot process.TestInstallToolBinary/test_e2e_user_agent, which also fail onmainin this environment.just lintis clean.tests/integration/test_ug_copilot_models.py, non-blocking Copilot lane):dbx-responses/<model>;/model dbx-responses/<model>, completes a GPT task and/exits.session-state/*/events.jsonl(assistant.message.model,session.start.selectedModel,session.model_change.newModel). The existing headless journeys now also assert the answering model and that no registry is left.ugfrom this branch, dogfood):claude-haiku-4-5,dbx-responses/system.ai.gpt-5-4-nanoandclaude-sonnet-4-5headless tasks completed with clean stdout;system.ai.gemini-3-flashfailed before launch with the list;/model dbx-responses/system.ai.gpt-5-4-nanoprinted "Model changed ... for this session", and GPT answered;/integration-testusesmain's workflow.Not in this PR: open-source models (next on this stack), Model Provider Service models in the registry, GPT token limits, mixing GitHub-seat models into
/model(depends on the seat-based-auth decision).Stacked on #1191.
This pull request and its description were written by Isaac.