🚀 Camoufox default web engine — auto-install + auto-start
Full integration:
- install.sh now installs camoufox-browser via npm (preferred) or Docker
- install.sh creates systemd/launchd auto-start service on Linux/macOS
- New browser_manager.rs — health-check, auto-start, wait-for-ready
- AgentLoop::run() auto-inits Camoufox and registers web_search/web_browse
- System prompt tells LLM to use web_search/web_browse as PRIMARY tools
- Graceful fallback: if Camoufox unavailable, logs warning, uses bash tools
- ToolExecutor is now Arc<Mutex<>> for runtime tool registration
GyanDoot v2 now: install → Camoufox auto-starts → web tools available →
"search NSE India" uses Camoufox with anti-detection 🎯
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -675,6 +675,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"dirs",
|
||||||
"gyandoot-browser",
|
"gyandoot-browser",
|
||||||
"gyandoot-core",
|
"gyandoot-core",
|
||||||
"gyandoot-memory",
|
"gyandoot-memory",
|
||||||
|
|||||||
@@ -22,3 +22,4 @@ uuid.workspace = true
|
|||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
dirs.workspace = true
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gyandoot_browser::BrowserClient;
|
||||||
use gyandoot_core::{
|
use gyandoot_core::{
|
||||||
AgentConfig, AgentError, AgentResult, AgentStatus, ConversationContext, Message,
|
AgentConfig, AgentError, AgentResult, AgentStatus, ConversationContext, Message,
|
||||||
MessageRole,
|
MessageRole,
|
||||||
@@ -102,30 +103,42 @@ impl Default for RetryConfig {
|
|||||||
Self {
|
Self {
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
base_delay_ms: 1000,
|
base_delay_ms: 1000,
|
||||||
max_delay_ms: 30_000,
|
max_delay_ms: 10000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main agent execution loop, usable in both interactive and
|
/// Structured output from a conversation turn (non-interactive callers).
|
||||||
/// non-interactive contexts.
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConversationOutput {
|
||||||
|
pub text: String,
|
||||||
|
pub tool_results: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── AgentLoop ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// The core agent inference loop.
|
||||||
|
///
|
||||||
|
/// Reads messages, builds conversation context, calls the LLM provider,
|
||||||
|
/// executes tools, and manages context window limits.
|
||||||
pub struct AgentLoop {
|
pub struct AgentLoop {
|
||||||
config: AgentConfig,
|
config: AgentConfig,
|
||||||
provider: Box<dyn Provider + Send + Sync>,
|
provider: Box<dyn Provider + Send + Sync>,
|
||||||
tool_executor: ToolExecutor,
|
tool_executor: Arc<Mutex<ToolExecutor>>,
|
||||||
context_compressor: ContextCompressor,
|
context_compressor: ContextCompressor,
|
||||||
memory: Arc<Mutex<MemPalace>>,
|
memory: Arc<Mutex<MemPalace>>,
|
||||||
_skills_manager: SkillsManager,
|
_skills_manager: SkillsManager,
|
||||||
sanitizer: Sanitizer,
|
sanitizer: Sanitizer,
|
||||||
status: Arc<Mutex<AgentStatus>>,
|
status: Arc<Mutex<AgentStatus>>,
|
||||||
retry_cfg: RetryConfig,
|
retry_cfg: RetryConfig,
|
||||||
|
browser_client: Option<BrowserClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentLoop {
|
impl AgentLoop {
|
||||||
/// Create a new agent loop from a fully-resolved configuration.
|
/// Create a new agent loop from a fully-resolved configuration.
|
||||||
pub fn new(config: AgentConfig) -> Self {
|
pub fn new(config: AgentConfig) -> Self {
|
||||||
let provider = create_provider(&config);
|
let provider = create_provider(&config);
|
||||||
let tool_executor = ToolExecutor::with_defaults();
|
let tool_executor = Arc::new(Mutex::new(ToolExecutor::with_defaults()));
|
||||||
let context_compressor = ContextCompressor::new(config.max_tokens);
|
let context_compressor = ContextCompressor::new(config.max_tokens);
|
||||||
let memory = Arc::new(Mutex::new(MemPalace::new("default")));
|
let memory = Arc::new(Mutex::new(MemPalace::new("default")));
|
||||||
let skills_manager = SkillsManager::new();
|
let skills_manager = SkillsManager::new();
|
||||||
@@ -140,6 +153,7 @@ impl AgentLoop {
|
|||||||
sanitizer: Sanitizer::new(gyandoot_security::sanitizer::SanitizerConfig::default()),
|
sanitizer: Sanitizer::new(gyandoot_security::sanitizer::SanitizerConfig::default()),
|
||||||
status: Arc::new(Mutex::new(AgentStatus::Idle)),
|
status: Arc::new(Mutex::new(AgentStatus::Idle)),
|
||||||
retry_cfg: RetryConfig::default(),
|
retry_cfg: RetryConfig::default(),
|
||||||
|
browser_client: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,16 +173,32 @@ impl AgentLoop {
|
|||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
info!("Agent loop started (interactive)");
|
info!("Agent loop started (interactive)");
|
||||||
|
|
||||||
|
// ── Auto-init Camoufox and register browser tools ──────────────
|
||||||
|
{
|
||||||
|
let mut tool_executor = self.tool_executor.lock().await;
|
||||||
|
match crate::browser_manager::init_browser().await {
|
||||||
|
crate::browser_manager::BrowserInitResult::Ready(client) => {
|
||||||
|
info!("Camoufox browser ready — registering web_search and web_browse tools");
|
||||||
|
crate::browser_tools::register_browser_tools(&mut tool_executor, client.clone());
|
||||||
|
// Store client for potential later use
|
||||||
|
// (browser_client field is set via unsafe trick — skip for now)
|
||||||
|
}
|
||||||
|
crate::browser_manager::BrowserInitResult::Unavailable(msg) => {
|
||||||
|
warn!("Camoufox not available: {}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Identity & memory injection on startup ─────────────────────
|
// ── Identity & memory injection on startup ─────────────────────
|
||||||
let (agent_name, agent_role, user_name, user_role) = {
|
let (agent_name, agent_role, user_name, user_role) = {
|
||||||
let memory = self.memory.lock().await;
|
let memory = self.memory.lock().await;
|
||||||
let (l0, _) = memory.identity_get().unwrap_or_default();
|
let (l0, _) = memory.identity_get().unwrap_or_default();
|
||||||
|
|
||||||
// Priority: config identity > L0 file > config.agent.name > defaults
|
// Priority: config identity > config.agent.name > defaults
|
||||||
let a_name = if !self.config.identity.agent_name.is_empty() {
|
let a_name = if !self.config.identity.agent_name.is_empty() {
|
||||||
self.config.identity.agent_name.clone()
|
self.config.identity.agent_name.clone()
|
||||||
} else {
|
} else {
|
||||||
self.config.name.clone() // fallback to agent name from config
|
self.config.name.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let a_role = if !self.config.identity.agent_role.is_empty() {
|
let a_role = if !self.config.identity.agent_role.is_empty() {
|
||||||
@@ -192,11 +222,11 @@ impl AgentLoop {
|
|||||||
(a_name, a_role, u_name, u_role)
|
(a_name, a_role, u_name, u_role)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build system prompt with identity + dialogue memory
|
// Build system prompt with identity + tool capabilities + dialogue memory
|
||||||
let mut system_prompt = self.config.system_prompt.clone();
|
let mut system_prompt = self.config.system_prompt.clone();
|
||||||
if !agent_name.is_empty() {
|
if !agent_name.is_empty() {
|
||||||
system_prompt = format!(
|
system_prompt = format!(
|
||||||
"{}\n\n## Your Identity\n- Name: {}\n- Role: {}\n- You are speaking to: {} ({})\n\n## Your Capabilities\nYou have access to tools that let you:\n- Read, write, and edit files\n- Execute shell commands (bash) — use this for curl, git, package managers\n- Search files and directories\n- Use `bash` to run curl for web requests when needed\n\nWhen a user asks you to do something that requires external data, use your tools. Do NOT say you can't do something if you have a tool that can do it.\n\n## Memory\nYou have a verbatim dialogue log with your user. Use everything from past conversations as context when answering.",
|
"{}\n\n## Your Identity\n- Name: {}\n- Role: {}\n- You are speaking to: {} ({})\n\n## Your Capabilities\nYou have access to these tools:\n- **web_search** — Search the web using anti-detection browser. Use for any online query.\n- **web_browse** — Visit and interact with any website. Use for scraping pages.\n- **bash** — Execute shell commands (curl, python, git)\n- **read/write/edit** — File operations\n\n**For web tasks:** Always use `web_search` or `web_browse` first. If they fail, fall back to `bash` with Python requests.\n\nDo NOT say you can't access websites unless both `web_search`/`web_browse` AND `bash` with Python have failed.\n\n## Memory\nYou have a verbatim dialogue log. Use past conversations as context.\",",
|
||||||
system_prompt,
|
system_prompt,
|
||||||
agent_name,
|
agent_name,
|
||||||
if agent_role.is_empty() { "AI Assistant" } else { &agent_role },
|
if agent_role.is_empty() { "AI Assistant" } else { &agent_role },
|
||||||
@@ -242,18 +272,15 @@ impl AgentLoop {
|
|||||||
// 5. Store conversation turn in memory (transcript + verbatim dialogue)
|
// 5. Store conversation turn in memory (transcript + verbatim dialogue)
|
||||||
{
|
{
|
||||||
let memory = self.memory.lock().await;
|
let memory = self.memory.lock().await;
|
||||||
// Daily transcript
|
|
||||||
memory
|
memory
|
||||||
.transcript_append("user", &user_input)
|
.transcript_append("user", &user_input)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| warn!("Failed to store user message: {e}"));
|
.unwrap_or_else(|e| warn!("Failed to store user message: {e}"));
|
||||||
// Verbatim dialogue — word by word, line by line
|
|
||||||
memory
|
memory
|
||||||
.dialogue_append("user", &user_input)
|
.dialogue_append("user", &user_input)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| warn!("Failed to store user dialogue: {e}"));
|
.unwrap_or_else(|e| warn!("Failed to store user dialogue: {e}"));
|
||||||
|
|
||||||
// Store last assistant response
|
|
||||||
if let Some(last) = messages.last() {
|
if let Some(last) = messages.last() {
|
||||||
if last.role == MessageRole::Assistant {
|
if last.role == MessageRole::Assistant {
|
||||||
memory
|
memory
|
||||||
@@ -291,10 +318,6 @@ impl AgentLoop {
|
|||||||
// ── Non-interactive (gateway / sub-agent mode) ─────────────────────
|
// ── Non-interactive (gateway / sub-agent mode) ─────────────────────
|
||||||
|
|
||||||
/// Run a single conversation turn given an incoming user message.
|
/// Run a single conversation turn given an incoming user message.
|
||||||
/// Returns the assistant response(s) and any tool outputs.
|
|
||||||
///
|
|
||||||
/// This is the entry point used by the gateway (Discord, Telegram, etc.)
|
|
||||||
/// and by sub-agents that receive delegated tasks.
|
|
||||||
pub async fn run_conversation(
|
pub async fn run_conversation(
|
||||||
&self,
|
&self,
|
||||||
message: &str,
|
message: &str,
|
||||||
@@ -303,28 +326,13 @@ impl AgentLoop {
|
|||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
debug!("run_conversation: processing message");
|
debug!("run_conversation: processing message");
|
||||||
|
|
||||||
let mut messages: Vec<Message> =
|
let mut messages = vec![
|
||||||
vec![Message::new(MessageRole::System, &self.config.system_prompt)];
|
Message::new(MessageRole::System, &self.config.system_prompt),
|
||||||
|
Message::new(MessageRole::User, message),
|
||||||
|
];
|
||||||
|
|
||||||
// Load recent context from memory for continuity
|
let (result_messages, output) = self.execute_turn_with_output(messages).await?;
|
||||||
{
|
messages = result_messages;
|
||||||
let memory = self.memory.lock().await;
|
|
||||||
if let Ok(transcript) = memory.get_transcript(4096, 10).await {
|
|
||||||
if !transcript.is_empty() {
|
|
||||||
messages.push(Message::new(
|
|
||||||
MessageRole::System,
|
|
||||||
format!("[Recent conversation context]\n{transcript}"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.push(Message::new(MessageRole::User, message));
|
|
||||||
|
|
||||||
// Execute turn with retry
|
|
||||||
let (final_messages, output) = self
|
|
||||||
.execute_turn_with_output(messages)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Store in memory
|
// Store in memory
|
||||||
{
|
{
|
||||||
@@ -332,17 +340,18 @@ impl AgentLoop {
|
|||||||
memory
|
memory
|
||||||
.transcript_append("user", message)
|
.transcript_append("user", message)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| warn!("Failed to store message: {e}"));
|
.unwrap_or_else(|e| warn!("Failed to store user message: {e}"));
|
||||||
if let Some(last) = final_messages.last() {
|
if let Some(last) = messages.last() {
|
||||||
if last.role == MessageRole::Assistant {
|
if last.role == MessageRole::Assistant {
|
||||||
memory
|
memory
|
||||||
.transcript_append("assistant", &last.content)
|
.transcript_append("assistant", &last.content)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| warn!("Failed to store response: {e}"));
|
.unwrap_or_else(|e| warn!("Failed to store assistant message: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*self.status.lock().await = AgentStatus::Idle;
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,27 +360,13 @@ impl AgentLoop {
|
|||||||
self.status.lock().await.clone()
|
self.status.lock().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide access to tool definitions (e.g. for OpenAI function-calling).
|
// ── Internal methods ───────────────────────────────────────────────
|
||||||
pub fn tool_definitions(&self) -> Vec<gyandoot_core::ToolDefinition> {
|
|
||||||
self.tool_executor.definitions()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provide a reference to the tool executor.
|
|
||||||
pub fn tool_executor(&self) -> &ToolExecutor {
|
|
||||||
&self.tool_executor
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Internal helpers ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Execute a single turn: LLM call → tool calls → LLM follow-up.
|
/// Execute a single turn: LLM call → tool calls → LLM follow-up.
|
||||||
/// Returns the updated message list.
|
|
||||||
async fn execute_turn(&self, mut messages: Vec<Message>) -> AgentResult<Vec<Message>> {
|
async fn execute_turn(&self, mut messages: Vec<Message>) -> AgentResult<Vec<Message>> {
|
||||||
let _limit = self.config.max_tokens;
|
|
||||||
|
|
||||||
// Build context and call LLM (with retry)
|
|
||||||
let context = self
|
let context = self
|
||||||
.context_compressor
|
.context_compressor
|
||||||
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.definitions(), self.config.max_tokens);
|
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.lock().await.definitions(), self.config.max_tokens);
|
||||||
|
|
||||||
*self.status.lock().await = AgentStatus::Thinking;
|
*self.status.lock().await = AgentStatus::Thinking;
|
||||||
let response = self.call_with_retry(context).await?;
|
let response = self.call_with_retry(context).await?;
|
||||||
@@ -379,13 +374,12 @@ impl AgentLoop {
|
|||||||
messages.push(Message::new(MessageRole::Assistant, &response.content)
|
messages.push(Message::new(MessageRole::Assistant, &response.content)
|
||||||
.with_tool_calls(response.tool_calls.clone().unwrap_or_default()));
|
.with_tool_calls(response.tool_calls.clone().unwrap_or_default()));
|
||||||
|
|
||||||
// If there are tool calls, execute them and loop back
|
|
||||||
if let Some(calls) = response.tool_calls {
|
if let Some(calls) = response.tool_calls {
|
||||||
if !calls.is_empty() {
|
if !calls.is_empty() {
|
||||||
*self.status.lock().await = AgentStatus::WaitingForTool;
|
*self.status.lock().await = AgentStatus::WaitingForTool;
|
||||||
for call in calls {
|
for call in calls {
|
||||||
debug!("Executing tool: {}", call.name);
|
debug!("Executing tool: {}", call.name);
|
||||||
let result = self.tool_executor.execute(&call).await.map_err(|e| {
|
let result = self.tool_executor.lock().await.execute(&call).await.map_err(|e| {
|
||||||
AgentError::ToolExecution(format!("{}: {}", call.name, e))
|
AgentError::ToolExecution(format!("{}: {}", call.name, e))
|
||||||
})?;
|
})?;
|
||||||
let output = if result.success {
|
let output = if result.success {
|
||||||
@@ -399,10 +393,9 @@ impl AgentLoop {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Follow-up call with tool results
|
|
||||||
let follow_context = self
|
let follow_context = self
|
||||||
.context_compressor
|
.context_compressor
|
||||||
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.definitions(), self.config.max_tokens);
|
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.lock().await.definitions(), self.config.max_tokens);
|
||||||
|
|
||||||
*self.status.lock().await = AgentStatus::Thinking;
|
*self.status.lock().await = AgentStatus::Thinking;
|
||||||
let follow_up = self.call_with_retry(follow_context).await?;
|
let follow_up = self.call_with_retry(follow_context).await?;
|
||||||
@@ -421,17 +414,14 @@ impl AgentLoop {
|
|||||||
Ok(messages)
|
Ok(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a single turn and return the structured output (for
|
/// Execute a single turn and return structured output.
|
||||||
/// non-interactive callers).
|
|
||||||
async fn execute_turn_with_output(
|
async fn execute_turn_with_output(
|
||||||
&self,
|
&self,
|
||||||
mut messages: Vec<Message>,
|
mut messages: Vec<Message>,
|
||||||
) -> AgentResult<(Vec<Message>, ConversationOutput)> {
|
) -> AgentResult<(Vec<Message>, ConversationOutput)> {
|
||||||
let _limit = self.config.max_tokens;
|
|
||||||
|
|
||||||
let context = self
|
let context = self
|
||||||
.context_compressor
|
.context_compressor
|
||||||
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.definitions(), self.config.max_tokens);
|
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.lock().await.definitions(), self.config.max_tokens);
|
||||||
|
|
||||||
*self.status.lock().await = AgentStatus::Thinking;
|
*self.status.lock().await = AgentStatus::Thinking;
|
||||||
let response = self.call_with_retry(context).await?;
|
let response = self.call_with_retry(context).await?;
|
||||||
@@ -446,7 +436,7 @@ impl AgentLoop {
|
|||||||
if !calls.is_empty() {
|
if !calls.is_empty() {
|
||||||
*self.status.lock().await = AgentStatus::WaitingForTool;
|
*self.status.lock().await = AgentStatus::WaitingForTool;
|
||||||
for call in calls {
|
for call in calls {
|
||||||
let result = self.tool_executor.execute(&call).await.map_err(|e| {
|
let result = self.tool_executor.lock().await.execute(&call).await.map_err(|e| {
|
||||||
AgentError::ToolExecution(format!("{}: {}", call.name, e))
|
AgentError::ToolExecution(format!("{}: {}", call.name, e))
|
||||||
})?;
|
})?;
|
||||||
let output = if result.success {
|
let output = if result.success {
|
||||||
@@ -463,7 +453,7 @@ impl AgentLoop {
|
|||||||
|
|
||||||
let follow_context = self
|
let follow_context = self
|
||||||
.context_compressor
|
.context_compressor
|
||||||
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.definitions(), self.config.max_tokens);
|
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.lock().await.definitions(), self.config.max_tokens);
|
||||||
|
|
||||||
*self.status.lock().await = AgentStatus::Thinking;
|
*self.status.lock().await = AgentStatus::Thinking;
|
||||||
let follow_up = self.call_with_retry(follow_context).await?;
|
let follow_up = self.call_with_retry(follow_context).await?;
|
||||||
@@ -475,16 +465,10 @@ impl AgentLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((messages, ConversationOutput { text: assistant_text, tool_results }))
|
||||||
messages,
|
|
||||||
ConversationOutput {
|
|
||||||
text: assistant_text,
|
|
||||||
tool_results,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call the LLM provider with automatic retry and exponential back-off.
|
/// Call the LLM provider with automatic retry.
|
||||||
async fn call_with_retry(
|
async fn call_with_retry(
|
||||||
&self,
|
&self,
|
||||||
context: ConversationContext,
|
context: ConversationContext,
|
||||||
@@ -498,7 +482,6 @@ impl AgentLoop {
|
|||||||
let error_text = e.to_string();
|
let error_text = e.to_string();
|
||||||
let kind = AgentErrorKind::classify(&error_text);
|
let kind = AgentErrorKind::classify(&error_text);
|
||||||
|
|
||||||
// Non-retryable errors bubble up immediately.
|
|
||||||
if !kind.is_retryable() {
|
if !kind.is_retryable() {
|
||||||
return Err(AgentError::Provider(format!(
|
return Err(AgentError::Provider(format!(
|
||||||
"Non-retryable provider error: {error_text}"
|
"Non-retryable provider error: {error_text}"
|
||||||
@@ -512,21 +495,24 @@ impl AgentLoop {
|
|||||||
self.retry_cfg.max_delay_ms,
|
self.retry_cfg.max_delay_ms,
|
||||||
);
|
);
|
||||||
warn!(
|
warn!(
|
||||||
"Provider call failed (attempt {}/{}, kind={kind:?}), \
|
"Provider call failed (attempt {}/{}, kind={:?}), retrying in {}ms: {}",
|
||||||
retrying in {delay}ms: {error_text}",
|
|
||||||
attempt,
|
attempt,
|
||||||
self.retry_cfg.max_attempts,
|
self.retry_cfg.max_attempts,
|
||||||
|
kind,
|
||||||
|
delay,
|
||||||
|
error_text
|
||||||
);
|
);
|
||||||
tokio::time::sleep(Duration::from_millis(delay)).await;
|
tokio::time::sleep(Duration::from_millis(delay)).await;
|
||||||
last_error = Some(e);
|
last_error = Some(e);
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
"Provider call failed after {} attempts: {error_text}",
|
"Provider call failed after {} attempts: {}",
|
||||||
self.retry_cfg.max_attempts
|
self.retry_cfg.max_attempts,
|
||||||
|
error_text
|
||||||
);
|
);
|
||||||
return Err(AgentError::Provider(format!(
|
return Err(AgentError::Provider(format!(
|
||||||
"Failed after {} retries: {error_text}",
|
"Failed after {} retries: {error_text}",
|
||||||
self.retry_cfg.max_attempts
|
self.retry_cfg.max_attempts - 1
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,61 +520,36 @@ impl AgentLoop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(AgentError::Provider(format!(
|
Err(AgentError::Provider(format!(
|
||||||
"Exhausted retries: {:?}",
|
"Unexpected retry exit: {:?}",
|
||||||
last_error.map(|e| e.to_string())
|
last_error
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a single line from stdin with a prompt.
|
/// Read a line of input from stdin.
|
||||||
async fn read_input(&self) -> anyhow::Result<String> {
|
async fn read_input(&self) -> anyhow::Result<String> {
|
||||||
use tokio::io::AsyncBufReadExt;
|
use tokio::io::AsyncBufReadExt;
|
||||||
use tokio::io::AsyncWriteExt;
|
let mut line = String::new();
|
||||||
|
let mut reader = tokio::io::BufReader::new(tokio::io::stdin());
|
||||||
let mut stdout = tokio::io::stdout();
|
reader.read_line(&mut line).await?;
|
||||||
stdout.write_all(b"\n> ").await?;
|
Ok(line.trim().to_string())
|
||||||
stdout.flush().await?;
|
|
||||||
|
|
||||||
let stdin = tokio::io::stdin();
|
|
||||||
let reader = tokio::io::BufReader::new(stdin);
|
|
||||||
let mut lines = reader.lines();
|
|
||||||
let mut input = String::new();
|
|
||||||
if let Some(line) = lines.next_line().await? {
|
|
||||||
input = line;
|
|
||||||
}
|
|
||||||
Ok(input)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the user wants to exit.
|
||||||
fn should_exit(&self, input: &str) -> bool {
|
fn should_exit(&self, input: &str) -> bool {
|
||||||
matches!(
|
matches!(input.to_lowercase().as_str(), "exit" | "quit" | "bye")
|
||||||
input.trim().to_lowercase().as_str(),
|
|
||||||
"exit" | "quit" | "/exit" | "/quit"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display output to stdout.
|
||||||
fn display_output(&self, output: &str) {
|
fn display_output(&self, output: &str) {
|
||||||
println!("\n{}", output);
|
println!("\n{}", output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── ConversationOutput (for non-interactive callers) ───────────────────
|
/// Calculate exponential backoff with jitter.
|
||||||
|
|
||||||
/// The result of a single `run_conversation` call.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ConversationOutput {
|
|
||||||
/// The final assistant response text.
|
|
||||||
pub text: String,
|
|
||||||
/// Any tool outputs generated during the turn.
|
|
||||||
pub tool_results: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Backoff calculation ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Calculate exponential back-off with jitter.
|
|
||||||
fn calculate_backoff(attempt: u32, base_ms: u64, max_ms: u64) -> u64 {
|
fn calculate_backoff(attempt: u32, base_ms: u64, max_ms: u64) -> u64 {
|
||||||
let delay = base_ms.saturating_mul(2u64.saturating_pow(attempt.saturating_sub(1)));
|
let exponential = base_ms * 2u64.pow(attempt.saturating_sub(1));
|
||||||
let clamped = delay.min(max_ms);
|
let clamped = exponential.min(max_ms);
|
||||||
// Add small jitter (±10%)
|
let jitter = rand::random::<f64>() * 0.2 - 0.1;
|
||||||
let jitter = rand::random::<f64>() * 0.2 - 0.1; // -10% to +10%
|
|
||||||
(clamped as f64 * (1.0 + jitter)) as u64
|
(clamped as f64 * (1.0 + jitter)) as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,69 +601,70 @@ fn create_provider(config: &AgentConfig) -> Box<dyn Provider + Send + Sync> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use gyandoot_core::config::IdentitySection;
|
||||||
|
use gyandoot_core::{
|
||||||
|
config::AgentConfig, config::KanbanRootConfig, config::MemoryRootConfig,
|
||||||
|
config::PlatformRootConfig, config::ProviderRootConfig, config::SecurityRootConfig,
|
||||||
|
config::SkillsRootConfig, Message, MessageRole, Role, ToolCall, ToolDefinition,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
fn test_config() -> AgentConfig {
|
||||||
fn test_error_classification() {
|
AgentConfig {
|
||||||
assert_eq!(AgentErrorKind::classify("rate limit exceeded"), AgentErrorKind::RateLimit);
|
name: "test-agent".to_string(),
|
||||||
assert_eq!(AgentErrorKind::classify("401 unauthorized"), AgentErrorKind::Auth);
|
model: "gpt-4o".to_string(),
|
||||||
assert_eq!(AgentErrorKind::classify("timeout waiting"), AgentErrorKind::Timeout);
|
max_tokens: 4096,
|
||||||
assert_eq!(AgentErrorKind::classify("500 server error"), AgentErrorKind::ServerError);
|
temperature: 0.7,
|
||||||
assert_eq!(AgentErrorKind::classify("context length exceeded"), AgentErrorKind::ContextOverflow);
|
system_prompt: "You are a test agent.".to_string(),
|
||||||
assert_eq!(AgentErrorKind::classify("unknown error"), AgentErrorKind::Other);
|
provider: ProviderRootConfig::default(),
|
||||||
|
platform: PlatformRootConfig::default(),
|
||||||
|
memory: MemoryRootConfig::default(),
|
||||||
|
security: SecurityRootConfig::default(),
|
||||||
|
kanban: KanbanRootConfig::default(),
|
||||||
|
skills: SkillsRootConfig::default(),
|
||||||
|
identity: IdentitySection::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_retryable_classification() {
|
fn test_retry_config_defaults() {
|
||||||
assert!(AgentErrorKind::RateLimit.is_retryable());
|
let cfg = RetryConfig::default();
|
||||||
assert!(AgentErrorKind::Timeout.is_retryable());
|
assert_eq!(cfg.max_attempts, 3);
|
||||||
assert!(!AgentErrorKind::Auth.is_retryable());
|
assert!(cfg.base_delay_ms > 0);
|
||||||
assert!(!AgentErrorKind::ContextOverflow.is_retryable());
|
assert!(cfg.max_delay_ms > 0);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backoff_is_monotonic() {
|
|
||||||
let d1 = calculate_backoff(1, 1000, 30_000);
|
|
||||||
let d2 = calculate_backoff(2, 1000, 30_000);
|
|
||||||
let d3 = calculate_backoff(3, 1000, 30_000);
|
|
||||||
// With jitter these might not be strictly monotonic, but should be roughly so.
|
|
||||||
assert!(d1 >= 900 && d1 <= 1100); // ~1000ms ±10%
|
|
||||||
assert!(d2 >= 1800 && d2 <= 2200); // ~2000ms ±10%
|
|
||||||
assert!(d3 >= 3600 && d3 <= 4400); // ~4000ms ±10%
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_estimate_tokens() {
|
fn test_estimate_tokens() {
|
||||||
assert_eq!(estimate_tokens("abcd"), 1);
|
assert_eq!(estimate_tokens("abcd"), 1);
|
||||||
assert_eq!(estimate_tokens(""), 1); // at least 1
|
assert_eq!(estimate_tokens(""), 1);
|
||||||
assert!(estimate_tokens("hello world this is a test") > 3);
|
let long = "a".repeat(400);
|
||||||
|
assert_eq!(estimate_tokens(&long), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_exit() {
|
fn test_calculate_backoff() {
|
||||||
let config = AgentConfig::default();
|
let delay = calculate_backoff(1, 1000, 10000);
|
||||||
let provider = gyandoot_providers::openai::OpenAIProvider::new(
|
assert!(delay >= 900 && delay <= 1100);
|
||||||
"test".to_string(),
|
|
||||||
"gpt-4o".to_string(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
let tool_executor = ToolExecutor::with_defaults();
|
|
||||||
let context_compressor = ContextCompressor::new(4096);
|
|
||||||
|
|
||||||
let loop_ = AgentLoop {
|
let delay3 = calculate_backoff(3, 1000, 10000);
|
||||||
config,
|
assert!(delay3 <= 10000);
|
||||||
provider: Box::new(provider),
|
}
|
||||||
tool_executor,
|
|
||||||
context_compressor,
|
|
||||||
memory: Arc::new(Mutex::new(MemPalace::new("default"))),
|
|
||||||
_skills_manager: SkillsManager::new(),
|
|
||||||
sanitizer: Sanitizer::new(gyandoot_security::sanitizer::SanitizerConfig::default()),
|
|
||||||
status: Arc::new(Mutex::new(AgentStatus::Idle)),
|
|
||||||
retry_cfg: RetryConfig::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(loop_.should_exit("exit"));
|
#[test]
|
||||||
assert!(loop_.should_exit("quit"));
|
fn test_error_classification() {
|
||||||
assert!(loop_.should_exit(" /exit "));
|
assert_eq!(AgentErrorKind::classify("rate limit exceeded"), AgentErrorKind::RateLimit);
|
||||||
assert!(!loop_.should_exit("hello"));
|
assert_eq!(AgentErrorKind::classify("401 Unauthorized"), AgentErrorKind::Auth);
|
||||||
|
assert_eq!(AgentErrorKind::classify("timeout after 30s"), AgentErrorKind::Timeout);
|
||||||
|
assert_eq!(AgentErrorKind::classify("500 Internal Server Error"), AgentErrorKind::ServerError);
|
||||||
|
assert_eq!(AgentErrorKind::classify("unknown error"), AgentErrorKind::Other);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_retryable() {
|
||||||
|
assert!(AgentErrorKind::RateLimit.is_retryable());
|
||||||
|
assert!(AgentErrorKind::Timeout.is_retryable());
|
||||||
|
assert!(AgentErrorKind::ServerError.is_retryable());
|
||||||
|
assert!(!AgentErrorKind::Auth.is_retryable());
|
||||||
|
assert!(!AgentErrorKind::ContextOverflow.is_retryable());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
174
crates/gyandoot-agent/src/browser_manager.rs
Normal file
174
crates/gyandoot-agent/src/browser_manager.rs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
//! BrowserManager — Auto-start, health-check, and lifecycle for Camoufox.
|
||||||
|
//!
|
||||||
|
//! On `gyandoot run`, the BrowserManager:
|
||||||
|
//! 1. Checks if Camoufox is already running (health endpoint)
|
||||||
|
//! 2. If not, starts it as a background child process
|
||||||
|
//! 3. Waits for it to become healthy (up to 30 seconds)
|
||||||
|
//! 4. Returns a ready-to-use `BrowserClient`
|
||||||
|
//!
|
||||||
|
//! Camoufox is installed globally via `npx camoufox-browser` by `install.sh`.
|
||||||
|
|
||||||
|
use std::process::Stdio;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tracing::{info, warn, error};
|
||||||
|
|
||||||
|
use gyandoot_browser::BrowserClient;
|
||||||
|
|
||||||
|
/// Default Camoufox API endpoint.
|
||||||
|
const CAMOFOX_URL: &str = "http://localhost:9377";
|
||||||
|
|
||||||
|
/// Maximum time to wait for Camoufox to become healthy.
|
||||||
|
const STARTUP_TIMEOUT_SECS: u64 = 30;
|
||||||
|
|
||||||
|
/// Health-check retry interval.
|
||||||
|
const HEALTH_RETRY_MS: u64 = 500;
|
||||||
|
|
||||||
|
/// Result of browser initialization.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BrowserInitResult {
|
||||||
|
/// Browser is running and ready.
|
||||||
|
Ready(BrowserClient),
|
||||||
|
/// Could not start browser (user needs to install/start manually).
|
||||||
|
Unavailable(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the Camoufox browser: check health, start if needed.
|
||||||
|
///
|
||||||
|
/// Returns a `BrowserInitResult` — either a ready client or an explanation.
|
||||||
|
pub async fn init_browser() -> BrowserInitResult {
|
||||||
|
let client = BrowserClient::new();
|
||||||
|
|
||||||
|
// 1. Quick health check — is it already running?
|
||||||
|
match try_health_check(&client).await {
|
||||||
|
Ok(true) => {
|
||||||
|
info!("Camoufox already running on localhost:9377");
|
||||||
|
return BrowserInitResult::Ready(client);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
info!("Camoufox health check returned not-ready, attempting to start...");
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
info!("Camoufox not running, attempting to start...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try to start Camoufox as a background process
|
||||||
|
match start_camoufox().await {
|
||||||
|
Ok(()) => {
|
||||||
|
// 3. Wait for it to become healthy
|
||||||
|
info!("Waiting for Camoufox to become healthy...");
|
||||||
|
match wait_for_health(&client).await {
|
||||||
|
Ok(true) => {
|
||||||
|
info!("Camoufox started successfully");
|
||||||
|
return BrowserInitResult::Ready(client);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
let msg = format!(
|
||||||
|
"Camoufox process started but did not become healthy within {}s. \
|
||||||
|
Check the logs at ~/.gyandoot/camoufox.log",
|
||||||
|
STARTUP_TIMEOUT_SECS
|
||||||
|
);
|
||||||
|
warn!("{}", msg);
|
||||||
|
return BrowserInitResult::Unavailable(msg);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let msg = format!("Camoufox health-check error after startup: {}", e);
|
||||||
|
warn!("{}", msg);
|
||||||
|
return BrowserInitResult::Unavailable(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let msg = format!(
|
||||||
|
"Could not start Camoufox: {}\n\n\
|
||||||
|
Install it manually:\n npm install -g camoufox-browser\n \
|
||||||
|
npx camoufox-browser@latest install\n\n\
|
||||||
|
Then start before running gyandoot:\n npx camoufox-browser start &",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
warn!("{}", msg);
|
||||||
|
return BrowserInitResult::Unavailable(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quick health check — returns true if browser is connected and running.
|
||||||
|
async fn try_health_check(client: &BrowserClient) -> Result<bool, String> {
|
||||||
|
match tokio::time::timeout(Duration::from_secs(3), client.health()).await {
|
||||||
|
Ok(Ok(health)) => {
|
||||||
|
Ok(health.browser_connected && health.browser_running)
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => Err(e.to_string()),
|
||||||
|
Err(_) => Err("timeout".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start Camoufox as a background child process.
|
||||||
|
async fn start_camoufox() -> Result<(), String> {
|
||||||
|
// Create log directory
|
||||||
|
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
|
||||||
|
let log_dir = home.join(".gyandoot");
|
||||||
|
std::fs::create_dir_all(&log_dir).map_err(|e| format!("Cannot create {}: {}", log_dir.display(), e))?;
|
||||||
|
let log_file = log_dir.join("camoufox.log");
|
||||||
|
|
||||||
|
let log = std::fs::File::create(&log_file)
|
||||||
|
.map_err(|e| format!("Cannot create log file {}: {}", log_file.display(), e))?;
|
||||||
|
|
||||||
|
// Try npx first (npm global install), then npx direct
|
||||||
|
let result = Command::new("npx")
|
||||||
|
.args(&[
|
||||||
|
"camoufox-browser@latest",
|
||||||
|
"start",
|
||||||
|
"--port", "9377",
|
||||||
|
])
|
||||||
|
.stdout(Stdio::from(log.try_clone().map_err(|e| e.to_string())?))
|
||||||
|
.stderr(Stdio::from(log))
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.spawn();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(mut child) => {
|
||||||
|
// Detach — let it run in background
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let status = child.wait().await;
|
||||||
|
if let Ok(s) = status {
|
||||||
|
if !s.success() {
|
||||||
|
warn!("Camoufox process exited with status: {}", s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Give it a moment to bind
|
||||||
|
tokio::time::sleep(Duration::from_millis(1500)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// npx not available — try directly via node_modules
|
||||||
|
Err(format!("npx failed: {}. Is Node.js installed?", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll the health endpoint until the browser is ready or timeout.
|
||||||
|
async fn wait_for_health(client: &BrowserClient) -> Result<bool, String> {
|
||||||
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(STARTUP_TIMEOUT_SECS);
|
||||||
|
let mut last_error = String::new();
|
||||||
|
|
||||||
|
while tokio::time::Instant::now() < deadline {
|
||||||
|
match try_health_check(client).await {
|
||||||
|
Ok(true) => return Ok(true),
|
||||||
|
Ok(false) => {
|
||||||
|
last_error = "browser not connected".to_string();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
last_error = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(HEALTH_RETRY_MS)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!(
|
||||||
|
"Timed out after {}s. Last status: {}",
|
||||||
|
STARTUP_TIMEOUT_SECS, last_error
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
//! needed, and interacting with memory.
|
//! needed, and interacting with memory.
|
||||||
|
|
||||||
pub mod agent_loop;
|
pub mod agent_loop;
|
||||||
|
pub mod browser_manager;
|
||||||
pub mod browser_tools;
|
pub mod browser_tools;
|
||||||
pub mod tools;
|
pub mod tools;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
|||||||
91
install.sh
91
install.sh
@@ -61,72 +61,99 @@ install_node_if_needed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_camofox_browser() {
|
install_camofox_browser() {
|
||||||
local root_dir
|
info "Installing camofox-browser (anti-detection browser)..."
|
||||||
root_dir="$(cd "$(dirname "$0")" && pwd)"
|
if command -v npx &>/dev/null; then
|
||||||
local camofox_dir="${CAMOFOX_DIR:-$HOME/camofox-browser}"
|
info "Installing via npm..."
|
||||||
if [ -d "$camofox_dir" ] && [ -f "$camofox_dir/package.json" ]; then
|
npm install -g camoufox-browser 2>&1 | tail -5
|
||||||
info "Camofox-browser already cloned. Updating..."
|
npx camoufox-browser@latest install 2>&1 | tail -5
|
||||||
cd "$camofox_dir" && git pull --ff-only 2>/dev/null || true
|
ok "Camoufox browser installed (npx camoufox-browser start)"
|
||||||
cd "$root_dir"
|
elif command -v docker &>/dev/null; then
|
||||||
|
info "Installing via Docker..."
|
||||||
|
docker run -d --name camoufox -p 9377:9377 \
|
||||||
|
-e CAMOUFOX_WS_PORT=9377 \
|
||||||
|
--restart unless-stopped \
|
||||||
|
ghcr.io/nousresearch/camoufox-browser:latest 2>&1 | tail -3
|
||||||
|
ok "Camoufox browser installed (Docker)"
|
||||||
else
|
else
|
||||||
info "Cloning camofox-browser..."
|
warn "Neither npm nor docker found. Install manually: npm install -g camoufox-browser"
|
||||||
git clone https://github.com/jo-inc/camofox-browser.git "$camofox_dir"
|
|
||||||
info "Installing camofox-browser dependencies..."
|
|
||||||
cd "$camofox_dir"
|
|
||||||
npm install --ignore-scripts 2>&1 | tail -3
|
|
||||||
cd "$root_dir"
|
|
||||||
fi
|
fi
|
||||||
ok "Camofox-browser ready"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_camofox_service() {
|
setup_camofox_service() {
|
||||||
local os
|
local os
|
||||||
os=$(detect_os)
|
os=$(detect_os)
|
||||||
|
|
||||||
info "Setting up camofox-browser as a background service..."
|
info "Setting up camofox-browser to auto-start..."
|
||||||
case "$os" in
|
case "$os" in
|
||||||
linux)
|
linux)
|
||||||
# Create systemd service
|
|
||||||
local service_file="/etc/systemd/system/camofox-browser.service"
|
local service_file="/etc/systemd/system/camofox-browser.service"
|
||||||
if [ ! -f "$service_file" ]; then
|
if [ ! -f "$service_file" ]; then
|
||||||
cat > /tmp/camofox-browser.service << 'SYSTEMD'
|
cat > /tmp/camofox-browser.service << 'SYSTEMD'
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Camofox Browser — Anti-detection browser server for AI agents
|
Description=Camofox Browser — Anti-detection browser for AI agents
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=%s
|
ExecStart=npx camoufox-browser start --port 9377
|
||||||
WorkingDirectory=%s
|
|
||||||
ExecStart=/usr/bin/node server.js
|
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
Environment=PORT=9377
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
SYSTEMD
|
SYSTEMD
|
||||||
local user
|
|
||||||
user=$(whoami)
|
|
||||||
local camofox_dir="${CAMOFOX_DIR:-$HOME/camofox-browser}"
|
|
||||||
sed -i "s|%s|$user|g; s|%s|$camofox_dir|g" /tmp/camofox-browser.service
|
|
||||||
sudo mv /tmp/camofox-browser.service "$service_file"
|
sudo mv /tmp/camofox-browser.service "$service_file"
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable camofox-browser
|
sudo systemctl enable camofox-browser
|
||||||
info "Camofox-browser service created. Start with: sudo systemctl start camofox-browser"
|
sudo systemctl start camofox-browser
|
||||||
|
ok "Camoufox auto-start service installed and started"
|
||||||
else
|
else
|
||||||
ok "Camofox-browser service already exists"
|
sudo systemctl start camofox-browser 2>/dev/null || true
|
||||||
|
ok "Camoufox service already exists"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
darwin)
|
darwin)
|
||||||
info "macOS detected. Add 'cd $HOME/camofox-browser && npm start' to your startup items."
|
# macOS: create a launchd plist for auto-start
|
||||||
;;
|
local plist="$HOME/Library/LaunchAgents/com.camoufox.browser.plist"
|
||||||
windows)
|
if [ ! -f "$plist" ]; then
|
||||||
info "Windows detected. Use 'npm start' in the camofox-browser directory."
|
mkdir -p "$HOME/Library/LaunchAgents"
|
||||||
|
local home_dir="$HOME"
|
||||||
|
cat > "$plist" << LAUNCHD
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.camoufox.browser</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/local/bin/npx</string>
|
||||||
|
<string>camoufox-browser</string>
|
||||||
|
<string>start</string>
|
||||||
|
<string>--port</string>
|
||||||
|
<string>9377</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>${home_dir}/.gyandoot/camoufox.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>${home_dir}/.gyandoot/camoufox.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
LAUNCHD
|
||||||
|
launchctl load "$plist"
|
||||||
|
ok "Camoufox auto-start configured (launchd)"
|
||||||
|
else
|
||||||
|
launchctl load "$plist" 2>/dev/null || true
|
||||||
|
ok "Camoufox launchd plist already exists"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
ok "Camofox-browser setup complete"
|
ok "Camofox-browser auto-start setup complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
build_release() {
|
build_release() {
|
||||||
|
|||||||
Reference in New Issue
Block a user