diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap index 93fe2496a6..56bd60816b 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap @@ -124,7 +124,7 @@ Manage agent sessions: list, view, start, send messages, and interrupt agents. You have these skills at your disposal, when it is clear they can help you solve a problem or you are asked to use them: -• goose-doc-guide - Reference goose documentation to create, configure, or explain goose-specific features like recipes, extensions, sessions, and providers. You MUST fetch relevant goose docs before answering. You MUST NOT rely on training data or assumptions for any goose-specific fields, values, names, syntax, or commands. +• goose-doc-guide - Reference goose documentation to create, configure, or explain goose-specific features like recipes, extensions, sessions, and providers. You MUST read the relevant goose docs before answering. You MUST NOT rely on training data or assumptions for any goose-specific fields, values, names, syntax, or commands. ## summarize diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index 0cd9a22ada..2137c18b68 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -1161,6 +1161,14 @@ impl Config { } } + pub fn get_goose_docs_root(&self) -> Result, ConfigError> { + match self.get_param::("GOOSE_DOCS_ROOT") { + Ok(root) => Ok(Some(root.trim().to_string()).filter(|root| !root.is_empty())), + Err(ConfigError::NotFound(_)) => Ok(None), + Err(e) => Err(e), + } + } + pub fn get_goose_thinking_effort(&self) -> Option { self.get_param::("GOOSE_THINKING_EFFORT") .ok() @@ -2540,6 +2548,48 @@ extensions: } } + #[test] + fn get_goose_docs_root_reads_config_file() { + let _guard = env_lock::lock_env([("GOOSE_DOCS_ROOT", None::<&str>)]); + let config = new_test_config(); + config + .set_param("GOOSE_DOCS_ROOT", "/tmp/goose-docs") + .unwrap(); + + assert_eq!( + config.get_goose_docs_root().unwrap(), + Some("/tmp/goose-docs".to_string()) + ); + } + + #[test] + fn get_goose_docs_root_reads_env_value() { + let _guard = env_lock::lock_env([("GOOSE_DOCS_ROOT", Some("/tmp/env-docs"))]); + let config = new_test_config(); + + assert_eq!( + config.get_goose_docs_root().unwrap(), + Some("/tmp/env-docs".to_string()) + ); + } + + #[test] + fn get_goose_docs_root_returns_none_when_unset() { + let _guard = env_lock::lock_env([("GOOSE_DOCS_ROOT", None::<&str>)]); + let config = new_test_config(); + + assert_eq!(config.get_goose_docs_root().unwrap(), None); + } + + #[test] + fn get_goose_docs_root_ignores_blank_value() { + let _guard = env_lock::lock_env([("GOOSE_DOCS_ROOT", None::<&str>)]); + let config = new_test_config(); + config.set_param("GOOSE_DOCS_ROOT", " ").unwrap(); + + assert_eq!(config.get_goose_docs_root().unwrap(), None); + } + #[test] fn get_goose_thinking_effort_reads_env() { let _guard = env_lock::lock_env([ diff --git a/crates/goose/src/skills/builtins/goose_doc_guide.md b/crates/goose/src/skills/builtins/goose_doc_guide.md index 62c8257b05..5d578ec293 100644 --- a/crates/goose/src/skills/builtins/goose_doc_guide.md +++ b/crates/goose/src/skills/builtins/goose_doc_guide.md @@ -1,6 +1,6 @@ --- name: goose-doc-guide -description: Reference goose documentation to create, configure, or explain goose-specific features like recipes, extensions, sessions, and providers. You MUST fetch relevant goose docs before answering. You MUST NOT rely on training data or assumptions for any goose-specific fields, values, names, syntax, or commands. +description: Reference goose documentation to create, configure, or explain goose-specific features like recipes, extensions, sessions, and providers. You MUST read the relevant goose docs before answering. You MUST NOT rely on training data or assumptions for any goose-specific fields, values, names, syntax, or commands. --- Use this skill when working with **goose-specific features**: @@ -13,22 +13,28 @@ Do NOT use this skill for: - General coding tasks unrelated to goose - Running existing recipes (just run them directly) +The docs root for this session is `{{GOOSE_DOCS_ROOT}}`. It may be a local +filesystem path or an HTTP(S) URL. When it is a local path read files with the +shell/file tools; when it is not set or is an HTTP(S) URL fetch them from the +canonical location which is https://goose-docs.ai. Everything below refers to +this docs root as ``. + ## Steps (COMPLETE ALL BEFORE RESPONDING) -1. **Fetch official docs** - - Fetch the doc map from `https://goose-docs.ai/goose-docs-map.md` +1. **Read official docs** + - Read the doc map from `/goose-docs-map.md` - Search the doc map for pages relevant to the user's topic and get the paths for these pages - Use the EXACT paths from the doc map. For example: - If doc map shows: `docs/guides/sessions/session-management.md` - - Fetch: `https://goose-docs.ai/docs/guides/sessions/session-management.md` + - Read: `/docs/guides/sessions/session-management.md` - Do NOT modify or guess paths. - - **ONLY fetch paths that are explicitly listed in the doc map - do not guess or infer URLs** - - Make multiple fetch calls in parallel and save to temp files - - Use the temp files for subsequent searches instead of re-fetching + - **ONLY read paths that are explicitly listed in the doc map - do not guess or infer paths** + - Read multiple docs in parallel and save to temp files + - Use the temp files for subsequent searches instead of re-reading 2. **Create/modify content** - For goose configuration files: - Consult schema/field reference documentation first - - **Search the fetched docs to extract the complete schema for each element you plan to use** + - **Search the docs to extract the complete schema for each element you plan to use** - Extract example snippets to understand usage patterns - Create your configuration based on reference specs, following example patterns - **⚠️ STOP: Before showing the user, verify output content MUST match the schema and reference in the goose official documentation:** @@ -43,14 +49,15 @@ Do NOT use this skill for: - [ ] You MUST NOT rely on training data or assumptions for any goose-specific fields, values, names, syntax, or commands. - [ ] **Did you include "How to Use", CLI commands, or usage instructions?** - If YES and user didn't ask for it → **REMOVE IT NOW** - - If YES and user asked for it → verify exact commands from fetched docs before including + - If YES and user asked for it → verify exact commands from the docs before including - [ ] List all goose-specific items in your answer (commands, fields, syntax, values, how to use, explanations, etc.) - - [ ] For each item, verify it is correct according to the fetched docs. If not found, either fetch the relevant docs NOW and verify, or remove it (if user asked for it, state "I could not find documentation for [X]"). + - [ ] For each item, verify it is correct according to the docs. If not found, either read the relevant docs NOW and verify, or remove it (if user asked for it, state "I could not find documentation for [X]"). 4. **Provide your answer and include a "Verification Completed" section** - For EACH goose-specific item in your response, cite the specific doc file where you verified it 5. **List documentation links** - Only include docs actually used + - Always link to the canonical site `https://goose-docs.ai/`, even if you read the docs from a local path. Never expose local filesystem paths. - Remove `.md` suffix from URLs - - Example: If you fetched `https://goose-docs.ai/docs/guides/sessions/session-management.md`, list it as `https://goose-docs.ai/docs/guides/sessions/session-management` + - Example: If you read `docs/guides/sessions/session-management.md`, list it as `https://goose-docs.ai/docs/guides/sessions/session-management` diff --git a/crates/goose/src/skills/mod.rs b/crates/goose/src/skills/mod.rs index 07da20c6ba..65d43fe3d6 100644 --- a/crates/goose/src/skills/mod.rs +++ b/crates/goose/src/skills/mod.rs @@ -8,7 +8,7 @@ pub mod client; pub use client::{SkillsClient, EXTENSION_NAME}; -use crate::config::paths::Paths; +use crate::config::{paths::Paths, Config}; use crate::plugins::installed_plugin_skill_dirs; use crate::sources::parse_frontmatter; use agent_client_protocol::Error; @@ -99,7 +99,27 @@ pub(crate) fn validate_skill_name(name: &str) -> Result<(), Error> { Ok(()) } -fn loaded_skill_context(skill: &SourceEntry, content: &str) -> String { +const DEFAULT_GOOSE_DOCS_ROOT: &str = "https://goose-docs.ai"; +const GOOSE_DOCS_ROOT_PLACEHOLDER: &str = "{{GOOSE_DOCS_ROOT}}"; + +/// Substitute the `{{GOOSE_DOCS_ROOT}}` placeholder in the builtin +/// `goose-doc-guide` skill with the resolved docs root. Resolution is +/// deterministic: the configured `GOOSE_DOCS_ROOT` if set, otherwise the +/// canonical online docs root. +fn resolve_docs_root_placeholder(skill: &SourceEntry, content: &str, docs_root: &str) -> String { + if skill.name != "goose-doc-guide" || skill.source_type != SourceType::BuiltinSkill { + return content.to_string(); + } + + content.replace(GOOSE_DOCS_ROOT_PLACEHOLDER, docs_root) +} + +fn loaded_skill_context(skill: &SourceEntry, content: &str) -> Result { + let docs_root = Config::global() + .get_goose_docs_root()? + .unwrap_or_else(|| DEFAULT_GOOSE_DOCS_ROOT.to_string()); + let content = resolve_docs_root_placeholder(skill, content, &docs_root); + let title = format!("{} ({})", skill.name, skill.source_type); let mut output = format!( "# Loaded Skill: {title}\n\n{}\n\n## Content\n\n{}\n", @@ -128,7 +148,7 @@ fn loaded_skill_context(skill: &SourceEntry, content: &str) -> String { } } - output + Ok(output) } pub fn loaded_skill_context_with_args(skill: &SourceEntry, args: Option<&str>) -> Result { @@ -138,7 +158,7 @@ pub fn loaded_skill_context_with_args(skill: &SourceEntry, args: Option<&str>) - skill.content.clone() }; - Ok(loaded_skill_context(skill, &content)) + loaded_skill_context(skill, &content) } pub fn skill_argument_hint(skill: &SourceEntry) -> Option { @@ -518,6 +538,20 @@ mod tests { } } + fn builtin_goose_doc_guide_skill() -> SourceEntry { + SourceEntry { + source_type: SourceType::BuiltinSkill, + name: "goose-doc-guide".to_string(), + description: "Test docs skill".to_string(), + content: "Docs root: {{GOOSE_DOCS_ROOT}}.".to_string(), + path: "builtin://skills/goose-doc-guide".to_string(), + global: true, + writable: true, + supporting_files: Vec::new(), + properties: HashMap::new(), + } + } + #[test] fn loaded_skill_context_with_args_replaces_arguments_placeholder_with_raw_args() { let skill = skill_with_content("Review $ARGUMENTS carefully."); @@ -553,4 +587,24 @@ mod tests { assert!(rendered.contains(&resolved_path)); assert!(rendered.contains("load_skill(name: \"test-skill/scripts/my-tool.exe\")")); } + + #[test] + fn resolve_docs_root_placeholder_substitutes_builtin_goose_doc_guide_root() { + let skill = builtin_goose_doc_guide_skill(); + + let rendered = + resolve_docs_root_placeholder(&skill, &skill.content, "/tmp/goose docs/root"); + + assert_eq!(rendered, "Docs root: /tmp/goose docs/root."); + } + + #[test] + fn resolve_docs_root_placeholder_ignores_non_builtin_goose_doc_guide_skills() { + let mut skill = builtin_goose_doc_guide_skill(); + skill.source_type = SourceType::Skill; + + let rendered = resolve_docs_root_placeholder(&skill, &skill.content, "/tmp/goose-docs"); + + assert_eq!(rendered, skill.content); + } } diff --git a/documentation/docs/guides/config-files.md b/documentation/docs/guides/config-files.md index 89f0042668..0f5a306441 100644 --- a/documentation/docs/guides/config-files.md +++ b/documentation/docs/guides/config-files.md @@ -48,6 +48,7 @@ The following settings can be configured at the root level of your config.yaml f | `GOOSE_CLI_DARK_THEME` | Custom syntax highlighting theme for dark mode | [bat theme name](https://github.com/sharkdp/bat#adding-new-themes) | "zenburn" | No | | `GOOSE_CLI_SHOW_COST` | Show estimated cost for token use in the CLI | true/false | false | No | | `GOOSE_ALLOWLIST` | URL for allowed extensions | Valid URL | None | No | +| `GOOSE_DOCS_ROOT` | Documentation root used by `goose-doc-guide` (e.g. for offline/air-gapped docs) | Local path or HTTP(S) URL containing `goose-docs-map.md` and `docs/` | `https://goose-docs.ai` | No | | `GOOSE_RECIPE_GITHUB_REPO` | GitHub repository for recipes | Format: "org/repo" | None | No | | `GOOSE_AUTO_COMPACT_THRESHOLD` | Set the percentage threshold at which goose [automatically summarizes your session](/docs/guides/sessions/smart-context-management#automatic-compaction). | Float between 0.0 and 1.0 (disabled at 0.0)| 0.8 | No | | `SECURITY_PROMPT_ENABLED` | Enable [prompt injection detection](/docs/guides/security/prompt-injection-detection) to identify potentially harmful commands | true/false | false | No | @@ -81,6 +82,9 @@ GOOSE_CLI_MIN_PRIORITY: 0.2 # Recipe Configuration GOOSE_RECIPE_GITHUB_REPO: "aaif-goose/goose-recipes" +# Documentation Configuration +GOOSE_DOCS_ROOT: "/path/to/goose-docs" + # Search Path Configuration GOOSE_SEARCH_PATHS: - "/usr/local/bin" diff --git a/documentation/docs/guides/environment-variables.md b/documentation/docs/guides/environment-variables.md index ce6c62b9f8..990ff2cb1a 100644 --- a/documentation/docs/guides/environment-variables.md +++ b/documentation/docs/guides/environment-variables.md @@ -554,6 +554,14 @@ export GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS=300 export GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS=60 ``` +## Documentation Configuration + +This variable controls where the `goose-doc-guide` skill reads goose documentation from. + +| Variable | Purpose | Values | Default | +|----------|---------|---------|---------| +| `GOOSE_DOCS_ROOT` | Documentation root for the `goose-doc-guide` skill, used for [offline/air-gapped docs](/docs/guides/offline-docs) | Local path or HTTP(S) URL containing `goose-docs-map.md` and `docs/` | `https://goose-docs.ai` | + ## Development & Testing These variables are primarily used for development, testing, and debugging goose itself. diff --git a/documentation/docs/guides/offline-docs.md b/documentation/docs/guides/offline-docs.md new file mode 100644 index 0000000000..ef86b956d9 --- /dev/null +++ b/documentation/docs/guides/offline-docs.md @@ -0,0 +1,91 @@ +--- +title: Offline / Air-gapped Docs +sidebar_position: 95 +sidebar_label: Offline Docs +--- + +# Offline / Air-gapped Docs + +The `goose-doc-guide` skill reads official goose documentation before answering +goose-specific questions. By default it reads from `https://goose-docs.ai`. In +an offline or air-gapped environment, point goose at a **local copy** instead by +setting `GOOSE_DOCS_ROOT`. + +- If `GOOSE_DOCS_ROOT` is set (in `config.yaml` or the environment), goose uses + it as the docs root — either a local filesystem path or an HTTP(S) URL. +- If it is not set, goose falls back to `https://goose-docs.ai`. + +When the root is a local path, goose reads the docs with its file tools; no +network access is required. + +## Docs layout + +A docs root contains a docs map and a `docs/` tree: + +``` +/ +├── goose-docs-map.md +└── docs/ + ├── getting-started/... + └── guides/... +``` + +`goose-docs-map.md` is the index the skill searches first; every page it reads +is referenced by a path listed there. + +## Building a local docs root + +Generate the tree from a goose checkout using the same version as your goose +binary, so the docs match the runtime. For example: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +GOOSE_VERSION="${1:-v1.41.0}" +DOCS_ROOT="${2:-/opt/goose-docs}" +REPO="${GOOSE_REPO:-$(git rev-parse --show-toplevel)}" + +cd "$REPO" +git checkout --quiet "$GOOSE_VERSION" + +cd documentation +npm ci +node scripts/generate-docs-map.js + +rm -rf "$DOCS_ROOT" +mkdir -p "$DOCS_ROOT/docs" +cp static/goose-docs-map.md "$DOCS_ROOT/goose-docs-map.md" +cp -r docs/getting-started docs/guides "$DOCS_ROOT/docs/" + +CONFIG="${GOOSE_CONFIG_PATH:-$HOME/.config/goose/config.yaml}" +mkdir -p "$(dirname "$CONFIG")" +touch "$CONFIG" +sed -i.bak '/^GOOSE_DOCS_ROOT:/d' "$CONFIG" && rm -f "$CONFIG.bak" +echo "GOOSE_DOCS_ROOT: \"$DOCS_ROOT\"" >> "$CONFIG" +``` + +## Configuring goose + +Set `GOOSE_DOCS_ROOT` in `config.yaml`: + +```yaml +GOOSE_DOCS_ROOT: "/opt/goose-docs" +``` + +Or via the environment: + +```bash +export GOOSE_DOCS_ROOT=/opt/goose-docs +``` + +For a managed distribution, bake the docs tree into your image and set +`GOOSE_DOCS_ROOT` in the shipped `config.yaml` or launcher environment. + +## Notes + +- Documentation links in goose's answers always render as canonical + `https://goose-docs.ai/...` URLs, even when read locally. +- A custom HTTP(S) mirror also works: set `GOOSE_DOCS_ROOT` to its root URL. +- For MCP extension runtime issues offline, see + [Airgapped/Offline Environment Issues](/docs/troubleshooting/known-issues#airgappedoffline-environment-issues). diff --git a/documentation/docs/troubleshooting/known-issues.md b/documentation/docs/troubleshooting/known-issues.md index 38f1174569..e322953e38 100644 --- a/documentation/docs/troubleshooting/known-issues.md +++ b/documentation/docs/troubleshooting/known-issues.md @@ -507,6 +507,9 @@ goose Desktop uses **"shims"** (packaged versions of `npx` and `uvx`) that autom 4. **Require more changes**: In a corporate proxy environment or airgapped environment where the above doesn't work, it is recommended that you customize and package up goose Desktop with shims/config that will work given the network constraints you have (for example, TLS certificate limitations, [proxy configuration](/docs/guides/environment-variables#network-configuration), inability to download required content etc). +#### Documentation access: +The `goose-doc-guide` skill reads goose documentation from `https://goose-docs.ai` by default, which is unavailable offline. To have goose read from a local copy of the docs instead, see [Offline / Air-gapped Docs](/docs/guides/offline-docs). + --- ### Need Further Help?