Watch
1
0
Fork
You've already forked souveraine
0

alpha-readiness: strip LAN IPs, provider-agnostic wizard labels, fix config path and clipboard

- Default API/STT/TTS URLs changed from Casey's LAN to 127.0.0.1
- Wizard labels renamed: "Bifrost URL" → "API Endpoint URL", provider-agnostic
- Config discovery and wizard persistence now use ~/.souveraine/ over CWD
- Clipboard copy: added OSC 52 fallback, visible error feedback on failure
- Cleaned souveraine.example.toml: removed leaked key and personal paths
This commit is contained in:
Fimeg 2026-05-20 09:16:11 -04:00
commit 637e854901
8 changed files with 79 additions and 52 deletions

View file

@ -4,8 +4,8 @@
# === BIFROST INFERENCE ===
[bifrost]
# Bifrost is the OpenAI-compatible API gateway
base_url = "http://10.10.20.120:3360"
api_key = "sk-bf-ae0d5801-9936-4fa9-ac9e-e956ffce6cfa"
base_url = "http://127.0.0.1:3360"
api_key = "" # Set via `souveraine auth set` or BIFROST_KEY env var
virtual_key = "" # x-bf-vk header if required by provider
# Default model for conversation
@ -97,7 +97,7 @@ preserve_recent = 2
[memory]
git_enabled = true
auto_commit = true
base_path = "~/.pi/unified"
# base_path = "~/.souveraine/agents"
# === WEBSOCKET SERVER ===
[websocket]

View file

@ -5,12 +5,10 @@ use tracing::{debug, info, warn};
/// Bifrost Inference Client
///
/// Bifrost is an OpenAI-compatible API gateway: http://10.10.20.120:3360/v1
/// Uses Bearer token auth + optional x-bf-vk header for provider virtual keys.
/// OpenAI format for chat completions + tool calls.
/// Bifrost is an OpenAI-compatible API gateway: http://127.0.0.1:3360/v1
#[derive(Debug, Clone)]
pub struct BifrostClient {
/// Base URL including /v1 (e.g. "http://10.10.20.120:3360/v1")
/// Base URL including /v1 (e.g. "http://127.0.0.1:3360/v1")
base_url: String,
/// Bearer token for auth
api_key: String,
@ -673,7 +671,7 @@ mod tests {
#[test]
fn test_client_creation() {
let client = BifrostClient::new(
"http://10.10.20.120:3360",
"http://127.0.0.1:3360",
"sk-bf-test",
"",
"openai/deepseek-v4-pro",

View file

@ -162,7 +162,7 @@ impl Default for AuthConfig {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BifrostConfig {
/// Bifrost API base URL (e.g. "http://10.10.20.120:3360")
/// Bifrost API base URL (e.g. "http://127.0.0.1:3360")
#[serde(default = "default_bifrost_url")]
pub base_url: String,
@ -803,7 +803,7 @@ impl Default for FederationConfig {
/// trust root for verifying every event the peer sends.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerConfig {
/// Peer's federation endpoint, e.g. `ws://10.10.20.50:8484`.
/// Peer's federation endpoint, e.g. `ws://192.168.1.50:8484`.
pub url: String,
/// Peer's Ed25519 public key, hex-encoded.
pub pubkey: String,
@ -851,8 +851,8 @@ impl Default for VoiceConfig {
}
}
fn default_stt_url() -> String { "http://10.10.20.19:7862".to_string() }
fn default_tts_url() -> String { "http://10.10.20.19:7861".to_string() }
fn default_stt_url() -> String { "http://127.0.0.1:7862".to_string() }
fn default_tts_url() -> String { "http://127.0.0.1:7861".to_string() }
fn default_voice_id() -> String { "en-Soother_woman".to_string() }
fn default_ptt_key() -> String { "Space".to_string() }
@ -862,8 +862,8 @@ impl ConsciousnessConfig {
let candidates = [
"souveraine.toml",
"souveraine.yaml",
"~/.config/souveraine/config.toml",
"~/.config/souveraine/config.yaml",
"~/.souveraine/config.toml",
"~/.souveraine/config.yaml",
];
for path_str in &candidates {
let expanded = shellexpand::tilde(path_str);
@ -915,7 +915,7 @@ fn default_warning_1_threshold() -> f32 { 0.8 }
fn default_warning_2_threshold() -> f32 { 0.95 }
fn default_sub_inter_round_delay() -> u64 { 300 }
fn default_auto_model() -> String { "auto".to_string() }
fn default_bifrost_url() -> String { "http://10.10.20.120:3360".to_string() }
fn default_bifrost_url() -> String { "http://127.0.0.1:3360".to_string() }
fn default_server_bind() -> String { "127.0.0.1".to_string() }
fn default_server_port() -> u16 { 8484 }
fn default_server_url() -> String { "http://127.0.0.1:8484".to_string() }

View file

@ -1,6 +1,6 @@
//! Voice service HTTP clients — STT (Faster-Whisper) and TTS (VibeVoice).
//!
//! Pure HTTP, no audio I/O. Both services run on 10.10.20.19.
//! Pure HTTP, no audio I/O. Both services run on 127.0.0.1 by default.
//!
//! - STT: `POST /transcribe` — multipart form, `audio` field (WAV), returns
//! `{ "text": string, "language"?: string }`.
@ -138,9 +138,9 @@ pub fn clean_text_for_tts(text: &str) -> String {
/// No retry logic — if a request fails, the caller treats the error as a
/// sensorium event (the voice is hoarse; the body has bad days).
pub struct VoiceClient {
/// Faster-Whisper base URL, e.g. `http://10.10.20.19:7862`
/// Faster-Whisper base URL, e.g. `http://127.0.0.1:7862`
stt_url: String,
/// VibeVoice base URL, e.g. `http://10.10.20.19:7861`
/// VibeVoice base URL, e.g. `http://127.0.0.1:7861`
tts_url: String,
/// Voice ID for synthesis, e.g. `en-Soother_woman`
voice: String,

View file

@ -22,7 +22,7 @@ const CONFIG_TEMPLATE: &str = r##"# Souveraine — The world where your agents l
# Generated by `souveraine init`
[bifrost]
base_url = "http://10.10.20.120:3360"
base_url = "http://127.0.0.1:3360"
primary_model = "openai/kimi-k2.6"
# Bearer token for auth (env: BIFROST_KEY)
api_key = ""
@ -1267,8 +1267,8 @@ async fn load_config() -> anyhow::Result<ConsciousnessConfig> {
let config_paths = [
PathBuf::from("souveraine.toml"),
PathBuf::from("souveraine.yaml"),
PathBuf::from("~/.config/souveraine/config.toml"),
PathBuf::from("~/.config/souveraine/config.yaml"),
PathBuf::from("~/.souveraine/config.toml"),
PathBuf::from("~/.souveraine/config.yaml"),
];
for path in &config_paths {

View file

@ -176,7 +176,12 @@ impl App {
}
}
{
let path = self.config_path.clone().unwrap_or_else(|| PathBuf::from("souveraine.toml"));
let path = self.config_path.clone().unwrap_or_else(|| {
let home = dirs::home_dir().unwrap_or_default();
let dir = home.join(".souveraine");
let _ = std::fs::create_dir_all(&dir);
dir.join("config.toml")
});
let cfg = self.config.read().await;
if let Err(e) = cfg.save(&path) {
warn!("setup wizard could not save config to {}: {}", path.display(), e);

View file

@ -767,6 +767,29 @@ impl ChatState {
}
}
/// Copy text to clipboard — tries arboard system clipboard first,
/// falls back to OSC 52 escape sequence (works in tmux, SSH, kitty, etc.).
fn copy_to_clipboard(text: &str) -> bool {
// arboard is the most reliable when desktop clipboard is available
match arboard::Clipboard::new().and_then(|mut c| c.set_text(text.to_string())) {
Ok(()) => return true,
Err(e) => tracing::warn!("arboard clipboard failed, trying OSC 52: {}", e),
}
// OSC 52 escape: \x1b]52;c;{base64}\x07
use std::io::Write;
let b64 = base64::engine::general_purpose::STANDARD.encode(text.as_bytes());
let osc = if std::env::var("TMUX").is_ok() {
// Tmux passthrough: \x1bPtmux;\x1b]52;c;{b64}\x07\x1b\\
format!("\x1bPtmux;\x1b]52;c;{b64}\x07\x1b\\")
} else {
format!("\x1b]52;c;{b64}\x07")
};
let _ = std::io::stdout().write_all(osc.as_bytes()).and_then(|_| std::io::stdout().flush());
// Assume success — OSC 52 either works or silently ignores
true
}
pub fn copy_message_at(&mut self, col: u16, row: u16) -> bool {
let idx = {
let layout = self.msg_layout.borrow();
@ -786,15 +809,16 @@ impl ChatState {
};
let Some(idx) = idx else { return false };
let Some(text) = self.message_copy_text(idx) else { return false };
match arboard::Clipboard::new().and_then(|mut c| c.set_text(text)) {
Ok(()) => {
self.copy_flash = Some(Instant::now());
true
}
Err(e) => {
tracing::warn!("clipboard copy failed: {}", e);
false
}
if Self::copy_to_clipboard(&text) {
self.copy_flash = Some(Instant::now());
true
} else {
self.system_message(
"*[clipboard copy failed — install xclip/wl-clipboard or use a terminal that supports OSC 52]*".to_string(),
);
false
}
}
}

View file

@ -2,7 +2,7 @@
//!
//! Three flows:
//! - `FreshInstall`: no config, no agents — full walkthrough
//! - `ImportAgent`: config exists, but no agents — skip Bifrost, create/import
//! - `ImportAgent`: config exists, but no agents — skip API config, create/import
//! - `FederationSync`: wants to sync from a federation peer (env override)
use tokio::sync::oneshot;
@ -230,7 +230,7 @@ pub enum SetupFlow {
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SetupStep {
Welcome,
BifrostConfig,
ApiConfig,
CreateAgent,
ImportOrFederation,
FederationConfig,
@ -249,7 +249,7 @@ pub struct SetupState {
pub step: SetupStep,
pub complete: bool,
// ── Bifrost fields ──
// ── API connection fields ──
pub bifrost_url: String,
pub bifrost_key: String,
@ -291,7 +291,7 @@ impl SetupState {
flow,
step,
complete: false,
bifrost_url: "http://10.10.20.120:3360".to_string(),
bifrost_url: "http://127.0.0.1:3360".to_string(),
bifrost_key: String::new(),
agent_name: "Souveraine".to_string(),
model_handle: default_model.to_string(),
@ -375,9 +375,9 @@ impl SetupState {
show_submit: true,
hint: None,
},
SetupStep::BifrostConfig => FormState {
SetupStep::ApiConfig => FormState {
slots: vec![
FormSlot::text("Bifrost URL", "http://10.10.20.120:3360", false),
FormSlot::text("API Endpoint URL", "http://127.0.0.1:3360", false),
FormSlot::text("API Key", "", true),
],
focus: 0,
@ -426,8 +426,8 @@ impl SetupState {
pub fn advance(&mut self) {
use SetupStep::*;
self.step = match self.step {
Welcome => BifrostConfig,
BifrostConfig => {
Welcome => ApiConfig,
ApiConfig => {
self.bifrost_url = self.form.slots[0].value();
self.bifrost_key = self.form.slots[1].value();
CreateAgent
@ -453,7 +453,7 @@ impl SetupState {
use SetupStep::*;
self.step = match self.step {
Welcome => return,
BifrostConfig => {
ApiConfig => {
self.bifrost_url = self.form.slots[0].value();
self.bifrost_key = self.form.slots[1].value();
Welcome
@ -462,7 +462,7 @@ impl SetupState {
self.agent_name = self.form.slots[0].value();
self.model_handle = self.form.slots[1].value();
match self.flow {
SetupFlow::FreshInstall => BifrostConfig,
SetupFlow::FreshInstall => ApiConfig,
SetupFlow::ImportAgent => ImportOrFederation,
SetupFlow::FederationSync => FederationConfig,
}
@ -518,7 +518,7 @@ impl SetupState {
// Submit button focused: Enter advances
if matches!(key.code, KeyCode::Enter) && self.form.is_submit_focused() {
match self.step {
SetupStep::BifrostConfig => {
SetupStep::ApiConfig => {
self.bifrost_url = self.form.slots[0].value();
self.bifrost_key = self.form.slots[1].value();
}
@ -592,7 +592,7 @@ impl SetupState {
match self.step {
SetupStep::Welcome => self.draw_welcome(frame, area),
SetupStep::BifrostConfig => self.draw_form(frame, area, "Bifrost Connection"),
SetupStep::ApiConfig => self.draw_form(frame, area, "API Connection"),
SetupStep::CreateAgent => self.draw_create_agent(frame, area),
SetupStep::ImportOrFederation => self.draw_import_screen(frame, area),
SetupStep::FederationConfig => self.draw_form(frame, area, "Federation Sync"),
@ -617,7 +617,7 @@ impl SetupState {
)),
Line::from(""),
Line::from(Span::styled("You'll set up:", Style::default().fg(Color::Gray))),
Line::from(Span::styled(" \u{2022} A Bifrost connection (or go local-only)", Style::default().fg(Color::Gray))),
Line::from(Span::styled(" \u{2022} An API endpoint (or go local-only — models on your machine)", Style::default().fg(Color::Gray))),
Line::from(Span::styled(" \u{2022} Your first agent", Style::default().fg(Color::Gray))),
Line::from(Span::styled(" \u{2022} Optional import from Letta or federation", Style::default().fg(Color::Gray))),
Line::from(""),
@ -632,7 +632,7 @@ impl SetupState {
frame.render_widget(para, centered_rect(area, 60, 60));
}
/// Generic form renderer for simple input forms (Bifrost, Federation).
/// Generic form renderer for simple input forms (API, Federation).
fn draw_form(&self, frame: &mut Frame, area: Rect, title: &str) {
let form_area = centered_rect(area, 50, 50);
let mut lines: Vec<Line> = Vec::new();
@ -724,7 +724,7 @@ impl SetupState {
lines.push(Line::from(Span::styled(format!(" {}{}", mv, extra), Style::default().fg(m_fg))));
if !self.models_fetching {
lines.push(Line::from(Span::styled(
" [r] fetch models from Bifrost",
" [r] fetch models from endpoint",
Style::default().fg(Color::Rgb(80, 120, 140)),
)));
}
@ -833,7 +833,7 @@ impl SetupState {
let has_key = !self.bifrost_key.is_empty();
lines.push(Line::from(Span::styled(format!(" Agent: {}", self.agent_name), Style::default().fg(Color::White))));
lines.push(Line::from(Span::styled(
format!(" Bifrost: {} ({})", self.bifrost_url, if has_key { "key set" } else { "no key \u{2014} local fallback" }),
format!(" API: {} ({})", self.bifrost_url, if has_key { "key set" } else { "no key \u{2014} local fallback" }),
Style::default().fg(Color::Rgb(150, 150, 150)),
)));
lines.push(Line::from(Span::styled(format!(" Model: {}", self.model_handle), Style::default().fg(Color::Rgb(150, 200, 255)))));
@ -869,7 +869,7 @@ impl SetupState {
SetupFlow::FederationSync => 2,
};
let current = match (self.flow, self.step) {
(SetupFlow::FreshInstall, SetupStep::BifrostConfig) => 1,
(SetupFlow::FreshInstall, SetupStep::ApiConfig) => 1,
(SetupFlow::FreshInstall, SetupStep::CreateAgent) => 2,
(SetupFlow::FreshInstall, SetupStep::ImportOrFederation) => 3,
(SetupFlow::FreshInstall, SetupStep::Complete) => 4,
@ -945,7 +945,7 @@ mod tests {
assert!(state.form.show_submit);
assert!(state.form.is_submit_focused());
state.advance();
assert_eq!(state.step, SetupStep::BifrostConfig);
assert_eq!(state.step, SetupStep::ApiConfig);
state.advance();
assert_eq!(state.step, SetupStep::CreateAgent);
state.advance();
@ -971,7 +971,7 @@ mod tests {
#[test]
fn test_text_input() {
let mut state = SetupState::new(SetupFlow::FreshInstall, "kimi-k2.6");
state.advance(); // → BifrostConfig
state.advance(); // → ApiConfig
state.form.focus = 0;
state.form.slots[0].kind = SlotKind::Text {
value: String::new(),
@ -992,13 +992,13 @@ mod tests {
state.advance();
state.advance();
state.go_back();
assert_eq!(state.step, SetupStep::BifrostConfig);
assert_eq!(state.step, SetupStep::ApiConfig);
}
#[test]
fn test_model_picker_cycle() {
let mut state = SetupState::new(SetupFlow::FreshInstall, "kimi-k2.6");
state.advance(); // BifrostConfig
state.advance(); // ApiConfig
state.advance(); // CreateAgent
// Focus is on slot 0 (name). Advance to slot 1 (model picker).