mirror of
https://github.com/block/goose.git
synced 2026-10-19 09:34:12 +00:00
feat: a lean ACP-only Goose binary (#11961)
This commit is contained in:
parent
201837dffc
commit
e678c3b64a
19 changed files with 505 additions and 219 deletions
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
|
|
@ -111,6 +111,30 @@ jobs:
|
|||
env:
|
||||
RUST_MIN_STACK: 8388608
|
||||
|
||||
rust-build-lean:
|
||||
name: Build Lean ACP Binary
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true' || github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
|
||||
|
||||
- name: Cache Cargo artifacts
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: lean-acp
|
||||
|
||||
- name: Build lean configuration and check its size budget
|
||||
run: |
|
||||
source ./bin/activate-hermit
|
||||
# use the non-hermit rust toolchain because the rust-cache action does not
|
||||
# play nicely with hermit-managed rust
|
||||
hermit uninstall rustup
|
||||
just check-lean-size
|
||||
|
||||
goose-sdk-uniffi:
|
||||
name: Check goose-sdk UniFFI
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -177,10 +201,10 @@ jobs:
|
|||
run: |
|
||||
gnome-keyring-daemon --components=secrets --daemonize --unlock <<< 'foobar'
|
||||
export CARGO_INCREMENTAL=0
|
||||
cargo test -p goose --no-default-features --features ${{ matrix.tls-feature }},code-mode,live-voice
|
||||
cargo test -p goose --no-default-features --features ${{ matrix.tls-feature }},code-mode,tree-sitter,live-voice,scheduler,platform-apps,chat-recall,acp-http
|
||||
cargo test -p goose-providers --no-default-features --features ${{ matrix.tls-feature }},live-websocket
|
||||
cargo test -p goose-cli --no-default-features --features ${{ matrix.tls-feature }},code-mode,live-voice -- --skip scenario_tests::scenarios::tests
|
||||
cargo test -p goose-cli --no-default-features --features ${{ matrix.tls-feature }},code-mode,live-voice --jobs 1 scenario_tests::scenarios::tests
|
||||
cargo test -p goose-cli --no-default-features --features ${{ matrix.tls-feature }},code-mode,tree-sitter,live-voice,scheduler,platform-apps,chat-recall,acp-http -- --skip scenario_tests::scenarios::tests
|
||||
cargo test -p goose-cli --no-default-features --features ${{ matrix.tls-feature }},code-mode,tree-sitter,live-voice,scheduler,platform-apps,chat-recall,acp-http --jobs 1 scenario_tests::scenarios::tests
|
||||
env:
|
||||
RUST_MIN_STACK: 8388608
|
||||
|
||||
|
|
|
|||
|
|
@ -126,3 +126,11 @@ cudaforge = { git = "https://github.com/jbg/cudaforge", rev = "e7c1967340e40673d
|
|||
# full debug info and stay fully debuggable; release builds are unaffected.
|
||||
[profile.dev.package."*"]
|
||||
debug = false
|
||||
|
||||
[profile.lean]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = "symbols"
|
||||
|
|
|
|||
33
Justfile
33
Justfile
|
|
@ -165,6 +165,39 @@ check-acp-artifacts: generate-acp-types generate-acp-docs
|
|||
fi
|
||||
echo "✅ Generated ACP artifacts are up-to-date"
|
||||
|
||||
# Build the lean ACP binary
|
||||
build-lean:
|
||||
cargo build -p goose --bin goose-acp \
|
||||
--profile lean \
|
||||
--no-default-features \
|
||||
--features native-tls
|
||||
|
||||
# Budgets are per-platform. ELF carries several MiB that Mach-O does not for the
|
||||
# same code: DWARF .eh_frame instead of compact unwind info, and a .rela.dyn
|
||||
# table of relative relocations that Mach-O encodes as LINKEDIT rebase opcodes.
|
||||
# A single cross-platform number would either be unreachable on Linux or
|
||||
# useless as a regression signal on macOS.
|
||||
|
||||
# Enforce the lean binary's size budget (override with GOOSE_LEAN_MAX_BYTES)
|
||||
check-lean-size: build-lean
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
case "$(uname -s)" in
|
||||
Darwin) default_max_bytes=17825792 ;;
|
||||
*) default_max_bytes=23068672 ;;
|
||||
esac
|
||||
max_bytes="${GOOSE_LEAN_MAX_BYTES:-$default_max_bytes}"
|
||||
|
||||
binary="target/lean/goose-acp"
|
||||
bytes=$(wc -c < "$binary" | tr -d '[:space:]')
|
||||
mib=$(awk -v bytes="$bytes" 'BEGIN { printf "%.2f", bytes / 1024 / 1024 }')
|
||||
printf '%s: %s bytes (%s MiB)\n' "$binary" "$bytes" "$mib"
|
||||
|
||||
if (( bytes > max_bytes )); then
|
||||
printf 'lean binary exceeds budget of %s bytes; set GOOSE_LEAN_MAX_BYTES to override\n' "$max_bytes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate ACP JSON schema from Rust types
|
||||
generate-acp-schema:
|
||||
@echo "Generating ACP schema..."
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ path = "src/bin/mcp_conformance_driver.rs"
|
|||
clap_mangen = { version = "0.3", default-features = false }
|
||||
goose = { path = "../goose", default-features = false }
|
||||
agent-client-protocol = { workspace = true, features = ["unstable"], optional = true }
|
||||
goose-mcp = { path = "../goose-mcp", default-features = false }
|
||||
goose-mcp = { path = "../goose-mcp", default-features = false, optional = true }
|
||||
goose-providers = { path = "../goose-providers", default-features = false }
|
||||
goose-roaming = { path = "../goose-roaming", optional = true }
|
||||
rmcp = { workspace = true }
|
||||
|
|
@ -94,6 +94,7 @@ libc = { version = "0.2.182", default-features = false, features = ["std"] }
|
|||
[features]
|
||||
default = [
|
||||
"code-mode",
|
||||
"tree-sitter",
|
||||
"local-inference",
|
||||
"aws-providers",
|
||||
"telemetry",
|
||||
|
|
@ -103,9 +104,15 @@ default = [
|
|||
"live-voice",
|
||||
"system-keyring",
|
||||
"update",
|
||||
"bundled-mcp",
|
||||
"scheduler",
|
||||
"platform-apps",
|
||||
"chat-recall",
|
||||
"acp-http",
|
||||
]
|
||||
roaming = ["dep:goose-roaming", "dep:qrcode", "dep:fs2", "dep:agent-client-protocol"]
|
||||
code-mode = ["goose/code-mode"]
|
||||
tree-sitter = ["goose/tree-sitter"]
|
||||
live-voice = ["goose/live-voice"]
|
||||
local-inference = ["goose/local-inference"]
|
||||
aws-providers = ["goose/aws-providers"]
|
||||
|
|
@ -117,7 +124,24 @@ nostr = ["goose/nostr"]
|
|||
otel = ["goose/otel"]
|
||||
system-keyring = ["goose/system-keyring"]
|
||||
update = ["dep:sigstore-verify", "dep:snap"]
|
||||
portable-default = ["rustls-tls", "live-voice", "aws-providers", "telemetry", "otel"]
|
||||
bundled-mcp = ["dep:goose-mcp", "goose-mcp/bundled-mcp"]
|
||||
scheduler = ["goose/scheduler"]
|
||||
platform-apps = ["goose/platform-apps"]
|
||||
chat-recall = ["goose/chat-recall"]
|
||||
acp-http = ["goose/acp-http"]
|
||||
portable-default = [
|
||||
"rustls-tls",
|
||||
"tree-sitter",
|
||||
"live-voice",
|
||||
"aws-providers",
|
||||
"telemetry",
|
||||
"otel",
|
||||
"bundled-mcp",
|
||||
"scheduler",
|
||||
"platform-apps",
|
||||
"chat-recall",
|
||||
"acp-http",
|
||||
]
|
||||
# disables the update command
|
||||
disable-update = []
|
||||
rustls-tls = [
|
||||
|
|
@ -126,7 +150,7 @@ rustls-tls = [
|
|||
"axum-server/tls-rustls",
|
||||
"sigstore-verify?/rustls",
|
||||
"goose/rustls-tls",
|
||||
"goose-mcp/rustls-tls",
|
||||
"goose-mcp?/rustls-tls",
|
||||
"goose-providers/rustls-tls",
|
||||
]
|
||||
native-tls = [
|
||||
|
|
@ -135,7 +159,7 @@ native-tls = [
|
|||
"axum-server/tls-openssl",
|
||||
"sigstore-verify?/native-tls",
|
||||
"goose/native-tls",
|
||||
"goose-mcp/native-tls",
|
||||
"goose-mcp?/native-tls",
|
||||
"goose-providers/native-tls",
|
||||
]
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ use clap::{Args, CommandFactory, Parser, Subcommand};
|
|||
use clap_complete::{generate, Shell as ClapShell};
|
||||
use clap_complete_nushell::Nushell as ClapNushell;
|
||||
use goose::agents::GoosePlatform;
|
||||
#[cfg(feature = "bundled-mcp")]
|
||||
use goose::builtin_extension::register_builtin_extensions;
|
||||
use goose::config::{Config, GooseMode};
|
||||
#[cfg(feature = "telemetry")]
|
||||
use goose::posthog::get_telemetry_choice;
|
||||
use goose::recipe::Recipe;
|
||||
#[cfg(feature = "acp-http")]
|
||||
use goose::source_roots::SourceRoot;
|
||||
#[cfg(feature = "bundled-mcp")]
|
||||
use goose_mcp::mcp_server_runner::{serve, McpCommand};
|
||||
#[cfg(feature = "bundled-mcp")]
|
||||
use goose_mcp::{AutoVisualiserRouter, ComputerControllerServer, MemoryServer, TutorialServer};
|
||||
|
||||
#[cfg(feature = "telemetry")]
|
||||
|
|
@ -24,6 +28,7 @@ use crate::commands::term::{
|
|||
handle_term_info, handle_term_init, handle_term_log, handle_term_run, Shell,
|
||||
};
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
use crate::commands::schedule::{
|
||||
handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove,
|
||||
handle_schedule_run_now, handle_schedule_services_status, handle_schedule_services_stop,
|
||||
|
|
@ -39,8 +44,10 @@ use goose::session::session_manager::SessionType;
|
|||
use goose::session::SessionManager;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(feature = "acp-http")]
|
||||
const GOOSE_SERVER_SECRET_KEY_ENV: &str = "GOOSE_SERVER__SECRET_KEY";
|
||||
|
||||
#[cfg(feature = "acp-http")]
|
||||
fn generate_serve_secret_key() -> String {
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
|
||||
|
|
@ -605,6 +612,7 @@ enum SessionCommand {
|
|||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum SchedulerCommand {
|
||||
#[command(about = "Add a new scheduled job")]
|
||||
|
|
@ -819,6 +827,7 @@ enum Command {
|
|||
Doctor {},
|
||||
|
||||
/// Manage system prompts and behaviors
|
||||
#[cfg(feature = "bundled-mcp")]
|
||||
#[command(about = "Run one of the mcp servers bundled with goose")]
|
||||
Mcp {
|
||||
#[arg(value_parser = clap::value_parser!(McpCommand))]
|
||||
|
|
@ -851,6 +860,7 @@ enum Command {
|
|||
},
|
||||
|
||||
/// Start ACP server over HTTP and WebSocket
|
||||
#[cfg(feature = "acp-http")]
|
||||
#[command(about = "Start ACP server over HTTP and WebSocket")]
|
||||
Serve {
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
|
|
@ -1020,6 +1030,7 @@ enum Command {
|
|||
},
|
||||
|
||||
/// Manage scheduled jobs
|
||||
#[cfg(feature = "scheduler")]
|
||||
#[command(about = "Manage scheduled jobs", visible_alias = "sched")]
|
||||
Schedule {
|
||||
#[command(subcommand)]
|
||||
|
|
@ -1381,14 +1392,17 @@ fn get_command_name(command: &Option<Command>) -> &'static str {
|
|||
Some(Command::Configure {}) => "configure",
|
||||
Some(Command::Doctor {}) => "doctor",
|
||||
Some(Command::Info { .. }) => "info",
|
||||
#[cfg(feature = "bundled-mcp")]
|
||||
Some(Command::Mcp { .. }) => "mcp",
|
||||
Some(Command::Acp { .. }) => "acp",
|
||||
#[cfg(feature = "roaming")]
|
||||
Some(Command::Roam { .. }) => "roam",
|
||||
#[cfg(feature = "acp-http")]
|
||||
Some(Command::Serve { .. }) => "serve",
|
||||
Some(Command::Session { .. }) => "session",
|
||||
Some(Command::Run { .. }) => "run",
|
||||
Some(Command::Gateway { .. }) => "gateway",
|
||||
#[cfg(feature = "scheduler")]
|
||||
Some(Command::Schedule { .. }) => "schedule",
|
||||
#[cfg(feature = "update")]
|
||||
Some(Command::Update { .. }) => "update",
|
||||
|
|
@ -1611,6 +1625,7 @@ async fn handle_mcp_probe(extension_command: String, script_path: Option<String>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "bundled-mcp")]
|
||||
async fn handle_mcp_command(server: McpCommand) -> Result<()> {
|
||||
let name = server.name();
|
||||
let _ = crate::logging::setup_logging(Some(&format!("mcp-{name}")));
|
||||
|
|
@ -1623,6 +1638,7 @@ async fn handle_mcp_command(server: McpCommand) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "acp-http")]
|
||||
struct ServeCommandArgs {
|
||||
host: String,
|
||||
|
||||
|
|
@ -1758,6 +1774,7 @@ async fn start_roam_share(
|
|||
Ok(node)
|
||||
}
|
||||
|
||||
#[cfg(feature = "acp-http")]
|
||||
async fn handle_serve_command(args: ServeCommandArgs) -> Result<()> {
|
||||
use axum::http::HeaderValue;
|
||||
use goose::acp::server::AcpBuiltinSelection;
|
||||
|
|
@ -2355,6 +2372,7 @@ async fn handle_gateway_command(command: GatewayCommand) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
async fn handle_schedule_command(command: SchedulerCommand) -> Result<()> {
|
||||
match command {
|
||||
SchedulerCommand::Add {
|
||||
|
|
@ -2779,6 +2797,7 @@ async fn handle_default_session() -> Result<()> {
|
|||
}
|
||||
|
||||
pub async fn cli() -> anyhow::Result<()> {
|
||||
#[cfg(feature = "bundled-mcp")]
|
||||
register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone());
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
|
@ -2799,6 +2818,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||
Some(Command::Configure {}) => handle_configure().await,
|
||||
Some(Command::Doctor {}) => crate::commands::doctor::handle_doctor().await,
|
||||
Some(Command::Info { verbose, check }) => handle_info(verbose, check).await,
|
||||
#[cfg(feature = "bundled-mcp")]
|
||||
Some(Command::Mcp { server }) => handle_mcp_command(server).await,
|
||||
Some(Command::Acp {
|
||||
builtins,
|
||||
|
|
@ -2806,6 +2826,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||
}) => goose::acp::server::run(builtins, enable_scheduler).await,
|
||||
#[cfg(feature = "roaming")]
|
||||
Some(Command::Roam { command }) => handle_roam_command(command).await,
|
||||
#[cfg(feature = "acp-http")]
|
||||
Some(Command::Serve {
|
||||
host,
|
||||
port,
|
||||
|
|
@ -2885,6 +2906,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||
.await
|
||||
}
|
||||
Some(Command::Gateway { command }) => handle_gateway_command(command).await,
|
||||
#[cfg(feature = "scheduler")]
|
||||
Some(Command::Schedule { command }) => handle_schedule_command(command).await,
|
||||
#[cfg(feature = "update")]
|
||||
Some(Command::Update {
|
||||
|
|
@ -3097,6 +3119,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "acp-http")]
|
||||
#[test]
|
||||
fn serve_command_accepts_dangerously_unauthenticated_flag() {
|
||||
let cli = Cli::try_parse_from([
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub mod roam_client;
|
|||
pub mod roam_full_bridge;
|
||||
#[cfg(feature = "roaming")]
|
||||
pub mod roam_proxy;
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub mod schedule;
|
||||
pub mod session;
|
||||
pub mod skills;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,13 @@ description = "Conversation compaction for Goose"
|
|||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
rustls-tls = []
|
||||
native-tls = []
|
||||
|
||||
[dependencies]
|
||||
goose-provider-types = { version = "0.1.0-alpha.9", path = "../goose-provider-types" }
|
||||
goose-provider-types = { version = "0.1.0-alpha.9", path = "../goose-provider-types", default-features = false }
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
include_dir = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -12,9 +12,15 @@ description.workspace = true
|
|||
workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["bundled-mcp"]
|
||||
rustls-tls = []
|
||||
native-tls = []
|
||||
autovisualiser = []
|
||||
computer-controller = ["dep:lopdf", "dep:docx-rs", "dep:image", "dep:umya-spreadsheet"]
|
||||
document-tools = ["computer-controller"]
|
||||
memory-server = []
|
||||
tutorial-server = ["dep:include_dir"]
|
||||
bundled-mcp = ["autovisualiser", "computer-controller", "document-tools", "memory-server", "tutorial-server"]
|
||||
|
||||
[dependencies]
|
||||
rmcp = { workspace = true, features = ["server", "client", "transport-io", "macros"] }
|
||||
|
|
@ -31,11 +37,11 @@ indoc = { workspace = true }
|
|||
chrono = { workspace = true }
|
||||
etcetera = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
include_dir = { workspace = true }
|
||||
include_dir = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
lopdf = { version = "0.45", default-features = false }
|
||||
docx-rs = { version = "0.4.18", default-features = false, features = ["image"] }
|
||||
image = { version = "0.24.4", default-features = false, features = ["bmp", "dds", "dxt", "farbfeld", "gif", "hdr", "ico", "jpeg", "jpeg_rayon", "openexr", "png", "pnm", "tga", "tiff", "webp"] }
|
||||
umya-spreadsheet = { version = "3", default-features = false }
|
||||
lopdf = { version = "0.45", default-features = false, optional = true }
|
||||
docx-rs = { version = "0.4.18", default-features = false, features = ["image"], optional = true }
|
||||
image = { version = "0.24.4", default-features = false, features = ["bmp", "dds", "dxt", "farbfeld", "gif", "hdr", "ico", "jpeg", "jpeg_rayon", "openexr", "png", "pnm", "tga", "tiff", "webp"], optional = true }
|
||||
umya-spreadsheet = { version = "3", default-features = false, optional = true }
|
||||
shell-words = { workspace = true }
|
||||
process-wrap = { version = "9", default-features = false, features = ["std", "process-session"] }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
use etcetera::AppStrategyArgs;
|
||||
use once_cell::sync::Lazy;
|
||||
#[cfg(any(
|
||||
feature = "autovisualiser",
|
||||
feature = "computer-controller",
|
||||
feature = "memory-server",
|
||||
feature = "tutorial-server"
|
||||
))]
|
||||
use rmcp::{ServerHandler, ServiceExt};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -11,23 +17,43 @@ pub static APP_STRATEGY: Lazy<AppStrategyArgs> = Lazy::new(|| AppStrategyArgs {
|
|||
app_name: "goose".to_string(),
|
||||
});
|
||||
|
||||
#[cfg(feature = "autovisualiser")]
|
||||
pub mod autovisualiser;
|
||||
#[cfg(feature = "computer-controller")]
|
||||
pub mod computercontroller;
|
||||
#[cfg(any(
|
||||
feature = "autovisualiser",
|
||||
feature = "computer-controller",
|
||||
feature = "memory-server",
|
||||
feature = "tutorial-server"
|
||||
))]
|
||||
pub mod mcp_server_runner;
|
||||
#[cfg(feature = "memory-server")]
|
||||
mod memory;
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(all(target_os = "macos", feature = "computer-controller"))]
|
||||
pub mod peekaboo;
|
||||
#[cfg(feature = "computer-controller")]
|
||||
pub mod subprocess;
|
||||
#[cfg(feature = "tutorial-server")]
|
||||
pub mod tutorial;
|
||||
|
||||
#[cfg(feature = "autovisualiser")]
|
||||
pub use autovisualiser::AutoVisualiserRouter;
|
||||
#[cfg(feature = "computer-controller")]
|
||||
pub use computercontroller::ComputerControllerServer;
|
||||
#[cfg(feature = "memory-server")]
|
||||
pub use memory::MemoryServer;
|
||||
#[cfg(feature = "tutorial-server")]
|
||||
pub use tutorial::TutorialServer;
|
||||
|
||||
/// Type definition for a function that spawns and serves a builtin extension server
|
||||
pub type SpawnServerFn = fn(tokio::io::DuplexStream, tokio::io::DuplexStream);
|
||||
|
||||
#[cfg(any(
|
||||
feature = "autovisualiser",
|
||||
feature = "computer-controller",
|
||||
feature = "memory-server",
|
||||
feature = "tutorial-server"
|
||||
))]
|
||||
fn spawn_and_serve<S>(
|
||||
name: &'static str,
|
||||
server: S,
|
||||
|
|
@ -45,6 +71,12 @@ fn spawn_and_serve<S>(
|
|||
});
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "autovisualiser",
|
||||
feature = "computer-controller",
|
||||
feature = "memory-server",
|
||||
feature = "tutorial-server"
|
||||
))]
|
||||
macro_rules! builtin {
|
||||
($name:ident, $server_ty:ty) => {{
|
||||
fn spawn(r: tokio::io::DuplexStream, w: tokio::io::DuplexStream) {
|
||||
|
|
@ -55,10 +87,15 @@ macro_rules! builtin {
|
|||
}
|
||||
|
||||
pub static BUILTIN_EXTENSIONS: Lazy<HashMap<&'static str, SpawnServerFn>> = Lazy::new(|| {
|
||||
HashMap::from([
|
||||
builtin!(autovisualiser, AutoVisualiserRouter),
|
||||
builtin!(computercontroller, ComputerControllerServer),
|
||||
builtin!(memory, MemoryServer),
|
||||
builtin!(tutorial, TutorialServer),
|
||||
])
|
||||
#[allow(unused_mut)]
|
||||
let mut extensions = HashMap::new();
|
||||
#[cfg(feature = "autovisualiser")]
|
||||
extensions.extend([builtin!(autovisualiser, AutoVisualiserRouter)]);
|
||||
#[cfg(feature = "computer-controller")]
|
||||
extensions.extend([builtin!(computercontroller, ComputerControllerServer)]);
|
||||
#[cfg(feature = "memory-server")]
|
||||
extensions.extend([builtin!(memory, MemoryServer)]);
|
||||
#[cfg(feature = "tutorial-server")]
|
||||
extensions.extend([builtin!(tutorial, TutorialServer)]);
|
||||
extensions
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,18 @@ otel = [
|
|||
"dep:opentelemetry-stdout",
|
||||
]
|
||||
code-mode = ["dep:pctx_code_mode"]
|
||||
tree-sitter = [
|
||||
"dep:tree-sitter",
|
||||
"dep:tree-sitter-go",
|
||||
"dep:tree-sitter-java",
|
||||
"dep:tree-sitter-javascript",
|
||||
"dep:tree-sitter-kotlin-ng",
|
||||
"dep:tree-sitter-python",
|
||||
"dep:tree-sitter-ruby",
|
||||
"dep:tree-sitter-rust",
|
||||
"dep:tree-sitter-swift",
|
||||
"dep:tree-sitter-typescript",
|
||||
]
|
||||
live-voice = ["goose-providers/live-websocket"]
|
||||
local-inference = [
|
||||
"dep:candle-core",
|
||||
|
|
@ -58,6 +70,7 @@ rustls-tls = [
|
|||
"oauth2/reqwest",
|
||||
"oauth2/rustls-tls",
|
||||
"goose-providers/rustls-tls",
|
||||
"goose-context-management/rustls-tls",
|
||||
]
|
||||
native-tls = [
|
||||
"dep:rcgen",
|
||||
|
|
@ -76,10 +89,26 @@ native-tls = [
|
|||
"oauth2/reqwest",
|
||||
"oauth2/native-tls",
|
||||
"goose-providers/native-tls",
|
||||
"goose-context-management/native-tls",
|
||||
]
|
||||
system-keyring = ["dep:keyring"]
|
||||
portable-default = ["rustls-tls", "live-voice", "aws-providers", "telemetry", "otel"]
|
||||
portable-default = [
|
||||
"rustls-tls",
|
||||
"tree-sitter",
|
||||
"live-voice",
|
||||
"aws-providers",
|
||||
"telemetry",
|
||||
"otel",
|
||||
"scheduler",
|
||||
"platform-apps",
|
||||
"chat-recall",
|
||||
"acp-http",
|
||||
]
|
||||
nostr = ["dep:nostr", "dep:nostr-sdk"]
|
||||
platform-apps = []
|
||||
chat-recall = []
|
||||
scheduler = ["dep:tokio-cron-scheduler"]
|
||||
acp-http = ["dep:agent-client-protocol-http"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -143,7 +172,7 @@ goose-download-manager = { path = "../goose-download-manager" }
|
|||
goose-sdk-types = { path = "../goose-sdk-types" }
|
||||
goose-agent = { path = "../goose-agent" }
|
||||
rand = { workspace = true }
|
||||
tokio-cron-scheduler = { version = "0.15", default-features = false }
|
||||
tokio-cron-scheduler = { version = "0.15", default-features = false, optional = true }
|
||||
urlencoding = { workspace = true }
|
||||
v_htmlescape = { version = "0.17", default-features = false, features = ["std"] }
|
||||
sqlx = { version = "0.9.0", default-features = false, features = [
|
||||
|
|
@ -178,7 +207,7 @@ agent-client-protocol = { workspace = true, features = [
|
|||
"unstable_end_turn_token_usage",
|
||||
"unstable_session_fork",
|
||||
] }
|
||||
agent-client-protocol-http = { workspace = true, features = ["server"] }
|
||||
agent-client-protocol-http = { workspace = true, features = ["server"], optional = true }
|
||||
unicode-normalization = { version = "0.1.22", default-features = false, features = ["std"] }
|
||||
|
||||
# For local Whisper transcription (optional, behind "local-inference" feature)
|
||||
|
|
@ -198,23 +227,23 @@ shellexpand = { workspace = true }
|
|||
indexmap = { version = "2.9", default-features = false, features = ["serde", "std"] }
|
||||
ignore = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
tree-sitter = { workspace = true }
|
||||
tree-sitter-go = { workspace = true }
|
||||
tree-sitter-java = { workspace = true }
|
||||
tree-sitter-javascript = { workspace = true }
|
||||
tree-sitter-kotlin-ng = { workspace = true }
|
||||
tree-sitter-python = { workspace = true }
|
||||
tree-sitter-ruby = { workspace = true }
|
||||
tree-sitter-rust = { workspace = true }
|
||||
tree-sitter-swift = { workspace = true }
|
||||
tree-sitter-typescript = { workspace = true }
|
||||
tree-sitter = { workspace = true, optional = true }
|
||||
tree-sitter-go = { workspace = true, optional = true }
|
||||
tree-sitter-java = { workspace = true, optional = true }
|
||||
tree-sitter-javascript = { workspace = true, optional = true }
|
||||
tree-sitter-kotlin-ng = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
tree-sitter-ruby = { workspace = true, optional = true }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-swift = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
which = { workspace = true }
|
||||
pastey = { version = "0.2", default-features = false }
|
||||
shell-words = { workspace = true }
|
||||
goose-acp-macros = { path = "../goose-acp-macros", default-features = false }
|
||||
tower-http = { workspace = true, features = ["cors"] }
|
||||
tracing-appender = { workspace = true }
|
||||
process-wrap = { version = "9", default-features = false, features = ["std"] }
|
||||
process-wrap = { version = "9", default-features = false, features = ["std", "process-session"] }
|
||||
nostr = { version = "0.45.1", default-features = false, features = ["nip44", "os-rng", "std"], optional = true }
|
||||
nostr-sdk = { version = "0.45.1", default-features = false, optional = true }
|
||||
rustls = { workspace = true, optional = true }
|
||||
|
|
@ -285,9 +314,14 @@ name = "test_whisper"
|
|||
path = "examples/test_whisper.rs"
|
||||
required-features = ["local-inference"]
|
||||
|
||||
[[bin]]
|
||||
name = "goose-acp"
|
||||
path = "src/bin/goose-acp.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "analyze_cli"
|
||||
path = "src/bin/analyze_cli.rs"
|
||||
required-features = ["tree-sitter"]
|
||||
|
||||
[[bin]]
|
||||
name = "build_canonical_models"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
mod common;
|
||||
pub(crate) mod fs;
|
||||
mod handoff;
|
||||
#[cfg(feature = "acp-http")]
|
||||
mod mcp_app_proxy;
|
||||
mod provider;
|
||||
mod response_builder;
|
||||
|
|
@ -8,6 +9,7 @@ pub mod server;
|
|||
pub mod server_factory;
|
||||
pub(crate) mod tool_call_notifier;
|
||||
pub(crate) mod tools;
|
||||
#[cfg(feature = "acp-http")]
|
||||
pub mod transport;
|
||||
|
||||
pub use common::{map_permission_response, PermissionDecision};
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ use crate::acp::server::{
|
|||
};
|
||||
use crate::agents::GoosePlatform;
|
||||
use crate::config::paths::Paths;
|
||||
#[cfg(feature = "scheduler")]
|
||||
use crate::scheduler_trait::SchedulerTrait;
|
||||
#[cfg(feature = "scheduler")]
|
||||
use crate::session::SessionManager;
|
||||
use crate::source_roots::SourceRoot;
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "scheduler")]
|
||||
use tokio::sync::OnceCell;
|
||||
use tracing::info;
|
||||
|
||||
|
|
@ -27,6 +30,7 @@ pub struct AcpServerFactoryConfig {
|
|||
pub struct AcpServer {
|
||||
config: AcpServerFactoryConfig,
|
||||
data_dir: std::path::PathBuf,
|
||||
#[cfg(feature = "scheduler")]
|
||||
scheduler: OnceCell<Arc<dyn SchedulerTrait>>,
|
||||
active_runs: Arc<ActiveRunRegistry>,
|
||||
live_voice: Arc<crate::acp::server::LiveVoiceService>,
|
||||
|
|
@ -40,12 +44,14 @@ impl AcpServer {
|
|||
Self {
|
||||
config,
|
||||
data_dir,
|
||||
#[cfg(feature = "scheduler")]
|
||||
scheduler: OnceCell::new(),
|
||||
active_runs,
|
||||
live_voice,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
/// Start the scheduler now instead of on first client connect, so a
|
||||
/// headless `goose serve` runs scheduled jobs; on failure `create_agent`
|
||||
/// retries. No-op when the scheduler is disabled.
|
||||
|
|
@ -53,6 +59,7 @@ impl AcpServer {
|
|||
self.scheduler().await.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
async fn scheduler(&self) -> Result<Option<Arc<dyn SchedulerTrait>>> {
|
||||
if !self.config.enable_scheduler {
|
||||
return Ok(None);
|
||||
|
|
@ -92,9 +99,10 @@ impl AcpServer {
|
|||
) -> Result<Arc<GooseAcpAgent>> {
|
||||
let config = crate::config::Config::global();
|
||||
let disable_session_naming = config.get_goose_disable_session_naming().unwrap_or(false);
|
||||
#[cfg(feature = "scheduler")]
|
||||
let scheduler = self.scheduler().await?;
|
||||
#[cfg(feature = "scheduler")]
|
||||
if let Some(scheduler) = &scheduler {
|
||||
// Listing syncs from storage, registering jobs persisted by other processes.
|
||||
scheduler.list_scheduled_jobs().await;
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +138,10 @@ impl AcpServer {
|
|||
goose_platform: self.config.goose_platform.clone(),
|
||||
additional_source_roots: self.config.additional_source_roots.clone(),
|
||||
session_cwd,
|
||||
#[cfg(feature = "scheduler")]
|
||||
scheduler,
|
||||
#[cfg(not(feature = "scheduler"))]
|
||||
scheduler: None,
|
||||
active_runs: self.active_runs.clone(),
|
||||
live_voice: self.live_voice.clone(),
|
||||
})
|
||||
|
|
@ -140,7 +151,7 @@ impl AcpServer {
|
|||
Ok(Arc::new(agent))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, feature = "scheduler"))]
|
||||
pub(crate) fn new_for_test(
|
||||
config: AcpServerFactoryConfig,
|
||||
data_dir: std::path::PathBuf,
|
||||
|
|
@ -150,6 +161,7 @@ impl AcpServer {
|
|||
Self {
|
||||
config,
|
||||
data_dir,
|
||||
#[cfg(feature = "scheduler")]
|
||||
scheduler: OnceCell::new(),
|
||||
active_runs,
|
||||
live_voice,
|
||||
|
|
@ -157,7 +169,7 @@ impl AcpServer {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, feature = "scheduler"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ mod large_response_handler;
|
|||
pub mod mcp_client;
|
||||
pub mod moim;
|
||||
pub mod platform_extensions;
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub mod platform_tools;
|
||||
pub mod prompt_manager;
|
||||
pub mod reply_parts;
|
||||
pub mod retry;
|
||||
#[cfg(feature = "scheduler")]
|
||||
mod schedule_tool;
|
||||
pub mod state_machine;
|
||||
pub mod subagent_execution_tool;
|
||||
|
|
@ -34,6 +36,7 @@ pub use extension_manager::ExtensionManager;
|
|||
pub use goose_agent::events::AgentEvent;
|
||||
pub(crate) use large_response_handler::max_tool_response_size;
|
||||
pub use prompt_manager::PromptManager;
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub use schedule_tool::ScheduleTool;
|
||||
pub use subagent_handler::SUBAGENT_TOOL_REQUEST_TYPE;
|
||||
pub use subagent_task_config::TaskConfig;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
#[cfg(feature = "tree-sitter")]
|
||||
pub mod analyze;
|
||||
#[cfg(feature = "platform-apps")]
|
||||
pub mod apps;
|
||||
#[cfg(feature = "chat-recall")]
|
||||
pub mod chatrecall;
|
||||
#[cfg(feature = "code-mode")]
|
||||
pub mod code_execution;
|
||||
pub mod developer;
|
||||
pub mod ext_manager;
|
||||
pub mod orchestrator;
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub mod scheduler;
|
||||
pub mod summarize;
|
||||
pub mod summon;
|
||||
|
|
@ -30,6 +34,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
|
|||
|| {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
#[cfg(feature = "tree-sitter")]
|
||||
map.insert(
|
||||
analyze::EXTENSION_NAME,
|
||||
PlatformExtensionDef {
|
||||
|
|
@ -58,6 +63,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
|
|||
},
|
||||
);
|
||||
|
||||
#[cfg(feature = "platform-apps")]
|
||||
map.insert(
|
||||
apps::EXTENSION_NAME,
|
||||
PlatformExtensionDef {
|
||||
|
|
@ -72,6 +78,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
|
|||
},
|
||||
);
|
||||
|
||||
#[cfg(feature = "chat-recall")]
|
||||
map.insert(
|
||||
chatrecall::EXTENSION_NAME,
|
||||
PlatformExtensionDef {
|
||||
|
|
@ -102,6 +109,7 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
|
|||
},
|
||||
);
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
map.insert(
|
||||
scheduler::EXTENSION_NAME,
|
||||
PlatformExtensionDef {
|
||||
|
|
|
|||
34
crates/goose/src/bin/goose-acp.rs
Normal file
34
crates/goose/src/bin/goose-acp.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use anyhow::{bail, Result};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let mut builtins = Vec::new();
|
||||
let mut args = std::env::args().skip(1);
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--with-builtin" => {
|
||||
let Some(value) = args.next() else {
|
||||
bail!("--with-builtin requires a comma-separated value");
|
||||
};
|
||||
builtins.extend(
|
||||
value
|
||||
.split(',')
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(str::to_owned),
|
||||
);
|
||||
}
|
||||
"-h" | "--help" => {
|
||||
println!("Usage: goose-acp [--with-builtin <NAME,...>]");
|
||||
return Ok(());
|
||||
}
|
||||
"-V" | "--version" => {
|
||||
println!("goose-acp {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
unknown => bail!("unknown argument: {unknown}"),
|
||||
}
|
||||
}
|
||||
|
||||
goose::acp::server::run(builtins, false).await
|
||||
}
|
||||
|
|
@ -348,6 +348,7 @@ fn malformed_arguments_contain_unresolved_execute_alias(
|
|||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "tree-sitter")]
|
||||
fn decode_quoted_string(literal: &str) -> Option<String> {
|
||||
let quote = literal.chars().next()?;
|
||||
if !matches!(quote, '"' | '\'') || !literal.ends_with(quote) {
|
||||
|
|
@ -375,10 +376,12 @@ fn decode_quoted_string(literal: &str) -> Option<String> {
|
|||
(!escaped).then_some(decoded)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tree-sitter")]
|
||||
fn node_text<'a>(node: tree_sitter::Node<'_>, source: &'a str) -> Option<&'a str> {
|
||||
source.get(node.byte_range())
|
||||
}
|
||||
|
||||
#[cfg(feature = "tree-sitter")]
|
||||
fn is_developer_shell_call(node: tree_sitter::Node<'_>, source: &str) -> bool {
|
||||
let Some(function) = node.child_by_field_name("function") else {
|
||||
return false;
|
||||
|
|
@ -400,6 +403,7 @@ fn is_developer_shell_call(node: tree_sitter::Node<'_>, source: &str) -> bool {
|
|||
&& node_text(property, source) == Some("shell")
|
||||
}
|
||||
|
||||
#[cfg(feature = "tree-sitter")]
|
||||
fn shell_command_from_call(node: tree_sitter::Node<'_>, source: &str) -> Option<String> {
|
||||
let arguments = node.child_by_field_name("arguments")?;
|
||||
if arguments.named_child_count() != 1 {
|
||||
|
|
@ -436,6 +440,7 @@ fn shell_command_from_call(node: tree_sitter::Node<'_>, source: &str) -> Option<
|
|||
node_text(value, source).and_then(decode_quoted_string)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tree-sitter")]
|
||||
fn extract_shell_command_from_execute_code(code: &str) -> Option<String> {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser
|
||||
|
|
@ -464,6 +469,11 @@ fn extract_shell_command_from_execute_code(code: &str) -> Option<String> {
|
|||
command
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tree-sitter"))]
|
||||
fn extract_shell_command_from_execute_code(_code: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn maybe_convert_execute_to_shell_tool_call(
|
||||
raw_tool_name: &str,
|
||||
arguments_value: &Value,
|
||||
|
|
@ -1514,6 +1524,7 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tree-sitter")]
|
||||
#[test]
|
||||
fn execute_code_only_extracts_real_developer_shell_call() {
|
||||
let code = r#"
|
||||
|
|
@ -1532,6 +1543,7 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tree-sitter")]
|
||||
#[test]
|
||||
fn execute_code_rejects_ambiguous_or_unsupported_shell_calls() {
|
||||
let cases = [
|
||||
|
|
|
|||
187
crates/goose/src/scheduler/common.rs
Normal file
187
crates/goose/src/scheduler/common.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::paths::Paths;
|
||||
|
||||
pub(crate) const MAX_SCHEDULE_RECIPE_BYTES: u64 = 1024 * 1024;
|
||||
|
||||
#[cfg_attr(not(feature = "scheduler"), allow(dead_code))]
|
||||
pub struct ValidatedScheduleRecipe {
|
||||
pub(super) bytes: Vec<u8>,
|
||||
pub(super) source: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "scheduler"), allow(dead_code))]
|
||||
impl ValidatedScheduleRecipe {
|
||||
pub(crate) fn new(bytes: Vec<u8>, source: PathBuf) -> Self {
|
||||
Self { bytes, source }
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "scheduler"), allow(dead_code))]
|
||||
pub(crate) fn open_regular_schedule_recipe(path: &Path) -> io::Result<File> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
if !metadata.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Recipe path must reference a regular file",
|
||||
));
|
||||
}
|
||||
|
||||
let mut options = OpenOptions::new();
|
||||
options.read(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW);
|
||||
}
|
||||
let file = options.open(path)?;
|
||||
if !file.metadata()?.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Recipe path must reference a regular file",
|
||||
));
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "scheduler"), allow(dead_code))]
|
||||
pub(super) fn write_schedule_recipe_bytes(
|
||||
destination: &Path,
|
||||
bytes: &[u8],
|
||||
) -> Result<(), SchedulerError> {
|
||||
if bytes.len() as u64 > MAX_SCHEDULE_RECIPE_BYTES {
|
||||
return Err(SchedulerError::RecipeLoadError(format!(
|
||||
"Recipe file exceeds the {MAX_SCHEDULE_RECIPE_BYTES} byte limit"
|
||||
)));
|
||||
}
|
||||
|
||||
let result = (|| {
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true).create(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let mut file = options.open(destination)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
file.set_permissions(fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
file.set_len(0)?;
|
||||
file.write_all(bytes)
|
||||
})();
|
||||
if let Err(error) = result {
|
||||
let _ = fs::remove_file(destination);
|
||||
return Err(SchedulerError::StorageError(error));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_default_scheduler_storage_path() -> Result<PathBuf, io::Error> {
|
||||
let data_dir = Paths::data_dir();
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
Ok(data_dir.join("schedule.json"))
|
||||
}
|
||||
|
||||
pub fn get_default_scheduled_recipes_dir() -> Result<PathBuf, SchedulerError> {
|
||||
let data_dir = Paths::data_dir();
|
||||
let recipes_dir = data_dir.join("scheduled_recipes");
|
||||
fs::create_dir_all(&recipes_dir).map_err(SchedulerError::StorageError)?;
|
||||
Ok(recipes_dir)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SchedulerError {
|
||||
JobIdExists(String),
|
||||
JobNotFound(String),
|
||||
StorageError(io::Error),
|
||||
RecipeLoadError(String),
|
||||
AgentSetupError(String),
|
||||
PersistError(String),
|
||||
CronParseError(String),
|
||||
SchedulerInternalError(String),
|
||||
AnyhowError(anyhow::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SchedulerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SchedulerError::JobIdExists(id) => write!(f, "Job ID '{}' already exists.", id),
|
||||
SchedulerError::JobNotFound(id) => write!(f, "Job ID '{}' not found.", id),
|
||||
SchedulerError::StorageError(e) => write!(f, "Storage error: {}", e),
|
||||
SchedulerError::RecipeLoadError(e) => write!(f, "Recipe load error: {}", e),
|
||||
SchedulerError::AgentSetupError(e) => write!(f, "Agent setup error: {}", e),
|
||||
SchedulerError::PersistError(e) => write!(f, "Failed to persist schedules: {}", e),
|
||||
SchedulerError::CronParseError(e) => write!(f, "Invalid cron string: {}", e),
|
||||
SchedulerError::SchedulerInternalError(e) => {
|
||||
write!(f, "Scheduler internal error: {}", e)
|
||||
}
|
||||
SchedulerError::AnyhowError(e) => write!(f, "Scheduler operation failed: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SchedulerError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
SchedulerError::StorageError(e) => Some(e),
|
||||
SchedulerError::AnyhowError(e) => Some(e.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for SchedulerError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
SchedulerError::StorageError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for SchedulerError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
SchedulerError::PersistError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for SchedulerError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
SchedulerError::AnyhowError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct ScheduledJob {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub cron: String,
|
||||
pub last_run: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub currently_running: bool,
|
||||
#[serde(default)]
|
||||
pub paused: bool,
|
||||
#[serde(default)]
|
||||
pub current_session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub process_start_time: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub parameters: Vec<(String, String)>,
|
||||
/// Original directory of the recipe file before it was copied to scheduled_recipes/.
|
||||
/// Preserved so that relative paths (sub-recipes, template includes) resolve correctly
|
||||
/// against the source tree rather than the scheduler's internal storage directory.
|
||||
#[serde(default)]
|
||||
pub recipe_base_dir: Option<String>,
|
||||
}
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_cron_scheduler::{job::JobId, Job, JobScheduler as TokioJobScheduler};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::agents::{Agent, AgentConfig, AgentEvent, GoosePlatform, SessionConfig};
|
||||
use crate::config::paths::Paths;
|
||||
use crate::config::permission::PermissionManager;
|
||||
use crate::config::{resolve_extensions_for_new_session, Config, GooseMode};
|
||||
use crate::conversation::message::Message;
|
||||
|
|
@ -33,61 +31,15 @@ use crate::session::{Session, SessionManager};
|
|||
type RunningTasksMap = HashMap<String, CancellationToken>;
|
||||
type JobsMap = HashMap<String, (JobId, ScheduledJob)>;
|
||||
|
||||
pub(crate) const MAX_SCHEDULE_RECIPE_BYTES: u64 = 1024 * 1024;
|
||||
|
||||
pub struct ValidatedScheduleRecipe {
|
||||
bytes: Vec<u8>,
|
||||
source: PathBuf,
|
||||
}
|
||||
|
||||
impl ValidatedScheduleRecipe {
|
||||
pub(crate) fn new(bytes: Vec<u8>, source: PathBuf) -> Self {
|
||||
Self { bytes, source }
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_regular_schedule_recipe(path: &Path) -> io::Result<File> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
if !metadata.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Recipe path must reference a regular file",
|
||||
));
|
||||
}
|
||||
|
||||
let mut options = OpenOptions::new();
|
||||
options.read(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW);
|
||||
}
|
||||
let file = options.open(path)?;
|
||||
if !file.metadata()?.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Recipe path must reference a regular file",
|
||||
));
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
use super::*;
|
||||
|
||||
fn read_validated_schedule_recipe(source_path: &Path) -> Result<Vec<u8>, SchedulerError> {
|
||||
let source = open_regular_schedule_recipe(source_path).map_err(|error| {
|
||||
let mut source = open_regular_schedule_recipe(source_path).map_err(|error| {
|
||||
SchedulerError::RecipeLoadError(format!("Cannot read recipe file: {error}"))
|
||||
})?;
|
||||
let metadata = source.metadata().map_err(|error| {
|
||||
SchedulerError::RecipeLoadError(format!("Cannot inspect recipe file: {error}"))
|
||||
})?;
|
||||
if !metadata.is_file() {
|
||||
return Err(SchedulerError::RecipeLoadError(
|
||||
"Recipe path must reference a regular file".to_string(),
|
||||
));
|
||||
}
|
||||
if metadata.len() > MAX_SCHEDULE_RECIPE_BYTES {
|
||||
return Err(SchedulerError::RecipeLoadError(format!(
|
||||
"Recipe file exceeds the {MAX_SCHEDULE_RECIPE_BYTES} byte limit"
|
||||
|
|
@ -96,6 +48,7 @@ fn read_validated_schedule_recipe(source_path: &Path) -> Result<Vec<u8>, Schedul
|
|||
|
||||
let mut bytes = Vec::new();
|
||||
source
|
||||
.by_ref()
|
||||
.take(MAX_SCHEDULE_RECIPE_BYTES + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|error| {
|
||||
|
|
@ -120,134 +73,6 @@ fn read_validated_schedule_recipe(source_path: &Path) -> Result<Vec<u8>, Schedul
|
|||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn write_schedule_recipe_bytes(destination: &Path, bytes: &[u8]) -> Result<(), SchedulerError> {
|
||||
if bytes.len() as u64 > MAX_SCHEDULE_RECIPE_BYTES {
|
||||
return Err(SchedulerError::RecipeLoadError(format!(
|
||||
"Recipe file exceeds the {MAX_SCHEDULE_RECIPE_BYTES} byte limit"
|
||||
)));
|
||||
}
|
||||
|
||||
let result = (|| {
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true).create(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let mut file = options.open(destination)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
file.set_permissions(fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
file.set_len(0)?;
|
||||
file.write_all(bytes)
|
||||
})();
|
||||
if let Err(error) = result {
|
||||
let _ = fs::remove_file(destination);
|
||||
return Err(SchedulerError::StorageError(error));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_default_scheduler_storage_path() -> Result<PathBuf, io::Error> {
|
||||
let data_dir = Paths::data_dir();
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
Ok(data_dir.join("schedule.json"))
|
||||
}
|
||||
|
||||
pub fn get_default_scheduled_recipes_dir() -> Result<PathBuf, SchedulerError> {
|
||||
let data_dir = Paths::data_dir();
|
||||
let recipes_dir = data_dir.join("scheduled_recipes");
|
||||
fs::create_dir_all(&recipes_dir).map_err(SchedulerError::StorageError)?;
|
||||
Ok(recipes_dir)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SchedulerError {
|
||||
JobIdExists(String),
|
||||
JobNotFound(String),
|
||||
StorageError(io::Error),
|
||||
RecipeLoadError(String),
|
||||
AgentSetupError(String),
|
||||
PersistError(String),
|
||||
CronParseError(String),
|
||||
SchedulerInternalError(String),
|
||||
AnyhowError(anyhow::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SchedulerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SchedulerError::JobIdExists(id) => write!(f, "Job ID '{}' already exists.", id),
|
||||
SchedulerError::JobNotFound(id) => write!(f, "Job ID '{}' not found.", id),
|
||||
SchedulerError::StorageError(e) => write!(f, "Storage error: {}", e),
|
||||
SchedulerError::RecipeLoadError(e) => write!(f, "Recipe load error: {}", e),
|
||||
SchedulerError::AgentSetupError(e) => write!(f, "Agent setup error: {}", e),
|
||||
SchedulerError::PersistError(e) => write!(f, "Failed to persist schedules: {}", e),
|
||||
SchedulerError::CronParseError(e) => write!(f, "Invalid cron string: {}", e),
|
||||
SchedulerError::SchedulerInternalError(e) => {
|
||||
write!(f, "Scheduler internal error: {}", e)
|
||||
}
|
||||
SchedulerError::AnyhowError(e) => write!(f, "Scheduler operation failed: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SchedulerError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
SchedulerError::StorageError(e) => Some(e),
|
||||
SchedulerError::AnyhowError(e) => Some(e.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for SchedulerError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
SchedulerError::StorageError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for SchedulerError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
SchedulerError::PersistError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for SchedulerError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
SchedulerError::AnyhowError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct ScheduledJob {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub cron: String,
|
||||
pub last_run: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub currently_running: bool,
|
||||
#[serde(default)]
|
||||
pub paused: bool,
|
||||
#[serde(default)]
|
||||
pub current_session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub process_start_time: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub parameters: Vec<(String, String)>,
|
||||
/// Original directory of the recipe file before it was copied to scheduled_recipes/.
|
||||
/// Preserved so that relative paths (sub-recipes, template includes) resolve correctly
|
||||
/// against the source tree rather than the scheduler's internal storage directory.
|
||||
#[serde(default)]
|
||||
pub recipe_base_dir: Option<String>,
|
||||
}
|
||||
|
||||
async fn persist_jobs(
|
||||
storage_path: &Path,
|
||||
jobs: &Arc<Mutex<JobsMap>>,
|
||||
|
|
@ -1333,6 +1158,7 @@ impl SchedulerTrait for Scheduler {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs::File;
|
||||
use tempfile::tempdir;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
7
crates/goose/src/scheduler/mod.rs
Normal file
7
crates/goose/src/scheduler/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
mod common;
|
||||
#[cfg(feature = "scheduler")]
|
||||
mod full;
|
||||
|
||||
pub use common::*;
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub use full::Scheduler;
|
||||
Loading…
Add table
Add a link
Reference in a new issue