Support Anthropic subscription OAuth
This commit is contained in:
@@ -110,12 +110,14 @@ The gateway container needs:
|
||||
- Docker network
|
||||
- `claw_gateway`
|
||||
|
||||
- `ANTHROPIC_AUTH_TOKEN`
|
||||
- Claude subscription OAuth token such as `sk-ant-oat...`
|
||||
- the gateway now sends the extra Claude Code headers required for this path
|
||||
|
||||
- `ANTHROPIC_API_KEY`
|
||||
- standard Anthropic API key
|
||||
|
||||
Bearer-only auth is not currently supported by the Anthropic Messages API used by this runtime.
|
||||
If you pass only `ANTHROPIC_AUTH_TOKEN`, workers will fail with:
|
||||
|
||||
- `401 Unauthorized (authentication_error): OAuth authentication is currently not supported.`
|
||||
If you use `ANTHROPIC_AUTH_TOKEN`, the gateway workers add the Claude Code OAuth headers and agent identity prefix that `nanobot` also uses for subscription auth.
|
||||
|
||||
The gateway copies inherited auth env vars into worker containers according to `CLAW_GATEWAY_INHERITED_ENV`.
|
||||
|
||||
@@ -156,7 +158,7 @@ Example:
|
||||
"template_dir": "/unraid/templates-user",
|
||||
"template_file_prefix": "claw-worker-",
|
||||
"template_archive_dir": "/appdata/template-archive",
|
||||
"inherited_env": ["ANTHROPIC_API_KEY"]
|
||||
"inherited_env": ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"]
|
||||
},
|
||||
"worker_defaults": {
|
||||
"bind_port": 8080,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<DateInstalled/>
|
||||
<DonateText/>
|
||||
<DonateLink/>
|
||||
<Requires>Docker socket access and an Anthropic API key</Requires>
|
||||
<Requires>Docker socket access and either an Anthropic OAuth token or Anthropic API key</Requires>
|
||||
<Config Name="Docker Socket" Target="/var/run/docker.sock" Default="/var/run/docker.sock" Mode="rw" Description="Docker socket for worker orchestration; this grants root-equivalent control of the host" Type="Path" Display="always" Required="true" Mask="false">/var/run/docker.sock</Config>
|
||||
<Config Name="Unraid Templates" Target="/unraid/templates-user" Default="/boot/config/plugins/dockerMan/templates-user" Mode="rw" Description="Unraid template directory where the gateway writes managed worker templates" Type="Path" Display="always" Required="true" Mask="false">/boot/config/plugins/dockerMan/templates-user</Config>
|
||||
<Config Name="Gateway AppData" Target="/appdata" Default="/mnt/user/appdata/claw-telegram-gateway" Mode="rw" Description="Gateway state and manifest directory" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/claw-telegram-gateway</Config>
|
||||
@@ -33,9 +33,9 @@
|
||||
<Config Name="Template Dir" Target="CLAW_GATEWAY_TEMPLATE_DIR" Default="/unraid/templates-user" Mode="" Description="Unraid template directory path inside the gateway container" Type="Variable" Display="advanced" Required="true" Mask="false">/unraid/templates-user</Config>
|
||||
<Config Name="Template Archive Dir" Target="CLAW_GATEWAY_TEMPLATE_ARCHIVE_DIR" Default="/appdata/template-archive" Mode="" Description="Archive location for removed worker templates" Type="Variable" Display="advanced" Required="true" Mask="false">/appdata/template-archive</Config>
|
||||
<Config Name="Worker Network" Target="CLAW_GATEWAY_WORKER_NETWORK" Default="claw_gateway" Mode="" Description="Docker network name shared by the gateway and workers" Type="Variable" Display="advanced" Required="true" Mask="false">claw_gateway</Config>
|
||||
<Config Name="Inherited Env" Target="CLAW_GATEWAY_INHERITED_ENV" Default="ANTHROPIC_API_KEY" Mode="" Description="Comma-separated env vars copied from gateway into worker containers" Type="Variable" Display="advanced" Required="false" Mask="false">ANTHROPIC_API_KEY</Config>
|
||||
<Config Name="Inherited Env" Target="CLAW_GATEWAY_INHERITED_ENV" Default="ANTHROPIC_AUTH_TOKEN,ANTHROPIC_API_KEY" Mode="" Description="Comma-separated env vars copied from gateway into worker containers" Type="Variable" Display="advanced" Required="false" Mask="false">ANTHROPIC_AUTH_TOKEN,ANTHROPIC_API_KEY</Config>
|
||||
<Config Name="Worker Ready Timeout (s)" Target="CLAW_GATEWAY_WORKER_READY_TIMEOUT_SECS" Default="15" Mode="" Description="Seconds to wait for a worker container to pass its health check" Type="Variable" Display="advanced" Required="false" Mask="false">15</Config>
|
||||
<Config Name="Anthropic API Key" Target="ANTHROPIC_API_KEY" Default="" Mode="" Description="Anthropic API key inherited by worker containers for Messages API access" Type="Variable" Display="always" Required="true" Mask="true"></Config>
|
||||
<Config Name="Anthropic OAuth Token" Target="ANTHROPIC_AUTH_TOKEN" Default="" Mode="" Description="Not used by the current gateway deployment because the Anthropic API still requires x-api-key auth" Type="Variable" Display="advanced" Required="false" Mask="true"></Config>
|
||||
<Config Name="Anthropic OAuth Token" Target="ANTHROPIC_AUTH_TOKEN" Default="" Mode="" Description="Claude subscription OAuth token (sk-ant-oat...) inherited by worker containers" Type="Variable" Display="always" Required="false" Mask="true"></Config>
|
||||
<Config Name="Anthropic API Key" Target="ANTHROPIC_API_KEY" Default="" Mode="" Description="Anthropic API key inherited by worker containers for direct API access" Type="Variable" Display="always" Required="false" Mask="true"></Config>
|
||||
<TailscaleStateDir/>
|
||||
</Container>
|
||||
|
||||
@@ -24,6 +24,9 @@ const ALT_REQUEST_ID_HEADER: &str = "x-request-id";
|
||||
const DEFAULT_INITIAL_BACKOFF: Duration = Duration::from_millis(200);
|
||||
const DEFAULT_MAX_BACKOFF: Duration = Duration::from_secs(2);
|
||||
const DEFAULT_MAX_RETRIES: u32 = 2;
|
||||
const CLAUDE_CODE_OAUTH_BETA_HEADER: &str =
|
||||
"claude-code-20250219,oauth-2025-04-20,context-management-2025-06-27";
|
||||
const CLAUDE_CODE_OAUTH_USER_AGENT: &str = "claude-cli/2.1.2 (external, cli)";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AuthSource {
|
||||
@@ -83,6 +86,12 @@ impl AuthSource {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn uses_claude_code_oauth(&self) -> bool {
|
||||
self.bearer_token()
|
||||
.is_some_and(is_claude_code_oauth_token)
|
||||
}
|
||||
|
||||
pub fn apply(&self, mut request_builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
if let Some(api_key) = self.api_key() {
|
||||
request_builder = request_builder.header("x-api-key", api_key);
|
||||
@@ -90,10 +99,22 @@ impl AuthSource {
|
||||
if let Some(token) = self.bearer_token() {
|
||||
request_builder = request_builder.bearer_auth(token);
|
||||
}
|
||||
if self.uses_claude_code_oauth() {
|
||||
request_builder = request_builder
|
||||
.header("anthropic-beta", CLAUDE_CODE_OAUTH_BETA_HEADER)
|
||||
.header("anthropic-dangerous-direct-browser-access", "true")
|
||||
.header("user-agent", CLAUDE_CODE_OAUTH_USER_AGENT)
|
||||
.header("x-app", "cli");
|
||||
}
|
||||
request_builder
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_claude_code_oauth_token(token: &str) -> bool {
|
||||
token.contains("sk-ant-oat")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct OAuthTokenSet {
|
||||
pub access_token: String,
|
||||
@@ -840,7 +861,10 @@ struct AnthropicErrorBody {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ALT_REQUEST_ID_HEADER, REQUEST_ID_HEADER};
|
||||
use super::{
|
||||
ALT_REQUEST_ID_HEADER, CLAUDE_CODE_OAUTH_BETA_HEADER, CLAUDE_CODE_OAUTH_USER_AGENT,
|
||||
REQUEST_ID_HEADER,
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
@@ -1226,7 +1250,7 @@ mod tests {
|
||||
fn auth_source_applies_headers() {
|
||||
let auth = AuthSource::ApiKeyAndBearer {
|
||||
api_key: "test-key".to_string(),
|
||||
bearer_token: "proxy-token".to_string(),
|
||||
bearer_token: "sk-ant-oat01-proxy-token".to_string(),
|
||||
};
|
||||
let request = auth
|
||||
.apply(reqwest::Client::new().post("https://example.test"))
|
||||
@@ -1239,7 +1263,25 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get("authorization").and_then(|v| v.to_str().ok()),
|
||||
Some("Bearer proxy-token")
|
||||
Some("Bearer sk-ant-oat01-proxy-token")
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get("anthropic-beta").and_then(|v| v.to_str().ok()),
|
||||
Some(CLAUDE_CODE_OAUTH_BETA_HEADER)
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get("anthropic-dangerous-direct-browser-access")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get("x-app").and_then(|v| v.to_str().ok()),
|
||||
Some("cli")
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get("user-agent").and_then(|v| v.to_str().ok()),
|
||||
Some(CLAUDE_CODE_OAUTH_USER_AGENT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ use tools::{GlobalToolRegistry, RuntimeToolDefinition};
|
||||
|
||||
const DEFAULT_PROMPT_DATE: &str = "2026-03-31";
|
||||
const APPROVAL_POLL_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const CLAUDE_CODE_OAUTH_SYSTEM_PREFIX: &str =
|
||||
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
||||
|
||||
pub type AllowedToolSet = BTreeSet<String>;
|
||||
|
||||
@@ -175,7 +177,8 @@ impl RuntimeHost {
|
||||
event_tx: UnboundedSender<RuntimeEvent>,
|
||||
) -> Result<(), HostError> {
|
||||
let session = self.load_or_create_session(session_path)?;
|
||||
let system_prompt = build_system_prompt(&self.config.cwd)?;
|
||||
let auth = resolve_cli_auth_source(&self.config.cwd)?;
|
||||
let system_prompt = build_system_prompt(&self.config.cwd, &auth)?;
|
||||
let mut runtime = build_runtime(
|
||||
&self.config,
|
||||
session,
|
||||
@@ -183,6 +186,7 @@ impl RuntimeHost {
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("telegram-session"),
|
||||
auth,
|
||||
)?;
|
||||
let mut observer = BridgeObserver::new(event_tx.clone());
|
||||
let mut prompter = BridgePermissionPrompter::new(
|
||||
@@ -263,9 +267,21 @@ impl From<serde_json::Error> for HostError {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_system_prompt(cwd: &Path) -> Result<Vec<String>, HostError> {
|
||||
load_system_prompt(cwd, DEFAULT_PROMPT_DATE, std::env::consts::OS, "unknown")
|
||||
.map_err(|error| HostError::Other(error.to_string()))
|
||||
fn build_system_prompt(cwd: &Path, auth: &AuthSource) -> Result<Vec<String>, HostError> {
|
||||
let system_prompt = load_system_prompt(cwd, DEFAULT_PROMPT_DATE, std::env::consts::OS, "unknown")
|
||||
.map_err(|error| HostError::Other(error.to_string()))?;
|
||||
Ok(with_claude_code_oauth_prefix(system_prompt, auth))
|
||||
}
|
||||
|
||||
fn with_claude_code_oauth_prefix(mut system_prompt: Vec<String>, auth: &AuthSource) -> Vec<String> {
|
||||
if auth.uses_claude_code_oauth()
|
||||
&& !system_prompt
|
||||
.iter()
|
||||
.any(|segment| segment.contains(CLAUDE_CODE_OAUTH_SYSTEM_PREFIX))
|
||||
{
|
||||
system_prompt.insert(0, CLAUDE_CODE_OAUTH_SYSTEM_PREFIX.to_string());
|
||||
}
|
||||
system_prompt
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -577,6 +593,7 @@ fn build_runtime(
|
||||
config: &RuntimeHostConfig,
|
||||
session: Session,
|
||||
session_id: &str,
|
||||
auth: AuthSource,
|
||||
) -> Result<BuiltRuntime, HostError> {
|
||||
let runtime_plugin_state = build_runtime_plugin_state(&config.cwd)?;
|
||||
let RuntimePluginState {
|
||||
@@ -593,10 +610,11 @@ fn build_runtime(
|
||||
let runtime = ConversationRuntime::new_with_features(
|
||||
session,
|
||||
AnthropicBridgeClient::new(
|
||||
&config.cwd,
|
||||
session_id,
|
||||
auth.clone(),
|
||||
config.model.clone(),
|
||||
config.allowed_tools.clone(),
|
||||
tool_registry.clone(),
|
||||
)?,
|
||||
BridgeToolExecutor::new(
|
||||
config.allowed_tools.clone(),
|
||||
@@ -604,7 +622,7 @@ fn build_runtime(
|
||||
mcp_state.clone(),
|
||||
),
|
||||
policy,
|
||||
build_system_prompt(&config.cwd)?,
|
||||
build_system_prompt(&config.cwd, &auth)?,
|
||||
&feature_config,
|
||||
);
|
||||
Ok(BuiltRuntime::new(runtime, plugin_registry, mcp_state))
|
||||
@@ -833,16 +851,15 @@ struct AnthropicBridgeClient {
|
||||
|
||||
impl AnthropicBridgeClient {
|
||||
fn new(
|
||||
cwd: &Path,
|
||||
session_id: &str,
|
||||
auth: AuthSource,
|
||||
model: String,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
tool_registry: GlobalToolRegistry,
|
||||
) -> Result<Self, HostError> {
|
||||
let runtime_plugin_state = build_runtime_plugin_state(cwd)?;
|
||||
let tool_registry = runtime_plugin_state.tool_registry.clone();
|
||||
Ok(Self {
|
||||
runtime: tokio::runtime::Runtime::new().map_err(HostError::Io)?,
|
||||
client: AnthropicClient::from_auth(resolve_cli_auth_source(cwd)?)
|
||||
client: AnthropicClient::from_auth(auth)
|
||||
.with_base_url(api::read_base_url())
|
||||
.with_prompt_cache(PromptCache::new(session_id)),
|
||||
model,
|
||||
@@ -852,6 +869,30 @@ impl AnthropicBridgeClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod oauth_tests {
|
||||
use super::{with_claude_code_oauth_prefix, AuthSource, CLAUDE_CODE_OAUTH_SYSTEM_PREFIX};
|
||||
|
||||
#[test]
|
||||
fn oauth_system_prefix_is_prepended_for_claude_code_tokens() {
|
||||
let system_prompt = with_claude_code_oauth_prefix(
|
||||
vec!["existing prompt".to_string()],
|
||||
&AuthSource::BearerToken("sk-ant-oat01-test".to_string()),
|
||||
);
|
||||
assert_eq!(system_prompt[0], CLAUDE_CODE_OAUTH_SYSTEM_PREFIX);
|
||||
assert_eq!(system_prompt[1], "existing prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_system_prefix_is_not_duplicated() {
|
||||
let system_prompt = with_claude_code_oauth_prefix(
|
||||
vec![CLAUDE_CODE_OAUTH_SYSTEM_PREFIX.to_string()],
|
||||
&AuthSource::BearerToken("sk-ant-oat01-test".to_string()),
|
||||
);
|
||||
assert_eq!(system_prompt, vec![CLAUDE_CODE_OAUTH_SYSTEM_PREFIX.to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_cli_auth_source(cwd: &Path) -> Result<AuthSource, HostError> {
|
||||
resolve_startup_auth_source(|| {
|
||||
let config = ConfigLoader::default_for(cwd).load().map_err(|error| {
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
<Config Name="CLAW_GATEWAY_TEMPLATE_DIR" Target="CLAW_GATEWAY_TEMPLATE_DIR" Default="/unraid/templates-user" Mode="" Description="Unraid template directory path inside the gateway container" Type="Variable" Display="advanced" Required="true" Mask="false">/unraid/templates-user</Config>
|
||||
<Config Name="CLAW_GATEWAY_TEMPLATE_ARCHIVE_DIR" Target="CLAW_GATEWAY_TEMPLATE_ARCHIVE_DIR" Default="/appdata/template-archive" Mode="" Description="Archive location for removed worker templates" Type="Variable" Display="advanced" Required="true" Mask="false">/appdata/template-archive</Config>
|
||||
<Config Name="CLAW_GATEWAY_WORKER_NETWORK" Target="CLAW_GATEWAY_WORKER_NETWORK" Default="claw_gateway" Mode="" Description="Docker network name shared by the gateway and workers" Type="Variable" Display="advanced" Required="true" Mask="false">claw_gateway</Config>
|
||||
<Config Name="CLAW_GATEWAY_INHERITED_ENV" Target="CLAW_GATEWAY_INHERITED_ENV" Default="ANTHROPIC_API_KEY" Mode="" Description="Comma-separated env vars copied from gateway into worker containers" Type="Variable" Display="advanced" Required="false" Mask="false">ANTHROPIC_API_KEY</Config>
|
||||
<Config Name="CLAW_GATEWAY_INHERITED_ENV" Target="CLAW_GATEWAY_INHERITED_ENV" Default="ANTHROPIC_AUTH_TOKEN,ANTHROPIC_API_KEY" Mode="" Description="Comma-separated env vars copied from gateway into worker containers" Type="Variable" Display="advanced" Required="false" Mask="false">ANTHROPIC_AUTH_TOKEN,ANTHROPIC_API_KEY</Config>
|
||||
<Config Name="CLAW_GATEWAY_WORKER_READY_TIMEOUT_SECS" Target="CLAW_GATEWAY_WORKER_READY_TIMEOUT_SECS" Default="15" Mode="" Description="Seconds to wait for a worker container to pass its health check" Type="Variable" Display="advanced" Required="false" Mask="false">15</Config>
|
||||
<Config Name="ANTHROPIC_API_KEY" Target="ANTHROPIC_API_KEY" Default="" Mode="" Description="Anthropic API key inherited by worker containers for Messages API access" Type="Variable" Display="always" Required="true" Mask="true"></Config>
|
||||
<Config Name="ANTHROPIC_AUTH_TOKEN" Target="ANTHROPIC_AUTH_TOKEN" Default="" Mode="" Description="Not used by the current gateway deployment because the Anthropic API still requires x-api-key auth" Type="Variable" Display="advanced" Required="false" Mask="true"></Config>
|
||||
<Config Name="ANTHROPIC_AUTH_TOKEN" Target="ANTHROPIC_AUTH_TOKEN" Default="" Mode="" Description="Claude subscription OAuth token (sk-ant-oat...) inherited by worker containers" Type="Variable" Display="always" Required="false" Mask="true"></Config>
|
||||
<Config Name="ANTHROPIC_API_KEY" Target="ANTHROPIC_API_KEY" Default="" Mode="" Description="Anthropic API key inherited by worker containers for direct API access" Type="Variable" Display="always" Required="false" Mask="true"></Config>
|
||||
<TailscaleStateDir></TailscaleStateDir>
|
||||
</Container>
|
||||
|
||||
@@ -29,6 +29,8 @@ use tools::{GlobalToolRegistry, RuntimeToolDefinition};
|
||||
|
||||
const DEFAULT_PROMPT_DATE: &str = "2026-03-31";
|
||||
const APPROVAL_POLL_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const CLAUDE_CODE_OAUTH_SYSTEM_PREFIX: &str =
|
||||
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
||||
|
||||
pub type AllowedToolSet = BTreeSet<String>;
|
||||
|
||||
@@ -173,7 +175,8 @@ impl RuntimeHost {
|
||||
event_tx: UnboundedSender<RuntimeEvent>,
|
||||
) -> Result<(), HostError> {
|
||||
let session = self.load_or_create_session(session_path)?;
|
||||
let system_prompt = build_system_prompt(&self.config.cwd)?;
|
||||
let auth = resolve_cli_auth_source(&self.config.cwd)?;
|
||||
let system_prompt = build_system_prompt(&self.config.cwd, &auth)?;
|
||||
let mut runtime = build_runtime(
|
||||
&self.config,
|
||||
session,
|
||||
@@ -181,6 +184,7 @@ impl RuntimeHost {
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("telegram-session"),
|
||||
auth,
|
||||
)?;
|
||||
let mut observer = BridgeObserver::new(event_tx.clone());
|
||||
let mut prompter = BridgePermissionPrompter::new(
|
||||
@@ -261,9 +265,21 @@ impl From<serde_json::Error> for HostError {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_system_prompt(cwd: &Path) -> Result<Vec<String>, HostError> {
|
||||
load_system_prompt(cwd, DEFAULT_PROMPT_DATE, std::env::consts::OS, "unknown")
|
||||
.map_err(|error| HostError::Other(error.to_string()))
|
||||
fn build_system_prompt(cwd: &Path, auth: &AuthSource) -> Result<Vec<String>, HostError> {
|
||||
let system_prompt = load_system_prompt(cwd, DEFAULT_PROMPT_DATE, std::env::consts::OS, "unknown")
|
||||
.map_err(|error| HostError::Other(error.to_string()))?;
|
||||
Ok(with_claude_code_oauth_prefix(system_prompt, auth))
|
||||
}
|
||||
|
||||
fn with_claude_code_oauth_prefix(mut system_prompt: Vec<String>, auth: &AuthSource) -> Vec<String> {
|
||||
if auth.uses_claude_code_oauth()
|
||||
&& !system_prompt
|
||||
.iter()
|
||||
.any(|segment| segment.contains(CLAUDE_CODE_OAUTH_SYSTEM_PREFIX))
|
||||
{
|
||||
system_prompt.insert(0, CLAUDE_CODE_OAUTH_SYSTEM_PREFIX.to_string());
|
||||
}
|
||||
system_prompt
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -575,6 +591,7 @@ fn build_runtime(
|
||||
config: &RuntimeHostConfig,
|
||||
session: Session,
|
||||
session_id: &str,
|
||||
auth: AuthSource,
|
||||
) -> Result<BuiltRuntime, HostError> {
|
||||
let runtime_plugin_state = build_runtime_plugin_state(&config.cwd)?;
|
||||
let RuntimePluginState {
|
||||
@@ -591,10 +608,11 @@ fn build_runtime(
|
||||
let runtime = ConversationRuntime::new_with_features(
|
||||
session,
|
||||
AnthropicBridgeClient::new(
|
||||
&config.cwd,
|
||||
session_id,
|
||||
auth.clone(),
|
||||
config.model.clone(),
|
||||
config.allowed_tools.clone(),
|
||||
tool_registry.clone(),
|
||||
)?,
|
||||
BridgeToolExecutor::new(
|
||||
config.allowed_tools.clone(),
|
||||
@@ -602,7 +620,7 @@ fn build_runtime(
|
||||
mcp_state.clone(),
|
||||
),
|
||||
policy,
|
||||
build_system_prompt(&config.cwd)?,
|
||||
build_system_prompt(&config.cwd, &auth)?,
|
||||
&feature_config,
|
||||
);
|
||||
Ok(BuiltRuntime::new(runtime, plugin_registry, mcp_state))
|
||||
@@ -831,16 +849,15 @@ struct AnthropicBridgeClient {
|
||||
|
||||
impl AnthropicBridgeClient {
|
||||
fn new(
|
||||
cwd: &Path,
|
||||
session_id: &str,
|
||||
auth: AuthSource,
|
||||
model: String,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
tool_registry: GlobalToolRegistry,
|
||||
) -> Result<Self, HostError> {
|
||||
let runtime_plugin_state = build_runtime_plugin_state(cwd)?;
|
||||
let tool_registry = runtime_plugin_state.tool_registry.clone();
|
||||
Ok(Self {
|
||||
runtime: tokio::runtime::Runtime::new().map_err(HostError::Io)?,
|
||||
client: AnthropicClient::from_auth(resolve_cli_auth_source(cwd)?)
|
||||
client: AnthropicClient::from_auth(auth)
|
||||
.with_base_url(api::read_base_url())
|
||||
.with_prompt_cache(PromptCache::new(session_id)),
|
||||
model,
|
||||
@@ -850,6 +867,30 @@ impl AnthropicBridgeClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod oauth_tests {
|
||||
use super::{with_claude_code_oauth_prefix, AuthSource, CLAUDE_CODE_OAUTH_SYSTEM_PREFIX};
|
||||
|
||||
#[test]
|
||||
fn oauth_system_prefix_is_prepended_for_claude_code_tokens() {
|
||||
let system_prompt = with_claude_code_oauth_prefix(
|
||||
vec!["existing prompt".to_string()],
|
||||
&AuthSource::BearerToken("sk-ant-oat01-test".to_string()),
|
||||
);
|
||||
assert_eq!(system_prompt[0], CLAUDE_CODE_OAUTH_SYSTEM_PREFIX);
|
||||
assert_eq!(system_prompt[1], "existing prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_system_prefix_is_not_duplicated() {
|
||||
let system_prompt = with_claude_code_oauth_prefix(
|
||||
vec![CLAUDE_CODE_OAUTH_SYSTEM_PREFIX.to_string()],
|
||||
&AuthSource::BearerToken("sk-ant-oat01-test".to_string()),
|
||||
);
|
||||
assert_eq!(system_prompt, vec![CLAUDE_CODE_OAUTH_SYSTEM_PREFIX.to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_cli_auth_source(cwd: &Path) -> Result<AuthSource, HostError> {
|
||||
resolve_startup_auth_source(|| {
|
||||
let config = ConfigLoader::default_for(cwd).load().map_err(|error| {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user