Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions crates/goose/src/config/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@
}
}

fn additional_config_paths_from_env() -> Vec<PathBuf> {
env::var_os("GOOSE_ADDITIONAL_CONFIG_FILES")
.map(|value| env::split_paths(&value).collect())
.unwrap_or_default()
}

impl Default for Config {
fn default() -> Self {
let config_dir = Paths::config_dir();
Expand All @@ -170,6 +176,7 @@
if let Some(defaults) = bundled_defaults_path() {
config_paths.insert(0, defaults);
}
config_paths.extend(additional_config_paths_from_env());
config_paths.push(user_config_path.clone());

let no_secrets_config = Self {
Expand Down Expand Up @@ -2031,4 +2038,42 @@

Ok(())
}

#[test]
fn test_additional_config_files_env_is_loaded_between_defaults_and_user(
) -> Result<(), ConfigError> {
let extra_file = NamedTempFile::new().unwrap();
let config_root = tempdir().unwrap();

Check failure on line 2046 in crates/goose/src/config/base.rs

View workflow job for this annotation

GitHub Actions / Build and Test Rust Project

cannot find function `tempdir` in this scope

Check failure on line 2046 in crates/goose/src/config/base.rs

View workflow job for this annotation

GitHub Actions / Check MSRV

cannot find function `tempdir` in this scope

Check failure on line 2046 in crates/goose/src/config/base.rs

View workflow job for this annotation

GitHub Actions / Lint Rust Code

cannot find function `tempdir` in this scope
let original_root = env::var_os("GOOSE_PATH_ROOT");
let original_extra = env::var_os("GOOSE_ADDITIONAL_CONFIG_FILES");

std::fs::write(extra_file.path(), "GOOSE_PROVIDER: databricks\n").unwrap();
std::fs::write(
config_root.path().join(CONFIG_YAML_NAME),
"GOOSE_MODEL: gpt-4o\n",
)
.unwrap();

env::set_var("GOOSE_PATH_ROOT", config_root.path());
env::set_var("GOOSE_ADDITIONAL_CONFIG_FILES", extra_file.path());

let config = Config::default();

let provider: String = config.get_param("GOOSE_PROVIDER")?;
assert_eq!(provider, "databricks");

let model: String = config.get_param("GOOSE_MODEL")?;
assert_eq!(model, "gpt-4o");

match original_root {
Some(value) => env::set_var("GOOSE_PATH_ROOT", value),
None => env::remove_var("GOOSE_PATH_ROOT"),
}
match original_extra {
Some(value) => env::set_var("GOOSE_ADDITIONAL_CONFIG_FILES", value),
None => env::remove_var("GOOSE_ADDITIONAL_CONFIG_FILES"),
}

Ok(())
}
}
1 change: 1 addition & 0 deletions ui/goose2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ ThemeProvider manages three axes:
- Title bar uses `titleBarStyle: "Overlay"` with `hiddenTitle: true` for a custom titlebar.
- `tauri-plugin-window-state` persists window size and position.
- Traffic light offset: `pl-20` (80px) to accommodate macOS window controls.
- Distro bundle behavior, including feature flags, is documented in `distro/README.md`.

## Architecture

Expand Down
100 changes: 100 additions & 0 deletions ui/goose2/distro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Goose2 distro bundles

A Goose2 distro bundle is an optional app-specific package of configuration and policy that the Tauri shell loads at startup.

## What a distro bundle is

A distro bundle lives under `ui/goose2/distro/` in development, and is bundled into the packaged app as a Tauri resource in production.

Current supported files:

- `distro.json` — distro manifest
- `config.yaml` — optional Goose config passed to `goose serve`
- `bin/` — optional executables or helper scripts prepended to `PATH` for `goose serve`

## How it is discovered

The Tauri app resolves the distro bundle in this order:

1. `GOOSE_DISTRO_DIR`, if set
2. bundled Tauri resource dir at `resource_dir()/distro`

In development, `just dev` and `just dev-debug` automatically export `GOOSE_DISTRO_DIR` to `ui/goose2/distro` when that directory exists.

## Manifest shape

Example:

```json
{
"appVersion": "development",
"featureToggles": {
"costTracking": false
},
"security": {
"providerAllowlist": "databricks"
}
}
```

### Fields

- `appVersion?: string`
- optional app version tag supplied by the distro

- `featureToggles?: Record<string, boolean>`
- optional UI/product flags controlled by the distro
- currently supported:
- `costTracking`
- `false` hides cost UI in the token/context usage surfaces
- omitted behaves as enabled

- `security?: { providerAllowlist?: string, extensionAllowlist?: string }`
- optional policy controls
- currently used:
- `providerAllowlist`
- comma-separated provider ids
- limits visible model providers in Settings
- limits visible Goose model options in the chat model picker

## Runtime effects

When a distro bundle is present, Goose2 does two kinds of things with it.

### Frontend behavior

The frontend loads `get_distro_bundle` during app startup and stores the manifest in Zustand.

Today it uses that manifest to:

- filter model providers shown in provider settings via `providerAllowlist`
- filter Goose model options shown in the chat input model picker via `providerAllowlist`
- hide cost UI when `featureToggles.costTracking === false`

### Backend / shell behavior

When the Tauri shell launches the long-lived `goose serve` process, it applies the distro bundle like this:

- prepends `distro/bin` to `PATH` when present
- adds `distro/config.yaml` to `GOOSE_ADDITIONAL_CONFIG_FILES` when present
- sets `GOOSE_DISTRO_DIR` to the resolved distro root

This is shell-level behavior, so it is implemented as Tauri-side setup rather than an ACP method.

## Development notes

- packaged apps discover distro content from bundled Tauri resources
- local development uses `GOOSE_DISTRO_DIR`
- after changing `distro.json`, restart `just dev` so startup reloads the manifest

## Scope guidance

Use distro bundles for packaged-app policy and shell-level defaults.

Good fits:

- feature flags for Goose2 UI behavior
- allowlists that constrain visible product choices
- config or helper binaries that should be present when `goose serve` starts

Avoid using distro bundles as a replacement for normal app state, user settings, or ACP-backed domain data.
Empty file added ui/goose2/distro/config.yaml
Empty file.
5 changes: 5 additions & 0 deletions ui/goose2/distro/distro.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"featureToggles": {
"costTracking": true
}
}
16 changes: 16 additions & 0 deletions ui/goose2/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ dev: setup
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
PROJECT_DIR=$(pwd)
REPO_ROOT=$(cd ../.. && pwd)
DISTRO_DIR="${PROJECT_DIR}/distro"
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "${DISTRO_DIR}" ]]; then
export GOOSE_DISTRO_DIR="${DISTRO_DIR}"
fi
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
Expand All @@ -107,6 +111,10 @@ dev: setup
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
fi

if [[ -n "${GOOSE_DISTRO_DIR:-}" ]]; then
echo "Using distro dir: ${GOOSE_DISTRO_DIR}"
fi

# In worktrees, generate a labeled icon so you can tell instances apart
if git rev-parse --is-inside-work-tree &>/dev/null; then
GIT_DIR=$(git rev-parse --git-dir)
Expand Down Expand Up @@ -138,6 +146,10 @@ dev-debug: setup
# Override with e.g. RUST_LOG=info just dev-debug to disable.
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
REPO_ROOT=$(cd ../.. && pwd)
DISTRO_DIR="$(pwd)/distro"
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "${DISTRO_DIR}" ]]; then
export GOOSE_DISTRO_DIR="${DISTRO_DIR}"
fi
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
Expand All @@ -155,6 +167,10 @@ dev-debug: setup
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
fi

if [[ -n "${GOOSE_DISTRO_DIR:-}" ]]; then
echo "Using distro dir: ${GOOSE_DISTRO_DIR}"
fi

# In worktrees, generate a labeled icon so you can tell instances apart
if git rev-parse --is-inside-work-tree &>/dev/null; then
GIT_DIR=$(git rev-parse --git-dir)
Expand Down
7 changes: 7 additions & 0 deletions ui/goose2/src-tauri/src/commands/distro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use crate::services::distro_bundle::{DistroBundleInfo, DistroBundleState};
use tauri::State;

#[tauri::command]
pub fn get_distro_bundle(state: State<'_, DistroBundleState>) -> DistroBundleInfo {
state.info()
}
1 change: 1 addition & 0 deletions ui/goose2/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod acp;
pub mod agent_setup;
pub mod agents;
pub mod credentials;
pub mod distro;
pub mod doctor;
pub mod git;
pub mod git_changes;
Expand Down
14 changes: 10 additions & 4 deletions ui/goose2/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ mod commands;
mod services;
mod types;

use services::distro_bundle::DistroBundleState;
use services::goose_config::GooseConfig;
use services::personas::PersonaStore;
use tauri::Manager;
use tauri_plugin_window_state::StateFlags;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
Expand All @@ -24,14 +26,18 @@ pub fn run() {
tauri_plugin_window_state::Builder::default()
.with_state_flags(StateFlags::all() & !StateFlags::VISIBLE)
.build(),
)
.manage(PersonaStore::new())
.manage(GooseConfig::new());
);

#[cfg(feature = "app-test-driver")]
let builder = builder.plugin(tauri_plugin_app_test_driver::init());

builder
.manage(PersonaStore::new())
.manage(GooseConfig::new())
.setup(|app| {
app.manage(DistroBundleState::new(app.handle()));
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::agents::list_personas,
commands::agents::create_persona,
Expand Down Expand Up @@ -76,6 +82,7 @@ pub fn run() {
commands::agent_setup::install_agent,
commands::agent_setup::authenticate_agent,
commands::path_resolver::resolve_path,
commands::distro::get_distro_bundle,
commands::system::get_home_dir,
commands::system::save_exported_session_file,
commands::system::path_exists,
Expand All @@ -84,7 +91,6 @@ pub fn run() {
commands::system::list_files_for_mentions,
commands::system::read_image_attachment,
])
.setup(|_app| Ok(()))
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|_app, _event| {});
Expand Down
34 changes: 34 additions & 0 deletions ui/goose2/src-tauri/src/services/acp/goose_serve.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use tauri::Manager;
use tauri_plugin_shell::ShellExt;

use std::ffi::OsString;
use std::path::PathBuf;
use std::time::{Duration, Instant};

use crate::services::distro_bundle::DistroBundleState;

use tokio::process::{Child, Command};
use tokio::sync::OnceCell;

Expand Down Expand Up @@ -56,6 +60,18 @@ impl GooseServeProcess {
let mut command: Command = get_goose_command(&app_handle)?;
let binary_display = command.as_std().get_program().to_string_lossy().to_string();

if let Some(distro_state) = app_handle.try_state::<DistroBundleState>() {
if let Some(bundle) = distro_state.bundle() {
if let Some(bin_dir) = &bundle.bin_dir {
prepend_path_env(&mut command, bin_dir);
}
if let Some(config_path) = &bundle.config_path {
command.env("GOOSE_ADDITIONAL_CONFIG_FILES", config_path);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve existing additional config env when setting distro config

Setting GOOSE_ADDITIONAL_CONFIG_FILES here overwrites any value inherited from the parent environment, so existing extra config layers are dropped whenever a distro config is present. In deployments that already rely on this env var (for policy or per-site defaults), those files stop loading after this change; append the distro path to the existing value instead of replacing it.

Useful? React with 👍 / 👎.

}
command.env("GOOSE_DISTRO_DIR", &bundle.root_dir);
}
}

command
.arg("serve")
.arg("--host")
Expand Down Expand Up @@ -138,6 +154,24 @@ fn default_serve_working_dir() -> PathBuf {
.join("artifacts")
}

fn prepend_path_env(command: &mut Command, extra_dir: &std::path::Path) {
let mut paths = vec![extra_dir.to_path_buf()];
if let Some(existing) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&existing));
}

if let Ok(joined) = std::env::join_paths(paths) {
command.env("PATH", joined);
} else {
let mut fallback = OsString::from(extra_dir.as_os_str());
if let Some(existing) = std::env::var_os("PATH") {
fallback.push(if cfg!(windows) { ";" } else { ":" });
fallback.push(existing);
}
command.env("PATH", fallback);
}
}

fn reserve_free_port() -> Result<u16, String> {
let listener = std::net::TcpListener::bind((LOCALHOST, 0))
.map_err(|error| format!("Failed to reserve Goose serve port: {error}"))?;
Expand Down
Loading
Loading