Repository navigation
Conversation
4a35939 to
4d3de6d
Compare
|
/integration-test 4d3de6d |
|
/user-journey-check |
|
User-journey check failure on 4d3de6d: https://github.com/databricks/unity-gateway/actions/runs/39327792719 |
|
/integration-test 810de53 |
|
/user-journey-check |
|
User-journey check failure on 810de53: https://github.com/databricks/unity-gateway/actions/runs/39330448036 |
|
Fork integration cancelled on 4d3de6d: https://github.com/databricks/unity-gateway/actions/runs/39327788767 |
|
/integration-test 6fdbcd4 |
|
/user-journey-check |
|
User-journey check success on 6fdbcd4: https://github.com/databricks/unity-gateway/actions/runs/39331233684 |
|
Fork integration cancelled on 810de53: https://github.com/databricks/unity-gateway/actions/runs/39330443015 |
|
Fork integration failure on 6fdbcd4: https://github.com/databricks/unity-gateway/actions/runs/39331229444 |
|
/integration-test 6fdbcd4 |
|
Fork integration success on 6fdbcd4: https://github.com/databricks/unity-gateway/actions/runs/39334584282 |
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>
6fdbcd4 to
f6ebc19
Compare
|
/integration-test f6ebc19 |
|
/user-journey-check |
|
User-journey check success on f6ebc19: https://github.com/databricks/unity-gateway/actions/runs/39403460686 |
|
/ug-review |
| return state | ||
|
|
||
|
|
||
| def _token_provider(state: dict) -> Callable[[bool], str]: |
There was a problem hiding this comment.
Fixed in d480b81: moved it to custom_oauth.launch_token, and both codex._launch_token and Copilot's proxy token provider now call it. I left smart_routing/v2._launch_token alone because it differs: it only uses custom OAuth when the CLI backend is enabled, so folding it in would change smart-routing behavior.
| assert_no_bearer_on_disk(session) | ||
|
|
||
|
|
||
| def test_ug_copilot_headless_gpt_responses_task(live_session, workspace): |
There was a problem hiding this comment.
Keeping these as two named tests. tests/AGENTS.md asks for explicit journey names instead of hiding the provider behind parametrization, and for the configure command to stay visible in each test. They also cover different wire APIs (Anthropic Messages vs Responses), so a failure name tells you which one broke.
| """No file ug or Copilot wrote under the session home holds the Databricks bearer.""" | ||
| bearer = session.env.get("DATABRICKS_BEARER") | ||
| assert bearer, "The live suite authenticates with DATABRICKS_BEARER." | ||
| for root in (session.home / ".copilot", session.home / ".ucode"): |
There was a problem hiding this comment.
the docstrings say no file under the session home holds the bearer, but this only scans .copilot/.ucode. a leak to e.g. ~/.databrickscfg or a shell rc still passes. can we rglob session.home?
Suggestion:
| for root in (session.home / ".copilot", session.home / ".ucode"): | |
| for path in session.home.rglob("*"): |
There was a problem hiding this comment.
Fixed in d480b81: it now scans every file under session.home, comparing bytes so large cache files stay cheap.
| finally: | ||
| cache.stop() | ||
| server.shutdown() | ||
| client.close() |
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed in d480b81: added server.server_close() after server.shutdown().
Co-authored-by: Isaac <no-reply@databricks.com>
|
/integration-test d480b81 |
|
/user-journey-check |
|
User-journey check success on d480b81: https://github.com/databricks/unity-gateway/actions/runs/39404962298 |
|
Fork integration cancelled on f6ebc19: https://github.com/databricks/unity-gateway/actions/runs/39403455354 |
|
Fork integration failure on d480b81: https://github.com/databricks/unity-gateway/actions/runs/39404958508 |
| def test_ug_copilot_refreshes_a_revoked_token_mid_session(live_session, workspace): | ||
| """Scenario: sign in through a bearer command, then revoke the session's token mid-task. | ||
|
|
||
| The headless Claude task first runs a shell step that deletes every PAT minted so far, | ||
| including the one ug's proxy holds, then reads the fixture. | ||
|
|
||
| Expected: the gateway rejects the revoked token, ug mints a new one without restarting | ||
| Copilot, and Copilot's completed answer contains the fixture's value. Limits: needs the | ||
| e2e principal to create PATs; a gateway that still honors a just-deleted PAT fails the | ||
| mint-after-revoke assertion. | ||
| """ |
There was a problem hiding this comment.
We will be able to test to token expire and refresh senario by revoking the databricks tokens.
There was a problem hiding this comment.
tests/integration/utils/pat_bearer.py is able to revoke tokens to reproduce this case.
|
Moved to #1217 (in-repo branch, so CI runs this PR's own workflow). Closing in favor of it. |
🥞 Stacked PR
Use this link to review incremental changes.
What did you change, and why?
Change:
ug copilotnow runs Copilot CLI against a per-session loopback proxy (gateway_proxy.start_gateway_auth_proxy) instead of handing it a Databricks token.COPILOT_PROVIDER_BASE_URL=http://127.0.0.1:<port>/ai-gateway/...and a random per-session secret asCOPILOT_PROVIDER_BEARER_TOKEN.x-api-key) and only on/ai-gateway/{anthropic,mlflow/v1,openai/v1}/(decoded until stable;./..segments rejected).TokenCache(refresh near expiry, force refresh plus one retry on 401/403), and addsucode/<v> copilot/<v>User-Agent, request tags, and managedcopilot_http_headers.databricks auth login --host <ws>, then restartug copilot."). The error goes to~/.ucode/logs/copilot-proxy.log, not over the TUI.~/.copilot/ucode.envand from a UG-produced backup, including beforeug revert, so revert cannot restore one.COPILOT_PROVIDER_API_KEY/_API_KEY_COMMAND/_TRANSPORTso they can't bypass the proxy.copilotintegration lane (Linux + Windows,live and copilot, outsidecujs) mirroring OpenCode's, and amends thetests/AGENTS.mdscope line to allow Copilot headless tests.Why: Copilot reads its provider credential once at startup.
ucode.env, which Copilot never re-reads, so sessions failed once the OAuth token expired (~1h).ucode.env, its backup, and the child env.save_statefrom a background thread, which could reset the active workspace during a concurrentug configure.BEFORE: a
ug copilotsession stops working after token expiry until restarted;~/.copilot/ucode.envand~/.ucode/copilot-ucode-env.backupcontain the Databricks token; Ctrl-C on Windows raisesValueErrorfromsend_signal(SIGINT).AFTER: requests carry a freshly minted token for the life of the session; no file or Copilot env var holds a Databricks credential; Ctrl-C is delivered to Copilot by the terminal and UG tears the proxy down after Copilot exits.
How do you know it works?
Testing:
tests/test_gateway_proxy_integration.py::TestGatewayAuthProxyEndToEnd(real sockets):x-api-keyaccepted and not forwarded;../,%2e%2e,%252e%252e, otel);tests/test_agent_copilot.py:tests/test_e2e.py::TestCopilotLaunchnow runs through the real proxy (and patchesucode.agents.copilot.save_state, which the old patch target missed).tests/integration/test_ug_copilot_headless.py: a Claude (Anthropic) task and a GPT (Responses) task, each asserting noDATABRICKS_BEARERon disk under~/.copilot/~/.ucode. Not run yet: needs the e2e workspace and Copilot 1.0.91. This draft is so the lane can run in CI.just lint(ruff check/format, ty) passes. The remaining unit failures inTestInstallToolBinary/test_e2e_user_agentalso fail on a cleanmaincheckout here.Open questions for reviewers:
tests/AGENTS.mdscope change needs CODEOWNERS sign-off.~/.copilot/providers.jsoncan still override UG's provider; guard left for a follow-up.Not in this PR: binary resolver (#970), version floor / wizard /
forceLoginOrgsdoctor,--workspace,-pstdout banner, MPS, OTEL, OSS models.This pull request and its description were written by Isaac.