Four features, plus a latent bug fix.
llama.cpp backend. Claude Code only ever speaks the Anthropic Messages
API — confirmed empirically by pointing it at a logging server, which
received POST /v1/messages?beta=true. llama-server implements that
natively (verified in its README, alongside --port default 8080), so
this is a plain base-URL backend with no translation shim, the same
shape as Ollama. Its --api-key defaults to none, so the auth token is a
placeholder Claude Code requires and llama-server ignores.
Model alias fix. ANTHROPIC_DEFAULT_HAIKU_MODEL is documented as "also
used for background functionality", and Triple-C set none of the alias
vars. So on every custom-endpoint backend, Claude Code resolved `haiku`
to an Anthropic model id and sent it to a local server that does not
have it — background features failed silently. All four
ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL vars are now pinned to
the backend's configured model, with an optional Haiku override, and
blanked for Anthropic and Bedrock so those keep Claude Code's defaults.
The deprecated ANTHROPIC_SMALL_FAST_MODEL is never emitted. Existing
Ollama and OpenAI-Compatible containers are recreated once so the new
env reaches them; the snapshot is preserved.
Model gateway. Optional LiteLLM sibling container, off by default,
mirroring stt.rs — this is what makes real OpenAI usable, since
api.openai.com has no /v1/messages. Pinned to v1.96.0 by tag and digest:
the 1.82.7/1.82.8 malware was PyPI-only and never affected the official
images, which is precisely why this builds FROM the image rather than
pip-installing, but 1.84.0 is still the floor for proxy CVEs (API-key
SQLi, Host-header auth bypass, MCP auth bypass). Binds 0.0.0.0 because
project containers consume it, and therefore always sets a master_key —
LiteLLM without one accepts any key. The provider key lives in the OS
keychain and is uploaded into a volume, never an image layer or label.
URL relay. A container-side xdg-open/BROWSER shim opens URLs in the
host's browser. Uses an OSC sequence to /dev/tty rather than a printed
sentinel, because the shim usually runs as a grandchild of a process
capturing its children's output. Degrades to printing the URL when no
terminal is attached, so scheduled tasks do not hang. Only http/https,
with control characters rejected before new URL() — which strips
newlines, so java\nscript: would otherwise parse as javascript:. Nothing
auto-opens; the user confirms. The web terminal shows a tap-to-open
banner instead, since that browser may be a phone across a tunnel.
Browser view. A Project Home tab that watches and takes over the browser
Claude drives with Playwright, using Playwright's own dashboard. Zero
image cost — Playwright stays user-installed. It does not reuse the auth
bridge's PortForward, which binds an unauthenticated port: correct for a
throwaway OAuth listener, wrong for mouse and keyboard control of a
browser in a passwordless-sudo container. Instead a token-gated loopback
proxy checks Host, then token or a forbidden-header origin signal,
before a byte reaches the container. Host ports are confined to
47820..=47827 so CSP frame-src can enumerate them rather than widening
to a wildcard, with a test asserting the two agree.
188 frontend tests, 107 Rust tests, both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
223 lines
6.4 KiB
Rust
223 lines
6.4 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
use super::gateway_settings::GatewaySettings;
|
|
use super::project::{ClaudeCodeSettings, EnvVar};
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_global_instructions() -> Option<String> {
|
|
Some("If the project is not initialized with git, recommend to the user to initialize and use git to track changes. This makes it easier to revert should something break.\n\nUse subagents frequently. For long-running tasks, break the work into parallel subagents where possible. When handling multiple separate tasks, delegate each to its own subagent so they can run concurrently.".to_string())
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ImageSource {
|
|
Registry,
|
|
LocalBuild,
|
|
Custom,
|
|
}
|
|
|
|
impl Default for ImageSource {
|
|
fn default() -> Self {
|
|
Self::Registry
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GlobalAwsSettings {
|
|
#[serde(default)]
|
|
pub aws_config_path: Option<String>,
|
|
#[serde(default)]
|
|
pub aws_profile: Option<String>,
|
|
#[serde(default)]
|
|
pub aws_region: Option<String>,
|
|
#[serde(default)]
|
|
pub default_model_id: Option<String>,
|
|
}
|
|
|
|
impl Default for GlobalAwsSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
aws_config_path: None,
|
|
aws_profile: None,
|
|
aws_region: None,
|
|
default_model_id: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct GlobalOllamaSettings {
|
|
#[serde(default)]
|
|
pub base_url: Option<String>,
|
|
#[serde(default)]
|
|
pub default_model_id: Option<String>,
|
|
/// Global fallback for the `haiku` alias override. Blank means "use the
|
|
/// resolved model id", which is what makes background Claude Code calls
|
|
/// work against a server that only serves one model.
|
|
#[serde(default)]
|
|
pub default_haiku_model_id: Option<String>,
|
|
}
|
|
|
|
/// Global defaults for the llama.cpp (`llama-server`) backend.
|
|
/// Mirrors [`GlobalOllamaSettings`]; used when the per-project field is blank.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct GlobalLlamaCppSettings {
|
|
#[serde(default)]
|
|
pub base_url: Option<String>,
|
|
#[serde(default)]
|
|
pub default_model_id: Option<String>,
|
|
#[serde(default)]
|
|
pub default_haiku_model_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct GlobalOpenAiCompatibleSettings {
|
|
#[serde(default)]
|
|
pub base_url: Option<String>,
|
|
#[serde(default)]
|
|
pub default_model_id: Option<String>,
|
|
#[serde(default)]
|
|
pub default_haiku_model_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AppSettings {
|
|
#[serde(default)]
|
|
pub default_ssh_key_path: Option<String>,
|
|
#[serde(default)]
|
|
pub default_git_user_name: Option<String>,
|
|
#[serde(default)]
|
|
pub default_git_user_email: Option<String>,
|
|
#[serde(default)]
|
|
pub docker_socket_path: Option<String>,
|
|
#[serde(default)]
|
|
pub image_source: ImageSource,
|
|
#[serde(default)]
|
|
pub custom_image_name: Option<String>,
|
|
#[serde(default)]
|
|
pub global_aws: GlobalAwsSettings,
|
|
#[serde(default)]
|
|
pub global_ollama: GlobalOllamaSettings,
|
|
#[serde(default)]
|
|
pub global_llamacpp: GlobalLlamaCppSettings,
|
|
#[serde(default)]
|
|
pub global_openai_compatible: GlobalOpenAiCompatibleSettings,
|
|
#[serde(default = "default_global_instructions")]
|
|
pub global_claude_instructions: Option<String>,
|
|
#[serde(default)]
|
|
pub global_custom_env_vars: Vec<EnvVar>,
|
|
#[serde(default = "default_true")]
|
|
pub auto_check_updates: bool,
|
|
#[serde(default)]
|
|
pub dismissed_update_version: Option<String>,
|
|
#[serde(default)]
|
|
pub timezone: Option<String>,
|
|
#[serde(default)]
|
|
pub default_microphone: Option<String>,
|
|
#[serde(default)]
|
|
pub dismissed_image_digest: Option<String>,
|
|
#[serde(default)]
|
|
pub web_terminal: WebTerminalSettings,
|
|
#[serde(default)]
|
|
pub stt: SttSettings,
|
|
#[serde(default)]
|
|
pub gateway: GatewaySettings,
|
|
#[serde(default)]
|
|
pub global_claude_code_settings: Option<ClaudeCodeSettings>,
|
|
}
|
|
|
|
fn default_stt_model() -> String {
|
|
"tiny".to_string()
|
|
}
|
|
|
|
fn default_stt_port() -> u16 {
|
|
9876
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SttSettings {
|
|
#[serde(default)]
|
|
pub enabled: bool,
|
|
#[serde(default = "default_stt_model")]
|
|
pub model: String,
|
|
#[serde(default = "default_stt_port")]
|
|
pub port: u16,
|
|
#[serde(default)]
|
|
pub language: Option<String>,
|
|
}
|
|
|
|
impl Default for SttSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: false,
|
|
model: default_stt_model(),
|
|
port: 9876,
|
|
language: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SttStatus {
|
|
pub container_exists: bool,
|
|
pub running: bool,
|
|
pub port: u16,
|
|
pub model: String,
|
|
pub image_exists: bool,
|
|
}
|
|
|
|
fn default_web_terminal_port() -> u16 {
|
|
7681
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WebTerminalSettings {
|
|
#[serde(default)]
|
|
pub enabled: bool,
|
|
#[serde(default = "default_web_terminal_port")]
|
|
pub port: u16,
|
|
#[serde(default)]
|
|
pub access_token: Option<String>,
|
|
}
|
|
|
|
impl Default for WebTerminalSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: false,
|
|
port: 7681,
|
|
access_token: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for AppSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
default_ssh_key_path: None,
|
|
default_git_user_name: None,
|
|
default_git_user_email: None,
|
|
docker_socket_path: None,
|
|
image_source: ImageSource::default(),
|
|
custom_image_name: None,
|
|
global_aws: GlobalAwsSettings::default(),
|
|
global_ollama: GlobalOllamaSettings::default(),
|
|
global_llamacpp: GlobalLlamaCppSettings::default(),
|
|
global_openai_compatible: GlobalOpenAiCompatibleSettings::default(),
|
|
global_claude_instructions: default_global_instructions(),
|
|
global_custom_env_vars: Vec::new(),
|
|
auto_check_updates: true,
|
|
dismissed_update_version: None,
|
|
timezone: None,
|
|
default_microphone: None,
|
|
dismissed_image_digest: None,
|
|
web_terminal: WebTerminalSettings::default(),
|
|
stt: SttSettings::default(),
|
|
gateway: GatewaySettings::default(),
|
|
global_claude_code_settings: None,
|
|
}
|
|
}
|
|
}
|