diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5deec2a0..326db00a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,10 +138,10 @@ jobs: matrix: include: - target: x86_64-unknown-linux-gnu - os: ubuntu-latest + os: ubuntu-22.04 archive: tar.gz - target: aarch64-unknown-linux-gnu - os: ubuntu-latest + os: ubuntu-22.04 archive: tar.gz - target: x86_64-apple-darwin os: macos-latest diff --git a/crates/openfang-cli/src/bundled_agents.rs b/crates/openfang-cli/src/bundled_agents.rs new file mode 100644 index 00000000..d1e036c9 --- /dev/null +++ b/crates/openfang-cli/src/bundled_agents.rs @@ -0,0 +1,56 @@ +//! Compile-time embedded agent templates. +//! +//! All 30 bundled agent templates are embedded into the binary via `include_str!`. +//! This ensures `openfang agent new` works immediately after install — no filesystem +//! discovery needed. + +/// Returns all bundled agent templates as `(name, toml_content)` pairs. +pub fn bundled_agents() -> Vec<(&'static str, &'static str)> { + vec![ + ("analyst", include_str!("../../../agents/analyst/agent.toml")), + ("architect", include_str!("../../../agents/architect/agent.toml")), + ("assistant", include_str!("../../../agents/assistant/agent.toml")), + ("coder", include_str!("../../../agents/coder/agent.toml")), + ("code-reviewer", include_str!("../../../agents/code-reviewer/agent.toml")), + ("customer-support", include_str!("../../../agents/customer-support/agent.toml")), + ("data-scientist", include_str!("../../../agents/data-scientist/agent.toml")), + ("debugger", include_str!("../../../agents/debugger/agent.toml")), + ("devops-lead", include_str!("../../../agents/devops-lead/agent.toml")), + ("doc-writer", include_str!("../../../agents/doc-writer/agent.toml")), + ("email-assistant", include_str!("../../../agents/email-assistant/agent.toml")), + ("health-tracker", include_str!("../../../agents/health-tracker/agent.toml")), + ("hello-world", include_str!("../../../agents/hello-world/agent.toml")), + ("home-automation", include_str!("../../../agents/home-automation/agent.toml")), + ("legal-assistant", include_str!("../../../agents/legal-assistant/agent.toml")), + ("meeting-assistant", include_str!("../../../agents/meeting-assistant/agent.toml")), + ("ops", include_str!("../../../agents/ops/agent.toml")), + ("orchestrator", include_str!("../../../agents/orchestrator/agent.toml")), + ("personal-finance", include_str!("../../../agents/personal-finance/agent.toml")), + ("planner", include_str!("../../../agents/planner/agent.toml")), + ("recruiter", include_str!("../../../agents/recruiter/agent.toml")), + ("researcher", include_str!("../../../agents/researcher/agent.toml")), + ("sales-assistant", include_str!("../../../agents/sales-assistant/agent.toml")), + ("security-auditor", include_str!("../../../agents/security-auditor/agent.toml")), + ("social-media", include_str!("../../../agents/social-media/agent.toml")), + ("test-engineer", include_str!("../../../agents/test-engineer/agent.toml")), + ("translator", include_str!("../../../agents/translator/agent.toml")), + ("travel-planner", include_str!("../../../agents/travel-planner/agent.toml")), + ("tutor", include_str!("../../../agents/tutor/agent.toml")), + ("writer", include_str!("../../../agents/writer/agent.toml")), + ] +} + +/// Install bundled agent templates to `~/.openfang/agents/`. +/// Skips any template that already exists on disk (user customization preserved). +pub fn install_bundled_agents(agents_dir: &std::path::Path) { + for (name, content) in bundled_agents() { + let dest_dir = agents_dir.join(name); + let dest_file = dest_dir.join("agent.toml"); + if dest_file.exists() { + continue; // Preserve user customization + } + if std::fs::create_dir_all(&dest_dir).is_ok() { + let _ = std::fs::write(&dest_file, content); + } + } +} diff --git a/crates/openfang-cli/src/dotenv.rs b/crates/openfang-cli/src/dotenv.rs index 4c1892af..23179e92 100644 --- a/crates/openfang-cli/src/dotenv.rs +++ b/crates/openfang-cli/src/dotenv.rs @@ -11,19 +11,32 @@ pub fn env_file_path() -> Option { dirs::home_dir().map(|h| h.join(".openfang").join(".env")) } -/// Load `~/.openfang/.env` into `std::env`. +/// Load `~/.openfang/.env` and `~/.openfang/secrets.env` into `std::env`. /// /// System env vars take priority — existing vars are NOT overridden. -/// Silently does nothing if the file doesn't exist. +/// `secrets.env` is loaded second so `.env` values take priority over secrets +/// (but both yield to system env vars). +/// Silently does nothing if the files don't exist. pub fn load_dotenv() { - let path = match env_file_path() { + load_env_file(env_file_path()); + // Also load secrets.env (written by dashboard "Set API Key" button) + load_env_file(secrets_env_path()); +} + +/// Return the path to `~/.openfang/secrets.env`. +pub fn secrets_env_path() -> Option { + dirs::home_dir().map(|h| h.join(".openfang").join("secrets.env")) +} + +fn load_env_file(path: Option) { + let path = match path { Some(p) => p, None => return, }; let content = match std::fs::read_to_string(&path) { Ok(c) => c, - Err(_) => return, // file doesn't exist or unreadable — that's fine + Err(_) => return, }; for line in content.lines() { @@ -33,7 +46,6 @@ pub fn load_dotenv() { } if let Some((key, value)) = parse_env_line(trimmed) { - // Only set if not already in environment (system env takes priority) if std::env::var(&key).is_err() { std::env::set_var(&key, &value); } diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs index c468a3d8..fb819a3f 100644 --- a/crates/openfang-cli/src/main.rs +++ b/crates/openfang-cli/src/main.rs @@ -3,6 +3,7 @@ //! When a daemon is running (`openfang start`), the CLI talks to it over HTTP. //! Otherwise, commands boot an in-process kernel (single-shot mode). +mod bundled_agents; mod dotenv; mod launcher; mod mcp; @@ -1051,6 +1052,9 @@ fn cmd_init(quick: bool) { } } + // Install bundled agent templates (skips existing ones to preserve user edits) + bundled_agents::install_bundled_agents(&openfang_dir.join("agents")); + if quick { cmd_init_quick(&openfang_dir); } else { diff --git a/crates/openfang-cli/src/templates.rs b/crates/openfang-cli/src/templates.rs index e9b20c90..650bc126 100644 --- a/crates/openfang-cli/src/templates.rs +++ b/crates/openfang-cli/src/templates.rs @@ -54,11 +54,12 @@ pub fn discover_template_dirs() -> Vec { dirs } -/// Load all templates from discovered directories. +/// Load all templates from discovered directories, falling back to bundled templates. pub fn load_all_templates() -> Vec { let mut templates = Vec::new(); let mut seen_names = std::collections::HashSet::new(); + // First: load from filesystem (user-installed or dev repo) for dir in discover_template_dirs() { if let Ok(entries) = std::fs::read_dir(&dir) { for entry in entries.flatten() { @@ -86,6 +87,18 @@ pub fn load_all_templates() -> Vec { } } + // Fallback: load bundled templates for any not found on disk + for (name, content) in crate::bundled_agents::bundled_agents() { + if seen_names.insert(name.to_string()) { + let description = extract_description(content); + templates.push(AgentTemplate { + name: name.to_string(), + description, + content: content.to_string(), + }); + } + } + templates.sort_by(|a, b| a.name.cmp(&b.name)); templates } diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 830d9daa..13660c76 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -958,6 +958,18 @@ impl OpenFangKernel { manifest.exec_policy = Some(self.config.exec_policy.clone()); } + // Overlay kernel default_model onto agent if no custom key/url is set. + // This ensures agents respect the user's configured provider from `openfang init`. + if manifest.model.api_key_env.is_none() && manifest.model.base_url.is_none() { + let dm = &self.config.default_model; + if !dm.provider.is_empty() { + manifest.model.provider = dm.provider.clone(); + } + if !dm.model.is_empty() { + manifest.model.model = dm.model.clone(); + } + } + // Create workspace directory for the agent let workspace_dir = manifest.workspace.clone().unwrap_or_else(|| { self.config.effective_workspaces_dir().join(format!( diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index 4b269171..56dc4f8f 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -14,8 +14,10 @@ use crate::llm_driver::{DriverConfig, LlmDriver, LlmError}; use openfang_types::model_catalog::{ AI21_BASE_URL, ANTHROPIC_BASE_URL, CEREBRAS_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, LMSTUDIO_BASE_URL, - MISTRAL_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, + MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, + OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VLLM_BASE_URL, XAI_BASE_URL, + ZHIPU_BASE_URL, }; use std::sync::Arc; @@ -130,6 +132,31 @@ fn provider_defaults(provider: &str) -> Option { api_key_env: "GITHUB_TOKEN", key_required: true, }), + "moonshot" | "kimi" => Some(ProviderDefaults { + base_url: MOONSHOT_BASE_URL, + api_key_env: "MOONSHOT_API_KEY", + key_required: true, + }), + "qwen" | "dashscope" => Some(ProviderDefaults { + base_url: QWEN_BASE_URL, + api_key_env: "DASHSCOPE_API_KEY", + key_required: true, + }), + "minimax" => Some(ProviderDefaults { + base_url: MINIMAX_BASE_URL, + api_key_env: "MINIMAX_API_KEY", + key_required: true, + }), + "zhipu" | "glm" => Some(ProviderDefaults { + base_url: ZHIPU_BASE_URL, + api_key_env: "ZHIPU_API_KEY", + key_required: true, + }), + "qianfan" | "baidu" => Some(ProviderDefaults { + base_url: QIANFAN_BASE_URL, + api_key_env: "QIANFAN_API_KEY", + key_required: true, + }), _ => None, } } @@ -286,6 +313,11 @@ pub fn known_providers() -> &'static [&'static str] { "xai", "replicate", "github-copilot", + "moonshot", + "qwen", + "minimax", + "zhipu", + "qianfan", ] } @@ -373,7 +405,12 @@ mod tests { assert!(providers.contains(&"xai")); assert!(providers.contains(&"replicate")); assert!(providers.contains(&"github-copilot")); - assert_eq!(providers.len(), 21); + assert!(providers.contains(&"moonshot")); + assert!(providers.contains(&"qwen")); + assert!(providers.contains(&"minimax")); + assert!(providers.contains(&"zhipu")); + assert!(providers.contains(&"qianfan")); + assert_eq!(providers.len(), 26); } #[test] diff --git a/crates/openfang-skills/src/lib.rs b/crates/openfang-skills/src/lib.rs index 1b817370..b3914dc4 100644 --- a/crates/openfang-skills/src/lib.rs +++ b/crates/openfang-skills/src/lib.rs @@ -48,7 +48,6 @@ pub enum SkillError { #[serde(rename_all = "lowercase")] pub enum SkillRuntime { /// Python script executed in subprocess. - #[default] Python, /// WASM module executed in sandbox. Wasm, @@ -58,6 +57,7 @@ pub enum SkillRuntime { Builtin, /// Prompt-only skill: injects context into the LLM system prompt. /// No executable code — the Markdown body teaches the LLM. + #[default] PromptOnly, } @@ -101,7 +101,8 @@ pub struct SkillRequirements { pub struct SkillManifest { /// Skill metadata. pub skill: SkillMeta, - /// Runtime configuration. + /// Runtime configuration (defaults to PromptOnly if omitted). + #[serde(default)] pub runtime: SkillRuntimeConfig, /// Tools provided by this skill. #[serde(default)] @@ -144,7 +145,7 @@ fn default_version() -> String { } /// Runtime configuration section. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SkillRuntimeConfig { /// Runtime type. #[serde(rename = "type", default)] diff --git a/crates/openfang-types/src/config.rs b/crates/openfang-types/src/config.rs index 3f37fff4..3ded5f1e 100644 --- a/crates/openfang-types/src/config.rs +++ b/crates/openfang-types/src/config.rs @@ -929,7 +929,8 @@ pub struct KernelConfig { pub data_dir: PathBuf, /// Log level (trace, debug, info, warn, error). pub log_level: String, - /// gRPC API listen address. + /// API listen address (e.g., "0.0.0.0:4200"). + #[serde(alias = "listen_addr")] pub api_listen: String, /// Whether to enable the OFP network layer. pub network_enabled: bool, diff --git a/docker-compose.yml b/docker-compose.yml index e8fc9c5b..c264f40e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: openfang: build: . - image: ghcr.io/RightNow-AI/openfang:latest + image: ghcr.io/rightnow-ai/openfang:latest ports: - "4200:4200" volumes: diff --git a/openfang.toml.example b/openfang.toml.example index 298be5fc..e6d4c2b6 100644 --- a/openfang.toml.example +++ b/openfang.toml.example @@ -3,7 +3,7 @@ # API server settings # api_key = "" # Set to enable Bearer auth (recommended) -# listen_addr = "127.0.0.1:3000" # HTTP API bind address +# api_listen = "127.0.0.1:50051" # HTTP API bind address (use 0.0.0.0 for public) [default_model] provider = "anthropic" # "anthropic", "gemini", "openai", "groq", "ollama", etc.