Skip to content

Commit ad17c6c

Browse files
Ambient Code Botclaude
andcommitted
fix: preserve Google Workspace MCP credentials between turns and add runner feedback route (#1222)
Three fixes for issues reported in #1222: 1. **Google Workspace MCP auth**: Stop deleting the credentials file (`credentials.json`) in `clear_runtime_credentials()`. The workspace-mcp process reads from this file continuously; removing it between turns forces it into an inaccessible localhost OAuth flow. The file is overwritten with fresh tokens at the start of each run anyway. 2. **Credential refresh diagnostics**: After refreshing credentials, the `refresh_credentials` tool now checks MCP server auth status and includes diagnostic warnings in its response. This prevents false positives where the tool reports success but MCP servers still can't authenticate. 3. **Feedback endpoint auth**: Add a runner-accessible feedback route at `/api/projects/:projectName/agentic-sessions/:sessionName/runner/feedback` that bypasses `ValidateProjectContext()` middleware. In-session workflows (e.g. Amber) can now submit feedback using BOT_TOKEN without requiring a user OAuth token. The handler still performs its own SSAR auth check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 33be9af commit ad17c6c

4 files changed

Lines changed: 107 additions & 16 deletions

File tree

components/backend/routes.go

100644100755
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ func registerRoutes(r *gin.Engine) {
1818

1919
api.POST("/projects/:projectName/agentic-sessions/:sessionName/github/token", handlers.MintSessionGitHubToken)
2020

21+
// Runner-accessible feedback endpoint — accepts service account tokens
22+
// (BOT_TOKEN) so that in-session workflows (e.g. Amber) can submit
23+
// feedback without requiring a user OAuth token. The handler performs
24+
// its own auth via GetK8sClientsForRequest + SSAR check.
25+
api.POST("/projects/:projectName/agentic-sessions/:sessionName/runner/feedback", websocket.HandleAGUIFeedback)
26+
2127
projectGroup := api.Group("/projects/:projectName", handlers.ValidateProjectContext())
2228
{
2329
projectGroup.GET("/models", handlers.ListModelsForProject)

components/runners/ambient-runner/ambient_runner/bridges/claude/tools.py

100644100755
Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,30 @@
2222
logger = logging.getLogger(__name__)
2323

2424

25+
# ------------------------------------------------------------------
26+
# Credential refresh helpers
27+
# ------------------------------------------------------------------
28+
29+
30+
def _check_mcp_auth_after_refresh() -> str:
31+
"""Check MCP server auth status after a credential refresh.
32+
33+
Returns a diagnostic string with any warnings about MCP servers
34+
that may not be able to use the refreshed credentials. Empty
35+
string means all known servers look healthy.
36+
"""
37+
from ambient_runner.bridges.claude.mcp import check_mcp_authentication
38+
39+
warnings = []
40+
for server_name in ("google-workspace", "mcp-atlassian"):
41+
is_auth, msg = check_mcp_authentication(server_name)
42+
if is_auth is False:
43+
warnings.append(f"{server_name}: {msg}")
44+
elif is_auth is None and msg:
45+
warnings.append(f"{server_name}: {msg}")
46+
return "; ".join(warnings)
47+
48+
2549
# ------------------------------------------------------------------
2650
# Credential refresh tool
2751
# ------------------------------------------------------------------
@@ -70,14 +94,23 @@ async def refresh_credentials_tool(args: dict) -> dict:
7094

7195
integrations = get_active_integrations()
7296
summary = ", ".join(integrations) if integrations else "none detected"
97+
98+
# Verify MCP server auth status after refresh to detect
99+
# false positives (credentials written but MCP server can't use them).
100+
diagnostics = _check_mcp_auth_after_refresh()
101+
102+
parts = [
103+
f"Credentials refreshed successfully. "
104+
f"Active integrations: {summary}.",
105+
]
106+
if diagnostics:
107+
parts.append(f"MCP diagnostics: {diagnostics}")
108+
73109
return {
74110
"content": [
75111
{
76112
"type": "text",
77-
"text": (
78-
f"Credentials refreshed successfully. "
79-
f"Active integrations: {summary}."
80-
),
113+
"text": "\n".join(parts),
81114
}
82115
]
83116
}

components/runners/ambient-runner/ambient_runner/platform/auth.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -454,18 +454,13 @@ def clear_runtime_credentials() -> None:
454454
except OSError as e:
455455
logger.warning(f"Failed to remove token file {token_file}: {e}")
456456

457-
# Remove Google Workspace credential file if present (uses same hardcoded path as populate_runtime_credentials)
458-
google_cred_file = _GOOGLE_WORKSPACE_CREDS_FILE
459-
if google_cred_file.exists():
460-
try:
461-
google_cred_file.unlink()
462-
cleared.append("google_workspace_credentials_file")
463-
# Clean up empty parent dirs
464-
cred_dir = google_cred_file.parent
465-
if cred_dir.exists() and not any(cred_dir.iterdir()):
466-
cred_dir.rmdir()
467-
except OSError as e:
468-
logger.warning(f"Failed to remove Google credential file: {e}")
457+
# NOTE: Google Workspace credential file is intentionally NOT deleted here.
458+
# The workspace-mcp process runs as a long-lived child process of the Claude
459+
# CLI and reads credentials from this file. Deleting it between turns causes
460+
# workspace-mcp to lose its credentials and fall back to initiating a new
461+
# OAuth flow (with an inaccessible localhost:8000 callback URL).
462+
# The file is overwritten with fresh credentials at the start of each run
463+
# by populate_runtime_credentials(), so staleness is not a concern.
469464

470465
if cleared:
471466
logger.info(f"Cleared credentials: {', '.join(cleared)}")

components/runners/ambient-runner/tests/test_shared_session_credentials.py

100644100755
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ambient_runner.platform.auth import (
1414
_GITHUB_TOKEN_FILE,
1515
_GITLAB_TOKEN_FILE,
16+
_GOOGLE_WORKSPACE_CREDS_FILE,
1617
_fetch_credential,
1718
clear_runtime_credentials,
1819
populate_runtime_credentials,
@@ -139,6 +140,27 @@ def test_does_not_crash_when_vars_absent(self):
139140
# Should not raise
140141
clear_runtime_credentials()
141142

143+
def test_preserves_google_credentials_file(self, tmp_path, monkeypatch):
144+
"""clear_runtime_credentials must NOT delete the Google credentials file.
145+
146+
The workspace-mcp process reads credentials from this file. Deleting it
147+
between turns causes workspace-mcp to fall back to an inaccessible
148+
localhost OAuth flow (issue #1222).
149+
"""
150+
fake_cred_file = tmp_path / "credentials.json"
151+
fake_cred_file.write_text('{"token": "test-access-token"}')
152+
monkeypatch.setattr(
153+
"ambient_runner.platform.auth._GOOGLE_WORKSPACE_CREDS_FILE",
154+
fake_cred_file,
155+
)
156+
157+
clear_runtime_credentials()
158+
159+
assert fake_cred_file.exists(), (
160+
"Google credentials file must NOT be deleted — workspace-mcp needs it"
161+
)
162+
assert fake_cred_file.read_text() == '{"token": "test-access-token"}'
163+
142164
def test_does_not_clear_unrelated_vars(self):
143165
try:
144166
os.environ["PATH_BACKUP_TEST"] = "keep-me"
@@ -705,8 +727,43 @@ async def test_returns_success_on_successful_refresh(self):
705727
"ambient_runner.platform.utils.get_active_integrations",
706728
return_value=["github", "jira"],
707729
),
730+
patch(
731+
"ambient_runner.bridges.claude.tools._check_mcp_auth_after_refresh",
732+
return_value="",
733+
),
708734
):
709735
result = await tool_fn({})
710736

711737
assert result.get("isError") is None or result.get("isError") is False
712738
assert "successfully" in result["content"][0]["text"].lower()
739+
740+
@pytest.mark.asyncio
741+
async def test_includes_mcp_diagnostics_on_auth_warning(self):
742+
"""refresh_credentials_tool includes MCP diagnostic warnings when auth issues are detected."""
743+
from ambient_runner.bridges.claude.tools import create_refresh_credentials_tool
744+
745+
mock_context = MagicMock()
746+
tool_fn = create_refresh_credentials_tool(
747+
mock_context, self._make_tool_decorator()
748+
)
749+
750+
with (
751+
patch(
752+
"ambient_runner.platform.auth.populate_runtime_credentials",
753+
new_callable=AsyncMock,
754+
),
755+
patch(
756+
"ambient_runner.platform.utils.get_active_integrations",
757+
return_value=["github", "google"],
758+
),
759+
patch(
760+
"ambient_runner.bridges.claude.tools._check_mcp_auth_after_refresh",
761+
return_value="google-workspace: Google OAuth token expired - re-authenticate",
762+
),
763+
):
764+
result = await tool_fn({})
765+
766+
text = result["content"][0]["text"]
767+
assert "successfully" in text.lower()
768+
assert "MCP diagnostics:" in text
769+
assert "google-workspace" in text

0 commit comments

Comments
 (0)