Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ members = [
"crates/extensions/rara-git",
"crates/soul",
"crates/symphony",
"crates/rara-vault",
"crates/rara-dock",
"crates/rara-acp",
"crates/drivers/browser",
Expand Down Expand Up @@ -270,7 +269,6 @@ rara-sessions = { path = "crates/sessions" }
rara-skills = { path = "crates/skills" }
rara-soul = { path = "crates/soul" }
rara-symphony = { path = "crates/symphony" }
rara-vault = { path = "crates/rara-vault" }

# fff (Fast File Finder) — Cargo git dependencies (not vendored)
fff-search = { git = "https://github.com/dmtrKovalenko/fff.nvim", default-features = false }
Expand Down
2 changes: 2 additions & 0 deletions crates/app/src/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub struct PlatformBindingConfig {
pub(crate) async fn boot(
pool: sqlx::SqlitePool,
settings_provider: Arc<dyn rara_domain_shared::settings::SettingsProvider>,
settings_svc: Arc<rara_backend_admin::settings::SettingsSvc>,
users: &[UserConfig],
browser_manager: Option<rara_browser::BrowserManagerRef>,
) -> Result<BootResult, Whatever> {
Expand Down Expand Up @@ -214,6 +215,7 @@ pub(crate) async fn boot(
&mut tool_registry,
crate::tools::ToolDeps {
settings: settings_provider.clone(),
settings_svc: settings_svc.clone(),
composio_auth_provider,
skill_registry: skill_registry.clone(),
mcp_manager: mcp_manager.clone(),
Expand Down
8 changes: 4 additions & 4 deletions crates/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,9 @@ pub async fn start_with_options(
.whatever_context("Failed to initialize infrastructure services")?;
let pool = db_store.pool().clone();

let settings_svc =
rara_backend_admin::settings::SettingsSvc::load(db_store.kv_store(), pool.clone())
.await
.whatever_context("Failed to initialize runtime settings")?;
let settings_svc = rara_backend_admin::settings::SettingsSvc::load(pool.clone())
.await
.whatever_context("Failed to initialize runtime settings")?;

let settings_provider: Arc<dyn rara_domain_shared::settings::SettingsProvider> =
Arc::new(settings_svc.clone());
Expand Down Expand Up @@ -363,6 +362,7 @@ pub async fn start_with_options(
let rara = crate::boot::boot(
pool.clone(),
settings_provider.clone(),
Arc::new(settings_svc.clone()),
&config.users,
browser_manager,
)
Expand Down
3 changes: 2 additions & 1 deletion crates/app/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ pub fn rara_tool_names() -> Vec<rara_kernel::tool::ToolName> {
/// Dependencies required to construct all tools.
pub struct ToolDeps {
pub settings: Arc<dyn rara_domain_shared::settings::SettingsProvider>,
pub settings_svc: Arc<rara_backend_admin::settings::SettingsSvc>,
pub composio_auth_provider: Arc<dyn rara_composio::ComposioAuthProvider>,
pub skill_registry: rara_skills::registry::InMemoryRegistry,
pub mcp_manager: rara_mcp::manager::mgr::McpManager,
Expand Down Expand Up @@ -194,7 +195,7 @@ pub fn register_all(registry: &mut ToolRegistry, deps: ToolDeps) -> ToolRegistra
Arc::new(SendEmailTool::new(deps.settings.clone())),
Arc::new(SendFileTool::new()),
Arc::new(SetAvatarTool::new(deps.settings.clone())),
Arc::new(SettingsTool::new(deps.settings.clone())),
Arc::new(SettingsTool::new(deps.settings_svc.clone())),
// Skill tools
Arc::new(ListSkillsTool::new(deps.skill_registry.clone())),
Arc::new(CreateSkillTool::new(deps.skill_registry.clone())),
Expand Down
107 changes: 91 additions & 16 deletions crates/app/src/tools/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use std::sync::Arc;

use async_trait::async_trait;
use rara_backend_admin::settings::SettingsSvc;
use rara_domain_shared::settings::SettingsProvider;
use rara_kernel::tool::{ToolContext, ToolExecute};
use rara_tool_macro::ToolDef;
Expand All @@ -26,29 +27,60 @@ use serde_json::{Value, json};

const SENSITIVE_FRAGMENTS: &[&str] = &["api_key", "token", "password", "secret"];

/// Number of version entries returned by the history action.
const TOOL_HISTORY_LIMIT: i64 = 20;

/// Number of leading characters shown before masking a sensitive value.
const MASK_VISIBLE_LEN: usize = 6;

/// Available actions for the settings tool.
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum SettingsAction {
/// List all settings.
List,
/// Get a single setting by key.
Get,
/// Set a single setting by key.
Set,
/// Show the current settings version number.
Version,
/// Show recent version history.
History,
/// Show a point-in-time snapshot at a given version.
Snapshot,
/// Rollback settings to a given version.
Rollback,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct SettingsParams {
/// The action to perform: list, get, or set.
action: String,
/// The setting key (required for get and set).
key: Option<String>,
/// The action to perform.
action: SettingsAction,
/// The setting key (required for get/set).
key: Option<String>,
/// The value to set (required for set).
value: Option<String>,
value: Option<String>,
/// The version number (required for snapshot/rollback).
version: Option<i64>,
}

/// Agent tool that reads and modifies runtime settings.
#[derive(ToolDef)]
#[tool(
name = "settings",
description = "Read and modify runtime settings. Use 'list' to see all settings, 'get' to \
read a specific key, 'set' to update a value.",
description = "Read and modify runtime settings. Actions: 'list' to see all, 'get' to read a \
key, 'set' to update a value, 'version' for current version, 'history' for \
recent changes, 'snapshot' for point-in-time view, 'rollback' to revert to a \
version.",
tier = "deferred"
)]
pub struct SettingsTool {
settings: Arc<dyn SettingsProvider>,
settings: Arc<SettingsSvc>,
}
impl SettingsTool {
pub fn new(settings: Arc<dyn SettingsProvider>) -> Self { Self { settings } }
/// Create a new settings tool backed by the MVCC settings service.
pub fn new(settings: Arc<SettingsSvc>) -> Self { Self { settings } }
}

#[async_trait]
Expand All @@ -57,8 +89,8 @@ impl ToolExecute for SettingsTool {
type Params = SettingsParams;

async fn run(&self, params: SettingsParams, _context: &ToolContext) -> anyhow::Result<Value> {
match params.action.as_str() {
"list" => {
match params.action {
SettingsAction::List => {
let all = self.settings.list().await;
let masked: serde_json::Map<String, Value> = all
.into_iter()
Expand All @@ -69,7 +101,7 @@ impl ToolExecute for SettingsTool {
.collect();
Ok(json!({"settings": masked}))
}
"get" => {
SettingsAction::Get => {
let key = params
.key
.as_deref()
Expand All @@ -79,7 +111,7 @@ impl ToolExecute for SettingsTool {
None => Ok(json!({"key": key, "value": null})),
}
}
"set" => {
SettingsAction::Set => {
let key = params
.key
.as_deref()
Expand All @@ -91,7 +123,47 @@ impl ToolExecute for SettingsTool {
self.settings.set(key, value).await?;
Ok(json!({"key": key, "updated": true}))
}
other => Ok(json!({"error": format!("unknown action: {other}")})),
SettingsAction::Version => {
let ver = self.settings.current_version().await?;
Ok(json!({"version": ver}))
}
SettingsAction::History => {
let entries = self.settings.list_versions(TOOL_HISTORY_LIMIT).await?;
let masked: Vec<Value> = entries
.into_iter()
.map(|e| {
let masked_val = e.value.as_deref().map(|v| maybe_mask(&e.key, v));
json!({
"version": e.version,
"key": e.key,
"value": masked_val,
"changed_at": e.changed_at,
})
})
.collect();
Ok(json!({"versions": masked}))
}
SettingsAction::Snapshot => {
let ver = params
.version
.ok_or_else(|| anyhow::anyhow!("missing required parameter: version"))?;
let snap = self.settings.snapshot(ver).await?;
let masked: serde_json::Map<String, Value> = snap
.into_iter()
.map(|(k, v)| {
let display = maybe_mask(&k, &v);
(k, Value::String(display))
})
.collect();
Ok(json!({"version": ver, "settings": masked}))
}
SettingsAction::Rollback => {
let ver = params
.version
.ok_or_else(|| anyhow::anyhow!("missing required parameter: version"))?;
let new_ver = self.settings.rollback_to(ver).await?;
Ok(json!({"rolled_back_to": ver, "new_version": new_ver}))
}
}
}
}
Expand All @@ -102,10 +174,13 @@ fn maybe_mask(key: &str, value: &str) -> String {
.iter()
.any(|frag| key_lower.contains(frag));
if is_sensitive {
if value.len() < 6 {
// Use char iterator to avoid panic on multi-byte UTF-8 values.
let char_count = value.chars().count();
if char_count < MASK_VISIBLE_LEN {
"****".to_owned()
} else {
format!("{}****", &value[..6])
let prefix: String = value.chars().take(MASK_VISIBLE_LEN).collect();
format!("{prefix}****")
}
} else {
value.to_owned()
Expand Down
18 changes: 15 additions & 3 deletions crates/extensions/backend-admin/AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Unified HTTP admin routes for all backend subsystems — settings management, mo

### Key modules

- `src/settings/` — Runtime settings CRUD backed by a KV store. `SettingsSvc` loads settings at startup and provides a `SettingsProvider` trait implementation with change notifications via `watch::Receiver`.
- `src/settings/` — Runtime settings CRUD with MVCC versioning. `SettingsSvc` stores settings in a `settings_version` table (append-only log) where every mutation bumps a global version counter. Provides a `SettingsProvider` trait implementation with change notifications via `watch::Receiver`.
- `service.rs` — `SettingsSvc` backed directly by `SqlitePool` (no KVStore dependency). Public API: `get()`, `set()`, `delete()`, `batch_update()`, `current_version()`, `snapshot(version)`, `list_versions(limit)`, `rollback_to(version)`.
- `router.rs` — Axum routes under `/api/v1/settings/`. Version endpoints under `/api/v1/settings/versions/` for listing versions, getting current version, snapshots, and rollback.
- `src/chat/` — Chat and session HTTP endpoints (list sessions, send messages, stream responses).
- `src/kernel/` — Kernel control routes (agent info, execution traces, debug endpoints).
- `src/agents/` — Agent manifest listing and management routes.
Expand All @@ -24,20 +26,30 @@ Unified HTTP admin routes for all backend subsystems — settings management, mo
2. `state.routes()` returns an Axum router with all admin endpoints merged.
3. Routes are mounted into the main HTTP server by `rara-app`.

### Settings MVCC model

- The `settings_version` table is an append-only log. Each row contains a version number, the full settings snapshot (JSON), and a timestamp.
- Writes (`set`, `delete`, `batch_update`) read the current snapshot, apply the mutation, bump the version, and append a new row — all within a single SQLite transaction.
- Rollback is **forward-only**: `rollback_to(v)` reads the snapshot at version `v` and appends it as a new version. History is never rewritten.
- `SettingsSvc` depends only on `SqlitePool` — it does NOT use `KVStore` or any external store abstraction.

## Critical Invariants

- `SettingsSvc` is the single source of truth for runtime-mutable settings (LLM keys, Telegram tokens, etc.).
- Settings changes are broadcast via `tokio::sync::watch` — subscribers get notified of all changes.
- Admin routes should not bypass `SettingsSvc` to read/write settings directly in the KV store.
- Admin routes should not bypass `SettingsSvc` to read/write settings directly in the database.
- Every settings mutation MUST go through the versioned write path — never insert directly into `settings_version`.

## What NOT To Do

- Do NOT put repository implementations in this crate — it provides HTTP routes, not data access.
- Do NOT hardcode settings values — all mutable config goes through `SettingsSvc`.
- Do NOT duplicate route paths — each subsystem owns its own `/api/v1/<domain>/` namespace.
- Do NOT delete rows from `settings_version` — the table is append-only by design.
- Do NOT bypass `SettingsSvc` to write settings directly to SQLite — this breaks version consistency and watch notifications.

## Dependencies

**Upstream:** `rara-kernel` (for `KernelHandle`, session/tape types), `rara-skills`, `rara-mcp`, `yunara-store` (KV store), `axum`.
**Upstream:** `rara-kernel` (for `KernelHandle`, session/tape types), `rara-skills`, `rara-mcp`, `axum`, `sqlx`.

**Downstream:** `rara-app` (mounts routes into the HTTP server).
Loading