From 48886e474600d644391102798abd274ef256d70d Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 3 Sep 2026 12:44:30 -0700 Subject: [PATCH] [eric] providers: an Anthropic key that spans workspaces sends anthropic-workspace-id on every lane, with a Workspace ID field beside the key and a card that names the fix Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01C9zwUaHucUgrdxvK8FvjYT --- backend/apps/agents/agents.py | 5 +- backend/apps/agents/core/error_classify.py | 14 ++++ .../agents/manager/configure_provider_env.py | 6 +- .../agents/manager/run/handle_run_error.py | 15 +++- .../streaming/provider_error_speech.py | 12 +++ backend/apps/agents/proxy/anthropic_proxy.py | 2 + backend/apps/settings/credentials.py | 30 +++++++- backend/apps/settings/models.py | 2 + backend/apps/settings/settings.py | 3 +- backend/tests/test_anthropic_workspace_id.py | 74 +++++++++++++++++++ .../src/app/pages/AgentChat/AgentChat.tsx | 4 +- .../Settings/sections/models/ApiKeyCard.tsx | 16 ++++ .../sections/models/workspaceIdField.test.ts | 28 +++++++ frontend/src/shared/state/settingsSlice.ts | 2 + 14 files changed, 204 insertions(+), 9 deletions(-) create mode 100644 backend/tests/test_anthropic_workspace_id.py create mode 100644 frontend/src/app/pages/Settings/sections/models/workspaceIdField.test.ts diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index 63a3a1b3..c6c83a0c 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -704,6 +704,7 @@ async def probe_model(body: dict): import anthropic client = None + from backend.apps.settings.credentials import own_key_anthropic_client as p_own_key_client # Routing mirrors agent_manager: prefix takes precedence over Pro. resolved_is_9router = ( isinstance(resolved, str) @@ -715,7 +716,7 @@ async def probe_model(body: dict): return {"ok": True, "skipped": True} client = anthropic.AsyncAnthropic(api_key="9router", base_url="http://localhost:20128") elif route == "api" and api_type == "anthropic" and getattr(settings, "anthropic_api_key", None): - client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) + client = p_own_key_client(settings) elif api_type == "anthropic" and connection_mode == "openswarm-pro": bearer = getattr(settings, "openswarm_bearer_token", "") or "" proxy_url = (getattr(settings, "openswarm_proxy_url", None) or "https://api.openswarm.com").rstrip("/") @@ -723,7 +724,7 @@ async def probe_model(body: dict): return {"ok": True, "skipped": True} client = anthropic.AsyncAnthropic(auth_token=bearer, base_url=proxy_url) elif api_type == "anthropic" and getattr(settings, "anthropic_api_key", None): - client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) + client = p_own_key_client(settings) else: if not p_9r_running(): return {"ok": True, "skipped": True} diff --git a/backend/apps/agents/core/error_classify.py b/backend/apps/agents/core/error_classify.py index 56c7a0ee..71aae888 100644 --- a/backend/apps/agents/core/error_classify.py +++ b/backend/apps/agents/core/error_classify.py @@ -308,6 +308,16 @@ def is_translation_error(exc: BaseException, extra_text: str = "") -> bool: return bool(P_TRANSLATION_ERROR_PATTERNS.search(combined)) +# Anthropic's own wording for an identity-linked key sent without a workspace id (400), with one it +# cannot use (404 "Workspace `wrkspc_...` not found."), or with a malformed one. +P_WORKSPACE_ID_RE = re.compile(r"anthropic-workspace-id|workspace\s+`?wrkspc_[\w-]*`?\s+not\s+found", re.IGNORECASE) + + +def is_workspace_id_error(exc: BaseException, extra_text: str = "") -> bool: + """True when Anthropic refused the request over the workspace id, not the key itself.""" + return bool(P_WORKSPACE_ID_RE.search(f"{exc!s}\n{extra_text}")) + + @typechecked def is_auth_error(exc: BaseException, extra_text: str = "") -> bool: """True when the upstream error is a 401/403 auth failure. @@ -320,6 +330,10 @@ def is_auth_error(exc: BaseException, extra_text: str = "") -> bool: combined = f"{exc!s}\n{extra_text}".strip() if not combined: return False + # A missing or wrong workspace id is a credential problem the user fixes in Settings; it used to land + # here only because "invalid_request_error ... API key" happened to match the generic regex below. + if is_workspace_id_error(exc, extra_text): + return True # A tool-schema translation 400 can carry provider/connection wording that trips the auth regex below; it isn't auth, so don't claim it is. if is_translation_error(exc, extra_text): return False diff --git a/backend/apps/agents/manager/configure_provider_env.py b/backend/apps/agents/manager/configure_provider_env.py index 8c73f09b..368af169 100644 --- a/backend/apps/agents/manager/configure_provider_env.py +++ b/backend/apps/agents/manager/configure_provider_env.py @@ -63,8 +63,9 @@ async def configure_provider_env( api_route_provider = (model_entry or {}).get("api") if is_pinned_api_route else None if is_pinned_api_route and api_route_provider == "anthropic" and getattr(global_settings, "anthropic_api_key", None): + from backend.apps.settings.credentials import own_key_cli_env as p_own_key_cli_env options_kwargs["env"] = { - "ANTHROPIC_API_KEY": global_settings.anthropic_api_key, + **p_own_key_cli_env(global_settings), "ANTHROPIC_BASE_URL": "https://api.anthropic.com", # Pin subagents so they don't drift back to the proxy. "CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6", @@ -177,7 +178,8 @@ async def configure_provider_env( options_kwargs["env"]["CLAUDE_CODE_SUBAGENT_MODEL"] = "claude-haiku-4-5-20251001" logger.info(f"[MCP-DEBUG] Using OpenSwarm cloud proxy at {proxy_url}") elif api_type == "anthropic" and not resolved_is_9router and global_settings.anthropic_api_key: - options_kwargs["env"] = {"ANTHROPIC_API_KEY": global_settings.anthropic_api_key} + from backend.apps.settings.credentials import own_key_cli_env as p_own_key_cli_env_fallback + options_kwargs["env"] = p_own_key_cli_env_fallback(global_settings) logger.info("[MCP-DEBUG] Using direct Anthropic API key") elif await router_available(global_settings): # Gemini-bound ids go through the local proxy for schema scrubbing; everything else hits 9Router directly. diff --git a/backend/apps/agents/manager/run/handle_run_error.py b/backend/apps/agents/manager/run/handle_run_error.py index 699a4b2f..c8ed278e 100644 --- a/backend/apps/agents/manager/run/handle_run_error.py +++ b/backend/apps/agents/manager/run/handle_run_error.py @@ -8,7 +8,12 @@ import logging from typing import List from typeguard import typechecked -from backend.apps.agents.manager.streaming.provider_error_speech import CODEX_ROTATION_RESEND_NOTICE, CODEX_ROTATION_RETRY_NOTICE +from backend.apps.agents.manager.streaming.provider_error_speech import ( + ANTHROPIC_WORKSPACE_ID_MISSING_NOTICE, + ANTHROPIC_WORKSPACE_ID_WRONG_NOTICE, + CODEX_ROTATION_RESEND_NOTICE, + CODEX_ROTATION_RETRY_NOTICE, +) from backend.apps.agents.core.models import AgentSession, Message from backend.apps.agents.core.ws_manager import ws_manager @@ -25,6 +30,7 @@ from backend.apps.agents.core.error_classify import ( is_out_of_tokens, has_auth_status, is_auth_error, + is_workspace_id_error, is_content_policy_block, is_cert_failure, is_cli_binary_missing, @@ -537,7 +543,12 @@ async def handle_run_error(e: Exception, session: AgentSession, session_id: str, logger.info(f"auth self-heal armed for {session_id} (codex_rotation={p_codex_rotation})") return # Codex/OpenAI subscription tokens rotate every ~2-3 minutes, the user sees the rotation window as a 401 with "reset after 1m 59s" or similar. Don't ask them to reconnect; just tell them to wait it out and retry. - if ( + # First, above every other reading: the key is fine, the workspace id is missing or wrong. + if is_workspace_id_error(e, extra_text=p_stderr_tail): + p_has_workspace = bool((getattr(load_settings(), "anthropic_workspace_id", None) or "").strip()) + friendly_msg = ANTHROPIC_WORKSPACE_ID_WRONG_NOTICE if p_has_workspace else ANTHROPIC_WORKSPACE_ID_MISSING_NOTICE + reason = "anthropic_workspace_id" + elif ( ("codex/" in p_combined or "[codex/" in p_combined or p_model.startswith(("cx/", "gpt-"))) and ("authentication token is expired" in p_combined or "authentication token has expired" in p_combined or has_auth_status(p_combined)) ): diff --git a/backend/apps/agents/manager/streaming/provider_error_speech.py b/backend/apps/agents/manager/streaming/provider_error_speech.py index 5b7d0f78..18123ed8 100644 --- a/backend/apps/agents/manager/streaming/provider_error_speech.py +++ b/backend/apps/agents/manager/streaming/provider_error_speech.py @@ -266,6 +266,18 @@ CODEX_ROTATION_RETRY_NOTICE = ( "GPT subscription token just rotated (automatic, every couple minutes). Retrying your request " "automatically in about a minute, no action needed." ) +# Anthropic's personal and service-account keys can span workspaces; such a key needs the workspace id +# on every request, and the generic "re-enter your key" card sent one user around in circles (2026-09-03). +ANTHROPIC_WORKSPACE_ID_MISSING_NOTICE = ( + "Your Anthropic API key is linked to your account and can act in more than one workspace, so " + "Anthropic needs to know which one to use. Open Settings > Models and enter the Workspace ID next " + "to your Anthropic key. You can find it in the ID column at platform.claude.com/settings/workspaces." +) +ANTHROPIC_WORKSPACE_ID_WRONG_NOTICE = ( + "Anthropic did not accept the Workspace ID saved next to your Anthropic key. Open Settings > Models " + "and check it against the ID column at platform.claude.com/settings/workspaces; if the key is scoped " + "to a single workspace, leave the field empty." +) CODEX_ROTATION_RESEND_NOTICE = ( "GPT subscription token just rotated (automatic, every couple minutes). Send your message again " "in about a minute and it will go through; nothing to reconnect. If it keeps happening, open " diff --git a/backend/apps/agents/proxy/anthropic_proxy.py b/backend/apps/agents/proxy/anthropic_proxy.py index d141968b..9de546ad 100644 --- a/backend/apps/agents/proxy/anthropic_proxy.py +++ b/backend/apps/agents/proxy/anthropic_proxy.py @@ -358,9 +358,11 @@ def p_pick_upstream(model: str) -> tuple[str, dict[str, str]]: return (proxy, {"Authorization": f"Bearer {bearer}"}) ak = getattr(s, "anthropic_api_key", "") or "" if ak.strip(): + from backend.apps.settings.credentials import anthropic_workspace_header as p_workspace_header return ("https://api.anthropic.com", { "x-api-key": ak.strip(), "anthropic-version": "2023-06-01", + **p_workspace_header(s), }) return ("http://127.0.0.1:20128", {"x-api-key": "9router"}) diff --git a/backend/apps/settings/credentials.py b/backend/apps/settings/credentials.py index 4f1d20e7..61e01012 100644 --- a/backend/apps/settings/credentials.py +++ b/backend/apps/settings/credentials.py @@ -125,6 +125,34 @@ def get_provider_credentials(settings: AppSettings, provider: str) -> dict[str, raise ValueError(f"No credentials for provider: {provider}") +def anthropic_workspace_header(settings: AppSettings) -> dict[str, str]: + """Anthropic's identity-linked keys (personal and service-account) can span workspaces; such a key + must say which workspace every request acts in or the API answers 400 before the model is reached.""" + workspace = (getattr(settings, "anthropic_workspace_id", None) or "").strip() + return {"anthropic-workspace-id": workspace} if workspace else {} + + +def own_key_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic: + """The user's own Anthropic key as an SDK client. Every aux call on the key lane builds its client + here, so the workspace header cannot be present on one door and missing on another.""" + import anthropic + + return anthropic.AsyncAnthropic( + api_key=settings.anthropic_api_key, + default_headers=anthropic_workspace_header(settings) or None, + ) + + +def own_key_cli_env(settings: AppSettings) -> dict[str, str]: + """The user's own Anthropic key as the CLI's env. ANTHROPIC_CUSTOM_HEADERS is the CLI's documented + way to add a request header; a key with no workspace id gets exactly the env it always did.""" + env = {"ANTHROPIC_API_KEY": settings.anthropic_api_key or ""} + header = anthropic_workspace_header(settings) + if header: + env["ANTHROPIC_CUSTOM_HEADERS"] = "\n".join(f"{name}: {value}" for name, value in header.items()) + return env + + def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic: """Return an AsyncAnthropic client for the user's current connection mode.""" import anthropic @@ -138,7 +166,7 @@ def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic: # Prefer the user's own API key when present. if settings.anthropic_api_key: - return anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) + return own_key_anthropic_client(settings) # Fall back to 9Router (free for users with Claude/ChatGPT/Gemini subscriptions). if p_check_9router(): diff --git a/backend/apps/settings/models.py b/backend/apps/settings/models.py index a301e963..ba1efe16 100644 --- a/backend/apps/settings/models.py +++ b/backend/apps/settings/models.py @@ -118,6 +118,8 @@ class AppSettings(BaseModel): # Off = agents can still READ your settings (redacted) but every SettingsWrite is refused. agent_settings_write_enabled: bool = True anthropic_api_key: Optional[str] = None + # A personal or service-account key that spans workspaces must name the one each request acts in (wrkspc_...). + anthropic_workspace_id: Optional[str] = None browser_homepage: str = "https://www.google.com" # Opt-in: let a blocked browser agent borrow the sign-in you already have in your everyday # browser instead of stopping to ask you to log in again. Default OFF because reading your real diff --git a/backend/apps/settings/settings.py b/backend/apps/settings/settings.py index e0c9153d..8099c689 100644 --- a/backend/apps/settings/settings.py +++ b/backend/apps/settings/settings.py @@ -225,7 +225,7 @@ async def apply_settings_update(body: AppSettings, protect_fields: set[str] | No except Exception: pass - secret_keys = {"anthropic_api_key", "openai_api_key", "google_api_key", "openrouter_api_key", + secret_keys = {"anthropic_api_key", "anthropic_workspace_id", "openai_api_key", "google_api_key", "openrouter_api_key", "claude_subscription_token", "openai_subscription_token", "gemini_subscription_token", "openswarm_bearer_token", "free_trial_token", "installation_id", "analytics_token"} safe = {k: v for k, v in body.model_dump().items() if k not in secret_keys} @@ -380,6 +380,7 @@ async def reset_system_prompt(): # A preferences reset (the iOS "Reset All Settings" analogue): everything back to defaults EXCEPT the things a "reset my preferences" click must never silently sever, your connections (server-owned subscription fields AND your pasted provider credentials) and your identity. Hard-erase is the separate flow. P_RESET_PRESERVE_FIELDS = SERVER_OWNED_FIELDS + ( "anthropic_api_key", + "anthropic_workspace_id", "openai_api_key", "google_api_key", "openrouter_api_key", diff --git a/backend/tests/test_anthropic_workspace_id.py b/backend/tests/test_anthropic_workspace_id.py new file mode 100644 index 00000000..63418ce1 --- /dev/null +++ b/backend/tests/test_anthropic_workspace_id.py @@ -0,0 +1,74 @@ +"""Anthropic's identity-linked keys (personal and service-account) can span workspaces, and such a +key must send `anthropic-workspace-id` on every request or the API answers 400 before the model is +reached. A user hit exactly that on 2026-09-03 (install 59a37510, three turns) and our card told them +to re-enter the key. The workspace id is one setting, sent from the one helper every key lane uses, +and the refusal is classified on a declared signal with its own card, first in the auth chain.""" + +import os +import re + +from backend.apps.agents.core.error_classify import is_auth_error, is_workspace_id_error +from backend.apps.settings.credentials import anthropic_workspace_header, own_key_anthropic_client, own_key_cli_env +from backend.apps.settings.models import AppSettings + +P_REAL_400 = ( + 'The agent runtime reported this turn failed (stop_sequence). API Error: 400 {"type":"error","error":' + '{"type":"invalid_request_error","message":"anthropic-workspace-id is required when authenticating with an ' + 'identity-linked API key; send the id of the workspace this request acts in."},"request_id":null}' +) +P_REAL_404 = 'API Error: 404 {"type":"error","error":{"type":"not_found_error","message":"Workspace `wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ` not found."}}' +P_REAL_BAD = 'API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"anthropic-workspace-id header must be a valid workspace ID."}}' + + +def test_a_key_without_a_workspace_id_gets_exactly_the_env_it_always_did(): + settings = AppSettings(anthropic_api_key="sk-ant-test") + assert own_key_cli_env(settings) == {"ANTHROPIC_API_KEY": "sk-ant-test"} + assert anthropic_workspace_header(settings) == {} + + +def test_a_workspace_id_rides_the_cli_custom_headers_and_the_sdk_default_headers(): + settings = AppSettings(anthropic_api_key="sk-ant-test", anthropic_workspace_id=" wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ ") + env = own_key_cli_env(settings) + assert env["ANTHROPIC_API_KEY"] == "sk-ant-test" + assert env["ANTHROPIC_CUSTOM_HEADERS"] == "anthropic-workspace-id: wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ" + client = own_key_anthropic_client(settings) + assert client.default_headers["anthropic-workspace-id"] == "wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ" + + +def test_anthropics_three_workspace_refusals_are_classified_as_auth_on_a_declared_signal(): + for text in (P_REAL_400, P_REAL_404, P_REAL_BAD): + assert is_workspace_id_error(RuntimeError(text)), text + assert is_auth_error(RuntimeError(text)), text + # Controls: an ordinary 400, a stray "workspace" and a schema 400 stay what they were. + for text in ("line 400, in run", "the workspace is on the left", 'API Error: 400 {"message":"tools.0.input_schema: unsupported"}'): + assert not is_workspace_id_error(RuntimeError(text)), text + + +def test_the_workspace_card_is_decided_before_every_other_auth_reading(): + src = open(os.path.join(os.path.dirname(__file__), "..", "apps", "agents", "manager", "run", "handle_run_error.py")).read() + workspace = src.index("is_workspace_id_error(e, extra_text=p_stderr_tail)") + assert workspace < src.index('reason = "codex_token_rotating"') + assert workspace < src.index('reason = "anthropic_auth_invalid"') + assert 'reason = "anthropic_workspace_id"' in src + + +def test_no_lane_builds_a_client_or_env_from_the_raw_key_outside_credentials(): + """The class seal: the user's own key reaches the wire through own_key_* only, so a lane added + tomorrow cannot forget the workspace header the way three lanes had until 2026-09-03.""" + root = os.path.join(os.path.dirname(__file__), "..", "apps") + raw_client = re.compile(r"AsyncAnthropic\(\s*api_key=(?:global_)?settings\.anthropic_api_key") + raw_env = re.compile(r'"ANTHROPIC_API_KEY":\s*(?:global_)?settings\.anthropic_api_key') + offenders = [] + for dirpath, _dirs, files in os.walk(root): + for name in files: + if not name.endswith(".py"): + continue + path = os.path.join(dirpath, name) + text = open(path, encoding="utf-8", errors="replace").read() + if path.endswith(os.path.join("settings", "credentials.py")): + continue + if raw_client.search(text) or raw_env.search(text): + offenders.append(os.path.relpath(path, root)) + assert offenders == [], offenders + proxy = open(os.path.join(root, "agents", "proxy", "anthropic_proxy.py")).read() + assert "anthropic_workspace_header" in proxy diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index e70ef876..7c5d693f 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -1874,13 +1874,15 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose {session.context_overflow && (() => { const reason = session.context_overflow.reason; const isAuth = reason === 'openswarm_pro_auth_expired' || reason === 'anthropic_auth_invalid' || reason === 'auth_error'; + const isWorkspace = reason === 'anthropic_workspace_id'; const isOutOfTokens = reason === 'out_of_tokens'; const isFreeTrial = reason === 'free_trial_exhausted'; const isOutOfCredits = reason === 'out_of_credits'; - const opensSettings = isAuth || isFreeTrial || isOutOfCredits; + const opensSettings = isAuth || isWorkspace || isFreeTrial || isOutOfCredits; const title = isOutOfTokens ? 'Out of tokens' : isFreeTrial ? 'Free runs used up' : isOutOfCredits ? 'Out of credits' + : isWorkspace ? 'Workspace ID needed' : isAuth ? 'Sign-in required' : 'Context full'; const primaryLabel = isOutOfTokens ? 'Got it' : isFreeTrial ? 'Connect a model' diff --git a/frontend/src/app/pages/Settings/sections/models/ApiKeyCard.tsx b/frontend/src/app/pages/Settings/sections/models/ApiKeyCard.tsx index 6568fc72..50ce7608 100644 --- a/frontend/src/app/pages/Settings/sections/models/ApiKeyCard.tsx +++ b/frontend/src/app/pages/Settings/sections/models/ApiKeyCard.tsx @@ -77,6 +77,22 @@ const ApiKeyCard: React.FC<{ Get key + {config.field === 'anthropic_api_key' && value ? ( + + setForm({ ...form, anthropic_workspace_id: e.target.value.trim() || null })} + size="small" + fullWidth + placeholder="Workspace ID (wrkspc_...)" + inputProps={{ 'aria-label': 'Anthropic workspace ID' }} + sx={{ ...fieldSx, '& .MuiOutlinedInput-root': { ...fieldSx['& .MuiOutlinedInput-root'], fontFamily: c.font.mono } }} + /> + + Only for a key that can act in more than one workspace. Find the ID under Settings, Workspaces in the Claude Console. + + + ) : null} ); }; diff --git a/frontend/src/app/pages/Settings/sections/models/workspaceIdField.test.ts b/frontend/src/app/pages/Settings/sections/models/workspaceIdField.test.ts new file mode 100644 index 00000000..13ab2d4a --- /dev/null +++ b/frontend/src/app/pages/Settings/sections/models/workspaceIdField.test.ts @@ -0,0 +1,28 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +// Anthropic's identity-linked keys can span workspaces and then need a workspace id on every +// request. The field lives on the Anthropic card only, appears once a key is present, and the +// chat card for that refusal opens Settings under its own title instead of "Sign-in required". + +const card = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/Settings/sections/models/ApiKeyCard.tsx'), 'utf8'); +const chat = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/AgentChat/AgentChat.tsx'), 'utf8'); +const slice = fs.readFileSync(path.join(process.cwd(), 'src/shared/state/settingsSlice.ts'), 'utf8'); + +test('the workspace id field is gated on the Anthropic card with a key present', () => { + assert.match(card, /config\.field === 'anthropic_api_key' && value \? \(/); + assert.match(card, /anthropic_workspace_id: e\.target\.value\.trim\(\) \|\| null/); +}); + +test('the settings shape carries the field with a null default', () => { + assert.match(slice, /anthropic_workspace_id: string \| null;/); + assert.match(slice, /anthropic_workspace_id: null,/); +}); + +test('the workspace refusal opens Settings under its own title', () => { + assert.match(chat, /const isWorkspace = reason === 'anthropic_workspace_id';/); + assert.match(chat, /opensSettings = isAuth \|\| isWorkspace/); + assert.match(chat, /isWorkspace \? 'Workspace ID needed'/); +}); diff --git a/frontend/src/shared/state/settingsSlice.ts b/frontend/src/shared/state/settingsSlice.ts index a9b39351..ab370fcc 100644 --- a/frontend/src/shared/state/settingsSlice.ts +++ b/frontend/src/shared/state/settingsSlice.ts @@ -41,6 +41,7 @@ export interface AppSettings { dictation_sound_volume?: number; dictation_disabled_surfaces?: string; anthropic_api_key: string | null; + anthropic_workspace_id: string | null; openai_api_key?: string | null; google_api_key?: string | null; openrouter_api_key?: string | null; @@ -185,6 +186,7 @@ export const DEFAULT_SETTINGS: AppSettings = { dictation_sound_volume: 0.7, dictation_disabled_surfaces: '', anthropic_api_key: null, + anthropic_workspace_id: null, browser_homepage: 'https://duckduckgo.com', browser_import_signins: false, auto_select_mode_on_new_agent: false,