Skip to content

Commit b2b3b38

Browse files
committed
backend — runtime, sentinel, AI adapters, connectors, safety
Add springtale-runtime crate: shared init, state, and operations layer used by both springtaled and the Tauri desktop app. Unifies connector registry, rule engine, AI adapter, sentinel, and canvas into one boot sequence. springtale-sentinel (new): - Circuit breaker, rate limiter, toxic pair detection, dead man switch - Impact scoring, verdict system, audit trail with export - Store migrations (003_sentinel, 004_safety, 005_formations) springtale-ai adapters: - Ollama, OpenAI, Anthropic typed API clients with rustls-tls - NL→Rule parser (prompt templates + rule generation) - Voice adapter stubs (STT/TTS traits) - Adapter factory with config-driven selection 6 new connectors (Phase 2a chat + social): - connector-discord: gateway, 5 actions, message/reaction triggers - connector-slack: RTM gateway, 6 actions, message/reaction triggers - connector-irc: raw TCP gateway, 5 actions, message/join triggers - connector-nostr: relay gateway, 4 actions, note/DM triggers - connector-signal: signal-cli gateway, 3 actions, message triggers - connector-browser: headless browser, 5 actions (navigate/click/fill/extract/screenshot) Connector factory system: - ConnectorFactory trait + inventory-based auto-registration - Factory modules for all 13 connectors (7 existing + 6 new) springtale-bot extensions: - Cooperation framework (16 modules from COOPERATION.pdf) - Orchestrator with recursive pipelines, sub-agents, fuel system - AT Protocol bridge stub Safety & vault: - Duress passphrase, vault backup, panic wipe - HttpTransport with TLS server - Canvas typed block system - Heartbeat monitor - In-memory store backend for testing - Formations store + operations CLI + daemon API expansions: - 7 new CLI commands (vault, panic, travel, agent, memory, crypto, data) - 8 new API endpoints (canvas, formations, sessions, safety, config, SSE streams) - Runtime connector factory integration in springtaled boot
1 parent 614d33b commit b2b3b38

296 files changed

Lines changed: 23263 additions & 584 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 1250 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ members = [
1212
# Phase 1b
1313
"crates/springtale-bot",
1414
# Phase 2a
15-
# "crates/springtale-sentinel",
15+
"crates/springtale-sentinel",
16+
# Shared runtime
17+
"crates/springtale-runtime",
1618
# Phase 1a connectors
1719
"connectors/connector-kick",
1820
"connectors/connector-presearch",
@@ -23,10 +25,26 @@ members = [
2325
"connectors/connector-http",
2426
# Phase 1b connectors
2527
"connectors/connector-telegram",
28+
# Phase 2a connectors
29+
"connectors/connector-nostr",
30+
"connectors/connector-irc",
31+
"connectors/connector-discord",
32+
"connectors/connector-slack",
33+
"connectors/connector-signal",
34+
"connectors/connector-browser",
35+
# connector-matrix: DEFERRED — matrix-sdk 0.16 requires rusqlite 0.37 which
36+
# has CVE-2025-70873 (heap info disclosure). Our store uses rusqlite 0.39
37+
# (patched). Downgrading would expose activist/survivor data to heap leaks.
38+
# Waiting for matrix-sdk to update to rusqlite 0.39+.
2639
# Apps
2740
"apps/springtaled",
2841
"apps/springtale-cli",
2942
]
43+
exclude = [
44+
# Tauri desktop app has its own dependency tree (tauri 2.x crates).
45+
# Excluded from workspace to avoid version conflicts.
46+
"tauri/apps/desktop/src-tauri",
47+
]
3048

3149
[workspace.package]
3250
edition = "2024"
@@ -38,6 +56,7 @@ repository = "https://github.com/ScopeCreep-zip/Springtale"
3856
# ── Async Runtime ──────────────────────────────────────────────────────────────
3957
tokio = { version = "1", features = ["full"] }
4058
tokio-util = { version = "0.7", features = ["codec"] }
59+
tokio-stream = { version = "0.1", features = ["sync"] }
4160
async-trait = "0.1"
4261

4362
# ── Observability ──────────────────────────────────────────────────────────────
@@ -75,11 +94,13 @@ uuid = { version = "1", features = ["v4", "serde"] }
7594

7695
# ── HTTP Server ────────────────────────────────────────────────────────────────
7796
axum = { version = "0.8", features = ["macros", "multipart"] }
97+
axum-server = { version = "0.8", features = ["tls-rustls"] }
7898
tower = { version = "0.5", features = ["limit", "buffer"] }
79-
tower-http = { version = "0.6", features = ["cors", "trace", "auth", "limit", "timeout"] }
99+
tower-http = { version = "0.6", features = ["cors", "fs", "set-header", "trace", "auth", "limit", "timeout"] }
100+
rust-embed = { version = "8", features = ["interpolate-folder-path"] }
80101

81102
# ── HTTP Client ────────────────────────────────────────────────────────────────
82-
reqwest = { version = "0.13", default-features = false, features = ["json", "rustls", "stream"] }
103+
reqwest = { version = "0.13", default-features = false, features = ["json", "multipart", "rustls", "stream"] }
83104

84105
# ── Database (local-first) ─────────────────────────────────────────────────────
85106
rusqlite = { version = "0.39", features = ["bundled", "vtab"] }
@@ -116,9 +137,18 @@ futures-util = { version = "0.3", features = ["sink"] }
116137
# ── Regex ──────────────────────────────────────────────────────────────────────
117138
regex = "1"
118139

140+
# ── TLS ────────────────────────────────────────────────────────────────────────
141+
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
142+
tokio-rustls = "0.26"
143+
rustls-pemfile = "2"
144+
rustls-pki-types = "1"
145+
119146
# ── Config ─────────────────────────────────────────────────────────────────────
120147
figment = { version = "0.10", features = ["toml", "env"] }
121148

149+
# ── Compile-time Registration ─────────────────────────────────────────────────
150+
inventory = "0.3"
151+
122152
# ── Concurrency ────────────────────────────────────────────────────────────────
123153
dashmap = "6"
124154

@@ -143,4 +173,42 @@ springtale-store = { path = "crates/springtale-store" }
143173
springtale-ai = { path = "crates/springtale-ai" }
144174
springtale-bot = { path = "crates/springtale-bot" }
145175
springtale-mcp = { path = "crates/springtale-mcp" }
176+
springtale-sentinel = { path = "crates/springtale-sentinel" }
177+
springtale-runtime = { path = "crates/springtale-runtime" }
178+
connector-kick = { path = "connectors/connector-kick" }
179+
connector-presearch = { path = "connectors/connector-presearch" }
180+
connector-bluesky = { path = "connectors/connector-bluesky" }
181+
connector-github = { path = "connectors/connector-github" }
182+
connector-filesystem = { path = "connectors/connector-filesystem" }
183+
connector-shell = { path = "connectors/connector-shell" }
184+
connector-http = { path = "connectors/connector-http" }
146185
connector-telegram = { path = "connectors/connector-telegram" }
186+
connector-nostr = { path = "connectors/connector-nostr" }
187+
connector-irc = { path = "connectors/connector-irc" }
188+
connector-discord = { path = "connectors/connector-discord" }
189+
connector-slack = { path = "connectors/connector-slack" }
190+
connector-signal = { path = "connectors/connector-signal" }
191+
connector-browser = { path = "connectors/connector-browser" }
192+
193+
# ── Nostr Protocol ─────────────────────────────────────────────────────────────
194+
nostr-sdk = { version = "0.44", features = ["nip44", "nip59"] }
195+
196+
# ── IRC Protocol ───────────────────────────────────────────────────────────────
197+
# Privacy: ctcp feature DISABLED to suppress all CTCP replies (VERSION, TIME, PING).
198+
# This prevents bot fingerprinting — empty VERSION is MORE identifiable than no response.
199+
irc = { version = "1.1", default-features = false, features = ["tls-rust", "toml_config"] }
200+
201+
# ── Discord Protocol ──────────────────────────────────────────────────────────
202+
# Privacy: no MESSAGE_CONTENT intent by default. Slash commands preferred.
203+
twilight-gateway = { version = "0.17", default-features = false, features = ["rustls-webpki-roots", "zstd"] }
204+
twilight-http = { version = "0.17", default-features = false, features = ["rustls-webpki-roots"] }
205+
twilight-model = "0.17"
206+
207+
# ── Slack Protocol (Socket Mode) ──────────────────────────────────────────────
208+
# Socket Mode uses WebSocket — no public HTTP endpoint needed (local-first friendly).
209+
tokio-tungstenite = { version = "0.26", default-features = false, features = ["connect", "rustls-tls-webpki-roots"] }
210+
211+
# ── Browser Automation ────────────────────────────────────────────────────────
212+
# No TLS dependency — connects to pre-installed Chrome via DevTools Protocol.
213+
# Domain allow-list enforced per connector capability model.
214+
chromiumoxide = { version = "0.9", default-features = false }

apps/springtale-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ springtale-core = { workspace = true }
2525
springtale-crypto = { workspace = true }
2626
springtale-connector = { workspace = true }
2727
springtale-store = { workspace = true }
28+
springtale-runtime = { workspace = true }

apps/springtale-cli/src/cli.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,61 @@ pub enum Command {
4040
},
4141
/// Initialize Springtale (create data directory, vault, config).
4242
Init,
43+
/// Emergency data destruction — overwrites vault + database with random bytes.
44+
/// NO confirmation prompt. This is for emergencies (IPV, device seizure).
45+
Panic,
46+
/// Travel mode — encrypted backup + local wipe, or restore from backup.
47+
Travel {
48+
#[command(subcommand)]
49+
action: TravelAction,
50+
},
51+
/// Vault management — duress passphrase setup.
52+
Vault {
53+
#[command(subcommand)]
54+
action: VaultAction,
55+
},
56+
/// Bot memory inspection and maintenance.
57+
Memory {
58+
#[command(subcommand)]
59+
action: MemoryAction,
60+
},
61+
/// Data export and purge.
62+
Data {
63+
#[command(subcommand)]
64+
action: DataAction,
65+
},
66+
/// Agent configuration.
67+
Agent {
68+
#[command(subcommand)]
69+
action: AgentAction,
70+
},
71+
/// Cryptographic operations.
72+
Crypto {
73+
#[command(subcommand)]
74+
action: CryptoAction,
75+
},
76+
}
77+
78+
#[derive(Subcommand)]
79+
pub enum TravelAction {
80+
/// Export encrypted backup and wipe local data.
81+
Prepare {
82+
/// Path to save the encrypted backup file.
83+
#[arg(long)]
84+
backup_to: std::path::PathBuf,
85+
},
86+
/// Restore data from an encrypted backup.
87+
Restore {
88+
/// Path to the encrypted backup file.
89+
#[arg(long)]
90+
from: std::path::PathBuf,
91+
},
92+
}
93+
94+
#[derive(Subcommand)]
95+
pub enum VaultAction {
96+
/// Configure a duress passphrase (dual-region vault).
97+
DuressSetup,
4398
}
4499

45100
#[derive(Subcommand)]
@@ -87,10 +142,66 @@ pub enum RuleAction {
87142
/// Rule ID.
88143
id: String,
89144
},
145+
/// Delete a rule by ID.
146+
Delete {
147+
/// Rule ID.
148+
id: String,
149+
},
150+
/// Update a rule from a JSON or TOML file.
151+
Update {
152+
/// Rule ID to update.
153+
id: String,
154+
/// Path to the updated rule definition file.
155+
file: std::path::PathBuf,
156+
},
90157
}
91158

92159
#[derive(Subcommand)]
93160
pub enum ServerAction {
94161
/// Start springtaled inline.
95162
Start,
96163
}
164+
165+
#[derive(Subcommand)]
166+
pub enum MemoryAction {
167+
/// Inspect bot memory — list sessions and entry counts.
168+
Audit,
169+
/// Force memory compaction — delete oldest entries beyond limit.
170+
Compact {
171+
/// Maximum entries per session (default: 100).
172+
#[arg(long, default_value = "100")]
173+
max_entries: usize,
174+
},
175+
}
176+
177+
#[derive(Subcommand)]
178+
pub enum DataAction {
179+
/// Export all user data to a JSON file.
180+
Export {
181+
/// Output file path (default: stdout).
182+
#[arg(long)]
183+
output: Option<std::path::PathBuf>,
184+
/// Encrypt the export file.
185+
#[arg(long)]
186+
encrypt: bool,
187+
},
188+
/// Delete all user data (rules, events, sessions, memory) without destroying the vault.
189+
Purge,
190+
}
191+
192+
#[derive(Subcommand)]
193+
pub enum AgentAction {
194+
/// Set an agent's autonomy level (observe, suggest, act-with-approval, act-autonomously).
195+
SetAutonomy {
196+
/// Agent name.
197+
name: String,
198+
/// Autonomy level.
199+
level: String,
200+
},
201+
}
202+
203+
#[derive(Subcommand)]
204+
pub enum CryptoAction {
205+
/// Re-encrypt the vault with a new passphrase.
206+
RotateVaultKey,
207+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use anyhow::Result;
2+
use springtale_store::backend::sqlite::SqliteBackend;
3+
4+
use crate::cli::AgentAction;
5+
6+
/// Handle agent subcommands.
7+
pub async fn run(action: AgentAction, store: &SqliteBackend) -> Result<()> {
8+
match action {
9+
AgentAction::SetAutonomy { name, level } => {
10+
springtale_runtime::operations::agent::set_autonomy(store, &name, &level)
11+
.await
12+
.map_err(|e| anyhow::anyhow!("{e}"))?;
13+
println!("Agent '{name}' autonomy set to: {level}");
14+
}
15+
}
16+
Ok(())
17+
}

apps/springtale-cli/src/commands/connector.rs

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
use anyhow::Result;
22
use tabled::{Table, Tabled};
33

4-
use springtale_connector::ConnectorManifest;
54
use springtale_store::backend::sqlite::SqliteBackend;
6-
use springtale_store::backend::trait_::StorageBackend;
7-
use springtale_store::schema::connectors::ConnectorRow;
85

96
use crate::cli::ConnectorAction;
107
use crate::output;
@@ -24,7 +21,10 @@ struct ConnectorTableRow {
2421
pub async fn run(action: ConnectorAction, store: &SqliteBackend, json: bool) -> Result<()> {
2522
match action {
2623
ConnectorAction::List => {
27-
let connectors = store.list_connectors().await?;
24+
let connectors =
25+
springtale_runtime::operations::connectors::list_connectors_from_store(store)
26+
.await
27+
.map_err(|e| anyhow::anyhow!("{e}"))?;
2828

2929
if json {
3030
output::print_json(&connectors)?;
@@ -44,49 +44,42 @@ pub async fn run(action: ConnectorAction, store: &SqliteBackend, json: bool) ->
4444
}
4545
}
4646
ConnectorAction::Enable { name } => {
47-
store.set_connector_enabled(&name, true).await?;
47+
springtale_runtime::operations::connectors::enable_connector_in_store(store, &name)
48+
.await
49+
.map_err(|e| anyhow::anyhow!("{e}"))?;
4850
println!("Enabled connector: {name}");
4951
}
5052
ConnectorAction::Disable { name } => {
51-
store.set_connector_enabled(&name, false).await?;
53+
springtale_runtime::operations::connectors::disable_connector_in_store(store, &name)
54+
.await
55+
.map_err(|e| anyhow::anyhow!("{e}"))?;
5256
println!("Disabled connector: {name}");
5357
}
5458
ConnectorAction::Remove { name } => {
55-
store.remove_connector(&name).await?;
59+
springtale_runtime::operations::connectors::remove_connector_from_store(store, &name)
60+
.await
61+
.map_err(|e| anyhow::anyhow!("{e}"))?;
5662
println!("Removed connector: {name}");
5763
}
5864
ConnectorAction::Install { path } => {
5965
let contents = std::fs::read_to_string(&path).map_err(|e| {
6066
anyhow::anyhow!("failed to read manifest at {}: {e}", path.display())
6167
})?;
62-
let manifest: ConnectorManifest = toml::from_str(&contents)
68+
let manifest: springtale_connector::ConnectorManifest = toml::from_str(&contents)
6369
.map_err(|e| anyhow::anyhow!("failed to parse manifest TOML: {e}"))?;
6470

65-
// Validate manifest structure
66-
springtale_connector::manifest::verify::verify_manifest(&manifest)
67-
.map_err(|e| anyhow::anyhow!("manifest validation failed: {e}"))?;
68-
6971
if manifest.signature.is_some() {
7072
println!(
7173
" Note: manifest has signature — verification requires author key registry (Phase 2)"
7274
);
7375
}
7476

75-
let manifest_json = serde_json::to_string(&manifest)
76-
.map_err(|e| anyhow::anyhow!("failed to serialize manifest to JSON: {e}"))?;
77-
78-
let row = ConnectorRow {
79-
name: manifest.name.clone(),
80-
version: manifest.version.clone(),
81-
author: manifest.author.clone(),
82-
description: manifest.description.clone(),
83-
manifest_json,
84-
enabled: true,
85-
installed_at: chrono::Utc::now(),
86-
};
87-
88-
store.register_connector(&row).await?;
89-
println!("Installed connector: {}", manifest.name);
77+
let name = springtale_runtime::operations::connectors::install_connector_to_store(
78+
store, manifest,
79+
)
80+
.await
81+
.map_err(|e| anyhow::anyhow!("{e}"))?;
82+
println!("Installed connector: {name}");
9083
}
9184
}
9285
Ok(())

0 commit comments

Comments
 (0)