Skip to content

fix(runner): restore operator-path credential fetch broken by alpha migration#1439

Merged
mergify[bot] merged 2 commits intomainfrom
fix/runner-credential-fetch-dual-path
Apr 23, 2026
Merged

fix(runner): restore operator-path credential fetch broken by alpha migration#1439
mergify[bot] merged 2 commits intomainfrom
fix/runner-credential-fetch-dual-path

Conversation

@markturansky
Copy link
Copy Markdown
Contributor

@markturansky markturansky commented Apr 23, 2026

Summary

  • Root cause: The alpha migration (1aa8b428) rewrote _fetch_credential() to require CREDENTIAL_IDS env var and use the control-plane API path (/api/ambient/v1/projects/{id}/credentials/{id}/token). The operator does not inject CREDENTIAL_IDS, so all credential fetches silently returned empty — breaking GitHub, Jira, Google, and GitLab auth in operator-based deployments.
  • Fix: Restore the session-scoped backend URL (/projects/{project}/agentic-sessions/{session}/credentials/{type}) as fallback when CREDENTIAL_IDS is absent. When CREDENTIAL_IDS is present (control-plane deployments), the new CP path is used.
  • Response format compat: Accept both old (accessToken/apiToken) and new (token) response field formats for Google and Jira credentials, since the backend still returns the old format.

Fixes #1438

Test plan

  • ruff check passes
  • ruff format --check passes
  • python -m pytest tests/ — 713 passed, 11 skipped
  • Deploy to test cluster and verify GITHUB_TOKEN is available in runner session
  • Verify gh auth status works in a session
  • Verify git push/pull to private repos works

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • More flexible credential fetching with improved project/session resolution and clearer warnings when inputs are missing
    • Google credential handling improved: writes workspace-style credentials securely when applicable and preserves legacy/service-account behavior as fallback
    • Jira credential support extended to accept API tokens for environment population
  • Tests

    • Added regression and end-to-end tests covering credential fetching, runtime population/clearance, and provider response formats
    • Updated end-to-end screenshot tests to wait for visibility before theme switches and removed forced clicks

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 23, 2026

Deploy Preview for cheerful-kitten-f556a0 canceled.

Name Link
🔨 Latest commit 43eee2a
🔍 Latest deploy log https://app.netlify.com/projects/cheerful-kitten-f556a0/deploys/69ea54cfe6c60a0008f670b1

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Credential fetch/population now derives project from PROJECT_NAME or AGENTIC_SESSION_NAMESPACE, prefers operator session-scoped credential endpoints when context.session_id exists, returns {} with a consolidated warning on missing inputs, writes Google accessToken responses as workspace-MCP JSON with 0600, and prefers Jira apiToken. Tests added.

Changes

Cohort / File(s) Summary
Credential fetching & runtime population
components/runners/ambient-runner/ambient_runner/platform/auth.py
_fetch_credential resolves project from PROJECT_NAME or AGENTIC_SESSION_NAMESPACE, constructs control-plane or operator session-scoped credential URLs (uses session-scoped path when context.session_id present), logs a single warning and returns {} if it cannot form a URL. populate_runtime_credentials treats Google accessToken/refreshToken responses as workspace-MCP credentials (writes structured JSON with OAuth client info and expiry to _GOOGLE_WORKSPACE_CREDS_FILE with 0600); preserves legacy/service-account token behavior and falls back to workspace creds file when GOOGLE_APPLICATION_CREDENTIALS unset. Jira handling accepts apiToken or token, preferring apiToken.
Tests — regression & compatibility
components/runners/ambient-runner/tests/test_shared_session_credentials.py
Adds regression and E2E tests covering operator vs control-plane credential endpoints, fallback to session-scoped endpoint when CREDENTIAL_IDS unset, verifies populate_runtime_credentials sets and clears GITHUB_TOKEN, and validates provider response-format compatibility for Google (accessToken/refreshToken) and Jira (apiToken).
E2E — UI screenshots
e2e/cypress/e2e/screenshots.cy.ts
Adds explicit visibility checks and a short delay before theme toggles in screenshot tests; replaces forced clicks with visible-asserted clicks.

Sequence Diagram(s)

sequenceDiagram
    participant Runner as Ambient Runner
    participant Backend as Control-plane / Operator API
    participant Workspace as Session Workspace (env/files)

    Runner->>Runner: determine project (PROJECT_NAME or AGENTIC_SESSION_NAMESPACE)
    alt context.session_id present
        Runner->>Backend: GET /projects/{project}/agentic-sessions/{session}/credentials/{credential_type}
    else CREDENTIAL_IDS present
        Runner->>Backend: GET /api/ambient/v1/.../credentials/{credential_id}
    end
    Backend-->>Runner: credential response ({token} or {accessToken, refreshToken, expiresAt,...} or {apiToken})
    alt Google accessToken/refreshToken
        Runner->>Workspace: write workspace-MCP JSON to _GOOGLE_WORKSPACE_CREDS_FILE (0600)
    else Jira apiToken
        Runner->>Workspace: set JIRA_API_TOKEN env
    else legacy token
        Runner->>Workspace: set provider-specific env or credentials file (existing behavior)
    end
Loading
🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format (fix scope) and accurately describes the core change: restoring operator-path credential fetch.
Linked Issues check ✅ Passed Changes directly address #1438 objectives: restore credential propagation via dual-path fetch (control-plane + operator fallback), support response compatibility, and enable GitHub token availability in sessions.
Out of Scope Changes check ✅ Passed All changes are scoped to the credential fetch mechanism and its tests; screenshot test fix is related to test reliability and stays focused on the credential restoration objective.
Performance And Algorithmic Complexity ✅ Passed PR has no performance regressions; bounded O(1) cleanup loop, concurrent credential fetching with asyncio.gather(), linear processing with no N+1 patterns or unbounded operations.
Security And Secret Handling ✅ Passed PR implements secure secret handling: no plaintext credentials in logs, restrictive 0o600 file permissions, hostname allowlist validation, proper Bearer token format, sanitized user context fields, and fake test credentials only.
Kubernetes Resource Safety ✅ Passed PR modifies only Python auth code, tests, and TypeScript E2E tests. No Kubernetes resources (Jobs, Secrets, PVCs, Deployments, RBAC) created or modified.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/runner-credential-fetch-dual-path
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/runner-credential-fetch-dual-path

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
components/runners/ambient-runner/ambient_runner/platform/auth.py (1)

432-433: Consider logging a warning if OAuth client env vars are missing.

Empty string defaults for GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET will produce a credentials file with blank values. While these are validated as REQUIRED_ENV_VARS at Gemini CLI session startup (per session.py), a warning here would surface misconfigurations earlier.

🔧 Optional defensive logging
+            client_id = os.getenv("GOOGLE_OAUTH_CLIENT_ID", "")
+            client_secret = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", "")
+            if not client_id or not client_secret:
+                logger.warning(
+                    "GOOGLE_OAUTH_CLIENT_ID/SECRET not set; workspace-mcp may fail"
+                )
             creds_data = {
                 "token": google_creds.get("accessToken"),
                 "refresh_token": google_creds.get("refreshToken", ""),
                 "token_uri": "https://oauth2.googleapis.com/token",
-                "client_id": os.getenv("GOOGLE_OAUTH_CLIENT_ID", ""),
-                "client_secret": os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", ""),
+                "client_id": client_id,
+                "client_secret": client_secret,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/platform/auth.py` around
lines 432 - 433, Add a defensive warning when building the Google OAuth
credentials dict if the environment variables GOOGLE_OAUTH_CLIENT_ID or
GOOGLE_OAUTH_CLIENT_SECRET are empty: check os.getenv("GOOGLE_OAUTH_CLIENT_ID",
"") and os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", "") before creating the
credentials dict (the block that sets "client_id" and "client_secret") and emit
a warning via the module logger if either is blank so misconfiguration surfaces
earlier (this complements the REQUIRED_ENV_VARS validation done at CLI startup).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@components/runners/ambient-runner/ambient_runner/platform/auth.py`:
- Around line 432-433: Add a defensive warning when building the Google OAuth
credentials dict if the environment variables GOOGLE_OAUTH_CLIENT_ID or
GOOGLE_OAUTH_CLIENT_SECRET are empty: check os.getenv("GOOGLE_OAUTH_CLIENT_ID",
"") and os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", "") before creating the
credentials dict (the block that sets "client_id" and "client_secret") and emit
a warning via the module logger if either is blank so misconfiguration surfaces
earlier (this complements the REQUIRED_ENV_VARS validation done at CLI startup).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e1296c77-864e-47de-be69-7d539ce93986

📥 Commits

Reviewing files that changed from the base of the PR and between 278f6bb and ab8ef10.

📒 Files selected for processing (1)
  • components/runners/ambient-runner/ambient_runner/platform/auth.py

@markturansky markturansky force-pushed the fix/runner-credential-fetch-dual-path branch from ab8ef10 to b757b52 Compare April 23, 2026 16:50
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
components/runners/ambient-runner/tests/test_shared_session_credentials.py (2)

561-564: Avoid fixed handle_request() loop counts to reduce test flakiness

Using server.handle_request() with a hard-coded count (7) makes these tests fragile if provider fetch count changes. Prefer serve_forever() and explicit shutdown() in finally so tests are deterministic and don’t rely on request cardinality.

Suggested refactor
-        thread = Thread(
-            target=lambda: [server.handle_request() for _ in range(7)],
-            daemon=True,
-        )
+        thread = Thread(target=server.serve_forever, daemon=True)
         thread.start()
@@
         finally:
+            server.shutdown()
             server.server_close()
             thread.join(timeout=2)

Also applies to: 630-633, 700-702

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/tests/test_shared_session_credentials.py`
around lines 561 - 564, The test uses a Thread that calls
server.handle_request() in a fixed-count loop (target lambda with range(7)),
which is flaky; change each such Thread to call server.serve_forever() instead
and ensure the test invokes server.shutdown() in a finally block (or teardown)
so the server stops deterministically; update the Thread creation that
references server (and any other similar Thread targets in this file) to use
daemon=True with target=server.serve_forever and add a try/finally around the
request-driving code to call server.shutdown().

664-668: Add permission assertion for Google credentials file

This test verifies content but not secure mode. Add an assertion for 0600 to prevent regressions that could expose OAuth tokens via world/group-readable file permissions.

Suggested assertion
                 assert tmp_creds.exists()
                 written = json.loads(tmp_creds.read_text())
                 assert written["token"] == "ya29.access"
                 assert written["refresh_token"] == "1//refresh"
+                assert (tmp_creds.stat().st_mode & 0o777) == 0o600
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/tests/test_shared_session_credentials.py`
around lines 664 - 668, The test currently checks the file exists and its JSON
contents (using tmp_creds and written), but doesn't verify file permissions;
update the test (the block using tmp_creds.exists() / tmp_creds.read_text()) to
assert the credential file is created with mode 0o600 by checking
tmp_creds.stat().st_mode & 0o777 == 0o600 so the file is not
world/group-readable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@components/runners/ambient-runner/tests/test_shared_session_credentials.py`:
- Around line 561-564: The test uses a Thread that calls server.handle_request()
in a fixed-count loop (target lambda with range(7)), which is flaky; change each
such Thread to call server.serve_forever() instead and ensure the test invokes
server.shutdown() in a finally block (or teardown) so the server stops
deterministically; update the Thread creation that references server (and any
other similar Thread targets in this file) to use daemon=True with
target=server.serve_forever and add a try/finally around the request-driving
code to call server.shutdown().
- Around line 664-668: The test currently checks the file exists and its JSON
contents (using tmp_creds and written), but doesn't verify file permissions;
update the test (the block using tmp_creds.exists() / tmp_creds.read_text()) to
assert the credential file is created with mode 0o600 by checking
tmp_creds.stat().st_mode & 0o777 == 0o600 so the file is not
world/group-readable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9117c8c1-0dbc-4d3d-aa75-54cce61d5626

📥 Commits

Reviewing files that changed from the base of the PR and between ab8ef10 and 643f567.

📒 Files selected for processing (2)
  • components/runners/ambient-runner/ambient_runner/platform/auth.py
  • components/runners/ambient-runner/tests/test_shared_session_credentials.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/runners/ambient-runner/ambient_runner/platform/auth.py

…igration

The alpha migration (1aa8b42) rewrote _fetch_credential() to require
CREDENTIAL_IDS env var and use the control-plane API path. The operator
does not inject CREDENTIAL_IDS, so all credential fetches silently
returned empty — breaking GitHub, Jira, Google, and GitLab auth in
operator-based deployments.

Restore the session-scoped backend URL as fallback when CREDENTIAL_IDS
is absent, and accept both old (accessToken/apiToken) and new (token)
response field formats.

Fixes #1438

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@markturansky markturansky force-pushed the fix/runner-credential-fetch-dual-path branch from 643f567 to d75ea24 Compare April 23, 2026 17:19
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
components/runners/ambient-runner/tests/test_shared_session_credentials.py (1)

561-564: Make test HTTP server lifecycle deterministic (avoid fixed handle_request() loops).

Using a fixed count of handle_request() calls can leave daemon threads blocked when request counts change, which makes this suite brittle. Prefer serve_forever() + explicit shutdown() in finally.

Suggested refactor pattern
-        thread = Thread(
-            target=lambda: [server.handle_request() for _ in range(7)],
-            daemon=True,
-        )
+        thread = Thread(target=server.serve_forever, daemon=True)
         thread.start()

         try:
             ...
         finally:
+            server.shutdown()
             server.server_close()
             thread.join(timeout=2)

As per coding guidelines, "components/runners/ambient-runner/**/*.py: Check subprocess handling, timeout management, and that secrets are not logged."

Also applies to: 630-633, 699-702

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/tests/test_shared_session_credentials.py`
around lines 561 - 564, The test starts a daemon Thread that runs a fixed loop
calling server.handle_request(), which can hang or be misaligned with actual
request counts; replace this pattern by starting the server with
server.serve_forever() on a non-daemon Thread
(Thread(target=server.serve_forever)) and ensure you call server.shutdown() and
thread.join() in a finally block to deterministically stop the server; update
the instances around the Thread creation and the server.handle_request() loops
(including locations referenced by the tests) to use serve_forever()/shutdown()
lifecycle management and ensure the thread is joined to avoid leaked daemon
threads.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@components/runners/ambient-runner/tests/test_shared_session_credentials.py`:
- Around line 561-564: The test starts a daemon Thread that runs a fixed loop
calling server.handle_request(), which can hang or be misaligned with actual
request counts; replace this pattern by starting the server with
server.serve_forever() on a non-daemon Thread
(Thread(target=server.serve_forever)) and ensure you call server.shutdown() and
thread.join() in a finally block to deterministically stop the server; update
the instances around the Thread creation and the server.handle_request() loops
(including locations referenced by the tests) to use serve_forever()/shutdown()
lifecycle management and ensure the thread is joined to avoid leaked daemon
threads.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 199a485f-2fb6-44f9-bcf4-d84040efbf73

📥 Commits

Reviewing files that changed from the base of the PR and between 643f567 and 43eee2a.

📒 Files selected for processing (3)
  • components/runners/ambient-runner/ambient_runner/platform/auth.py
  • components/runners/ambient-runner/tests/test_shared_session_credentials.py
  • e2e/cypress/e2e/screenshots.cy.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/runners/ambient-runner/ambient_runner/platform/auth.py

@mergify mergify Bot added the queued label Apr 23, 2026
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented Apr 23, 2026

Merge Queue Status

  • Entered queue2026-04-23 17:31 UTC · Rule: default
  • Checks skipped · PR is already up-to-date
  • Merged2026-04-23 17:31 UTC · at 43eee2a05646b8930a314b2d86c2794566ea8227 · squash

This pull request spent 12 seconds in the queue, including 1 second running CI.

Required conditions to merge

@mergify mergify Bot merged commit d7533c8 into main Apr 23, 2026
51 checks passed
@mergify mergify Bot deleted the fix/runner-credential-fetch-dual-path branch April 23, 2026 17:31
@mergify mergify Bot removed the queued label Apr 23, 2026
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.

GitHub token not propagated to Claude Code session — auth broken despite valid config

1 participant