mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-14 05:37:40 +02:00
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9zwUaHucUgrdxvK8FvjYT
75 lines
4.4 KiB
Python
75 lines
4.4 KiB
Python
"""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
|