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:
parent
0792ece0ce
commit
637e854901
8 changed files with 79 additions and 52 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue