mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-04-25 17:25:11 +02:00
vault wiring
This commit is contained in:
29
Cargo.lock
generated
29
Cargo.lock
generated
@@ -3792,7 +3792,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-api"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3832,7 +3832,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-channels"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3864,7 +3864,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-cli"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@@ -3891,7 +3891,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-desktop"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"open",
|
||||
@@ -3917,7 +3917,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-extensions"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -3945,7 +3945,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-hands"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -3962,7 +3962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-kernel"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3995,11 +3995,12 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfang-memory"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4018,7 +4019,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-migrate"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 6.0.0",
|
||||
@@ -4037,7 +4038,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-runtime"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4071,7 +4072,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-skills"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
@@ -4094,7 +4095,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-types"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4113,7 +4114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-wire"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -8776,7 +8777,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
@@ -18,7 +18,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/RightNow-AI/openfang"
|
||||
|
||||
@@ -6953,7 +6953,10 @@ pub async fn set_provider_key(
|
||||
})
|
||||
};
|
||||
|
||||
// Write to secrets.env file
|
||||
// Store in vault (best-effort — no-op if vault not initialized)
|
||||
state.kernel.store_credential(&env_var, &key);
|
||||
|
||||
// Write to secrets.env file (dual-write for backward compat / vault corruption recovery)
|
||||
let secrets_path = state.kernel.config.home_dir.join("secrets.env");
|
||||
if let Err(e) = write_secret_env(&secrets_path, &env_var, &key) {
|
||||
return (
|
||||
@@ -7115,6 +7118,9 @@ pub async fn delete_provider_key(
|
||||
);
|
||||
}
|
||||
|
||||
// Remove from vault (best-effort)
|
||||
state.kernel.remove_credential(&env_var);
|
||||
|
||||
// Remove from secrets.env
|
||||
let secrets_path = state.kernel.config.home_dir.join("secrets.env");
|
||||
if let Err(e) = remove_secret_env(&secrets_path, &env_var) {
|
||||
@@ -10267,7 +10273,10 @@ pub async fn copilot_oauth_poll(
|
||||
Json(serde_json::json!({"status": "pending"})),
|
||||
),
|
||||
openfang_runtime::copilot_oauth::DeviceFlowStatus::Complete { access_token } => {
|
||||
// Save to secrets.env
|
||||
// Store in vault (best-effort)
|
||||
state.kernel.store_credential("GITHUB_TOKEN", &access_token);
|
||||
|
||||
// Save to secrets.env (dual-write)
|
||||
let secrets_path = state.kernel.config.home_dir.join("secrets.env");
|
||||
if let Err(e) = write_secret_env(&secrets_path, "GITHUB_TOKEN", &access_token) {
|
||||
return (
|
||||
|
||||
@@ -4813,6 +4813,10 @@ fn cmd_config_set_key(provider: &str) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try vault first (best-effort)
|
||||
save_credential_prefer_vault(&env_var, &key);
|
||||
|
||||
// Always save to dotenv as fallback
|
||||
match dotenv::save_env_key(&env_var, &key) {
|
||||
Ok(()) => {
|
||||
ui::success(&format!("Saved {env_var} to ~/.openfang/.env"));
|
||||
@@ -4835,6 +4839,18 @@ fn cmd_config_set_key(provider: &str) {
|
||||
fn cmd_config_delete_key(provider: &str) {
|
||||
let env_var = provider_to_env_var(provider);
|
||||
|
||||
// Remove from vault (best-effort)
|
||||
{
|
||||
let home = openfang_home();
|
||||
let vault_path = home.join("vault.enc");
|
||||
if vault_path.exists() {
|
||||
let mut vault = openfang_extensions::vault::CredentialVault::new(vault_path);
|
||||
if vault.unlock().is_ok() {
|
||||
let _ = vault.remove(&env_var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match dotenv::remove_env_key(&env_var) {
|
||||
Ok(()) => ui::success(&format!("Removed {env_var} from ~/.openfang/.env")),
|
||||
Err(e) => {
|
||||
@@ -4864,6 +4880,26 @@ fn cmd_config_test_key(provider: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to store a credential in the vault first; silently falls through if vault
|
||||
/// is not initialized or cannot be unlocked. The caller should always also
|
||||
/// write to dotenv as a fallback.
|
||||
fn save_credential_prefer_vault(env_var: &str, value: &str) {
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
let home = openfang_home();
|
||||
let vault_path = home.join("vault.enc");
|
||||
if !vault_path.exists() {
|
||||
return;
|
||||
}
|
||||
let mut vault = openfang_extensions::vault::CredentialVault::new(vault_path);
|
||||
if vault.unlock().is_err() {
|
||||
return;
|
||||
}
|
||||
if let Ok(()) = vault.set(env_var.to_string(), Zeroizing::new(value.to_string())) {
|
||||
println!(" {}", "Also stored in encrypted vault".dimmed());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Quick chat (OpenClaw alias)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -126,6 +126,17 @@ impl CredentialResolver {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a credential from the vault (if available).
|
||||
pub fn remove_from_vault(&mut self, key: &str) -> ExtensionResult<bool> {
|
||||
if let Some(ref mut vault) = self.vault {
|
||||
vault.remove(key)
|
||||
} else {
|
||||
Err(crate::ExtensionError::Vault(
|
||||
"No vault configured".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a dotenv file into a HashMap.
|
||||
|
||||
@@ -34,6 +34,7 @@ rand = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
cron = "0.15"
|
||||
zeroize = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
@@ -117,6 +117,8 @@ pub struct OpenFangKernel {
|
||||
Option<Arc<dyn openfang_runtime::embedding::EmbeddingDriver + Send + Sync>>,
|
||||
/// Hand registry — curated autonomous capability packages.
|
||||
pub hand_registry: openfang_hands::registry::HandRegistry,
|
||||
/// Credential resolver — vault → dotenv → env var priority chain.
|
||||
pub credential_resolver: std::sync::Mutex<openfang_extensions::credentials::CredentialResolver>,
|
||||
/// Extension/integration registry (bundled MCP templates + install state).
|
||||
pub extension_registry: std::sync::RwLock<openfang_extensions::registry::IntegrationRegistry>,
|
||||
/// Integration health monitor.
|
||||
@@ -562,16 +564,43 @@ impl OpenFangKernel {
|
||||
.map_err(|e| KernelError::BootFailed(format!("Memory init failed: {e}")))?,
|
||||
);
|
||||
|
||||
// Initialize credential resolver (vault → dotenv → env var)
|
||||
let credential_resolver = {
|
||||
let vault_path = config.home_dir.join("vault.enc");
|
||||
let vault = if vault_path.exists() {
|
||||
let mut v = openfang_extensions::vault::CredentialVault::new(vault_path);
|
||||
match v.unlock() {
|
||||
Ok(()) => {
|
||||
info!("Credential vault unlocked ({} entries)", v.len());
|
||||
Some(v)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Credential vault exists but could not unlock: {e} — falling back to env vars");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let dotenv_path = config.home_dir.join(".env");
|
||||
openfang_extensions::credentials::CredentialResolver::new(
|
||||
vault,
|
||||
Some(&dotenv_path),
|
||||
)
|
||||
};
|
||||
|
||||
// Create LLM driver.
|
||||
// For the API key, try: 1) explicit api_key_env from config, 2) provider_api_keys
|
||||
// mapping, 3) auth profiles, 4) convention {PROVIDER}_API_KEY. This ensures
|
||||
// custom providers (e.g. nvidia, azure) work without hardcoded env var names.
|
||||
let default_api_key = if !config.default_model.api_key_env.is_empty() {
|
||||
std::env::var(&config.default_model.api_key_env).ok()
|
||||
} else {
|
||||
// api_key_env not set — resolve using provider_api_keys / convention
|
||||
let env_var = config.resolve_api_key_env(&config.default_model.provider);
|
||||
std::env::var(&env_var).ok()
|
||||
// For the API key, try: 1) credential resolver (vault → dotenv → env var),
|
||||
// 2) provider_api_keys mapping, 3) convention {PROVIDER}_API_KEY.
|
||||
let default_api_key = {
|
||||
let env_var = if !config.default_model.api_key_env.is_empty() {
|
||||
config.default_model.api_key_env.clone()
|
||||
} else {
|
||||
config.resolve_api_key_env(&config.default_model.provider)
|
||||
};
|
||||
credential_resolver
|
||||
.resolve(&env_var)
|
||||
.map(|z: zeroize::Zeroizing<String>| z.to_string())
|
||||
};
|
||||
let driver_config = DriverConfig {
|
||||
provider: config.default_model.provider.clone(),
|
||||
@@ -600,7 +629,7 @@ impl OpenFangKernel {
|
||||
if let Some((provider, model, env_var)) = drivers::detect_available_provider() {
|
||||
let auto_config = DriverConfig {
|
||||
provider: provider.to_string(),
|
||||
api_key: std::env::var(env_var).ok(),
|
||||
api_key: credential_resolver.resolve(env_var).map(|z: zeroize::Zeroizing<String>| z.to_string()),
|
||||
base_url: config.provider_urls.get(provider).cloned(),
|
||||
skip_permissions: true,
|
||||
};
|
||||
@@ -633,12 +662,13 @@ impl OpenFangKernel {
|
||||
model_chain.push((d.clone(), String::new()));
|
||||
}
|
||||
for fb in &config.fallback_providers {
|
||||
let fb_api_key = if !fb.api_key_env.is_empty() {
|
||||
std::env::var(&fb.api_key_env).ok()
|
||||
} else {
|
||||
// Resolve using provider_api_keys / convention for custom providers
|
||||
let env_var = config.resolve_api_key_env(&fb.provider);
|
||||
std::env::var(&env_var).ok()
|
||||
let fb_api_key = {
|
||||
let env_var = if !fb.api_key_env.is_empty() {
|
||||
fb.api_key_env.clone()
|
||||
} else {
|
||||
config.resolve_api_key_env(&fb.provider)
|
||||
};
|
||||
credential_resolver.resolve(&env_var).map(|z: zeroize::Zeroizing<String>| z.to_string())
|
||||
};
|
||||
let fb_config = DriverConfig {
|
||||
provider: fb.provider.clone(),
|
||||
@@ -994,6 +1024,7 @@ impl OpenFangKernel {
|
||||
pairing,
|
||||
embedding_driver,
|
||||
hand_registry,
|
||||
credential_resolver: std::sync::Mutex::new(credential_resolver),
|
||||
extension_registry: std::sync::RwLock::new(extension_registry),
|
||||
extension_health,
|
||||
effective_mcp_servers: std::sync::RwLock::new(all_mcp_servers),
|
||||
@@ -3938,16 +3969,23 @@ impl OpenFangKernel {
|
||||
.await
|
||||
{
|
||||
Ok(Ok(result)) => {
|
||||
tracing::info!(job = %job_name, "Cron job completed successfully");
|
||||
kernel.cron_scheduler.record_success(job_id);
|
||||
// Deliver response to configured channel
|
||||
cron_deliver_response(
|
||||
match cron_deliver_response(
|
||||
&kernel,
|
||||
agent_id,
|
||||
&result.response,
|
||||
&delivery,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
tracing::info!(job = %job_name, "Cron job completed successfully");
|
||||
kernel.cron_scheduler.record_success(job_id);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(job = %job_name, error = %e, "Cron job delivery failed");
|
||||
kernel.cron_scheduler.record_failure(job_id, &e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let err_msg = format!("{e}");
|
||||
@@ -3997,15 +4035,23 @@ impl OpenFangKernel {
|
||||
.await
|
||||
{
|
||||
Ok(Ok((_run_id, output))) => {
|
||||
tracing::info!(job = %job_name, "Cron workflow completed");
|
||||
kernel.cron_scheduler.record_success(job_id);
|
||||
cron_deliver_response(
|
||||
match cron_deliver_response(
|
||||
&kernel,
|
||||
agent_id,
|
||||
&output,
|
||||
&delivery,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
tracing::info!(job = %job_name, "Cron workflow completed");
|
||||
kernel.cron_scheduler.record_success(job_id);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(job = %job_name, error = %e, "Cron workflow delivery failed");
|
||||
kernel.cron_scheduler.record_failure(job_id, &e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let err_msg = format!("{e}");
|
||||
@@ -4322,6 +4368,37 @@ impl OpenFangKernel {
|
||||
/// stored in the model catalog but NOT in `self.config.provider_urls` (which is
|
||||
/// the boot-time snapshot). This helper checks both sources so that custom
|
||||
/// providers work immediately without a daemon restart.
|
||||
/// Resolve a credential by env var name using the vault → dotenv → env var chain.
|
||||
pub fn resolve_credential(&self, key: &str) -> Option<String> {
|
||||
self.credential_resolver
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.resolve(key)
|
||||
.map(|z| z.to_string())
|
||||
}
|
||||
|
||||
/// Store a credential in the vault (best-effort — falls through silently if no vault).
|
||||
pub fn store_credential(&self, key: &str, value: &str) {
|
||||
let mut resolver = self
|
||||
.credential_resolver
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if let Err(e) = resolver.store_in_vault(key, zeroize::Zeroizing::new(value.to_string())) {
|
||||
debug!("Vault store skipped for {key}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a credential from the vault (best-effort — falls through silently if no vault).
|
||||
pub fn remove_credential(&self, key: &str) {
|
||||
let mut resolver = self
|
||||
.credential_resolver
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if let Err(e) = resolver.remove_from_vault(key) {
|
||||
debug!("Vault remove skipped for {key}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_provider_url(&self, provider: &str) -> Option<String> {
|
||||
// 1. Boot-time config (from config.toml [provider_urls])
|
||||
if let Some(url) = self.config.provider_urls.get(provider) {
|
||||
@@ -4357,33 +4434,26 @@ impl OpenFangKernel {
|
||||
let has_custom_key = manifest.model.api_key_env.is_some();
|
||||
let has_custom_url = manifest.model.base_url.is_some();
|
||||
|
||||
// Always create a fresh driver by reading current env vars.
|
||||
// This ensures API keys saved at runtime (via dashboard POST
|
||||
// /api/providers/{name}/key which calls std::env::set_var) are
|
||||
// picked up immediately — the boot-time default_driver cache is
|
||||
// only used as a final fallback when driver creation fails.
|
||||
// Always create a fresh driver by resolving credentials from the
|
||||
// vault → dotenv → env var chain. This ensures API keys saved at
|
||||
// runtime (via dashboard or vault) are picked up immediately.
|
||||
let primary = {
|
||||
let api_key = if has_custom_key {
|
||||
// Agent explicitly set an API key env var — use it
|
||||
manifest
|
||||
.model
|
||||
.api_key_env
|
||||
.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.and_then(|env| self.resolve_credential(env))
|
||||
} else if agent_provider == default_provider {
|
||||
// Same provider as effective default — use its env var
|
||||
if !effective_default.api_key_env.is_empty() {
|
||||
std::env::var(&effective_default.api_key_env).ok()
|
||||
self.resolve_credential(&effective_default.api_key_env)
|
||||
} else {
|
||||
let env_var = self.config.resolve_api_key_env(agent_provider);
|
||||
std::env::var(&env_var).ok()
|
||||
self.resolve_credential(&env_var)
|
||||
}
|
||||
} else {
|
||||
// Different provider — check auth profiles, provider_api_keys,
|
||||
// and convention-based env var. For custom providers (not in the
|
||||
// hardcoded list), this is the primary path for API key resolution.
|
||||
let env_var = self.config.resolve_api_key_env(agent_provider);
|
||||
std::env::var(&env_var).ok()
|
||||
self.resolve_credential(&env_var)
|
||||
};
|
||||
|
||||
// Don't inherit default provider's base_url when switching providers.
|
||||
@@ -5315,15 +5385,15 @@ async fn cron_deliver_response(
|
||||
agent_id: AgentId,
|
||||
response: &str,
|
||||
delivery: &openfang_types::scheduler::CronDelivery,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
use openfang_types::scheduler::CronDelivery;
|
||||
|
||||
if response.is_empty() {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match delivery {
|
||||
CronDelivery::None => {}
|
||||
CronDelivery::None => Ok(()),
|
||||
CronDelivery::Channel { channel, to } => {
|
||||
tracing::debug!(channel = %channel, to = %to, "Cron: delivering to channel");
|
||||
// Persist as last channel for this agent (survives restarts)
|
||||
@@ -5331,6 +5401,17 @@ async fn cron_deliver_response(
|
||||
let _ = kernel
|
||||
.memory
|
||||
.structured_set(agent_id, "delivery.last_channel", kv_val);
|
||||
// Deliver via the registered channel adapter
|
||||
kernel
|
||||
.send_channel_message(channel, to, response, None)
|
||||
.await
|
||||
.map(|_| {
|
||||
tracing::info!(channel = %channel, to = %to, "Cron: delivered to channel");
|
||||
})
|
||||
.map_err(|e| {
|
||||
tracing::warn!(channel = %channel, to = %to, error = %e, "Cron channel delivery failed");
|
||||
format!("channel delivery failed: {e}")
|
||||
})
|
||||
}
|
||||
CronDelivery::LastChannel => {
|
||||
match kernel
|
||||
@@ -5341,15 +5422,23 @@ async fn cron_deliver_response(
|
||||
let channel = val["channel"].as_str().unwrap_or("");
|
||||
let recipient = val["recipient"].as_str().unwrap_or("");
|
||||
if !channel.is_empty() && !recipient.is_empty() {
|
||||
tracing::info!(
|
||||
channel = %channel,
|
||||
recipient = %recipient,
|
||||
"Cron: delivering to last channel"
|
||||
);
|
||||
kernel
|
||||
.send_channel_message(channel, recipient, response, None)
|
||||
.await
|
||||
.map(|_| {
|
||||
tracing::info!(channel = %channel, recipient = %recipient, "Cron: delivered to last channel");
|
||||
})
|
||||
.map_err(|e| {
|
||||
tracing::warn!(channel = %channel, recipient = %recipient, error = %e, "Cron last-channel delivery failed");
|
||||
format!("last-channel delivery failed: {e}")
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!("Cron: no last channel found for agent {}", agent_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5357,22 +5446,24 @@ async fn cron_deliver_response(
|
||||
tracing::debug!(url = %url, "Cron: delivering via webhook");
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build();
|
||||
if let Ok(client) = client {
|
||||
let payload = serde_json::json!({
|
||||
"agent_id": agent_id.to_string(),
|
||||
"response": response,
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
});
|
||||
match client.post(url).json(&payload).send().await {
|
||||
Ok(resp) => {
|
||||
tracing::debug!(status = %resp.status(), "Cron webhook delivered");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Cron webhook delivery failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
.map_err(|e| format!("webhook client init failed: {e}"))?;
|
||||
let payload = serde_json::json!({
|
||||
"agent_id": agent_id.to_string(),
|
||||
"response": response,
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
});
|
||||
let resp = client
|
||||
.post(url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(error = %e, "Cron webhook delivery failed");
|
||||
format!("webhook delivery failed: {e}")
|
||||
})?;
|
||||
tracing::debug!(status = %resp.status(), "Cron webhook delivered");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user