Add configurable GOOSE_DOCS_ROOT for air-gapped docs access
Some checks failed
Cargo Deny / deny (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled

Resolve the goose-doc-guide docs root from the GOOSE_DOCS_ROOT config
value (falling back to https://goose-docs.ai), substituting it into the
builtin skill via a {{GOOSE_DOCS_ROOT}} placeholder.
This commit is contained in:
Alex Hancock 2026-07-05 11:56:58 -06:00
parent af2c765c82
commit 57932e0c5c
8 changed files with 233 additions and 16 deletions

View file

@ -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

View file

@ -1161,6 +1161,14 @@ impl Config {
}
}
pub fn get_goose_docs_root(&self) -> Result<Option<String>, ConfigError> {
match self.get_param::<String>("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<ThinkingEffort> {
self.get_param::<String>("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([

View file

@ -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 `<docs-root>`.
## 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 `<docs-root>/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-root>/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`

View file

@ -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<String> {
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<String> {
@ -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<String> {
@ -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);
}
}

View file

@ -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"

View file

@ -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.

View file

@ -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:
```
<docs-root>/
├── 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).

View file

@ -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?