diff --git a/.agents/skills/code-review/SKILL.md b/.agents/skills/code-review/SKILL.md
index c46a21a51db0..5da940f7ca68 100644
--- a/.agents/skills/code-review/SKILL.md
+++ b/.agents/skills/code-review/SKILL.md
@@ -92,6 +92,16 @@ You are a senior engineer conducting a thorough code review. Review **only the l
- **AnimatePresence**: Is it used properly with unique keys for dialog/modal transitions?
- **Reduced Motion**: Is `useReducedMotion()` respected for accessibility?
+### Async State, Defaults & Persistence
+- **Async Source of Truth**: During async provider/model/session mutations, does UI/session/localStorage state update only after the backend accepts the change? If the UI updates optimistically, is there an explicit rollback path?
+- **UI/Backend Drift**: Could the UI show provider/model/project/persona X while the backend is still on Y after a failed mutation, delayed prepare, or pending-to-real session handoff?
+- **Requested vs Fallback Authority**: Do explicit user or caller selections stay authoritative over sticky defaults, saved preferences, aliases, or fallback resolution?
+- **Dependent State Invalidation**: When a parent selection changes (provider/project/persona/workspace/etc.), are dependent values like `modelId`, `modelName`, defaults, or cached labels cleared or recomputed so stale state does not linger?
+- **Persisted Preference Validation**: Are stored selections validated against current inventory/capabilities before reuse, and do stale values fail soft instead of breaking creation flows?
+- **Compatibility of Fallbacks**: Are default or sticky selections guaranteed to remain compatible with the active concrete provider/backend, instead of leaking across providers?
+- **Best-Effort Lookups**: Do inventory/config/default-resolution lookups degrade gracefully on transient failure, or can they incorrectly block a primary flow that should still work with a safe fallback?
+- **Draft/Home/Handoff Paths**: If the product has draft, Home, pending, or pre-created sessions, did you review those handoff paths separately from the already-active session path?
+
### General Code Quality
- **Error Handling**: Are errors handled gracefully with user-friendly messages?
- **Loading States**: Are loading states shown during async operations?
@@ -104,13 +114,18 @@ You are a senior engineer conducting a thorough code review. Review **only the l
### Step 0: Run Quality Checks
-Before reading any code, run the project's CI gate to establish a baseline:
+Before reading any code, run the project's CI gate to establish a baseline. Use **check-only** commands so the baseline never mutates the working tree — otherwise auto-formatters can introduce unstaged diffs and you'll end up reviewing formatter output instead of the author's actual changes.
+
+Avoid `just check-everything` as the baseline in this repo: that recipe runs `cargo fmt --all` in write mode and will modify the working tree. Run the non-mutating equivalents instead:
```bash
-just ci
+cargo fmt --all -- --check
+cargo clippy --all-targets -- -D warnings
+(cd ui/desktop && pnpm run lint:check)
+./scripts/check-openapi-schema.sh
```
-This runs: `pnpm check` (Biome lint/format + file sizes), `pnpm typecheck`, `just clippy` (Rust linting), `pnpm test`, `pnpm build`, and `just tauri-check` (Rust type checking).
+If the project has a stronger pre-push or CI gate than this helper set, run that fuller gate when the review is meant to be PR-ready, but only after confirming it is also non-mutating (or run it from a clean stash). In this repo, targeted tests for the changed area plus the pre-push checks are often the practical follow-up.
Report the results as pass/fail. Any failures are automatically **P0** issues and should appear at the top of the findings list. Do not skip this step even if the user only wants a quick review.
@@ -120,7 +135,8 @@ For each file in the list:
1. Run `git diff main...HEAD -- ` to get the exact lines that changed
2. Review **only those changed lines** against the Review Checklist — do not flag issues in unchanged code
-3. Note the file path and line numbers from the diff output for each issue found
+3. For stateful UI or async flow changes, trace the full path end to end: user selection -> local/session state update -> persistence -> backend prepare/set/update call -> failure/rollback path
+4. Note the file path and line numbers from the diff output for each issue found
### Step 2: Categorize Issues
@@ -152,16 +168,17 @@ After reviewing all files, provide:
### Step 3b: Self-Check
-Before presenting findings to the user, silently review the issue list two more times:
+Before presenting findings to the user, silently review the issue list three times:
1. **Pass 1**: For each issue, ask — is this genuinely a problem, or could it be intentional/acceptable? Remove false positives.
2. **Pass 2**: For each remaining issue, ask — does the recommended fix actually improve the code, or is it a matter of preference?
+3. **Pass 3**: For async state/default-resolution issues, ask — can the UI, persisted state, and backend ever disagree after a failure, fallback, or session handoff?
-After both passes, tag each surviving issue as one of:
+After these passes, tag each surviving issue as one of:
- **[Must Fix]** — clear violation, will likely get flagged in PR review
- **[Your Call]** — valid concern but may be intentional or a reasonable tradeoff (e.g. stepping outside the design system for a specific reason). Present it but let the user decide.
-Only present issues that survived both passes.
+Only present issues that survived these passes.
### Step 4: Fix Issues
@@ -189,7 +206,7 @@ Once all issues are fixed, display:
**✅ Code review complete! All issues have been addressed.**
-Your code is ready to commit and push. Lefthook will run the full CI gate (`just ci`) automatically when you push.
+Your code is ready to commit and push. Lefthook and CI will run the repo's configured gates when you push.
Next steps: generate a PR summary that explains the intent of this change, what files were modified and why, and how to verify the changes work.
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index d95ef3c462ab..000000000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1,6 +0,0 @@
-# CODEOWNERS file for aaif-goose/goose repository
-# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
-
-# Documentation owned by DevRel
-/documentation/ @blackgirlbytes @angiejones @DOsinga
-
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000000..25f111e03732
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,33 @@
+version: 2
+updates:
+ # pnpm workspace for the UI (desktop, acp, text, sdk, goose-binary/*, goose2).
+ # Point at the workspace ROOT where pnpm-lock.yaml lives so Dependabot updates
+ # both the child package.json AND ui/pnpm-lock.yaml in one PR.
+ - package-ecosystem: "npm"
+ directory: "/ui"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ ui-minor-and-patch:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # Cargo workspace at the repo root.
+ - package-ecosystem: "cargo"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ cargo-minor-and-patch:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # GitHub Actions used by workflows in .github/workflows.
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/bundle-goose2-manual.yml b/.github/workflows/bundle-goose2-manual.yml
new file mode 100644
index 000000000000..8655287054ed
--- /dev/null
+++ b/.github/workflows/bundle-goose2-manual.yml
@@ -0,0 +1,26 @@
+name: "Manual Goose 2 Bundle (Unsigned)"
+
+on:
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: "Branch name to bundle app from"
+ required: true
+ type: string
+ cli-run-id:
+ description: "Run ID of a build-cli workflow to pull the goose binary from (optional, builds from source if empty)"
+ required: false
+ type: string
+ default: ""
+
+jobs:
+ bundle-goose2:
+ uses: ./.github/workflows/bundle-goose2.yml
+ permissions:
+ id-token: write
+ contents: read
+ actions: read
+ with:
+ signing: false
+ ref: ${{ inputs.branch }}
+ cli-run-id: ${{ inputs.cli-run-id }}
diff --git a/.github/workflows/bundle-goose2.yml b/.github/workflows/bundle-goose2.yml
new file mode 100644
index 000000000000..814ffbfa80ad
--- /dev/null
+++ b/.github/workflows/bundle-goose2.yml
@@ -0,0 +1,771 @@
+# Reusable workflow that bundles the Goose 2 (Tauri) desktop app.
+# Produces .app / .dmg on macOS and .deb / .AppImage on Linux.
+#
+# The justfile recipe is: `just bundle` → `pnpm tauri build`
+#
+# Called from release.yml, canary.yml, or manually via bundle-goose2-manual.yml.
+#
+# The goose CLI binary can either be built from source (default) or pulled
+# from a prior build-cli.yml run by passing `cli-run-id`. This avoids
+# redundant ~20min Rust builds during release pipelines.
+name: "Bundle Goose 2 Desktop"
+
+on:
+ workflow_call:
+ inputs:
+ version:
+ description: "Version to set for the build (leave empty to use Cargo.toml default)"
+ required: false
+ default: ""
+ type: string
+ signing:
+ description: "Whether to perform Apple signing and notarization (macOS only)"
+ required: false
+ default: false
+ type: boolean
+ quick_test:
+ description: "Whether to perform the quick launch test (macOS only)"
+ required: false
+ default: true
+ type: boolean
+ ref:
+ description: "Git ref to checkout (branch, tag, or SHA). Defaults to the triggering ref."
+ required: false
+ default: ""
+ type: string
+ environment:
+ description: "GitHub Environment containing signing secrets. Leave empty to skip."
+ required: false
+ default: ""
+ type: string
+ windows-signing:
+ description: "Whether to perform Windows signing via Azure Trusted Signing"
+ required: false
+ default: false
+ type: boolean
+ cli-run-id:
+ description: >
+ Run ID of a prior build-cli.yml workflow run to download the goose
+ binary from. When empty, the goose CLI is built from source.
+ required: false
+ default: ""
+ type: string
+
+jobs:
+ # ───────────────────────────────────────────────
+ # macOS ARM (Apple Silicon)
+ # ───────────────────────────────────────────────
+ bundle-macos-arm:
+ name: "macOS ARM64"
+ runs-on: macos-latest
+ environment: ${{ inputs.environment || '' }}
+ timeout-minutes: 60
+ env:
+ MACOSX_DEPLOYMENT_TARGET: "12.0"
+ permissions:
+ id-token: write
+ contents: read
+ actions: read
+ outputs:
+ artifact-url: ${{ steps.upload.outputs.artifact-url }}
+ steps:
+ - name: Debug workflow info
+ env:
+ INPUT_REF: ${{ inputs.ref }}
+ INPUT_VERSION: ${{ inputs.version }}
+ INPUT_SIGNING: ${{ inputs.signing }}
+ INPUT_CLI_RUN_ID: ${{ inputs.cli-run-id }}
+ run: |
+ echo "=== Goose 2 Bundle (macOS ARM64) ==="
+ echo "Ref: ${INPUT_REF:-}"
+ echo "Version: ${INPUT_VERSION:-}"
+ echo "Signing: ${INPUT_SIGNING}"
+ echo "CLI run ID: ${INPUT_CLI_RUN_ID:-}"
+ df -h
+
+ - name: Checkout code
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ with:
+ ref: ${{ inputs.ref != '' && inputs.ref || '' }}
+
+ # ── Version stamps ──
+ - name: Update versions
+ if: inputs.version != ''
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ # Root Cargo.toml (workspace version)
+ sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml
+ rm -f Cargo.toml.bak
+
+ # Tauri Cargo.toml
+ sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" ui/goose2/src-tauri/Cargo.toml
+ rm -f ui/goose2/src-tauri/Cargo.toml.bak
+
+ # package.json
+ source ./bin/activate-hermit
+ cd ui/goose2
+ npm pkg set "version=${VERSION}"
+
+ # ── Goose CLI: download from prior run OR build from source ──
+ - name: Download goose CLI from build-cli run
+ if: inputs.cli-run-id != ''
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
+ with:
+ name: goose-aarch64-apple-darwin
+ run-id: ${{ inputs.cli-run-id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ path: cli-artifact
+
+ - name: Extract downloaded goose CLI
+ if: inputs.cli-run-id != ''
+ run: |
+ TRIPLE="aarch64-apple-darwin"
+ mkdir -p target/release
+ # The artifact contains goose-.tar.bz2 with goose inside
+ tar -xjf "cli-artifact/goose-${TRIPLE}.tar.bz2" -C target/release/
+ chmod +x target/release/goose
+ cp target/release/goose "target/release/goose-${TRIPLE}"
+ ls -lh "target/release/goose-${TRIPLE}"
+
+ - name: Cache Rust dependencies
+ if: inputs.cli-run-id == ''
+ uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
+ with:
+ key: goose2-macos-arm64
+
+ - name: Build goose CLI (release)
+ if: inputs.cli-run-id == ''
+ run: |
+ source ./bin/activate-hermit
+ cargo build --release -p goose-cli --bin goose
+
+ - name: Prepare goose binary with target triple
+ if: inputs.cli-run-id == ''
+ run: |
+ TRIPLE=$(rustc --print host-tuple)
+ cp target/release/goose "target/release/goose-${TRIPLE}"
+ ls -lh "target/release/goose-${TRIPLE}"
+
+ # ── Frontend: pnpm install + SDK build ──
+ - name: Cache pnpm dependencies
+ uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ with:
+ path: |
+ ui/goose2/node_modules
+ ui/sdk/node_modules
+ .hermit/node/cache
+ key: goose2-pnpm-macos-arm64-${{ runner.os }}-${{ hashFiles('ui/pnpm-lock.yaml') }}
+ restore-keys: |
+ goose2-pnpm-macos-arm64-${{ runner.os }}-
+
+ - name: Install pnpm dependencies
+ run: |
+ source ./bin/activate-hermit
+ cd ui
+ pnpm install --frozen-lockfile
+
+ - name: Build SDK
+ run: |
+ source ./bin/activate-hermit
+ cd ui/sdk
+ pnpm build
+
+ # ── Apple signing ──
+ - name: Import Apple signing certificate
+ if: inputs.signing
+ uses: ./.github/actions/apple-codesign
+ with:
+ certificate-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
+ certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+
+ # ── Tauri bundle ──
+ - name: Bundle Goose 2 (pnpm tauri build)
+ env:
+ APPLE_SIGNING_IDENTITY: ${{ inputs.signing && 'Developer ID Application' || '' }}
+ APPLE_ID: ${{ inputs.signing && secrets.APPLE_ID || '' }}
+ APPLE_PASSWORD: ${{ inputs.signing && secrets.APPLE_ID_PASSWORD || '' }}
+ APPLE_TEAM_ID: ${{ inputs.signing && secrets.APPLE_TEAM_ID || '' }}
+ working-directory: ui/goose2
+ run: |
+ source ../../bin/activate-hermit
+ pnpm tauri build
+
+ - name: Clean up signing keychain
+ if: always()
+ run: |
+ if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then
+ security delete-keychain "$KEYCHAIN_PATH" || true
+ fi
+
+ # ── Upload ──
+ - name: List bundle output
+ run: |
+ BUNDLE_DIR="ui/goose2/src-tauri/target/release/bundle"
+ echo "=== Bundle contents ==="
+ find "$BUNDLE_DIR" -type f 2>/dev/null || echo "(no bundle output found)"
+
+ - name: Upload Goose 2 macOS ARM64 artifact
+ id: upload
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: Goose2-darwin-arm64
+ path: |
+ ui/goose2/src-tauri/target/release/bundle/dmg/*.dmg
+ ui/goose2/src-tauri/target/release/bundle/macos/*.app
+ if-no-files-found: error
+
+ # ── Smoke test ──
+ - name: Quick launch test
+ if: inputs.quick_test
+ run: |
+ APP_PATH=$(find ui/goose2/src-tauri/target/release/bundle/macos -maxdepth 1 -name "*.app" | head -1)
+ if [ -z "$APP_PATH" ]; then
+ echo "No .app found, skipping launch test"
+ exit 0
+ fi
+
+ xattr -cr "$APP_PATH"
+ echo "Opening $APP_PATH..."
+ open -g "$APP_PATH"
+ sleep 5
+
+ if pgrep -f "Goose.app/Contents/MacOS" > /dev/null; then
+ echo "✅ App is running"
+ else
+ echo "❌ App did not stay open"
+ exit 1
+ fi
+
+ pkill -f "Goose.app/Contents/MacOS" || true
+
+ # ───────────────────────────────────────────────
+ # macOS Intel (x86_64)
+ # ───────────────────────────────────────────────
+ bundle-macos-intel:
+ name: "macOS x86_64"
+ runs-on: macos-latest
+ environment: ${{ inputs.environment || '' }}
+ timeout-minutes: 60
+ env:
+ MACOSX_DEPLOYMENT_TARGET: "12.0"
+ permissions:
+ id-token: write
+ contents: read
+ actions: read
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ with:
+ ref: ${{ inputs.ref != '' && inputs.ref || '' }}
+
+ - name: Update versions
+ if: inputs.version != ''
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml
+ rm -f Cargo.toml.bak
+ sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" ui/goose2/src-tauri/Cargo.toml
+ rm -f ui/goose2/src-tauri/Cargo.toml.bak
+ source ./bin/activate-hermit
+ cd ui/goose2
+ npm pkg set "version=${VERSION}"
+
+ # ── Goose CLI: download from prior run OR build from source ──
+ - name: Download goose CLI from build-cli run
+ if: inputs.cli-run-id != ''
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
+ with:
+ name: goose-x86_64-apple-darwin
+ run-id: ${{ inputs.cli-run-id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ path: cli-artifact
+
+ - name: Extract downloaded goose CLI
+ if: inputs.cli-run-id != ''
+ run: |
+ TARGET="x86_64-apple-darwin"
+ mkdir -p target/release
+ tar -xjf "cli-artifact/goose-${TARGET}.tar.bz2" -C target/release/
+ chmod +x target/release/goose
+ cp target/release/goose "target/release/goose-${TARGET}"
+ ls -lh "target/release/goose-${TARGET}"
+
+ - name: Cache Rust dependencies
+ if: inputs.cli-run-id == ''
+ uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
+ with:
+ key: goose2-macos-x86_64
+
+ - name: Install Intel target for both toolchains
+ if: inputs.cli-run-id == ''
+ run: |
+ source ./bin/activate-hermit
+ rustup target add x86_64-apple-darwin
+ cd ui/goose2/src-tauri
+ rustup target add x86_64-apple-darwin
+
+ - name: Build goose CLI for Intel (x86_64-apple-darwin)
+ if: inputs.cli-run-id == ''
+ run: |
+ source ./bin/activate-hermit
+ cargo build --release -p goose-cli --bin goose --target x86_64-apple-darwin
+
+ - name: Prepare goose binary with target triple
+ if: inputs.cli-run-id == ''
+ run: |
+ TARGET="x86_64-apple-darwin"
+ mkdir -p target/release
+ cp "target/${TARGET}/release/goose" "target/release/goose-${TARGET}"
+ ls -lh "target/release/goose-${TARGET}"
+
+ # ── Intel target still needed for Tauri's own Rust build ──
+ - name: Install Intel target for Tauri toolchain
+ run: |
+ source ./bin/activate-hermit
+ rustup target add x86_64-apple-darwin
+ cd ui/goose2/src-tauri
+ rustup target add x86_64-apple-darwin
+
+ # ── Frontend ──
+ - name: Cache pnpm dependencies
+ uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ with:
+ path: |
+ ui/goose2/node_modules
+ ui/sdk/node_modules
+ .hermit/node/cache
+ key: goose2-pnpm-macos-intel-${{ runner.os }}-${{ hashFiles('ui/pnpm-lock.yaml') }}
+ restore-keys: |
+ goose2-pnpm-macos-intel-${{ runner.os }}-
+
+ - name: Install pnpm dependencies
+ run: |
+ source ./bin/activate-hermit
+ cd ui
+ pnpm install --frozen-lockfile
+
+ - name: Build SDK
+ run: |
+ source ./bin/activate-hermit
+ cd ui/sdk
+ pnpm build
+
+ # ── Apple signing ──
+ - name: Import Apple signing certificate
+ if: inputs.signing
+ uses: ./.github/actions/apple-codesign
+ with:
+ certificate-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
+ certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+
+ # ── Tauri bundle (cross-compile for Intel) ──
+ - name: Bundle Goose 2 for Intel
+ env:
+ APPLE_SIGNING_IDENTITY: ${{ inputs.signing && 'Developer ID Application' || '' }}
+ APPLE_ID: ${{ inputs.signing && secrets.APPLE_ID || '' }}
+ APPLE_PASSWORD: ${{ inputs.signing && secrets.APPLE_ID_PASSWORD || '' }}
+ APPLE_TEAM_ID: ${{ inputs.signing && secrets.APPLE_TEAM_ID || '' }}
+ working-directory: ui/goose2
+ run: |
+ source ../../bin/activate-hermit
+ pnpm tauri build --target x86_64-apple-darwin
+
+ - name: Clean up signing keychain
+ if: always()
+ run: |
+ if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then
+ security delete-keychain "$KEYCHAIN_PATH" || true
+ fi
+
+ # ── Upload ──
+ - name: List bundle output
+ run: |
+ BUNDLE_DIR="ui/goose2/src-tauri/target/x86_64-apple-darwin/release/bundle"
+ echo "=== Bundle contents ==="
+ find "$BUNDLE_DIR" -type f 2>/dev/null || echo "(no bundle output found)"
+
+ - name: Upload Goose 2 macOS Intel artifact
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: Goose2-darwin-x64
+ path: |
+ ui/goose2/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/*.dmg
+ ui/goose2/src-tauri/target/x86_64-apple-darwin/release/bundle/macos/*.app
+ if-no-files-found: error
+
+ - name: Quick launch test
+ if: inputs.quick_test
+ run: |
+ APP_PATH=$(find ui/goose2/src-tauri/target/x86_64-apple-darwin/release/bundle/macos -maxdepth 1 -name "*.app" 2>/dev/null | head -1)
+ if [ -z "$APP_PATH" ]; then
+ echo "No .app found, skipping launch test"
+ exit 0
+ fi
+
+ xattr -cr "$APP_PATH"
+ echo "Opening $APP_PATH..."
+ open -g "$APP_PATH"
+ sleep 5
+
+ if pgrep -f "Goose.app/Contents/MacOS" > /dev/null; then
+ echo "✅ App is running"
+ else
+ echo "❌ App did not stay open"
+ exit 1
+ fi
+
+ pkill -f "Goose.app/Contents/MacOS" || true
+
+ # ───────────────────────────────────────────────
+ # Linux x86_64
+ # ───────────────────────────────────────────────
+ bundle-linux:
+ name: "Linux x86_64"
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ permissions:
+ contents: read
+ actions: read
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ with:
+ ref: ${{ inputs.ref != '' && inputs.ref || '' }}
+
+ - name: Install system dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential \
+ libgtk-3-dev \
+ libwebkit2gtk-4.1-dev \
+ libappindicator3-dev \
+ librsvg2-dev \
+ patchelf \
+ protobuf-compiler
+
+ - name: Update versions
+ if: inputs.version != ''
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml
+ rm -f Cargo.toml.bak
+ sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" ui/goose2/src-tauri/Cargo.toml
+ rm -f ui/goose2/src-tauri/Cargo.toml.bak
+ source ./bin/activate-hermit
+ cd ui/goose2
+ npm pkg set "version=${VERSION}"
+
+ # ── Goose CLI: download from prior run OR build from source ──
+ - name: Download goose CLI from build-cli run
+ if: inputs.cli-run-id != ''
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
+ with:
+ name: goose-x86_64-unknown-linux-gnu
+ run-id: ${{ inputs.cli-run-id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ path: cli-artifact
+
+ - name: Extract downloaded goose CLI
+ if: inputs.cli-run-id != ''
+ run: |
+ TRIPLE="x86_64-unknown-linux-gnu"
+ mkdir -p target/release
+ tar -xjf "cli-artifact/goose-${TRIPLE}.tar.bz2" -C target/release/
+ chmod +x target/release/goose
+ cp target/release/goose "target/release/goose-${TRIPLE}"
+ ls -lh "target/release/goose-${TRIPLE}"
+
+ - name: Cache Rust dependencies
+ if: inputs.cli-run-id == ''
+ uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
+ with:
+ key: goose2-linux-x86_64
+
+ - name: Build goose CLI (release)
+ if: inputs.cli-run-id == ''
+ run: |
+ source ./bin/activate-hermit
+ cargo build --release -p goose-cli --bin goose
+
+ - name: Prepare goose binary with target triple
+ if: inputs.cli-run-id == ''
+ run: |
+ TRIPLE=$(rustc --print host-tuple)
+ cp target/release/goose "target/release/goose-${TRIPLE}"
+ ls -lh "target/release/goose-${TRIPLE}"
+
+ # ── Frontend ──
+ - name: Cache pnpm dependencies
+ uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ with:
+ path: |
+ ui/goose2/node_modules
+ ui/sdk/node_modules
+ .hermit/node/cache
+ key: goose2-pnpm-linux-${{ runner.os }}-${{ hashFiles('ui/pnpm-lock.yaml') }}
+ restore-keys: |
+ goose2-pnpm-linux-${{ runner.os }}-
+
+ - name: Install pnpm dependencies
+ run: |
+ source ./bin/activate-hermit
+ cd ui
+ pnpm install --frozen-lockfile
+
+ - name: Build SDK
+ run: |
+ source ./bin/activate-hermit
+ cd ui/sdk
+ pnpm build
+
+ # ── Tauri bundle ──
+ - name: Bundle Goose 2 (pnpm tauri build)
+ working-directory: ui/goose2
+ run: |
+ source ../../bin/activate-hermit
+ pnpm tauri build
+
+ # ── Upload ──
+ - name: List bundle output
+ run: |
+ BUNDLE_DIR="ui/goose2/src-tauri/target/release/bundle"
+ echo "=== Bundle contents ==="
+ find "$BUNDLE_DIR" -type f 2>/dev/null | head -30
+ echo ""
+ echo "=== File sizes ==="
+ find "$BUNDLE_DIR" -type f -exec ls -lh {} \; 2>/dev/null | head -20
+
+ - name: Upload .deb package
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: Goose2-linux-x64-deb
+ path: ui/goose2/src-tauri/target/release/bundle/deb/*.deb
+ if-no-files-found: warn
+
+ - name: Upload AppImage
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: Goose2-linux-x64-appimage
+ path: ui/goose2/src-tauri/target/release/bundle/appimage/*.AppImage
+ if-no-files-found: warn
+
+ - name: Upload RPM package
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: Goose2-linux-x64-rpm
+ path: ui/goose2/src-tauri/target/release/bundle/rpm/*.rpm
+ if-no-files-found: warn
+
+ # ───────────────────────────────────────────────
+ # Windows x86_64
+ # ───────────────────────────────────────────────
+ bundle-windows:
+ name: "Windows x86_64"
+ runs-on: windows-latest
+ timeout-minutes: 60
+ permissions:
+ id-token: write
+ contents: read
+ actions: read
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ with:
+ ref: ${{ inputs.ref != '' && inputs.ref || '' }}
+
+ # Hermit doesn't work on Windows — install node/pnpm directly
+ - name: Set up Node.js
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ with:
+ node-version: 24
+
+ - name: Install pnpm
+ run: npm install -g pnpm@10.33.0
+
+ - name: Update versions
+ if: inputs.version != ''
+ shell: bash
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml
+ rm -f Cargo.toml.bak
+ sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" ui/goose2/src-tauri/Cargo.toml
+ rm -f ui/goose2/src-tauri/Cargo.toml.bak
+ cd ui/goose2
+ npm pkg set "version=${VERSION}"
+
+ # ── Goose CLI: download from prior run OR build from source ──
+ - name: Download goose CLI from build-cli run
+ if: inputs.cli-run-id != ''
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
+ with:
+ name: goose-x86_64-pc-windows-msvc
+ run-id: ${{ inputs.cli-run-id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ path: cli-artifact
+
+ - name: Extract downloaded goose CLI
+ if: inputs.cli-run-id != ''
+ shell: bash
+ run: |
+ TARGET="x86_64-pc-windows-msvc"
+ mkdir -p target/release
+ # The zip contains goose-package/goose.exe
+ cd cli-artifact
+ 7z x "goose-${TARGET}.zip"
+ cd ..
+ cp cli-artifact/goose-package/goose.exe target/release/
+ # Tauri externalBin appends target triple + .exe on Windows
+ cp target/release/goose.exe "target/release/goose-${TARGET}.exe"
+ ls -lh "target/release/goose-${TARGET}.exe"
+
+ - name: Cache Rust dependencies
+ if: inputs.cli-run-id == ''
+ uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
+ with:
+ key: goose2-windows-x86_64
+
+ - name: Setup Rust
+ if: inputs.cli-run-id == ''
+ shell: bash
+ run: |
+ rustup show
+ rustup target add x86_64-pc-windows-msvc
+
+ - name: Build goose CLI (release)
+ if: inputs.cli-run-id == ''
+ shell: bash
+ run: |
+ cargo build --release --target x86_64-pc-windows-msvc -p goose-cli --bin goose
+
+ - name: Prepare goose binary with target triple
+ if: inputs.cli-run-id == ''
+ shell: bash
+ run: |
+ TARGET="x86_64-pc-windows-msvc"
+ cp "target/${TARGET}/release/goose.exe" "target/release/goose-${TARGET}.exe"
+ ls -lh "target/release/goose-${TARGET}.exe"
+
+ # ── Frontend ──
+ - name: Cache pnpm dependencies
+ uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ with:
+ path: |
+ ui/goose2/node_modules
+ ui/sdk/node_modules
+ key: goose2-pnpm-windows-${{ runner.os }}-${{ hashFiles('ui/pnpm-lock.yaml') }}
+ restore-keys: |
+ goose2-pnpm-windows-${{ runner.os }}-
+
+ - name: Install pnpm dependencies
+ shell: bash
+ run: |
+ cd ui
+ pnpm install --frozen-lockfile
+
+ - name: Build SDK
+ shell: bash
+ run: |
+ cd ui/sdk
+ pnpm build
+
+ # ── Tauri bundle ──
+ - name: Bundle Goose 2 (pnpm tauri build)
+ shell: bash
+ working-directory: ui/goose2
+ run: |
+ pnpm tauri build --target x86_64-pc-windows-msvc
+
+ # ── Upload ──
+ - name: List bundle output
+ shell: bash
+ run: |
+ BUNDLE_DIR="ui/goose2/src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
+ echo "=== Bundle contents ==="
+ find "$BUNDLE_DIR" -type f 2>/dev/null || echo "(no bundle output found)"
+
+ - name: Upload NSIS installer
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: Goose2-windows-x64-nsis
+ path: ui/goose2/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
+ if-no-files-found: warn
+
+ - name: Upload MSI installer
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: Goose2-windows-x64-msi
+ path: ui/goose2/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
+ if-no-files-found: warn
+
+ sign-windows:
+ name: "Sign Windows installers"
+ needs: bundle-windows
+ if: inputs.windows-signing
+ runs-on: windows-latest
+ environment: signing
+ permissions:
+ id-token: write
+ contents: read
+ actions: read
+ steps:
+ - name: Download NSIS installer
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-windows-x64-nsis
+ path: unsigned/nsis
+
+ - name: Download MSI installer
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-windows-x64-msi
+ path: unsigned/msi
+
+ - name: Azure login
+ uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
+ with:
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+
+ - name: Sign Windows installers with Azure Trusted Signing
+ uses: azure/trusted-signing-action@db7a3a6bd3912025c705162fb7475389f5b69ec6 # v1
+ with:
+ endpoint: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
+ trusted-signing-account-name: ${{ secrets.AZURE_SIGNING_ACCOUNT_NAME }}
+ certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }}
+ files-folder: ${{ github.workspace }}/unsigned
+ files-folder-filter: exe,msi
+ files-folder-recurse: true
+
+ - name: Verify signed installers
+ shell: pwsh
+ run: |
+ $files = Get-ChildItem -Path unsigned -Recurse -Include *.exe,*.msi
+ foreach ($file in $files) {
+ Write-Output "Verifying signature: $($file.FullName)"
+ $sig = Get-AuthenticodeSignature $file.FullName
+ if ($sig.Status -ne "Valid") {
+ throw "Signature invalid for $($file.Name): $($sig.Status)"
+ }
+ Write-Output "✅ Signature valid: $($file.Name)"
+ }
+
+ - name: Upload signed NSIS installer
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ with:
+ name: Goose2-windows-x64-nsis-signed
+ path: unsigned/nsis/*.exe
+ if-no-files-found: error
+
+ - name: Upload signed MSI installer
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ with:
+ name: Goose2-windows-x64-msi-signed
+ path: unsigned/msi/*.msi
+ if-no-files-found: error
diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index 181f9466dcdc..65c1741528ce 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -143,7 +143,7 @@ jobs:
# Create/update the canary release
- name: Release canary
- uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
+ uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
with:
tag: canary
name: Canary
diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml
index c2215b088720..e9081364f97f 100644
--- a/.github/workflows/cargo-deny.yml
+++ b/.github/workflows/cargo-deny.yml
@@ -24,6 +24,6 @@ jobs:
steps:
- uses: actions/checkout@v4
# https://github.com/EmbarkStudios/cargo-deny-action v2.0.15
- - uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979
+ - uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb
with:
command: check advisories
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1db093a0c056..8cea74121f80 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -41,7 +41,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
+ - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
- name: Run cargo fmt
run: cargo fmt --check
@@ -55,7 +55,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
+ - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
- name: Install Dependencies
run: |
@@ -100,6 +100,43 @@ jobs:
env:
CARGO_INCREMENTAL: "0"
+ rust-msrv:
+ name: Check MSRV
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.code == 'true' || github.event_name != 'pull_request'
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Read MSRV from Cargo.toml
+ id: msrv
+ run: |
+ msrv=$(sed -n 's/^rust-version = "\(.*\)"$/\1/p' Cargo.toml)
+ if [ -z "$msrv" ]; then
+ echo "Could not find rust-version in workspace Cargo.toml" >&2
+ exit 1
+ fi
+ echo "msrv=$msrv" >> "$GITHUB_OUTPUT"
+ echo "MSRV: $msrv"
+
+ - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
+ with:
+ toolchain: ${{ steps.msrv.outputs.msrv }}
+
+ - name: Install Dependencies
+ run: |
+ sudo apt update -y
+ sudo apt install -y libdbus-1-dev libxcb1-dev
+
+ - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
+ with:
+ key: msrv
+
+ - name: Check with MSRV toolchain
+ run: cargo check --workspace --locked --all-targets
+ env:
+ CARGO_INCREMENTAL: "0"
+
rust-lint:
name: Lint Rust Code
runs-on: ubuntu-latest
@@ -108,7 +145,7 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
+ - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
- uses: Swatinem/rust-cache@v2
@@ -130,7 +167,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
+ - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
- name: Install Dependencies
run: |
diff --git a/.github/workflows/docs-update-cli-ref.yml b/.github/workflows/docs-update-cli-ref.yml
index 37600bfe3158..8253f9888633 100644
--- a/.github/workflows/docs-update-cli-ref.yml
+++ b/.github/workflows/docs-update-cli-ref.yml
@@ -63,7 +63,7 @@ jobs:
sudo apt-get install -y jq ripgrep
- name: Set up Rust
- uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1
+ uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
with:
toolchain: stable
diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml
index 4a2d70b001fe..68a2eae8b8dd 100644
--- a/.github/workflows/goose-release-notes.yml
+++ b/.github/workflows/goose-release-notes.yml
@@ -170,7 +170,7 @@ jobs:
steps:
- name: Update release body
- uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
+ uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
with:
tag: ${{ needs.generate-release-notes.outputs.release_tag }}
token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/goose2-ci.yml b/.github/workflows/goose2-ci.yml
index 12f70754ce0a..57b4f12aa16c 100644
--- a/.github/workflows/goose2-ci.yml
+++ b/.github/workflows/goose2-ci.yml
@@ -91,7 +91,9 @@ jobs:
patchelf
- name: Install Rust
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
+ uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
+ with:
+ rust-src-dir: ui/goose2
- name: Cache Rust
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
@@ -158,8 +160,9 @@ jobs:
patchelf
- name: Install Rust
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
+ uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
with:
+ rust-src-dir: ui/goose2
components: rustfmt, clippy
- name: Cache Rust
diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml
index cdc0414ace61..f49d983dae5e 100644
--- a/.github/workflows/pr-smoke-test.yml
+++ b/.github/workflows/pr-smoke-test.yml
@@ -55,7 +55,7 @@ jobs:
with:
ref: ${{ github.event.inputs.branch || github.ref }}
- - uses: actions-rust-lang/setup-rust-toolchain@v1
+ - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
- name: Install Dependencies
run: |
@@ -108,9 +108,13 @@ jobs:
node-version: '22'
- name: Install agentic providers
- run: npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli @zed-industries/claude-agent-acp @zed-industries/codex-acp
+ run: npm install -g @anthropic-ai/claude-code @zed-industries/claude-agent-acp @zed-industries/codex-acp
- - name: Run Smoke Tests with Provider Script
+ - name: Install Node.js Dependencies
+ run: source ../../bin/activate-hermit && pnpm install --frozen-lockfile
+ working-directory: ui/desktop
+
+ - name: Run Smoke Tests (Normal Mode)
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -127,12 +131,10 @@ jobs:
SKIP_BUILD: 1
SKIP_PROVIDERS: ${{ vars.SKIP_PROVIDERS || '' }}
run: |
- # Ensure the HOME directory structure exists
mkdir -p $HOME/.local/share/goose/sessions
mkdir -p $HOME/.config/goose
-
- # Run the provider test script (binary already built and downloaded)
- bash scripts/test_providers.sh
+ source ../../bin/activate-hermit && pnpm run test:integration:providers
+ working-directory: ui/desktop
- name: Set up Python
uses: actions/setup-python@v5
@@ -188,6 +190,10 @@ jobs:
- name: Make Binary Executable
run: chmod +x target/debug/goose
+ - name: Install Node.js Dependencies
+ run: source ../../bin/activate-hermit && pnpm install --frozen-lockfile
+ working-directory: ui/desktop
+
- name: Run Provider Tests (Code Execution Mode)
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -205,7 +211,8 @@ jobs:
run: |
mkdir -p $HOME/.local/share/goose/sessions
mkdir -p $HOME/.config/goose
- bash scripts/test_providers_code_exec.sh
+ source ../../bin/activate-hermit && pnpm run test:integration:providers-code-exec
+ working-directory: ui/desktop
compaction-tests:
name: Compaction Tests
@@ -277,7 +284,8 @@ jobs:
GOOSE_PROVIDER: anthropic
GOOSE_MODEL: claude-sonnet-4-5-20250929
SHELL: /bin/bash
+ SKIP_BUILD: 1
run: |
echo 'export PATH=/some/fake/path:$PATH' >> $HOME/.bash_profile
- source ../../bin/activate-hermit && pnpm run test:integration:debug
+ source ../../bin/activate-hermit && pnpm run test:integration:goosed
working-directory: ui/desktop
diff --git a/.github/workflows/pr-website-preview.yml b/.github/workflows/pr-website-preview.yml
index 41cc51164b3d..4b1e6e35426e 100644
--- a/.github/workflows/pr-website-preview.yml
+++ b/.github/workflows/pr-website-preview.yml
@@ -51,7 +51,7 @@ jobs:
cleanup:
runs-on: ubuntu-latest
needs: deploy
- if: github.event.action == 'closed'
+ if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == 'aaif-goose/goose'
permissions:
contents: write
steps:
diff --git a/.github/workflows/release-goose2.yml b/.github/workflows/release-goose2.yml
new file mode 100644
index 000000000000..bc1d8e4ae766
--- /dev/null
+++ b/.github/workflows/release-goose2.yml
@@ -0,0 +1,192 @@
+on:
+ push:
+ tags:
+ - "v2.*"
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version string (e.g. 2.0.0-rc.1). Used when testing from a branch."
+ required: true
+ type: string
+ cli-run-id:
+ description: "Run ID of a build-cli workflow to pull goose binaries from (skips CLI build step)"
+ required: false
+ type: string
+ default: ""
+
+name: "Release Goose 2"
+
+permissions:
+ id-token: write # Sigstore OIDC signing + Azure OIDC (Windows signing)
+ contents: write # Creating releases + actions/checkout
+ actions: read # Downloading artifacts across workflow runs
+ attestations: write # SLSA build provenance attestations
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ prepare-version:
+ name: Prepare Version
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.set-version.outputs.version }}
+ steps:
+ - name: Extract version
+ id: set-version
+ run: |
+ if [ -n "${{ inputs.version }}" ]; then
+ VERSION="${{ inputs.version }}"
+ else
+ # Strip the leading "v" from the tag (e.g. v2.0.0 → 2.0.0)
+ VERSION="${GITHUB_REF_NAME#v}"
+ fi
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "Release version: $VERSION"
+
+ build-cli:
+ if: inputs.cli-run-id == ''
+ needs: [prepare-version]
+ uses: ./.github/workflows/build-cli.yml
+ with:
+ version: ${{ needs.prepare-version.outputs.version }}
+
+ bundle-goose2:
+ needs: [prepare-version, build-cli]
+ if: ${{ !cancelled() && needs.prepare-version.result == 'success' && (needs.build-cli.result == 'success' || needs.build-cli.result == 'skipped') }}
+ uses: ./.github/workflows/bundle-goose2.yml
+ permissions:
+ id-token: write
+ contents: read
+ actions: read
+ with:
+ version: ${{ needs.prepare-version.outputs.version }}
+ signing: true
+ windows-signing: true
+ environment: signing
+ cli-run-id: ${{ inputs.cli-run-id != '' && inputs.cli-run-id || github.run_id }}
+ secrets: inherit
+
+ install-script:
+ name: Upload Install Script
+ runs-on: ubuntu-latest
+ if: inputs.cli-run-id == ''
+ needs: [build-cli]
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ with:
+ name: download_cli.sh
+ path: download_cli.sh
+
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ needs: [prepare-version, build-cli, install-script, bundle-goose2]
+ if: ${{ !cancelled() && needs.bundle-goose2.result == 'success' }}
+ permissions:
+ contents: write
+ id-token: write
+ actions: read
+ attestations: write
+ steps:
+ - name: Download CLI artifacts
+ if: needs.build-cli.result == 'success'
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ pattern: goose-*
+ merge-multiple: true
+ path: release
+
+ - name: Download install script
+ if: needs.install-script.result == 'success'
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: download_cli.sh
+ path: release
+
+ - name: Download macOS ARM64
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-darwin-arm64
+ path: release
+
+ - name: Download macOS Intel
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-darwin-x64
+ path: release
+
+ - name: Download Linux .deb
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-linux-x64-deb
+ path: release
+ continue-on-error: true
+
+ - name: Download Linux AppImage
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-linux-x64-appimage
+ path: release
+ continue-on-error: true
+
+ - name: Download Linux RPM
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-linux-x64-rpm
+ path: release
+ continue-on-error: true
+
+ - name: Download signed Windows NSIS installer
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-windows-x64-nsis-signed
+ path: release
+
+ - name: Download signed Windows MSI installer
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ with:
+ name: Goose2-windows-x64-msi-signed
+ path: release
+
+ - name: List downloaded artifacts
+ run: |
+ echo "=== All release artifacts ==="
+ find release -type f | sort
+
+ - name: Attest build provenance
+ uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
+ with:
+ subject-path: |
+ release/goose-*.tar.bz2
+ release/goose-*.tar.gz
+ release/goose-*.zip
+ release/*.dmg
+ release/*.exe
+ release/*.msi
+ release/*.deb
+ release/*.rpm
+ release/*.AppImage
+ release/download_cli.sh
+
+ # Create/update the versioned pre-release (e.g. v2.0.0)
+ - name: Release versioned
+ uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ prerelease: true
+ artifacts: |
+ release/goose-*.tar.bz2
+ release/goose-*.tar.gz
+ release/goose-*.zip
+ release/*.dmg
+ release/*.exe
+ release/*.msi
+ release/*.deb
+ release/*.rpm
+ release/*.AppImage
+ release/download_cli.sh
+ allowUpdates: true
+ omitBody: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8c1a2671c833..60cea64628a0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -117,7 +117,7 @@ jobs:
# Create/update the versioned release
- name: Release versioned
- uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
+ uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: |
@@ -135,7 +135,7 @@ jobs:
# Create/update the stable release
- name: Release stable
- uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
+ uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
with:
tag: stable
name: Stable
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 8a689ff1dea9..dbe008a54e2e 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -75,9 +75,6 @@ jobs:
# Skip PRs with these labels (comma-separated)
exempt-pr-labels: 'keep-open,wip,work-in-progress,security,pinned,dependencies'
- # Skip draft PRs (they're typically work in progress)
- exempt-draft-pr: true
-
# === ISSUE CONFIGURATION (DISABLED) ===
# We only want to manage PRs, not issues
days-before-issue-stale: -1
diff --git a/.gitignore b/.gitignore
index 23f1a062021e..851b0ea38ca2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,7 @@ ui/desktop/src/bin/goose_llm.dll
# Claude
.claude/settings.local.json
+.claude/scheduled_tasks.lock
debug_*.txt
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 4e793eaacb99..63b3d6bcc9f2 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,8 +1,39 @@
+#!/usr/bin/env bash
+set -e
+
# Only auto-format desktop TS code if relevant files are modified
-if git diff --cached --name-only | grep -q "^ui/desktop/"; then
+if git diff --cached --no-renames --name-only | grep -q "^ui/desktop/"; then
if [ -d "ui/desktop" ]; then
- cd ui/desktop && pnpm exec lint-staged
+ (cd ui/desktop && pnpm exec lint-staged)
else
echo "Warning: ui/desktop directory does not exist, skipping lint-staged"
fi
fi
+
+# Run goose2 checks if any staged files are under ui/goose2/
+if git diff --cached --no-renames --name-only | grep -q '^ui/goose2/'; then
+ if [ -d "ui/goose2" ]; then
+ REPO_ROOT="$(pwd)"
+ echo "Running goose2 pre-commit checks..."
+
+ # Auto-format only staged files that biome can process, then re-stage them.
+ # Exclude justfile and .swift files — biome doesn't understand these formats
+ # and would fail with "no files were processed" when only such files are staged.
+ STAGED_FILES=$(git diff --cached --no-renames --diff-filter=ACMR --name-only \
+ | grep '^ui/goose2/' \
+ | grep -v -E '(^ui/goose2/justfile$|\.swift$)' \
+ | sed 's|^ui/goose2/||' || true)
+ if [ -n "$STAGED_FILES" ]; then
+ cd ui/goose2
+ echo "$STAGED_FILES" | xargs npx biome format --write
+ echo "$STAGED_FILES" | xargs npx biome check --fix
+ cd "$REPO_ROOT"
+ git diff --cached --no-renames --diff-filter=ACMR --name-only | grep '^ui/goose2/' | xargs git add
+ fi
+
+ # Run checks (biome check + file sizes + i18n + typecheck)
+ just goose2 check
+ else
+ echo "Warning: ui/goose2 directory does not exist, skipping goose2 checks"
+ fi
+fi
diff --git a/.husky/pre-push b/.husky/pre-push
new file mode 100755
index 000000000000..7e68deb749c3
--- /dev/null
+++ b/.husky/pre-push
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+# Run goose2 pre-push checks if any commits being pushed include goose2 changes.
+
+# --- Helper functions ---
+
+# Check if any ref being pushed includes changes under a given path prefix.
+# Reads git's pre-push stdin (local_ref local_oid remote_ref remote_oid).
+push_touches() {
+ local prefix="$1"
+ local z40="0000000000000000000000000000000000000000"
+
+ while read local_ref local_oid remote_ref remote_oid; do
+ [ "$local_oid" = "$z40" ] && continue # deleting ref
+
+ if [ "$remote_oid" = "$z40" ]; then
+ # New branch — compare against the merge base with the default branch
+ range="$(git merge-base HEAD main 2>/dev/null || echo "$local_oid")...$local_oid"
+ else
+ range="$remote_oid...$local_oid"
+ fi
+
+ if git diff --no-renames --name-only "$range" -- 2>/dev/null | grep -q "^${prefix}"; then
+ return 0
+ fi
+ done
+
+ return 1
+}
+
+# Run commands in parallel, exit 1 if any fail.
+# Usage: run_parallel "label1" "cmd1" "label2" "cmd2" ...
+run_parallel() {
+ local -a labels=() pids=()
+ while [[ $# -ge 2 ]]; do
+ labels+=("$1"); shift
+ ( eval "$1" ) & pids+=($!); shift
+ done
+
+ local -a failed=()
+ for i in "${!pids[@]}"; do
+ wait "${pids[$i]}" || failed+=("${labels[$i]}")
+ done
+
+ if [[ ${#failed[@]} -gt 0 ]]; then
+ echo "Failed: ${failed[*]}"
+ return 1
+ fi
+}
+
+# --- Hook body ---
+
+push_touches "ui/goose2/" || exit 0
+
+echo "Detected ui/goose2/ changes — running pre-push checks..."
+
+run_parallel \
+ "fmt-check" "just goose2 fmt-check" \
+ "clippy" "just goose2 clippy" \
+ "check" "just goose2 check" \
+ "test" "just goose2 test" \
+ "build" "just goose2 build" \
+ "tauri-check" "just goose2 tauri-check"
+
+exit_code=$?
+
+if [ $exit_code -eq 0 ]; then
+ echo "All goose2 pre-push checks passed."
+else
+ echo "goose2 pre-push checks failed."
+ exit 1
+fi
diff --git a/AGENTS.md b/AGENTS.md
index cb036c9eac70..ca6db72ed9f1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -47,7 +47,6 @@ git commit -s # required for DCO sign-off
```
crates/
├── goose # core logic
-├── goose-acp # Agent Client Protocol
├── goose-acp-macros # ACP proc macros
├── goose-cli # CLI entry
├── goose-server # backend (binary: goosed)
diff --git a/BUILDING_LINUX.md b/BUILDING_LINUX.md
index 4efa33040ccf..6fc0e210db37 100644
--- a/BUILDING_LINUX.md
+++ b/BUILDING_LINUX.md
@@ -9,7 +9,7 @@ This guide covers building the goose Desktop application from source on various
**Debian/Ubuntu:**
```bash
sudo apt update
-sudo apt install -y dpkg fakeroot build-essential libxcb1-dev libxcb-util-dev protobuf-compiler
+sudo apt install -y dpkg fakeroot build-essential clang libxcb1-dev libxcb-util-dev protobuf-compiler
```
**Arch/Manjaro:**
@@ -44,6 +44,7 @@ pkg install cmake protobuf clang build-essential
- **Rust**: Install via [rustup](https://rustup.rs/)
- **Node.js**: Version 22.9.0 or later (use [nvm](https://github.com/nvm-sh/nvm) for version management)
- **pnpm**: Version 10 or later (managed via Hermit, or install globally)
+- **just**: Install via `cargo install just` after Rust is installed. More [info](https://github.com/casey/just#packages)
## Build Process
@@ -53,11 +54,27 @@ git clone https://github.com/aaif-goose/goose.git
cd goose
```
-### 2. Build the Rust Backend
+### 2. Build
+
+Build Goose CLI:
+
+```bash
+cargo build --release -p goose-cli
+```
+
+Build Goose Server:
+
```bash
cargo build --release -p goose-server
```
+This command should give you a list of possible packages in the
+workspace:
+
+```bash
+cargo test -p
+```
+
### 3. Prepare the Desktop Application
```bash
cd ui/desktop
diff --git a/CUSTOM_DISTROS.md b/CUSTOM_DISTROS.md
index c9f08a2241ad..d4f7c15ecb47 100644
--- a/CUSTOM_DISTROS.md
+++ b/CUSTOM_DISTROS.md
@@ -416,7 +416,7 @@ For the full ACP specification, see the [Agent Client Protocol documentation](ht
- API client example: `ui/desktop/src/api/` (generated TypeScript client)
**ACP**:
-- ACP server implementation: `crates/goose-acp/src/server.rs`
+- ACP server implementation: `crates/goose/src/acp/server.rs`
- CLI integration: `crates/goose-cli/src/cli.rs` (Command::Acp)
- Protocol library: `sacp` crate (Rust implementation of ACP)
- Test client example: `test_acp_client.py`
diff --git a/Cargo.lock b/Cargo.lock
index 879339f3b093..55365d2f2f0b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4326,7 +4326,7 @@ dependencies = [
[[package]]
name = "goose"
-version = "1.31.0"
+version = "1.32.0"
dependencies = [
"agent-client-protocol-schema",
"ahash",
@@ -4358,8 +4358,12 @@ dependencies = [
"fs-err",
"fs2",
"futures",
+ "goose-acp-macros",
+ "goose-mcp",
+ "goose-sdk",
"goose-test-support",
"http 1.4.0",
+ "http-body-util",
"ignore",
"include_dir",
"indexmap 2.13.0",
@@ -4418,6 +4422,7 @@ dependencies = [
"tokio-cron-scheduler",
"tokio-stream",
"tokio-util",
+ "tower-http",
"tracing",
"tracing-futures",
"tracing-opentelemetry",
@@ -4445,45 +4450,9 @@ dependencies = [
"zip 8.4.0",
]
-[[package]]
-name = "goose-acp"
-version = "1.31.0"
-dependencies = [
- "agent-client-protocol-schema",
- "anyhow",
- "async-stream",
- "async-trait",
- "axum",
- "fs-err",
- "futures",
- "goose",
- "goose-acp-macros",
- "goose-mcp",
- "goose-sdk",
- "goose-test-support",
- "http-body-util",
- "regex",
- "rmcp",
- "sacp",
- "schemars 1.2.1",
- "serde",
- "serde_json",
- "sqlx",
- "strum 0.27.2",
- "tempfile",
- "test-case",
- "tokio",
- "tokio-util",
- "tower-http",
- "tracing",
- "url",
- "uuid",
- "wiremock",
-]
-
[[package]]
name = "goose-acp-macros"
-version = "1.31.0"
+version = "1.32.0"
dependencies = [
"quote",
"syn 2.0.117",
@@ -4491,7 +4460,7 @@ dependencies = [
[[package]]
name = "goose-cli"
-version = "1.31.0"
+version = "1.32.0"
dependencies = [
"anstream 0.6.21",
"anyhow",
@@ -4511,7 +4480,6 @@ dependencies = [
"etcetera 0.11.0",
"futures",
"goose",
- "goose-acp",
"goose-mcp",
"indicatif",
"open",
@@ -4544,7 +4512,7 @@ dependencies = [
[[package]]
name = "goose-mcp"
-version = "1.31.0"
+version = "1.32.0"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -4573,7 +4541,7 @@ dependencies = [
[[package]]
name = "goose-sdk"
-version = "1.31.0"
+version = "1.32.0"
dependencies = [
"agent-client-protocol-schema",
"sacp",
@@ -4586,7 +4554,7 @@ dependencies = [
[[package]]
name = "goose-server"
-version = "1.31.0"
+version = "1.32.0"
dependencies = [
"anyhow",
"aws-lc-rs",
@@ -4634,7 +4602,7 @@ dependencies = [
[[package]]
name = "goose-test"
-version = "1.31.0"
+version = "1.32.0"
dependencies = [
"clap",
"serde_json",
@@ -4642,7 +4610,7 @@ dependencies = [
[[package]]
name = "goose-test-support"
-version = "1.31.0"
+version = "1.32.0"
dependencies = [
"axum",
"env-lock",
@@ -8295,9 +8263,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
-version = "0.103.12"
+version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
@@ -10360,9 +10328,9 @@ dependencies = [
[[package]]
name = "thin-vec"
-version = "0.2.14"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
+checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6"
[[package]]
name = "thiserror"
diff --git a/Cargo.toml b/Cargo.toml
index 555f40951853..5da5c4a2e3d5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,8 @@ resolver = "2"
[workspace.package]
edition = "2021"
-version = "1.31.0"
+version = "1.32.0"
+rust-version = "1.91.1"
authors = ["AAIF "]
license = "Apache-2.0"
repository = "https://github.com/aaif-goose/goose"
diff --git a/Justfile b/Justfile
index fd9a43e9e446..3e7a22092964 100644
--- a/Justfile
+++ b/Justfile
@@ -205,7 +205,7 @@ check-acp-schema: generate-acp-types
#!/usr/bin/env bash
set -e
echo "🔍 Checking ACP schema and generated types are up-to-date..."
- if ! git diff --exit-code crates/goose-acp/acp-schema.json crates/goose-acp/acp-meta.json ui/sdk/src/generated/; then
+ if ! git diff --exit-code crates/goose/acp-schema.json crates/goose/acp-meta.json ui/sdk/src/generated/; then
echo ""
echo "❌ ACP generated files are out of date!"
echo ""
@@ -217,8 +217,8 @@ check-acp-schema: generate-acp-types
# Generate ACP JSON schema from Rust types
generate-acp-schema:
@echo "Generating ACP schema..."
- cd crates/goose-acp && cargo run --bin generate-acp-schema
- @echo "ACP schema generated: crates/goose-acp/acp-schema.json, crates/goose-acp/acp-meta.json"
+ cd crates/goose && cargo run --bin generate-acp-schema
+ @echo "ACP schema generated: crates/goose/acp-schema.json, crates/goose/acp-meta.json"
# Generate ACP TypeScript types from JSON schema (requires generate-acp-schema first)
generate-acp-types: generate-acp-schema
diff --git a/README.md b/README.md
index fed529b2da4f..a150908f8516 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ _your native open source AI agent — desktop app, CLI, and API — for code, wo
>
+
diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md
index 9a10dc6b5773..e031d00c0c4d 100644
--- a/RELEASE_CHECKLIST.md
+++ b/RELEASE_CHECKLIST.md
@@ -17,7 +17,7 @@ Make a copy of this document for each version and check off as steps are verifie
### Provider Testing
-- [ ] Run `./scripts/test_providers.sh` locally from the release branch and verify all providers/models work
+- [ ] Run `cd ui/desktop && pnpm run test:integration:providers` locally from the release branch and verify all providers/models work
- [ ] Launch goose, click reset providers, choose databricks and a model
### Starting Conversations
diff --git a/crates/goose-acp-macros/Cargo.toml b/crates/goose-acp-macros/Cargo.toml
index 43c428e325b1..982f87638d2b 100644
--- a/crates/goose-acp-macros/Cargo.toml
+++ b/crates/goose-acp-macros/Cargo.toml
@@ -1,6 +1,7 @@
[package]
name = "goose-acp-macros"
edition.workspace = true
+rust-version.workspace = true
version.workspace = true
authors.workspace = true
license.workspace = true
diff --git a/crates/goose-acp/Cargo.toml b/crates/goose-acp/Cargo.toml
deleted file mode 100644
index 8bc2b1e7eed5..000000000000
--- a/crates/goose-acp/Cargo.toml
+++ /dev/null
@@ -1,64 +0,0 @@
-[package]
-name = "goose-acp"
-version.workspace = true
-edition.workspace = true
-authors.workspace = true
-license.workspace = true
-repository.workspace = true
-description.workspace = true
-
-[[bin]]
-name = "generate-acp-schema"
-path = "src/bin/generate_acp_schema.rs"
-
-[features]
-default = ["code-mode", "rustls-tls"]
-code-mode = ["goose/code-mode"]
-rustls-tls = ["goose/rustls-tls", "goose-mcp/rustls-tls"]
-native-tls = ["goose/native-tls", "goose-mcp/native-tls"]
-
-[lints]
-workspace = true
-
-[dependencies]
-goose = { path = "../goose", default-features = false }
-goose-mcp = { path = "../goose-mcp", default-features = false }
-rmcp = { workspace = true }
-sacp = { workspace = true, features = ["unstable"] }
-agent-client-protocol-schema = { workspace = true }
-async-trait = { workspace = true }
-anyhow = { workspace = true }
-tokio = { workspace = true }
-tokio-util = { workspace = true, features = ["compat", "rt"] }
-tracing = { workspace = true }
-serde_json = { workspace = true }
-futures = { workspace = true }
-regex = { workspace = true }
-fs-err = "3"
-strum = { workspace = true }
-url = { workspace = true }
-
-# HTTP server dependencies
-axum = { workspace = true, features = ["ws"] }
-serde = { workspace = true, features = ["derive"] }
-tower-http = { workspace = true, features = ["cors"] }
-async-stream = { workspace = true }
-http-body-util = "0.1.3"
-uuid = { workspace = true, features = ["v7"] }
-schemars = { workspace = true, features = ["derive"] }
-goose-acp-macros = { path = "../goose-acp-macros" }
-goose-sdk = { path = "../goose-sdk" }
-
-[dev-dependencies]
-async-trait = { workspace = true }
-goose-test-support = { path = "../goose-test-support" }
-wiremock = { workspace = true }
-tempfile = { workspace = true }
-test-case = { workspace = true }
-axum = { workspace = true }
-rmcp = { workspace = true, features = ["transport-streamable-http-server"] }
-sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "sqlite"] }
-
-[package.metadata.cargo-machete]
-# Used to provide extras imports for sacp
-ignored = ["agent-client-protocol-schema"]
diff --git a/crates/goose-acp/acp-meta.json b/crates/goose-acp/acp-meta.json
deleted file mode 100644
index 944d227b663f..000000000000
--- a/crates/goose-acp/acp-meta.json
+++ /dev/null
@@ -1,109 +0,0 @@
-{
- "methods": [
- {
- "method": "_goose/extensions/add",
- "requestType": "AddExtensionRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "_goose/extensions/remove",
- "requestType": "RemoveExtensionRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "_goose/tools",
- "requestType": "GetToolsRequest",
- "responseType": "GetToolsResponse"
- },
- {
- "method": "_goose/resource/read",
- "requestType": "ReadResourceRequest",
- "responseType": "ReadResourceResponse"
- },
- {
- "method": "_goose/working_dir/update",
- "requestType": "UpdateWorkingDirRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "session/delete",
- "requestType": "DeleteSessionRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "_goose/config/extensions",
- "requestType": "GetExtensionsRequest",
- "responseType": "GetExtensionsResponse"
- },
- {
- "method": "_goose/session/extensions",
- "requestType": "GetSessionExtensionsRequest",
- "responseType": "GetSessionExtensionsResponse"
- },
- {
- "method": "_goose/providers/list",
- "requestType": "ListProvidersRequest",
- "responseType": "ListProvidersResponse"
- },
- {
- "method": "_goose/providers/details",
- "requestType": "GetProviderDetailsRequest",
- "responseType": "GetProviderDetailsResponse"
- },
- {
- "method": "_goose/providers/models",
- "requestType": "GetProviderModelsRequest",
- "responseType": "GetProviderModelsResponse"
- },
- {
- "method": "_goose/config/read",
- "requestType": "ReadConfigRequest",
- "responseType": "ReadConfigResponse"
- },
- {
- "method": "_goose/config/upsert",
- "requestType": "UpsertConfigRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "_goose/config/remove",
- "requestType": "RemoveConfigRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "_goose/secret/check",
- "requestType": "CheckSecretRequest",
- "responseType": "CheckSecretResponse"
- },
- {
- "method": "_goose/secret/upsert",
- "requestType": "UpsertSecretRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "_goose/secret/remove",
- "requestType": "RemoveSecretRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "_goose/session/export",
- "requestType": "ExportSessionRequest",
- "responseType": "ExportSessionResponse"
- },
- {
- "method": "_goose/session/import",
- "requestType": "ImportSessionRequest",
- "responseType": "ImportSessionResponse"
- },
- {
- "method": "_goose/session/archive",
- "requestType": "ArchiveSessionRequest",
- "responseType": "EmptyResponse"
- },
- {
- "method": "_goose/session/unarchive",
- "requestType": "UnarchiveSessionRequest",
- "responseType": "EmptyResponse"
- }
- ]
-}
diff --git a/crates/goose-acp/acp-schema.json b/crates/goose-acp/acp-schema.json
deleted file mode 100644
index 0f0db1759a37..000000000000
--- a/crates/goose-acp/acp-schema.json
+++ /dev/null
@@ -1,1004 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "title": "GooseExtensions",
- "$defs": {
- "AddExtensionRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- },
- "config": {
- "description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform).",
- "default": null
- }
- },
- "required": [
- "sessionId"
- ],
- "description": "Add an extension to an active session.",
- "x-side": "agent",
- "x-method": "_goose/extensions/add"
- },
- "EmptyResponse": {
- "type": "object",
- "description": "Empty success response for operations that return no data.",
- "x-side": "agent"
- },
- "RemoveExtensionRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- },
- "name": {
- "type": "string"
- }
- },
- "required": [
- "sessionId",
- "name"
- ],
- "description": "Remove an extension from an active session.",
- "x-side": "agent",
- "x-method": "_goose/extensions/remove"
- },
- "GetToolsRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- }
- },
- "required": [
- "sessionId"
- ],
- "description": "List all tools available in a session.",
- "x-side": "agent",
- "x-method": "_goose/tools"
- },
- "GetToolsResponse": {
- "type": "object",
- "properties": {
- "tools": {
- "type": "array",
- "items": {},
- "description": "Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`."
- }
- },
- "required": [
- "tools"
- ],
- "description": "Tools response.",
- "x-side": "agent",
- "x-method": "_goose/tools"
- },
- "ReadResourceRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- },
- "uri": {
- "type": "string"
- },
- "extensionName": {
- "type": "string"
- }
- },
- "required": [
- "sessionId",
- "uri",
- "extensionName"
- ],
- "description": "Read a resource from an extension.",
- "x-side": "agent",
- "x-method": "_goose/resource/read"
- },
- "ReadResourceResponse": {
- "type": "object",
- "properties": {
- "result": {
- "description": "The resource result from the extension (MCP ReadResourceResult).",
- "default": null
- }
- },
- "description": "Resource read response.",
- "x-side": "agent",
- "x-method": "_goose/resource/read"
- },
- "UpdateWorkingDirRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- },
- "workingDir": {
- "type": "string"
- }
- },
- "required": [
- "sessionId",
- "workingDir"
- ],
- "description": "Update the working directory for a session.",
- "x-side": "agent",
- "x-method": "_goose/working_dir/update"
- },
- "DeleteSessionRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- }
- },
- "required": [
- "sessionId"
- ],
- "description": "Delete a session.",
- "x-side": "agent",
- "x-method": "session/delete"
- },
- "GetExtensionsRequest": {
- "type": "object",
- "description": "List configured extensions and any warnings.",
- "x-side": "agent",
- "x-method": "_goose/config/extensions"
- },
- "GetExtensionsResponse": {
- "type": "object",
- "properties": {
- "extensions": {
- "type": "array",
- "items": {},
- "description": "Array of ExtensionEntry objects with `enabled` flag and config details."
- },
- "warnings": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": [
- "extensions",
- "warnings"
- ],
- "description": "List configured extensions and any warnings.",
- "x-side": "agent",
- "x-method": "_goose/config/extensions"
- },
- "GetSessionExtensionsRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- }
- },
- "required": [
- "sessionId"
- ],
- "x-side": "agent",
- "x-method": "_goose/session/extensions"
- },
- "GetSessionExtensionsResponse": {
- "type": "object",
- "properties": {
- "extensions": {
- "type": "array",
- "items": {}
- }
- },
- "required": [
- "extensions"
- ],
- "x-side": "agent",
- "x-method": "_goose/session/extensions"
- },
- "ListProvidersRequest": {
- "type": "object",
- "description": "List providers available through goose, including the config-default sentinel.",
- "x-side": "agent",
- "x-method": "_goose/providers/list"
- },
- "ListProvidersResponse": {
- "type": "object",
- "properties": {
- "providers": {
- "type": "array",
- "items": {
- "$ref": "#/$defs/ProviderListEntry"
- }
- }
- },
- "required": [
- "providers"
- ],
- "description": "Provider list response.",
- "x-side": "agent",
- "x-method": "_goose/providers/list"
- },
- "ProviderListEntry": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "id",
- "label"
- ]
- },
- "GetProviderDetailsRequest": {
- "type": "object",
- "description": "List providers with full metadata (config keys, setup steps, etc.).",
- "x-side": "agent",
- "x-method": "_goose/providers/details"
- },
- "GetProviderDetailsResponse": {
- "type": "object",
- "properties": {
- "providers": {
- "type": "array",
- "items": {
- "$ref": "#/$defs/ProviderDetailEntry"
- }
- }
- },
- "required": [
- "providers"
- ],
- "description": "Provider details response.",
- "x-side": "agent",
- "x-method": "_goose/providers/details"
- },
- "ProviderDetailEntry": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "displayName": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "defaultModel": {
- "type": "string"
- },
- "isConfigured": {
- "type": "boolean"
- },
- "providerType": {
- "type": "string"
- },
- "configKeys": {
- "type": "array",
- "items": {
- "$ref": "#/$defs/ProviderConfigKey"
- }
- },
- "setupSteps": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "default": []
- },
- "knownModels": {
- "type": "array",
- "items": {
- "$ref": "#/$defs/ModelEntry"
- },
- "default": []
- }
- },
- "required": [
- "name",
- "displayName",
- "description",
- "defaultModel",
- "isConfigured",
- "providerType",
- "configKeys"
- ]
- },
- "ProviderConfigKey": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "required": {
- "type": "boolean"
- },
- "secret": {
- "type": "boolean"
- },
- "default": {
- "type": [
- "string",
- "null"
- ],
- "default": null
- },
- "oauthFlow": {
- "type": "boolean",
- "default": false
- },
- "deviceCodeFlow": {
- "type": "boolean",
- "default": false
- },
- "primary": {
- "type": "boolean",
- "default": false
- }
- },
- "required": [
- "name",
- "required",
- "secret"
- ]
- },
- "ModelEntry": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "contextLimit": {
- "type": "integer",
- "minimum": 0
- }
- },
- "required": [
- "name",
- "contextLimit"
- ]
- },
- "GetProviderModelsRequest": {
- "type": "object",
- "properties": {
- "providerName": {
- "type": "string"
- }
- },
- "required": [
- "providerName"
- ],
- "description": "Fetch the full list of models available for a specific provider.",
- "x-side": "agent",
- "x-method": "_goose/providers/models"
- },
- "GetProviderModelsResponse": {
- "type": "object",
- "properties": {
- "models": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": [
- "models"
- ],
- "description": "Provider models response.",
- "x-side": "agent",
- "x-method": "_goose/providers/models"
- },
- "ReadConfigRequest": {
- "type": "object",
- "properties": {
- "key": {
- "type": "string"
- }
- },
- "required": [
- "key"
- ],
- "description": "Read a single non-secret config value.",
- "x-side": "agent",
- "x-method": "_goose/config/read"
- },
- "ReadConfigResponse": {
- "type": "object",
- "properties": {
- "value": {
- "default": null
- }
- },
- "description": "Config read response.",
- "x-side": "agent",
- "x-method": "_goose/config/read"
- },
- "UpsertConfigRequest": {
- "type": "object",
- "properties": {
- "key": {
- "type": "string"
- },
- "value": {}
- },
- "required": [
- "key",
- "value"
- ],
- "description": "Upsert a single non-secret config value.",
- "x-side": "agent",
- "x-method": "_goose/config/upsert"
- },
- "RemoveConfigRequest": {
- "type": "object",
- "properties": {
- "key": {
- "type": "string"
- }
- },
- "required": [
- "key"
- ],
- "description": "Remove a single non-secret config value.",
- "x-side": "agent",
- "x-method": "_goose/config/remove"
- },
- "CheckSecretRequest": {
- "type": "object",
- "properties": {
- "key": {
- "type": "string"
- }
- },
- "required": [
- "key"
- ],
- "description": "Check whether a secret exists. Never returns the actual value.",
- "x-side": "agent",
- "x-method": "_goose/secret/check"
- },
- "CheckSecretResponse": {
- "type": "object",
- "properties": {
- "exists": {
- "type": "boolean"
- }
- },
- "required": [
- "exists"
- ],
- "description": "Secret check response.",
- "x-side": "agent",
- "x-method": "_goose/secret/check"
- },
- "UpsertSecretRequest": {
- "type": "object",
- "properties": {
- "key": {
- "type": "string"
- },
- "value": {}
- },
- "required": [
- "key",
- "value"
- ],
- "description": "Set a secret value (write-only).",
- "x-side": "agent",
- "x-method": "_goose/secret/upsert"
- },
- "RemoveSecretRequest": {
- "type": "object",
- "properties": {
- "key": {
- "type": "string"
- }
- },
- "required": [
- "key"
- ],
- "description": "Remove a secret.",
- "x-side": "agent",
- "x-method": "_goose/secret/remove"
- },
- "ExportSessionRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- }
- },
- "required": [
- "sessionId"
- ],
- "description": "Export a session as a JSON string.",
- "x-side": "agent",
- "x-method": "_goose/session/export"
- },
- "ExportSessionResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "string"
- }
- },
- "required": [
- "data"
- ],
- "description": "Export session response — raw JSON of the goose session with `conversation`.",
- "x-side": "agent",
- "x-method": "_goose/session/export"
- },
- "ImportSessionRequest": {
- "type": "object",
- "properties": {
- "data": {
- "type": "string"
- }
- },
- "required": [
- "data"
- ],
- "description": "Import a session from a JSON string.",
- "x-side": "agent",
- "x-method": "_goose/session/import"
- },
- "ImportSessionResponse": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- },
- "title": {
- "type": [
- "string",
- "null"
- ]
- },
- "updatedAt": {
- "type": [
- "string",
- "null"
- ]
- },
- "messageCount": {
- "type": "integer",
- "minimum": 0
- }
- },
- "required": [
- "sessionId",
- "messageCount"
- ],
- "description": "Import session response — metadata about the newly created session.",
- "x-side": "agent",
- "x-method": "_goose/session/import"
- },
- "ArchiveSessionRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- }
- },
- "required": [
- "sessionId"
- ],
- "description": "Archive a session (soft delete).",
- "x-side": "agent",
- "x-method": "_goose/session/archive"
- },
- "UnarchiveSessionRequest": {
- "type": "object",
- "properties": {
- "sessionId": {
- "type": "string"
- }
- },
- "required": [
- "sessionId"
- ],
- "description": "Unarchive a previously archived session.",
- "x-side": "agent",
- "x-method": "_goose/session/unarchive"
- },
- "ExtRequest": {
- "properties": {
- "id": {
- "type": "string"
- },
- "method": {
- "type": "string"
- },
- "params": {
- "anyOf": [
- {
- "anyOf": [
- {
- "allOf": [
- {
- "$ref": "#/$defs/AddExtensionRequest"
- }
- ],
- "description": "Params for _goose/extensions/add",
- "title": "AddExtensionRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/RemoveExtensionRequest"
- }
- ],
- "description": "Params for _goose/extensions/remove",
- "title": "RemoveExtensionRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetToolsRequest"
- }
- ],
- "description": "Params for _goose/tools",
- "title": "GetToolsRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ReadResourceRequest"
- }
- ],
- "description": "Params for _goose/resource/read",
- "title": "ReadResourceRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/UpdateWorkingDirRequest"
- }
- ],
- "description": "Params for _goose/working_dir/update",
- "title": "UpdateWorkingDirRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/DeleteSessionRequest"
- }
- ],
- "description": "Params for session/delete",
- "title": "DeleteSessionRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetExtensionsRequest"
- }
- ],
- "description": "Params for _goose/config/extensions",
- "title": "GetExtensionsRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetSessionExtensionsRequest"
- }
- ],
- "description": "Params for _goose/session/extensions",
- "title": "GetSessionExtensionsRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ListProvidersRequest"
- }
- ],
- "description": "Params for _goose/providers/list",
- "title": "ListProvidersRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetProviderDetailsRequest"
- }
- ],
- "description": "Params for _goose/providers/details",
- "title": "GetProviderDetailsRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetProviderModelsRequest"
- }
- ],
- "description": "Params for _goose/providers/models",
- "title": "GetProviderModelsRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ReadConfigRequest"
- }
- ],
- "description": "Params for _goose/config/read",
- "title": "ReadConfigRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/UpsertConfigRequest"
- }
- ],
- "description": "Params for _goose/config/upsert",
- "title": "UpsertConfigRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/RemoveConfigRequest"
- }
- ],
- "description": "Params for _goose/config/remove",
- "title": "RemoveConfigRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/CheckSecretRequest"
- }
- ],
- "description": "Params for _goose/secret/check",
- "title": "CheckSecretRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/UpsertSecretRequest"
- }
- ],
- "description": "Params for _goose/secret/upsert",
- "title": "UpsertSecretRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/RemoveSecretRequest"
- }
- ],
- "description": "Params for _goose/secret/remove",
- "title": "RemoveSecretRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ExportSessionRequest"
- }
- ],
- "description": "Params for _goose/session/export",
- "title": "ExportSessionRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ImportSessionRequest"
- }
- ],
- "description": "Params for _goose/session/import",
- "title": "ImportSessionRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ArchiveSessionRequest"
- }
- ],
- "description": "Params for _goose/session/archive",
- "title": "ArchiveSessionRequest"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/UnarchiveSessionRequest"
- }
- ],
- "description": "Params for _goose/session/unarchive",
- "title": "UnarchiveSessionRequest"
- }
- ]
- },
- {
- "description": "Untyped params",
- "type": [
- "object",
- "null"
- ]
- }
- ]
- }
- },
- "required": [
- "id",
- "method"
- ],
- "type": "object",
- "x-docs-ignore": true
- },
- "ExtResponse": {
- "anyOf": [
- {
- "properties": {
- "id": {
- "type": "string"
- },
- "result": {
- "anyOf": [
- {
- "anyOf": [
- {
- "allOf": [
- {
- "$ref": "#/$defs/EmptyResponse"
- }
- ],
- "title": "EmptyResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetToolsResponse"
- }
- ],
- "title": "GetToolsResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ReadResourceResponse"
- }
- ],
- "title": "ReadResourceResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetExtensionsResponse"
- }
- ],
- "title": "GetExtensionsResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetSessionExtensionsResponse"
- }
- ],
- "title": "GetSessionExtensionsResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ListProvidersResponse"
- }
- ],
- "title": "ListProvidersResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetProviderDetailsResponse"
- }
- ],
- "title": "GetProviderDetailsResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/GetProviderModelsResponse"
- }
- ],
- "title": "GetProviderModelsResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ReadConfigResponse"
- }
- ],
- "title": "ReadConfigResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/CheckSecretResponse"
- }
- ],
- "title": "CheckSecretResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ExportSessionResponse"
- }
- ],
- "title": "ExportSessionResponse"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ImportSessionResponse"
- }
- ],
- "title": "ImportSessionResponse"
- }
- ]
- },
- {
- "description": "Untyped result"
- }
- ]
- }
- },
- "required": [
- "id"
- ],
- "title": "Success",
- "type": "object"
- },
- {
- "properties": {
- "error": {
- "type": "object",
- "properties": {
- "code": {
- "type": "integer"
- },
- "message": {
- "type": "string"
- },
- "data": {}
- },
- "required": [
- "code",
- "message"
- ]
- },
- "id": {
- "type": "string"
- }
- },
- "required": [
- "id",
- "error"
- ],
- "title": "Error",
- "type": "object"
- }
- ],
- "x-docs-ignore": true
- }
- },
- "anyOf": [
- {
- "allOf": [
- {
- "$ref": "#/$defs/ExtRequest"
- }
- ],
- "description": "Extension request (client → agent)",
- "title": "Request"
- },
- {
- "allOf": [
- {
- "$ref": "#/$defs/ExtResponse"
- }
- ],
- "description": "Extension response (agent → client)",
- "title": "Response"
- }
- ]
-}
diff --git a/crates/goose-acp/src/lib.rs b/crates/goose-acp/src/lib.rs
deleted file mode 100644
index d1bddef8c572..000000000000
--- a/crates/goose-acp/src/lib.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-#![recursion_limit = "256"]
-
-mod adapters;
-pub use goose_sdk::custom_requests;
-mod fs;
-pub mod server;
-pub mod server_factory;
-pub(crate) mod tools;
-pub mod transport;
diff --git a/crates/goose-acp/src/transport/http.rs b/crates/goose-acp/src/transport/http.rs
deleted file mode 100644
index 608009e45930..000000000000
--- a/crates/goose-acp/src/transport/http.rs
+++ /dev/null
@@ -1,322 +0,0 @@
-use anyhow::Result;
-use axum::{
- body::Body,
- extract::State,
- http::{Request, StatusCode},
- response::{IntoResponse, Response, Sse},
-};
-use http_body_util::BodyExt;
-use serde_json::Value;
-use std::{collections::HashMap, convert::Infallible, sync::Arc, time::Duration};
-use tokio::sync::{mpsc, Mutex, RwLock};
-use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
-use tracing::{error, info};
-
-use super::*;
-use crate::adapters::{ReceiverToAsyncRead, SenderToAsyncWrite};
-use crate::server_factory::AcpServer;
-
-pub(crate) struct HttpState {
- server: Arc,
- // Keyed by acp_session_id: a connection-scoped UUID serving many Goose sessions.
- sessions: RwLock>,
-}
-
-impl HttpState {
- pub fn new(server: Arc) -> Self {
- Self {
- server,
- sessions: RwLock::new(HashMap::new()),
- }
- }
-
- async fn create_session(&self) -> Result {
- let (to_agent_tx, to_agent_rx) = mpsc::channel::(256);
- let (from_agent_tx, from_agent_rx) = mpsc::unbounded_channel::();
-
- let agent = self.server.create_agent().await.map_err(|e| {
- error!("Failed to create agent: {}", e);
- StatusCode::INTERNAL_SERVER_ERROR
- })?;
-
- let acp_session_id = uuid::Uuid::new_v4().to_string();
-
- let read_stream = ReceiverToAsyncRead::new(to_agent_rx);
- let write_stream = SenderToAsyncWrite::new(from_agent_tx);
- let fut = crate::server::serve(agent, read_stream.compat(), write_stream.compat_write());
- let handle = tokio::spawn(async move {
- if let Err(e) = fut.await {
- error!("ACP session error: {}", e);
- }
- });
-
- self.sessions.write().await.insert(
- acp_session_id.clone(),
- TransportSession {
- to_agent_tx,
- from_agent_rx: Arc::new(Mutex::new(from_agent_rx)),
- handle,
- },
- );
-
- info!(acp_session_id = %acp_session_id, "Session created");
- Ok(acp_session_id)
- }
-
- async fn has_session(&self, acp_session_id: &str) -> bool {
- self.sessions.read().await.contains_key(acp_session_id)
- }
-
- async fn remove_session(&self, acp_session_id: &str) {
- if let Some(session) = self.sessions.write().await.remove(acp_session_id) {
- session.handle.abort();
- info!(acp_session_id = %acp_session_id, "Session removed");
- }
- }
-
- async fn send_message(&self, acp_session_id: &str, message: String) -> Result<(), StatusCode> {
- let sessions = self.sessions.read().await;
- let session = sessions.get(acp_session_id).ok_or(StatusCode::NOT_FOUND)?;
- session
- .to_agent_tx
- .send(message)
- .await
- .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
- }
-
- async fn get_receiver(
- &self,
- acp_session_id: &str,
- ) -> Result>>, StatusCode> {
- let sessions = self.sessions.read().await;
- let session = sessions.get(acp_session_id).ok_or(StatusCode::NOT_FOUND)?;
- Ok(session.from_agent_rx.clone())
- }
-}
-
-fn create_sse_stream(
- receiver: Arc>>,
- cleanup: Option<(Arc, String)>,
-) -> Sse>> {
- let stream = async_stream::stream! {
- let mut rx = receiver.lock().await;
- while let Some(msg) = rx.recv().await {
- yield Ok::<_, Infallible>(axum::response::sse::Event::default().data(msg));
- }
- if let Some((state, acp_session_id)) = cleanup {
- state.remove_session(&acp_session_id).await;
- }
- };
-
- Sse::new(stream).keep_alive(
- axum::response::sse::KeepAlive::new()
- .interval(Duration::from_secs(15))
- .text(""),
- )
-}
-
-async fn handle_initialize(state: Arc, json_message: &Value) -> Response {
- let acp_session_id = match state.create_session().await {
- Ok(id) => id,
- Err(status) => return status.into_response(),
- };
-
- let message_str = serde_json::to_string(json_message).unwrap();
- if let Err(status) = state.send_message(&acp_session_id, message_str).await {
- state.remove_session(&acp_session_id).await;
- return status.into_response();
- }
-
- let receiver = match state.get_receiver(&acp_session_id).await {
- Ok(r) => r,
- Err(status) => {
- state.remove_session(&acp_session_id).await;
- return status.into_response();
- }
- };
-
- let sse = create_sse_stream(receiver, Some((state.clone(), acp_session_id.clone())));
- let mut response = sse.into_response();
- response
- .headers_mut()
- .insert(HEADER_SESSION_ID, acp_session_id.parse().unwrap());
- response
-}
-
-async fn handle_request(
- state: Arc,
- acp_session_id: String,
- json_message: &Value,
-) -> Response {
- if !state.has_session(&acp_session_id).await {
- return (StatusCode::NOT_FOUND, "Session not found").into_response();
- }
-
- let message_str = serde_json::to_string(json_message).unwrap();
- if let Err(status) = state.send_message(&acp_session_id, message_str).await {
- return status.into_response();
- }
-
- let receiver = match state.get_receiver(&acp_session_id).await {
- Ok(r) => r,
- Err(status) => return status.into_response(),
- };
-
- create_sse_stream(receiver, None).into_response()
-}
-
-async fn handle_notification_or_response(
- state: Arc,
- acp_session_id: String,
- json_message: &Value,
-) -> Response {
- if !state.has_session(&acp_session_id).await {
- return (StatusCode::NOT_FOUND, "Session not found").into_response();
- }
-
- let message_str = serde_json::to_string(json_message).unwrap();
- if let Err(status) = state.send_message(&acp_session_id, message_str).await {
- return status.into_response();
- }
-
- StatusCode::ACCEPTED.into_response()
-}
-
-pub(crate) async fn handle_post(
- State(state): State>,
- request: Request,
-) -> Response {
- if !accepts_json_and_sse(&request) {
- return (
- StatusCode::NOT_ACCEPTABLE,
- "Not Acceptable: Client must accept both application/json and text/event-stream",
- )
- .into_response();
- }
-
- if !content_type_is_json(&request) {
- return (
- StatusCode::UNSUPPORTED_MEDIA_TYPE,
- "Unsupported Media Type: Content-Type must be application/json",
- )
- .into_response();
- }
-
- let acp_session_id = get_session_id(&request);
-
- let body_bytes = match request.into_body().collect().await {
- Ok(collected) => collected.to_bytes(),
- Err(e) => {
- error!("Failed to read request body: {}", e);
- return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response();
- }
- };
-
- let json_message: Value = match serde_json::from_slice(&body_bytes) {
- Ok(v) => v,
- Err(e) => {
- error!("Failed to parse JSON: {}", e);
- return (StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)).into_response();
- }
- };
-
- if json_message.is_array() {
- return (
- StatusCode::NOT_IMPLEMENTED,
- "Batch requests are not supported",
- )
- .into_response();
- }
-
- if is_initialize_request(&json_message) {
- handle_initialize(state.clone(), &json_message).await
- } else if is_jsonrpc_request(&json_message) {
- let Some(id) = acp_session_id else {
- return (
- StatusCode::BAD_REQUEST,
- "Bad Request: Acp-Session-Id header required",
- )
- .into_response();
- };
- handle_request(state.clone(), id, &json_message).await
- } else if is_jsonrpc_notification(&json_message) || is_jsonrpc_response(&json_message) {
- let Some(id) = acp_session_id else {
- return (
- StatusCode::BAD_REQUEST,
- "Bad Request: Acp-Session-Id header required",
- )
- .into_response();
- };
- handle_notification_or_response(state.clone(), id, &json_message).await
- } else {
- (StatusCode::BAD_REQUEST, "Invalid JSON-RPC message").into_response()
- }
-}
-
-pub(crate) async fn handle_get(state: Arc, request: Request) -> Response {
- if !accepts_mime_type(&request, EVENT_STREAM_MIME_TYPE) {
- return (
- StatusCode::NOT_ACCEPTABLE,
- "Not Acceptable: Client must accept text/event-stream",
- )
- .into_response();
- }
-
- let acp_session_id = match get_session_id(&request) {
- Some(id) => id,
- None => {
- return (
- StatusCode::BAD_REQUEST,
- "Bad Request: Acp-Session-Id header required",
- )
- .into_response();
- }
- };
-
- if !state.has_session(&acp_session_id).await {
- return (StatusCode::NOT_FOUND, "Session not found").into_response();
- }
-
- let receiver = match state.get_receiver(&acp_session_id).await {
- Ok(r) => r,
- Err(status) => return status.into_response(),
- };
-
- let stream = async_stream::stream! {
- let mut rx = receiver.lock().await;
- while let Some(msg) = rx.recv().await {
- yield Ok::<_, Infallible>(axum::response::sse::Event::default().data(msg));
- }
- };
-
- Sse::new(stream)
- .keep_alive(
- axum::response::sse::KeepAlive::new()
- .interval(Duration::from_secs(15))
- .text(""),
- )
- .into_response()
-}
-
-pub(crate) async fn handle_delete(
- State(state): State>,
- request: Request,
-) -> Response {
- let acp_session_id = match get_session_id(&request) {
- Some(id) => id,
- None => {
- return (
- StatusCode::BAD_REQUEST,
- "Bad Request: Acp-Session-Id header required",
- )
- .into_response();
- }
- };
-
- if !state.has_session(&acp_session_id).await {
- return (StatusCode::NOT_FOUND, "Session not found").into_response();
- }
-
- state.remove_session(&acp_session_id).await;
- StatusCode::ACCEPTED.into_response()
-}
diff --git a/crates/goose-acp/src/transport/websocket.rs b/crates/goose-acp/src/transport/websocket.rs
deleted file mode 100644
index 65a84737adbf..000000000000
--- a/crates/goose-acp/src/transport/websocket.rs
+++ /dev/null
@@ -1,158 +0,0 @@
-use anyhow::Result;
-use axum::{
- extract::ws::{Message, WebSocket, WebSocketUpgrade},
- http::StatusCode,
- response::{IntoResponse, Response},
-};
-use futures::{SinkExt, StreamExt};
-use std::{collections::HashMap, sync::Arc};
-use tokio::sync::{mpsc, Mutex, RwLock};
-use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
-use tracing::{debug, error, info, warn};
-
-use super::{TransportSession, HEADER_SESSION_ID};
-use crate::adapters::{ReceiverToAsyncRead, SenderToAsyncWrite};
-use crate::server_factory::AcpServer;
-
-pub(crate) struct WsState {
- server: Arc,
- // Keyed by acp_session_id: a connection-scoped UUID serving many Goose sessions.
- sessions: RwLock>,
-}
-
-impl WsState {
- pub fn new(server: Arc) -> Self {
- Self {
- server,
- sessions: RwLock::new(HashMap::new()),
- }
- }
-
- async fn create_connection(&self) -> Result {
- let (to_agent_tx, to_agent_rx) = mpsc::channel::(256);
- let (from_agent_tx, from_agent_rx) = mpsc::unbounded_channel::();
-
- let agent = self.server.create_agent().await?;
-
- let acp_session_id = uuid::Uuid::new_v4().to_string();
-
- let read_stream = ReceiverToAsyncRead::new(to_agent_rx);
- let write_stream = SenderToAsyncWrite::new(from_agent_tx);
- let fut = crate::server::serve(agent, read_stream.compat(), write_stream.compat_write());
- let handle = tokio::spawn(async move {
- if let Err(e) = fut.await {
- error!("ACP WebSocket session error: {}", e);
- }
- });
-
- self.sessions.write().await.insert(
- acp_session_id.clone(),
- TransportSession {
- to_agent_tx,
- from_agent_rx: Arc::new(Mutex::new(from_agent_rx)),
- handle,
- },
- );
-
- info!(acp_session_id = %acp_session_id, "WebSocket connection created");
- Ok(acp_session_id)
- }
-
- async fn remove_connection(&self, acp_session_id: &str) {
- if let Some(session) = self.sessions.write().await.remove(acp_session_id) {
- session.handle.abort();
- info!(acp_session_id = %acp_session_id, "WebSocket connection removed");
- }
- }
-}
-
-pub(crate) async fn handle_get(state: Arc, ws: WebSocketUpgrade) -> Response {
- let acp_session_id = match state.create_connection().await {
- Ok(id) => id,
- Err(e) => {
- error!("Failed to create WebSocket connection: {}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- "Failed to create WebSocket connection",
- )
- .into_response();
- }
- };
-
- let mut response = ws.on_upgrade({
- let acp_session_id = acp_session_id.clone();
- move |socket| handle_ws(socket, state, acp_session_id)
- });
- response
- .headers_mut()
- .insert(HEADER_SESSION_ID, acp_session_id.parse().unwrap());
- response
-}
-
-pub(crate) async fn handle_ws(socket: WebSocket, state: Arc, acp_session_id: String) {
- let (mut ws_tx, mut ws_rx) = socket.split();
-
- let (to_agent, from_agent) = {
- let sessions = state.sessions.read().await;
- match sessions.get(&acp_session_id) {
- Some(session) => (session.to_agent_tx.clone(), session.from_agent_rx.clone()),
- None => {
- error!(acp_session_id = %acp_session_id, "Session not found after creation");
- return;
- }
- }
- };
-
- debug!(acp_session_id = %acp_session_id, "Starting bidirectional message loop");
-
- let mut from_agent_rx = from_agent.lock().await;
-
- loop {
- tokio::select! {
- Some(msg_result) = ws_rx.next() => {
- match msg_result {
- Ok(Message::Text(text)) => {
- let text_str = text.to_string();
- debug!(acp_session_id = %acp_session_id, "Client → Agent: {} bytes", text_str.len());
- if let Err(e) = to_agent.send(text_str).await {
- error!(acp_session_id = %acp_session_id, "Failed to send to agent: {}", e);
- break;
- }
- }
- Ok(Message::Close(frame)) => {
- debug!(acp_session_id = %acp_session_id, "Client closed connection: {:?}", frame);
- break;
- }
- Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => {
- // Axum handles ping/pong automatically
- continue;
- }
- Ok(Message::Binary(_)) => {
- warn!(acp_session_id = %acp_session_id, "Ignoring binary message (ACP uses text)");
- continue;
- }
- Err(e) => {
- error!(acp_session_id = %acp_session_id, "WebSocket error: {}", e);
- break;
- }
- }
- }
-
- Some(text) = from_agent_rx.recv() => {
- debug!(acp_session_id = %acp_session_id, "Agent → Client: {} bytes", text.len());
- if let Err(e) = ws_tx.send(Message::Text(text.into())).await {
- error!(acp_session_id = %acp_session_id, "Failed to send to client: {}", e);
- break;
- }
- }
-
- else => {
- debug!(acp_session_id = %acp_session_id, "Both channels closed");
- break;
- }
- }
- }
-
- debug!(acp_session_id = %acp_session_id, "Cleaning up connection");
- state.remove_connection(&acp_session_id).await;
-}
diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml
index 6c20a644912a..7c801cdf6f1e 100644
--- a/crates/goose-cli/Cargo.toml
+++ b/crates/goose-cli/Cargo.toml
@@ -2,6 +2,7 @@
name = "goose-cli"
version.workspace = true
edition.workspace = true
+rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
@@ -21,7 +22,6 @@ path = "src/bin/generate_manpages.rs"
[dependencies]
clap_mangen = "0.2.31"
goose = { path = "../goose", default-features = false }
-goose-acp = { path = "../goose-acp", default-features = false }
goose-mcp = { path = "../goose-mcp" }
rmcp = { workspace = true }
clap = { workspace = true }
@@ -70,7 +70,7 @@ winapi = { workspace = true }
[features]
default = ["code-mode", "local-inference", "aws-providers", "telemetry", "otel", "rustls-tls"]
-code-mode = ["goose/code-mode", "goose-acp/code-mode"]
+code-mode = ["goose/code-mode"]
local-inference = ["goose/local-inference"]
aws-providers = ["goose/aws-providers"]
cuda = ["goose/cuda", "local-inference"]
diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs
index 5999e319612b..ecf7f81d766d 100644
--- a/crates/goose-cli/src/cli.rs
+++ b/crates/goose-cli/src/cli.rs
@@ -709,6 +709,8 @@ enum Command {
/// Show verbose information including current configuration
#[arg(short, long, help = "Show verbose information including config.yaml")]
verbose: bool,
+ #[arg(long, help = "Test provider connection and show status")]
+ check: bool,
},
#[command(about = "Check that your Goose setup is working")]
@@ -1064,8 +1066,9 @@ async fn handle_mcp_command(server: McpCommand) -> Result<()> {
}
async fn handle_serve_command(host: String, port: u16, builtins: Vec) -> Result<()> {
+ use goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig};
+ use goose::acp::transport::create_router;
use goose::config::paths::Paths;
- use goose_acp::server_factory::{AcpServer, AcpServerFactoryConfig};
use std::net::SocketAddr;
use std::sync::Arc;
use tracing::info;
@@ -1081,7 +1084,7 @@ async fn handle_serve_command(host: String, port: u16, builtins: Vec) ->
data_dir: Paths::data_dir(),
config_dir: Paths::config_dir(),
}));
- let router = goose_acp::transport::create_router(server);
+ let router = create_router(server);
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
info!("Starting ACP server on {}", addr);
@@ -1764,9 +1767,9 @@ 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 }) => handle_info(verbose),
+ Some(Command::Info { verbose, check }) => handle_info(verbose, check).await,
Some(Command::Mcp { server }) => handle_mcp_command(server).await,
- Some(Command::Acp { builtins }) => goose_acp::server::run(builtins).await,
+ Some(Command::Acp { builtins }) => goose::acp::server::run(builtins).await,
Some(Command::Serve {
host,
port,
diff --git a/crates/goose-cli/src/commands/info.rs b/crates/goose-cli/src/commands/info.rs
index 56c9d295b083..d702b3e43eb9 100644
--- a/crates/goose-cli/src/commands/info.rs
+++ b/crates/goose-cli/src/commands/info.rs
@@ -1,9 +1,12 @@
-use anyhow::Result;
+use anyhow::{anyhow, Result};
use console::style;
use goose::config::paths::Paths;
use goose::config::Config;
+use goose::conversation::message::Message;
+use goose::providers::errors::ProviderError;
use goose::session::session_manager::{DB_NAME, SESSIONS_FOLDER};
use serde_yaml;
+use std::time::Duration;
fn print_aligned(label: &str, value: &str, width: usize) {
println!(" {: String {
}
}
-pub fn handle_info(verbose: bool) -> Result<()> {
+struct ProviderCheckSuccess {
+ provider: String,
+ model: String,
+ elapsed: Duration,
+}
+
+enum ProviderCheckError {
+ NotConfigured {
+ label: &'static str,
+ error: String,
+ },
+ InvalidModel(String),
+ ProviderCreate {
+ error: String,
+ show_api_key_hint: bool,
+ },
+ ProviderRequest(ProviderError),
+}
+
+async fn check_provider(
+ config: &Config,
+) -> std::result::Result {
+ let (provider, model) = match (config.get_goose_provider(), config.get_goose_model()) {
+ (Ok(provider), Ok(model)) => (provider, model),
+ (Err(e), _) => {
+ return Err(ProviderCheckError::NotConfigured {
+ label: "Provider:",
+ error: e.to_string(),
+ });
+ }
+ (_, Err(e)) => {
+ return Err(ProviderCheckError::NotConfigured {
+ label: "Model:",
+ error: e.to_string(),
+ });
+ }
+ };
+
+ let model_config = goose::model::ModelConfig::new(&model)
+ .map_err(|e| ProviderCheckError::InvalidModel(e.to_string()))?
+ .with_canonical_limits(&provider);
+
+ let provider_client = goose::providers::create(&provider, model_config, Vec::new())
+ .await
+ .map_err(|e| {
+ let error = e.to_string();
+ ProviderCheckError::ProviderCreate {
+ show_api_key_hint: error.contains("not found") || error.contains("API_KEY"),
+ error,
+ }
+ })?;
+
+ let test_msg = Message::user().with_text("Say 'ok'");
+ let model_config = provider_client.get_model_config();
+ let start = std::time::Instant::now();
+ provider_client
+ .complete(&model_config, "check", "", &[test_msg], &[])
+ .await
+ .map_err(ProviderCheckError::ProviderRequest)?;
+
+ Ok(ProviderCheckSuccess {
+ provider,
+ model,
+ elapsed: start.elapsed(),
+ })
+}
+
+pub async fn handle_info(verbose: bool, check: bool) -> Result<()> {
let logs_dir = Paths::in_state_dir("logs");
let sessions_dir = Paths::in_data_dir(SESSIONS_FOLDER);
let sessions_db = sessions_dir.join(DB_NAME);
@@ -90,5 +160,115 @@ pub fn handle_info(verbose: bool) -> Result<()> {
}
}
+ if check {
+ println!("\n{}", style("Provider Check:").cyan().bold());
+
+ let result = check_provider(config).await;
+ match &result {
+ Ok(success) => {
+ print_aligned("Provider:", &success.provider, label_padding);
+ print_aligned("Model:", &success.model, label_padding);
+ print_aligned("Auth:", &style("ok").green().to_string(), label_padding);
+ print_aligned(
+ "Connection:",
+ &format!(
+ "{} (verified in {:.1}s)",
+ style("ok").green(),
+ success.elapsed.as_secs_f64()
+ ),
+ label_padding,
+ );
+ }
+ Err(ProviderCheckError::NotConfigured { label, error }) => {
+ print_aligned(
+ label,
+ &format!("{} {}", style("not configured:").red(), error),
+ label_padding,
+ );
+ print_aligned(
+ "Hint:",
+ &format!("Run '{}'", style("goose configure").cyan()),
+ label_padding,
+ );
+ }
+ Err(ProviderCheckError::InvalidModel(error)) => {
+ print_aligned(
+ "Model:",
+ &format!("{} {}", style("invalid:").red(), error),
+ label_padding,
+ );
+ }
+ Err(ProviderCheckError::ProviderCreate {
+ error,
+ show_api_key_hint,
+ }) => {
+ // Split auth failures (missing/invalid credential) from provider
+ // construction failures (unknown provider, malformed provider
+ // config). Labeling the latter as "Auth: FAILED" misdirects
+ // troubleshooting toward rotating API keys.
+ if *show_api_key_hint {
+ print_aligned(
+ "Auth:",
+ &format!("{} {}", style("FAILED").red().bold(), error),
+ label_padding,
+ );
+ print_aligned(
+ "Hint:",
+ &format!(
+ "Set the API key in your environment or run '{}'",
+ style("goose configure").cyan()
+ ),
+ label_padding,
+ );
+ } else {
+ print_aligned(
+ "Provider:",
+ &format!("{} {}", style("FAILED").red().bold(), error),
+ label_padding,
+ );
+ print_aligned(
+ "Hint:",
+ &format!(
+ "Check the provider name and config, or run '{}'",
+ style("goose configure").cyan()
+ ),
+ label_padding,
+ );
+ }
+ }
+ Err(ProviderCheckError::ProviderRequest(error)) => match error {
+ ProviderError::Authentication(_) => {
+ print_aligned(
+ "Auth:",
+ &format!("{} {}", style("FAILED").red().bold(), error),
+ label_padding,
+ );
+ print_aligned(
+ "Hint:",
+ &format!(
+ "Check your API key or run '{}'",
+ style("goose configure").cyan()
+ ),
+ label_padding,
+ );
+ }
+ _ => {
+ print_aligned(
+ "Check:",
+ &format!("{} {}", style("FAILED").red().bold(), error),
+ label_padding,
+ );
+ }
+ },
+ }
+
+ // Propagate non-zero exit status so automation (CI scripts, install
+ // checks, health probes) can rely on `goose info --check` as a
+ // pre-flight verifier.
+ if result.is_err() {
+ return Err(anyhow!("provider check failed"));
+ }
+ }
+
Ok(())
}
diff --git a/crates/goose-cli/src/scenario_tests/scenario_runner.rs b/crates/goose-cli/src/scenario_tests/scenario_runner.rs
index eae04307b366..ade183645f6c 100644
--- a/crates/goose-cli/src/scenario_tests/scenario_runner.rs
+++ b/crates/goose-cli/src/scenario_tests/scenario_runner.rs
@@ -232,6 +232,7 @@ where
Arc::new(mock_client),
None,
None,
+ None,
)
.await;
diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs
index e2edbfb12479..f9c3d2bf6795 100644
--- a/crates/goose-cli/src/session/completion.rs
+++ b/crates/goose-cli/src/session/completion.rs
@@ -121,6 +121,31 @@ impl GooseCompleter {
Ok((line.len(), vec![]))
}
+ /// Complete skill names for the /skills command
+ fn complete_skill_names(&self, line: &str) -> Result<(usize, Vec)> {
+ use goose::skills::list_installed_skills;
+
+ let cwd = std::env::current_dir().unwrap_or_default();
+ let skills = list_installed_skills(Some(&cwd));
+ let skill_names: Vec = skills.iter().map(|s| s.name.clone()).collect();
+
+ // Complete the last letter being typed (e.g. "/skills coding in")
+ let last = line.rsplit_once(' ').map_or("", |(_, w)| w);
+ let pos = line.len() - last.len();
+
+ let partial = last.to_lowercase();
+ let candidates: Vec = skill_names
+ .iter()
+ .filter(|name| name.to_lowercase().starts_with(&partial))
+ .map(|name| Pair {
+ display: name.clone(),
+ replacement: format!("{} ", name),
+ })
+ .collect();
+
+ Ok((pos, candidates))
+ }
+
/// Complete slash commands
fn complete_slash_commands(&self, line: &str) -> Result<(usize, Vec)> {
// Define available slash commands
@@ -136,6 +161,7 @@ impl GooseCompleter {
"/prompt",
"/mode",
"/recipe",
+ "/skills",
];
// Find commands that match the prefix
@@ -374,6 +400,10 @@ impl Completer for GooseCompleter {
return self.complete_mode_flags(line);
}
+ if line.starts_with("/skills ") {
+ return self.complete_skill_names(line);
+ }
+
return Ok((pos, vec![]));
}
diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs
index 8bc067ddb109..2641a77b725e 100644
--- a/crates/goose-cli/src/session/input.rs
+++ b/crates/goose-cli/src/session/input.rs
@@ -27,6 +27,8 @@ pub enum InputResult {
Compact,
ToggleFullToolOutput,
Edit(Option),
+ ListSkills,
+ LoadSkills(Vec),
}
#[derive(Debug)]
@@ -204,6 +206,7 @@ fn handle_slash_command(input: &str) -> Option {
const CMD_SUMMARIZE_DEPRECATED: &str = "/summarize";
const CMD_EDIT: &str = "/edit";
const CMD_EDIT_WITH_SPACE: &str = "/edit ";
+ const CMD_SKILLS: &str = "/skills";
match input {
"/exit" | "/quit" => Some(InputResult::Exit),
@@ -267,6 +270,16 @@ fn handle_slash_command(input: &str) -> Option {
s if s == CMD_CLEAR => Some(InputResult::Clear),
s if s.starts_with(CMD_RECIPE) => parse_recipe_command(s),
s if s == CMD_COMPACT => Some(InputResult::Compact),
+ // Match "/skills" exactly or "/skills " with args - avoids matching e.g. "/skillsextra"
+ s if s == CMD_SKILLS || s.starts_with(&format!("{CMD_SKILLS} ")) => {
+ let args = s.get(CMD_SKILLS.len()..).unwrap_or("").trim();
+ if args.is_empty() {
+ Some(InputResult::ListSkills)
+ } else {
+ let names: Vec = args.split_whitespace().map(String::from).collect();
+ Some(InputResult::LoadSkills(names))
+ }
+ }
s if s == CMD_SUMMARIZE_DEPRECATED => {
println!("{}", console::style("⚠️ Note: /summarize has been renamed to /compact and will be removed in a future release.").yellow());
Some(InputResult::Compact)
@@ -417,6 +430,7 @@ fn print_help() {
/compact - Compact the current conversation to reduce context length while preserving key information.
/edit [text] - Open your prompt editor to compose a message. Optionally pre-fill with text.
Uses $GOOSE_PROMPT_EDITOR, $VISUAL, or $EDITOR (in that order).
+/skills - List available skills or enable skills by name (usage: /skills [...])
/? or /help - Display this help message
/clear - Clears the current chat history
@@ -747,4 +761,48 @@ mod tests {
// Test /editfoo is not a valid command
assert!(handle_slash_command("/editfoo").is_none());
}
+
+ #[test]
+ fn test_skill_command() {
+ // Test with a single skill name
+ let Some(InputResult::LoadSkills(names)) = handle_slash_command("/skills coding") else {
+ panic!(
+ "Expected LoadSkills, got {:?}",
+ handle_slash_command("/skills coding")
+ );
+ };
+ assert_eq!(names, vec!["coding"]);
+
+ // Test with multiple skill names
+ let Some(InputResult::LoadSkills(names)) = handle_slash_command("/skills coding insight")
+ else {
+ panic!(
+ "Expected LoadSkills, got {:?}",
+ handle_slash_command("/skills coding insight")
+ );
+ };
+ assert_eq!(names, vec!["coding", "insight"]);
+
+ // Test with extra whitespace
+ let Some(InputResult::LoadSkills(names)) = handle_slash_command("/skills my-skill ")
+ else {
+ panic!(
+ "Expected LoadSkills, got {:?}",
+ handle_slash_command("/skills my-skill ")
+ );
+ };
+ assert_eq!(names, vec!["my-skill"]);
+
+ // Test with no name: ListSkills
+ assert!(matches!(
+ handle_slash_command("/skills"),
+ Some(InputResult::ListSkills)
+ ));
+
+ // Test with only whitespace after /skills: ListSkills
+ assert!(matches!(
+ handle_slash_command("/skills "),
+ Some(InputResult::ListSkills)
+ ));
+ }
}
diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs
index 535bc54b7ac6..4deb36ae3d14 100644
--- a/crates/goose-cli/src/session/mod.rs
+++ b/crates/goose-cli/src/session/mod.rs
@@ -666,6 +666,14 @@ impl CliSession {
}
}
}
+ InputResult::LoadSkills(names) => {
+ history.save(editor);
+ self.handle_load_skills(&names).await?;
+ }
+ InputResult::ListSkills => {
+ history.save(editor);
+ self.handle_list_skills().await?;
+ }
}
Ok(())
}
@@ -875,6 +883,55 @@ impl CliSession {
}
}
+ async fn handle_load_skills(&mut self, names: &[String]) -> Result<()> {
+ // NOTE: We don't validate the skill names here because the load_skill tool will
+ // handle that and provide feedback to the user if any skill names are invalid.
+ let message = format!(
+ "Use the load_skill tool to load the following skills: {}.",
+ names
+ .iter()
+ .map(|n| format!("\"{}\"", n))
+ .collect::>()
+ .join(", ")
+ );
+ self.push_message(Message::user().with_text(&message));
+ output::show_thinking();
+ let result = self
+ .process_agent_response(true, CancellationToken::default())
+ .await;
+ output::hide_thinking();
+ result?;
+
+ Ok(())
+ }
+
+ async fn handle_list_skills(&mut self) -> Result<()> {
+ use comfy_table::{presets, Cell, ContentArrangement, Table};
+ use goose::skills::list_installed_skills;
+ let cwd = std::env::current_dir().unwrap_or_default();
+ let skills = list_installed_skills(Some(&cwd));
+
+ if skills.is_empty() {
+ println!("{}", console::style("No skills available.").yellow());
+ return Ok(());
+ }
+
+ let mut table = Table::new();
+ table.set_content_arrangement(ContentArrangement::Dynamic);
+ table.load_preset(presets::ASCII_FULL);
+ table.set_header(vec!["Skill", "Description"]);
+
+ let mut sorted_skills = skills;
+ sorted_skills.sort_by(|a, b| a.name.cmp(&b.name));
+
+ for skill in &sorted_skills {
+ table.add_row(vec![Cell::new(&skill.name), Cell::new(&skill.description)]);
+ }
+
+ println!("{table}");
+ Ok(())
+ }
+
async fn handle_compact(&mut self) -> Result<()> {
let prompt = "Are you sure you want to compact this conversation? This will condense the message history.";
let should_summarize = match cliclack::confirm(prompt).initial_value(true).interact() {
diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml
index f1e5dfc9287b..755aab468120 100644
--- a/crates/goose-mcp/Cargo.toml
+++ b/crates/goose-mcp/Cargo.toml
@@ -2,6 +2,7 @@
name = "goose-mcp"
version.workspace = true
edition.workspace = true
+rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
diff --git a/crates/goose-sdk/Cargo.toml b/crates/goose-sdk/Cargo.toml
index 6eef747a3c4d..18fa86dafe56 100644
--- a/crates/goose-sdk/Cargo.toml
+++ b/crates/goose-sdk/Cargo.toml
@@ -2,6 +2,7 @@
name = "goose-sdk"
version.workspace = true
edition.workspace = true
+rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
diff --git a/crates/goose-sdk/src/custom_requests.rs b/crates/goose-sdk/src/custom_requests.rs
index bbc375be09f3..c8fc781ce78b 100644
--- a/crates/goose-sdk/src/custom_requests.rs
+++ b/crates/goose-sdk/src/custom_requests.rs
@@ -1,6 +1,7 @@
use sacp::{JsonRpcRequest, JsonRpcResponse};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
/// Schema descriptor for a single custom method, produced by the
/// `#[custom_methods]` macro's generated `custom_method_schemas()` function.
@@ -98,11 +99,43 @@ pub struct GetExtensionsRequest {}
/// List configured extensions and any warnings.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct GetExtensionsResponse {
- /// Array of ExtensionEntry objects with `enabled` flag and config details.
+ /// Array of ExtensionEntry objects with `enabled` flag, `configKey`, and flattened config details.
pub extensions: Vec,
pub warnings: Vec,
}
+/// Persist a new extension to the user's global goose config.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/config/extensions/add", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct AddConfigExtensionRequest {
+ pub name: String,
+ /// Extension configuration. Must be a JSON object matching one of the
+ /// `ExtensionConfig` variants (e.g. `stdio`, `streamable_http`, `builtin`).
+ /// `name` and `enabled` are injected server-side.
+ #[serde(default)]
+ pub extension_config: serde_json::Value,
+ #[serde(default)]
+ pub enabled: bool,
+}
+
+/// Remove a persisted extension from the user's global goose config.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/config/extensions/remove", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct RemoveConfigExtensionRequest {
+ pub config_key: String,
+}
+
+/// Toggle the `enabled` flag for a persisted extension in the user's global goose config.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/config/extensions/toggle", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct ToggleConfigExtensionRequest {
+ pub config_key: String,
+ pub enabled: bool,
+}
+
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/session/extensions", response = GetSessionExtensionsResponse)]
#[serde(rename_all = "camelCase")]
@@ -180,22 +213,22 @@ pub struct RemoveSecretRequest {
pub key: String,
}
-/// List providers available through goose, including the config-default sentinel.
+/// Update the project association for a session.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
-#[request(method = "_goose/providers/list", response = ListProvidersResponse)]
-pub struct ListProvidersRequest {}
-
-#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[request(method = "_goose/session/update_project", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
-pub struct ProviderListEntry {
- pub id: String,
- pub label: String,
+pub struct UpdateSessionProjectRequest {
+ pub session_id: String,
+ pub project_id: Option,
}
-/// Provider list response.
-#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
-pub struct ListProvidersResponse {
- pub providers: Vec,
+/// Rename a session.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/session/rename", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct RenameSessionRequest {
+ pub session_id: String,
+ pub title: String,
}
/// Archive a session (soft delete).
@@ -245,70 +278,459 @@ pub struct ImportSessionResponse {
pub message_count: u64,
}
-/// List providers with full metadata (config keys, setup steps, etc.).
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ProviderConfigKey {
+ pub name: String,
+ pub required: bool,
+ pub secret: bool,
+ #[serde(default)]
+ pub default: Option,
+ #[serde(default)]
+ pub oauth_flow: bool,
+ #[serde(default)]
+ pub device_code_flow: bool,
+ #[serde(default)]
+ pub primary: bool,
+}
+
+/// The type of source entity.
+#[derive(
+ Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
+)]
+#[serde(rename_all = "camelCase")]
+pub enum SourceType {
+ #[default]
+ Skill,
+ BuiltinSkill,
+ Recipe,
+ Subrecipe,
+ Agent,
+}
+
+impl std::fmt::Display for SourceType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ SourceType::Skill => write!(f, "skill"),
+ SourceType::BuiltinSkill => write!(f, "builtin skill"),
+ SourceType::Recipe => write!(f, "recipe"),
+ SourceType::Subrecipe => write!(f, "subrecipe"),
+ SourceType::Agent => write!(f, "agent"),
+ }
+ }
+}
+
+/// A source discovered by Goose and backed by an on-disk path. Sources may be
+/// either `global` (shared across all projects) or project-specific.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SourceEntry {
+ #[serde(rename = "type")]
+ pub source_type: SourceType,
+ pub name: String,
+ pub description: String,
+ pub content: String,
+ /// Absolute path to the source on disk. A directory for skills, a file for
+ /// recipes and agents.
+ pub directory: String,
+ /// True when the source lives in the user's global sources directory; false
+ /// when it lives inside a specific project.
+ pub global: bool,
+ /// Paths (absolute) of additional files that live alongside the source.
+ /// Only skills currently populate this; empty for other source types.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub supporting_files: Vec,
+}
+
+impl SourceEntry {
+ /// Render this source as a markdown block suitable for injecting into an
+ /// LLM context. Used by the skills and summon runtimes when loading a
+ /// source into the current conversation.
+ pub fn to_load_text(&self) -> String {
+ format!(
+ "## {} ({})\n\n{}\n\n### Content\n\n{}",
+ self.name, self.source_type, self.description, self.content
+ )
+ }
+}
+
+/// Create a new source in an explicit target scope (global or project-scoped).
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
-#[request(method = "_goose/providers/details", response = GetProviderDetailsResponse)]
-pub struct GetProviderDetailsRequest {}
+#[request(method = "_goose/sources/create", response = CreateSourceResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateSourceRequest {
+ #[serde(rename = "type")]
+ pub source_type: SourceType,
+ pub name: String,
+ pub description: String,
+ pub content: String,
+ pub global: bool,
+ /// Absolute path to the project root. Required when `global` is false.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub project_dir: Option,
+}
-/// Provider details response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
-pub struct GetProviderDetailsResponse {
- pub providers: Vec,
+#[serde(rename_all = "camelCase")]
+pub struct CreateSourceResponse {
+ pub source: SourceEntry,
}
-/// Fetch the full list of models available for a specific provider.
+/// List discovered sources.
+///
+/// Today this endpoint only returns skills. If `type` is omitted, it defaults
+/// to listing skill sources. Both global and project-scoped skills are included
+/// when `project_dir` is set.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
-#[request(method = "_goose/providers/models", response = GetProviderModelsResponse)]
+#[request(method = "_goose/sources/list", response = ListSourcesResponse)]
#[serde(rename_all = "camelCase")]
-pub struct GetProviderModelsRequest {
- pub provider_name: String,
+pub struct ListSourcesRequest {
+ #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
+ pub source_type: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub project_dir: Option,
}
-/// Provider models response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
-pub struct GetProviderModelsResponse {
- pub models: Vec,
+#[serde(rename_all = "camelCase")]
+pub struct ListSourcesResponse {
+ pub sources: Vec,
}
-#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+/// Update an existing source's name, description, and content by absolute path.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/sources/update", response = UpdateSourceResponse)]
#[serde(rename_all = "camelCase")]
-pub struct ProviderDetailEntry {
+pub struct UpdateSourceRequest {
+ #[serde(rename = "type")]
+ pub source_type: SourceType,
+ pub path: String,
pub name: String,
- pub display_name: String,
pub description: String,
- pub default_model: String,
- pub is_configured: bool,
- pub provider_type: String,
- pub config_keys: Vec,
+ pub content: String,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateSourceResponse {
+ pub source: SourceEntry,
+}
+
+/// Delete a source and its on-disk directory by absolute path.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/sources/delete", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct DeleteSourceRequest {
+ #[serde(rename = "type")]
+ pub source_type: SourceType,
+ pub path: String,
+}
+
+/// Export a source at an absolute path as a portable JSON payload.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/sources/export", response = ExportSourceResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct ExportSourceRequest {
+ #[serde(rename = "type")]
+ pub source_type: SourceType,
+ pub path: String,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct ExportSourceResponse {
+ pub json: String,
+ pub filename: String,
+}
+
+/// Import a source from a JSON export payload produced by `_goose/sources/export`.
+/// The imported source is written into the explicit target scope; on name
+/// collisions a `-imported` suffix is appended.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/sources/import", response = ImportSourcesResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct ImportSourcesRequest {
+ pub data: String,
+ pub global: bool,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub project_dir: Option,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct ImportSourcesResponse {
+ pub sources: Vec,
+}
+
+/// Transcribe audio via a dictation provider.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/dictation/transcribe", response = DictationTranscribeResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationTranscribeRequest {
+ /// Base64-encoded audio data
+ pub audio: String,
+ /// MIME type (e.g. "audio/wav", "audio/webm")
+ pub mime_type: String,
+ /// Provider to use: "openai", "groq", "elevenlabs", or "local"
+ pub provider: String,
+}
+
+/// Transcription result.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+pub struct DictationTranscribeResponse {
+ pub text: String,
+}
+
+/// Get the configuration status of all dictation providers.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/dictation/config", response = DictationConfigResponse)]
+pub struct DictationConfigRequest {}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct DictationModelOption {
+ pub id: String,
+ pub label: String,
+ pub description: String,
+}
+
+/// Per-provider configuration status.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationProviderStatusEntry {
+ pub configured: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub host: Option,
+ pub description: String,
+ pub uses_provider_config: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub settings_path: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub config_key: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub model_config_key: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default_model: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub selected_model: Option,
#[serde(default)]
- pub setup_steps: Vec,
+ pub available_models: Vec,
+}
+
+/// Dictation config response — map of provider name to status.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+pub struct DictationConfigResponse {
+ pub providers: HashMap,
+}
+
+/// List providers with setup metadata and the current model inventory snapshot.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/providers/list", response = ListProvidersResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct ListProvidersRequest {
+ /// Only return entries for these providers. Empty means all.
#[serde(default)]
- pub known_models: Vec,
+ pub provider_ids: Vec,
+}
+
+/// Provider list response.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+pub struct ListProvidersResponse {
+ pub entries: Vec,
+}
+
+/// Trigger a background refresh of provider inventories.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(
+ method = "_goose/providers/inventory/refresh",
+ response = RefreshProviderInventoryResponse
+)]
+#[serde(rename_all = "camelCase")]
+pub struct RefreshProviderInventoryRequest {
+ /// Which providers to refresh. Empty means all known providers.
+ #[serde(default)]
+ pub provider_ids: Vec,
+}
+
+/// Refresh acknowledgement.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct RefreshProviderInventoryResponse {
+ /// Which providers will be refreshed.
+ pub started: Vec,
+ /// Which providers were skipped and why.
+ #[serde(default)]
+ pub skipped: Vec,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ModelEntry {
- pub name: String,
- pub context_limit: usize,
+pub struct RefreshProviderInventorySkipDto {
+ pub provider_id: String,
+ pub reason: RefreshProviderInventorySkipReasonDto,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum RefreshProviderInventorySkipReasonDto {
+ #[default]
+ UnknownProvider,
+ NotConfigured,
+ DoesNotSupportRefresh,
+ AlreadyRefreshing,
}
+/// A single model in provider inventory.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ProviderConfigKey {
+pub struct ProviderInventoryModelDto {
+ /// Model identifier as the provider knows it.
+ pub id: String,
+ /// Human-readable display name.
pub name: String,
- pub required: bool,
- pub secret: bool,
- #[serde(default)]
- pub default: Option,
- #[serde(default)]
- pub oauth_flow: bool,
+ /// Model family for grouping in UI.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub family: Option,
+ /// Context window size in tokens.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub context_limit: Option,
+ /// Whether the model supports reasoning/extended thinking.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reasoning: Option,
+ /// Whether this model should appear in the compact recommended picker.
#[serde(default)]
- pub device_code_flow: bool,
- #[serde(default)]
- pub primary: bool,
+ pub recommended: bool,
+}
+
+/// Provider inventory entry.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ProviderInventoryEntryDto {
+ /// Provider identifier.
+ pub provider_id: String,
+ /// Human-readable provider name.
+ pub provider_name: String,
+ /// Description of the provider's capabilities.
+ pub description: String,
+ /// The default/recommended model for this provider.
+ pub default_model: String,
+ /// Whether Goose has enough configuration to use this provider.
+ pub configured: bool,
+ /// Provider classification such as `Preferred`, `Builtin`, `Declarative`, or `Custom`.
+ pub provider_type: String,
+ /// Required configuration keys and setup metadata.
+ pub config_keys: Vec,
+ /// Step-by-step setup instructions, when present.
+ pub setup_steps: Vec,
+ /// Whether this provider supports background inventory refresh.
+ pub supports_refresh: bool,
+ /// Whether a refresh is currently in flight.
+ pub refreshing: bool,
+ /// The list of available models.
+ pub models: Vec,
+ /// When this entry was last successfully refreshed (ISO 8601).
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub last_updated_at: Option,
+ /// When a refresh was most recently attempted (ISO 8601).
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub last_refresh_attempt_at: Option,
+ /// The last refresh failure message, if any.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub last_refresh_error: Option,
+ /// Whether we believe this data may be outdated.
+ pub stale: bool,
+ /// Guidance message shown when this provider manages its own model selection externally.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub model_selection_hint: Option,
}
/// Empty success response for operations that return no data.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct EmptyResponse {}
+
+/// List available local Whisper models with their download status.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(
+ method = "_goose/dictation/models/list",
+ response = DictationModelsListResponse
+)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationModelsListRequest {}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationModelsListResponse {
+ pub models: Vec,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationLocalModelStatus {
+ pub id: String,
+ pub label: String,
+ pub description: String,
+ pub size_mb: u32,
+ pub downloaded: bool,
+ pub download_in_progress: bool,
+}
+
+/// Kick off a background download of a local Whisper model.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/dictation/models/download", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationModelDownloadRequest {
+ pub model_id: String,
+}
+
+/// Poll the progress of an in-flight download.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(
+ method = "_goose/dictation/models/download/progress",
+ response = DictationModelDownloadProgressResponse
+)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationModelDownloadProgressRequest {
+ pub model_id: String,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationModelDownloadProgressResponse {
+ /// None when no download is active for this model id.
+ pub progress: Option,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationDownloadProgress {
+ pub bytes_downloaded: u64,
+ pub total_bytes: u64,
+ pub progress_percent: f32,
+ /// serde lowercase of DownloadStatus: "downloading" | "completed" | "failed" | "cancelled"
+ pub status: String,
+ pub error: Option,
+}
+
+/// Cancel an in-flight download.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/dictation/models/cancel", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationModelCancelRequest {
+ pub model_id: String,
+}
+
+/// Delete a downloaded local Whisper model from disk.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/dictation/models/delete", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationModelDeleteRequest {
+ pub model_id: String,
+}
+
+/// Persist the user's model selection for a given provider.
+#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
+#[request(method = "_goose/dictation/model/select", response = EmptyResponse)]
+#[serde(rename_all = "camelCase")]
+pub struct DictationModelSelectRequest {
+ pub provider: String,
+ pub model_id: String,
+}
diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml
index 2045caa67c49..46850dd6ad6a 100644
--- a/crates/goose-server/Cargo.toml
+++ b/crates/goose-server/Cargo.toml
@@ -2,6 +2,7 @@
name = "goose-server"
version.workspace = true
edition.workspace = true
+rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs
index 1d18e2b40fac..29825f038834 100644
--- a/crates/goose-server/src/routes/config_management.rs
+++ b/crates/goose-server/src/routes/config_management.rs
@@ -11,6 +11,7 @@ use goose::config::declarative_providers::LoadedProvider;
use goose::config::paths::Paths;
use goose::config::ExtensionEntry;
use goose::config::{Config, ConfigError};
+use goose::custom_requests::SourceType;
use goose::model::ModelConfig;
use goose::providers::base::{ProviderMetadata, ProviderType};
use goose::providers::canonical::maybe_get_canonical_model;
@@ -136,6 +137,7 @@ pub enum CommandType {
Builtin,
Recipe,
Skill,
+ Agent,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -143,6 +145,10 @@ pub struct SlashCommand {
pub command: String,
pub help: String,
pub command_type: CommandType,
+ /// For MCP-sourced skills, the name of the originating MCP extension
+ /// (e.g. "github"). `None` for filesystem skills and non-skill commands.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub origin: Option,
}
#[derive(Serialize, ToSchema)]
pub struct SlashCommandsResponse {
@@ -395,6 +401,9 @@ pub async fn get_provider_models(
pub struct SlashCommandsQuery {
/// Optional working directory to discover local skills from
pub working_dir: Option,
+ /// Optional session id; when provided, the endpoint also includes
+ /// MCP-served skills from the corresponding agent's connected extensions.
+ pub session_id: Option,
}
#[utoipa::path(
@@ -406,6 +415,7 @@ pub struct SlashCommandsQuery {
)
)]
pub async fn get_slash_commands(
+ axum::extract::State(state): axum::extract::State>,
axum::extract::Query(query): axum::extract::Query,
) -> Result, ErrorResponse> {
let mut commands: Vec<_> = slash_commands::list_commands()
@@ -414,6 +424,7 @@ pub async fn get_slash_commands(
command: command.command.clone(),
help: command.recipe_path.clone(),
command_type: CommandType::Recipe,
+ origin: None,
})
.collect();
@@ -422,20 +433,53 @@ pub async fn get_slash_commands(
command: cmd_def.name.to_string(),
help: cmd_def.description.to_string(),
command_type: CommandType::Builtin,
+ origin: None,
});
}
let working_dir = query.working_dir.map(std::path::PathBuf::from);
- for source in
- goose::agents::platform_extensions::skills::list_installed_skills(working_dir.as_deref())
- {
+ for source in goose::skills::list_installed_skills(working_dir.as_deref()) {
commands.push(SlashCommand {
command: source.name,
help: source.description,
command_type: CommandType::Skill,
+ origin: None,
});
}
+ let discover_dir = working_dir
+ .as_deref()
+ .unwrap_or_else(|| std::path::Path::new("."));
+ for source in
+ goose::agents::platform_extensions::summon::discover_filesystem_sources(discover_dir)
+ {
+ if matches!(
+ source.source_type,
+ SourceType::Agent | SourceType::Recipe | SourceType::Subrecipe
+ ) && !source.content.is_empty()
+ {
+ commands.push(SlashCommand {
+ command: source.name,
+ help: source.description,
+ command_type: CommandType::Agent,
+ origin: None,
+ });
+ }
+ }
+
+ if let Some(session_id) = query.session_id.as_deref() {
+ if let Ok(agent) = state.get_agent(session_id.to_string()).await {
+ for entry in agent.extension_manager.aggregated_mcp_skills().await {
+ commands.push(SlashCommand {
+ command: entry.name,
+ help: entry.description,
+ command_type: CommandType::Skill,
+ origin: Some(entry.server),
+ });
+ }
+ }
+ }
+
Ok(Json(SlashCommandsResponse { commands }))
}
diff --git a/crates/goose-server/src/tls.rs b/crates/goose-server/src/tls.rs
index 8f1431a2139e..273052ec6c3b 100644
--- a/crates/goose-server/src/tls.rs
+++ b/crates/goose-server/src/tls.rs
@@ -12,6 +12,7 @@
//! the server listener always uses OpenSSL when this feature is active.
use anyhow::{bail, Result};
+use goose::config::paths::Paths;
use rcgen::{CertificateParams, DnType, KeyPair, SanType};
use std::path::Path;
@@ -105,6 +106,65 @@ pub async fn setup_tls(cert_path: Option<&str>, key_path: Option<&str>) -> Resul
}
}
+fn tls_cache_dir() -> std::path::PathBuf {
+ Paths::config_dir().join("tls")
+}
+
+fn write_private_key(path: &std::path::Path, contents: &[u8]) {
+ #[cfg(unix)]
+ {
+ use std::io::Write;
+ use std::os::unix::fs::OpenOptionsExt;
+
+ let result = std::fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .mode(0o600)
+ .open(path);
+ if let Ok(mut file) = result {
+ let _ = file.write_all(contents);
+ }
+ }
+
+ #[cfg(not(unix))]
+ {
+ let _ = std::fs::write(path, contents);
+ }
+}
+
+async fn load_cached_tls() -> Option {
+ let dir = tls_cache_dir();
+ let cert_pem = std::fs::read(dir.join("server.pem")).ok()?;
+ let key_pem = std::fs::read(dir.join("server.key")).ok()?;
+
+ let der = pem::parse(&cert_pem).ok()?.into_contents();
+ let fingerprint = sha256_fingerprint(&der);
+
+ #[cfg(feature = "rustls-tls")]
+ let config = axum_server::tls_rustls::RustlsConfig::from_pem(cert_pem, key_pem)
+ .await
+ .ok()?;
+ #[cfg(feature = "native-tls")]
+ let config = axum_server::tls_openssl::OpenSSLConfig::from_pem(&cert_pem, &key_pem).ok()?;
+
+ Some(TlsSetup {
+ config,
+ fingerprint,
+ })
+}
+
+/// All errors are silently ignored — this is a best-effort optimisation and
+/// must never prevent the server from starting.
+fn save_tls_to_cache(cert_pem: &str, key_pem: &str) {
+ let dir = tls_cache_dir();
+ if std::fs::create_dir_all(&dir).is_err() {
+ return;
+ }
+ let _ = std::fs::write(dir.join("server.pem"), cert_pem);
+ write_private_key(&dir.join("server.key"), key_pem.as_bytes());
+}
+
/// Generate a self-signed TLS certificate for localhost (127.0.0.1) and
/// return a [`TlsSetup`] containing the server config and the SHA-256
/// fingerprint of the generated certificate (colon-separated hex).
@@ -115,6 +175,12 @@ pub async fn self_signed_config() -> Result {
#[cfg(feature = "rustls-tls")]
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
+ // Fast path: reuse a previously cached certificate if one exists.
+ if let Some(cached) = load_cached_tls().await {
+ println!("GOOSED_CERT_FINGERPRINT={}", cached.fingerprint);
+ return Ok(cached);
+ }
+
let (cert, key_pair) = generate_self_signed_cert()?;
let fingerprint = sha256_fingerprint(cert.der());
@@ -123,6 +189,9 @@ pub async fn self_signed_config() -> Result {
let cert_pem = cert.pem();
let key_pem = key_pair.serialize_pem();
+ // Persist for future restarts before moving the strings into the config.
+ save_tls_to_cache(&cert_pem, &key_pem);
+
#[cfg(feature = "rustls-tls")]
let config = axum_server::tls_rustls::RustlsConfig::from_pem(
cert_pem.into_bytes(),
diff --git a/crates/goose-test-support/Cargo.toml b/crates/goose-test-support/Cargo.toml
index 169779336fdd..02cd360f6c72 100644
--- a/crates/goose-test-support/Cargo.toml
+++ b/crates/goose-test-support/Cargo.toml
@@ -1,6 +1,7 @@
[package]
name = "goose-test-support"
edition.workspace = true
+rust-version.workspace = true
version.workspace = true
authors.workspace = true
license.workspace = true
diff --git a/crates/goose-test-support/src/session.rs b/crates/goose-test-support/src/session.rs
index c150e82cd8af..c3ac411d12c4 100644
--- a/crates/goose-test-support/src/session.rs
+++ b/crates/goose-test-support/src/session.rs
@@ -1,7 +1,12 @@
use std::sync::{Arc, Mutex};
pub const TEST_SESSION_ID: &str = "test-session-id";
-pub const TEST_MODEL: &str = "gpt-5-nano";
+// Use a Chat Completions model so the canned SSE fixtures (which return
+// Chat Completions format) are parsed correctly. gpt-5-nano now routes to
+// the Responses API which needs a different mock format.
+// TODO: add a Responses API mock to OpenAiFixture so tests can cover
+// responses-routed models like gpt-5-nano end-to-end.
+pub const TEST_MODEL: &str = "gpt-4.1";
const NOT_YET_SET: &str = "session-id-not-yet-set";
pub(crate) const SESSION_ID_HEADER: &str = "agent-session-id";
diff --git a/crates/goose-test/Cargo.toml b/crates/goose-test/Cargo.toml
index bae6ce32d0c7..21f84b372de2 100644
--- a/crates/goose-test/Cargo.toml
+++ b/crates/goose-test/Cargo.toml
@@ -1,6 +1,7 @@
[package]
name = "goose-test"
edition.workspace = true
+rust-version.workspace = true
version.workspace = true
authors.workspace = true
license.workspace = true
diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml
index 0960319d7623..7776b6eb0808 100644
--- a/crates/goose/Cargo.toml
+++ b/crates/goose/Cargo.toml
@@ -2,6 +2,7 @@
name = "goose"
version.workspace = true
edition.workspace = true
+rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
@@ -81,7 +82,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
serde_urlencoded = "0.7"
jsonschema = "0.30.0"
-uuid = { workspace = true }
+uuid = { workspace = true, features = ["v7"] }
regex = { workspace = true }
async-trait = { workspace = true }
async-stream = { workspace = true }
@@ -95,7 +96,7 @@ nanoid = "0.4"
sha2 = { workspace = true }
base64 = { workspace = true }
url = { workspace = true }
-axum = { workspace = true }
+axum = { workspace = true, features = ["ws"] }
webbrowser = { workspace = true }
lazy_static = "1.5.0"
tracing = { workspace = true }
@@ -113,6 +114,7 @@ strum = { workspace = true }
once_cell = { workspace = true }
etcetera = { workspace = true }
fs-err = "3"
+goose-sdk = { path = "../goose-sdk" }
rand = { workspace = true }
utoipa = { workspace = true, features = ["chrono"] }
tokio-cron-scheduler = "0.14.0"
@@ -188,6 +190,9 @@ pem = { version = "3", optional = true }
pkcs1 = { version = "0.7", default-features = false, features = ["pkcs8"], optional = true }
pkcs8 = { version = "0.10", default-features = false, features = ["alloc"], optional = true }
sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8"], optional = true }
+goose-acp-macros = { path = "../goose-acp-macros" }
+tower-http = { workspace = true, features = ["cors"] }
+http-body-util = "0.1.3"
[target.'cfg(target_os = "windows")'.dependencies]
@@ -220,6 +225,7 @@ opentelemetry_sdk = { workspace = true, features = ["testing"] }
goose-test-support = { path = "../goose-test-support" }
bytes.workspace = true
http.workspace = true
+goose-mcp = { path = "../goose-mcp" }
[[example]]
name = "agent"
@@ -242,6 +248,10 @@ path = "src/bin/analyze_cli.rs"
name = "build_canonical_models"
path = "src/providers/canonical/build_canonical_models.rs"
+[[bin]]
+name = "generate-acp-schema"
+path = "src/bin/generate_acp_schema.rs"
+
[package.metadata.cargo-machete]
ignored = [
@@ -249,4 +259,6 @@ ignored = [
"winapi",
# Used to provide extras imports for sacp
"agent-client-protocol-schema",
+ # Used via http transport
+ "http-body-util",
]
diff --git a/crates/goose/acp-meta.json b/crates/goose/acp-meta.json
new file mode 100644
index 000000000000..252639c00a3b
--- /dev/null
+++ b/crates/goose/acp-meta.json
@@ -0,0 +1,199 @@
+{
+ "methods": [
+ {
+ "method": "_goose/extensions/add",
+ "requestType": "AddExtensionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/extensions/remove",
+ "requestType": "RemoveExtensionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/tools",
+ "requestType": "GetToolsRequest",
+ "responseType": "GetToolsResponse"
+ },
+ {
+ "method": "_goose/resource/read",
+ "requestType": "ReadResourceRequest",
+ "responseType": "ReadResourceResponse"
+ },
+ {
+ "method": "_goose/working_dir/update",
+ "requestType": "UpdateWorkingDirRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "session/delete",
+ "requestType": "DeleteSessionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/config/extensions",
+ "requestType": "GetExtensionsRequest",
+ "responseType": "GetExtensionsResponse"
+ },
+ {
+ "method": "_goose/config/extensions/add",
+ "requestType": "AddConfigExtensionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/config/extensions/remove",
+ "requestType": "RemoveConfigExtensionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/config/extensions/toggle",
+ "requestType": "ToggleConfigExtensionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/session/extensions",
+ "requestType": "GetSessionExtensionsRequest",
+ "responseType": "GetSessionExtensionsResponse"
+ },
+ {
+ "method": "_goose/providers/list",
+ "requestType": "ListProvidersRequest",
+ "responseType": "ListProvidersResponse"
+ },
+ {
+ "method": "_goose/providers/inventory/refresh",
+ "requestType": "RefreshProviderInventoryRequest",
+ "responseType": "RefreshProviderInventoryResponse"
+ },
+ {
+ "method": "_goose/config/read",
+ "requestType": "ReadConfigRequest",
+ "responseType": "ReadConfigResponse"
+ },
+ {
+ "method": "_goose/config/upsert",
+ "requestType": "UpsertConfigRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/config/remove",
+ "requestType": "RemoveConfigRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/secret/check",
+ "requestType": "CheckSecretRequest",
+ "responseType": "CheckSecretResponse"
+ },
+ {
+ "method": "_goose/secret/upsert",
+ "requestType": "UpsertSecretRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/secret/remove",
+ "requestType": "RemoveSecretRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/session/export",
+ "requestType": "ExportSessionRequest",
+ "responseType": "ExportSessionResponse"
+ },
+ {
+ "method": "_goose/session/import",
+ "requestType": "ImportSessionRequest",
+ "responseType": "ImportSessionResponse"
+ },
+ {
+ "method": "_goose/session/update_project",
+ "requestType": "UpdateSessionProjectRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/session/rename",
+ "requestType": "RenameSessionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/session/archive",
+ "requestType": "ArchiveSessionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/session/unarchive",
+ "requestType": "UnarchiveSessionRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/sources/create",
+ "requestType": "CreateSourceRequest",
+ "responseType": "CreateSourceResponse"
+ },
+ {
+ "method": "_goose/sources/list",
+ "requestType": "ListSourcesRequest",
+ "responseType": "ListSourcesResponse"
+ },
+ {
+ "method": "_goose/sources/update",
+ "requestType": "UpdateSourceRequest",
+ "responseType": "UpdateSourceResponse"
+ },
+ {
+ "method": "_goose/sources/delete",
+ "requestType": "DeleteSourceRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/sources/export",
+ "requestType": "ExportSourceRequest",
+ "responseType": "ExportSourceResponse"
+ },
+ {
+ "method": "_goose/sources/import",
+ "requestType": "ImportSourcesRequest",
+ "responseType": "ImportSourcesResponse"
+ },
+ {
+ "method": "_goose/dictation/transcribe",
+ "requestType": "DictationTranscribeRequest",
+ "responseType": "DictationTranscribeResponse"
+ },
+ {
+ "method": "_goose/dictation/config",
+ "requestType": "DictationConfigRequest",
+ "responseType": "DictationConfigResponse"
+ },
+ {
+ "method": "_goose/dictation/models/list",
+ "requestType": "DictationModelsListRequest",
+ "responseType": "DictationModelsListResponse"
+ },
+ {
+ "method": "_goose/dictation/models/download",
+ "requestType": "DictationModelDownloadRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/dictation/models/download/progress",
+ "requestType": "DictationModelDownloadProgressRequest",
+ "responseType": "DictationModelDownloadProgressResponse"
+ },
+ {
+ "method": "_goose/dictation/models/cancel",
+ "requestType": "DictationModelCancelRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/dictation/models/delete",
+ "requestType": "DictationModelDeleteRequest",
+ "responseType": "EmptyResponse"
+ },
+ {
+ "method": "_goose/dictation/model/select",
+ "requestType": "DictationModelSelectRequest",
+ "responseType": "EmptyResponse"
+ }
+ ]
+}
diff --git a/crates/goose/acp-schema.json b/crates/goose/acp-schema.json
new file mode 100644
index 000000000000..1931938eabe2
--- /dev/null
+++ b/crates/goose/acp-schema.json
@@ -0,0 +1,2013 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "GooseExtensions",
+ "$defs": {
+ "AddExtensionRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ },
+ "config": {
+ "description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform).",
+ "default": null
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "description": "Add an extension to an active session.",
+ "x-side": "agent",
+ "x-method": "_goose/extensions/add"
+ },
+ "EmptyResponse": {
+ "type": "object",
+ "description": "Empty success response for operations that return no data.",
+ "x-side": "agent"
+ },
+ "RemoveExtensionRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId",
+ "name"
+ ],
+ "description": "Remove an extension from an active session.",
+ "x-side": "agent",
+ "x-method": "_goose/extensions/remove"
+ },
+ "GetToolsRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "description": "List all tools available in a session.",
+ "x-side": "agent",
+ "x-method": "_goose/tools"
+ },
+ "GetToolsResponse": {
+ "type": "object",
+ "properties": {
+ "tools": {
+ "type": "array",
+ "items": {},
+ "description": "Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`."
+ }
+ },
+ "required": [
+ "tools"
+ ],
+ "description": "Tools response.",
+ "x-side": "agent",
+ "x-method": "_goose/tools"
+ },
+ "ReadResourceRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ },
+ "uri": {
+ "type": "string"
+ },
+ "extensionName": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId",
+ "uri",
+ "extensionName"
+ ],
+ "description": "Read a resource from an extension.",
+ "x-side": "agent",
+ "x-method": "_goose/resource/read"
+ },
+ "ReadResourceResponse": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "The resource result from the extension (MCP ReadResourceResult).",
+ "default": null
+ }
+ },
+ "description": "Resource read response.",
+ "x-side": "agent",
+ "x-method": "_goose/resource/read"
+ },
+ "UpdateWorkingDirRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ },
+ "workingDir": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId",
+ "workingDir"
+ ],
+ "description": "Update the working directory for a session.",
+ "x-side": "agent",
+ "x-method": "_goose/working_dir/update"
+ },
+ "DeleteSessionRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "description": "Delete a session.",
+ "x-side": "agent",
+ "x-method": "session/delete"
+ },
+ "GetExtensionsRequest": {
+ "type": "object",
+ "description": "List configured extensions and any warnings.",
+ "x-side": "agent",
+ "x-method": "_goose/config/extensions"
+ },
+ "GetExtensionsResponse": {
+ "type": "object",
+ "properties": {
+ "extensions": {
+ "type": "array",
+ "items": {},
+ "description": "Array of ExtensionEntry objects with `enabled` flag, `configKey`, and flattened config details."
+ },
+ "warnings": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "extensions",
+ "warnings"
+ ],
+ "description": "List configured extensions and any warnings.",
+ "x-side": "agent",
+ "x-method": "_goose/config/extensions"
+ },
+ "AddConfigExtensionRequest": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "extensionConfig": {
+ "description": "Extension configuration. Must be a JSON object matching one of the\n`ExtensionConfig` variants (e.g. `stdio`, `streamable_http`, `builtin`).\n`name` and `enabled` are injected server-side.",
+ "default": null
+ },
+ "enabled": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "description": "Persist a new extension to the user's global goose config.",
+ "x-side": "agent",
+ "x-method": "_goose/config/extensions/add"
+ },
+ "RemoveConfigExtensionRequest": {
+ "type": "object",
+ "properties": {
+ "configKey": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "configKey"
+ ],
+ "description": "Remove a persisted extension from the user's global goose config.",
+ "x-side": "agent",
+ "x-method": "_goose/config/extensions/remove"
+ },
+ "ToggleConfigExtensionRequest": {
+ "type": "object",
+ "properties": {
+ "configKey": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "configKey",
+ "enabled"
+ ],
+ "description": "Toggle the `enabled` flag for a persisted extension in the user's global goose config.",
+ "x-side": "agent",
+ "x-method": "_goose/config/extensions/toggle"
+ },
+ "GetSessionExtensionsRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "x-side": "agent",
+ "x-method": "_goose/session/extensions"
+ },
+ "GetSessionExtensionsResponse": {
+ "type": "object",
+ "properties": {
+ "extensions": {
+ "type": "array",
+ "items": {}
+ }
+ },
+ "required": [
+ "extensions"
+ ],
+ "x-side": "agent",
+ "x-method": "_goose/session/extensions"
+ },
+ "ListProvidersRequest": {
+ "type": "object",
+ "properties": {
+ "providerIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Only return entries for these providers. Empty means all.",
+ "default": []
+ }
+ },
+ "description": "List providers with setup metadata and the current model inventory snapshot.",
+ "x-side": "agent",
+ "x-method": "_goose/providers/list"
+ },
+ "ListProvidersResponse": {
+ "type": "object",
+ "properties": {
+ "entries": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/ProviderInventoryEntryDto"
+ }
+ }
+ },
+ "required": [
+ "entries"
+ ],
+ "description": "Provider list response.",
+ "x-side": "agent",
+ "x-method": "_goose/providers/list"
+ },
+ "ProviderInventoryEntryDto": {
+ "type": "object",
+ "properties": {
+ "providerId": {
+ "type": "string",
+ "description": "Provider identifier."
+ },
+ "providerName": {
+ "type": "string",
+ "description": "Human-readable provider name."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of the provider's capabilities."
+ },
+ "defaultModel": {
+ "type": "string",
+ "description": "The default/recommended model for this provider."
+ },
+ "configured": {
+ "type": "boolean",
+ "description": "Whether Goose has enough configuration to use this provider."
+ },
+ "providerType": {
+ "type": "string",
+ "description": "Provider classification such as `Preferred`, `Builtin`, `Declarative`, or `Custom`."
+ },
+ "configKeys": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/ProviderConfigKey"
+ },
+ "description": "Required configuration keys and setup metadata."
+ },
+ "setupSteps": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Step-by-step setup instructions, when present."
+ },
+ "supportsRefresh": {
+ "type": "boolean",
+ "description": "Whether this provider supports background inventory refresh."
+ },
+ "refreshing": {
+ "type": "boolean",
+ "description": "Whether a refresh is currently in flight."
+ },
+ "models": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/ProviderInventoryModelDto"
+ },
+ "description": "The list of available models."
+ },
+ "lastUpdatedAt": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "When this entry was last successfully refreshed (ISO 8601)."
+ },
+ "lastRefreshAttemptAt": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "When a refresh was most recently attempted (ISO 8601)."
+ },
+ "lastRefreshError": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The last refresh failure message, if any."
+ },
+ "stale": {
+ "type": "boolean",
+ "description": "Whether we believe this data may be outdated."
+ },
+ "modelSelectionHint": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Guidance message shown when this provider manages its own model selection externally."
+ }
+ },
+ "required": [
+ "providerId",
+ "providerName",
+ "description",
+ "defaultModel",
+ "configured",
+ "providerType",
+ "configKeys",
+ "setupSteps",
+ "supportsRefresh",
+ "refreshing",
+ "models",
+ "stale"
+ ],
+ "description": "Provider inventory entry."
+ },
+ "ProviderConfigKey": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "secret": {
+ "type": "boolean"
+ },
+ "default": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "default": null
+ },
+ "oauthFlow": {
+ "type": "boolean",
+ "default": false
+ },
+ "deviceCodeFlow": {
+ "type": "boolean",
+ "default": false
+ },
+ "primary": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "required": [
+ "name",
+ "required",
+ "secret"
+ ]
+ },
+ "ProviderInventoryModelDto": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Model identifier as the provider knows it."
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable display name."
+ },
+ "family": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Model family for grouping in UI."
+ },
+ "contextLimit": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "format": "uint",
+ "minimum": 0,
+ "description": "Context window size in tokens."
+ },
+ "reasoning": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Whether the model supports reasoning/extended thinking."
+ },
+ "recommended": {
+ "type": "boolean",
+ "description": "Whether this model should appear in the compact recommended picker.",
+ "default": false
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "description": "A single model in provider inventory."
+ },
+ "RefreshProviderInventoryRequest": {
+ "type": "object",
+ "properties": {
+ "providerIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Which providers to refresh. Empty means all known providers.",
+ "default": []
+ }
+ },
+ "description": "Trigger a background refresh of provider inventories.",
+ "x-side": "agent",
+ "x-method": "_goose/providers/inventory/refresh"
+ },
+ "RefreshProviderInventoryResponse": {
+ "type": "object",
+ "properties": {
+ "started": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Which providers will be refreshed."
+ },
+ "skipped": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RefreshProviderInventorySkipDto"
+ },
+ "description": "Which providers were skipped and why.",
+ "default": []
+ }
+ },
+ "required": [
+ "started"
+ ],
+ "description": "Refresh acknowledgement.",
+ "x-side": "agent",
+ "x-method": "_goose/providers/inventory/refresh"
+ },
+ "RefreshProviderInventorySkipDto": {
+ "type": "object",
+ "properties": {
+ "providerId": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/$defs/RefreshProviderInventorySkipReasonDto"
+ }
+ },
+ "required": [
+ "providerId",
+ "reason"
+ ]
+ },
+ "RefreshProviderInventorySkipReasonDto": {
+ "type": "string",
+ "enum": [
+ "unknown_provider",
+ "not_configured",
+ "does_not_support_refresh",
+ "already_refreshing"
+ ]
+ },
+ "ReadConfigRequest": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key"
+ ],
+ "description": "Read a single non-secret config value.",
+ "x-side": "agent",
+ "x-method": "_goose/config/read"
+ },
+ "ReadConfigResponse": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "default": null
+ }
+ },
+ "description": "Config read response.",
+ "x-side": "agent",
+ "x-method": "_goose/config/read"
+ },
+ "UpsertConfigRequest": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {}
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "description": "Upsert a single non-secret config value.",
+ "x-side": "agent",
+ "x-method": "_goose/config/upsert"
+ },
+ "RemoveConfigRequest": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key"
+ ],
+ "description": "Remove a single non-secret config value.",
+ "x-side": "agent",
+ "x-method": "_goose/config/remove"
+ },
+ "CheckSecretRequest": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key"
+ ],
+ "description": "Check whether a secret exists. Never returns the actual value.",
+ "x-side": "agent",
+ "x-method": "_goose/secret/check"
+ },
+ "CheckSecretResponse": {
+ "type": "object",
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "exists"
+ ],
+ "description": "Secret check response.",
+ "x-side": "agent",
+ "x-method": "_goose/secret/check"
+ },
+ "UpsertSecretRequest": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {}
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "description": "Set a secret value (write-only).",
+ "x-side": "agent",
+ "x-method": "_goose/secret/upsert"
+ },
+ "RemoveSecretRequest": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key"
+ ],
+ "description": "Remove a secret.",
+ "x-side": "agent",
+ "x-method": "_goose/secret/remove"
+ },
+ "ExportSessionRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "description": "Export a session as a JSON string.",
+ "x-side": "agent",
+ "x-method": "_goose/session/export"
+ },
+ "ExportSessionResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "data"
+ ],
+ "description": "Export session response — raw JSON of the goose session with `conversation`.",
+ "x-side": "agent",
+ "x-method": "_goose/session/export"
+ },
+ "ImportSessionRequest": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "data"
+ ],
+ "description": "Import a session from a JSON string.",
+ "x-side": "agent",
+ "x-method": "_goose/session/import"
+ },
+ "ImportSessionResponse": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "updatedAt": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "messageCount": {
+ "type": "integer",
+ "minimum": 0
+ }
+ },
+ "required": [
+ "sessionId",
+ "messageCount"
+ ],
+ "description": "Import session response — metadata about the newly created session.",
+ "x-side": "agent",
+ "x-method": "_goose/session/import"
+ },
+ "UpdateSessionProjectRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ },
+ "projectId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "description": "Update the project association for a session.",
+ "x-side": "agent",
+ "x-method": "_goose/session/update_project"
+ },
+ "RenameSessionRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId",
+ "title"
+ ],
+ "description": "Rename a session.",
+ "x-side": "agent",
+ "x-method": "_goose/session/rename"
+ },
+ "ArchiveSessionRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "description": "Archive a session (soft delete).",
+ "x-side": "agent",
+ "x-method": "_goose/session/archive"
+ },
+ "UnarchiveSessionRequest": {
+ "type": "object",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "description": "Unarchive a previously archived session.",
+ "x-side": "agent",
+ "x-method": "_goose/session/unarchive"
+ },
+ "CreateSourceRequest": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/$defs/SourceType"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "content": {
+ "type": "string"
+ },
+ "global": {
+ "type": "boolean"
+ },
+ "projectDir": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Absolute path to the project root. Required when `global` is false."
+ }
+ },
+ "required": [
+ "type",
+ "name",
+ "description",
+ "content",
+ "global"
+ ],
+ "description": "Create a new source in an explicit target scope (global or project-scoped).",
+ "x-side": "agent",
+ "x-method": "_goose/sources/create"
+ },
+ "SourceType": {
+ "type": "string",
+ "enum": [
+ "skill",
+ "builtinSkill",
+ "recipe",
+ "subrecipe",
+ "agent"
+ ],
+ "description": "The type of source entity."
+ },
+ "CreateSourceResponse": {
+ "type": "object",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/SourceEntry"
+ }
+ },
+ "required": [
+ "source"
+ ],
+ "x-side": "agent",
+ "x-method": "_goose/sources/create"
+ },
+ "SourceEntry": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/$defs/SourceType"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "content": {
+ "type": "string"
+ },
+ "directory": {
+ "type": "string",
+ "description": "Absolute path to the source on disk. A directory for skills, a file for\nrecipes and agents."
+ },
+ "global": {
+ "type": "boolean",
+ "description": "True when the source lives in the user's global sources directory; false\nwhen it lives inside a specific project."
+ },
+ "supportingFiles": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Paths (absolute) of additional files that live alongside the source.\nOnly skills currently populate this; empty for other source types."
+ }
+ },
+ "required": [
+ "type",
+ "name",
+ "description",
+ "content",
+ "directory",
+ "global"
+ ],
+ "description": "A source discovered by Goose and backed by an on-disk path. Sources may be\neither `global` (shared across all projects) or project-specific."
+ },
+ "ListSourcesRequest": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/SourceType"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "projectDir": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "description": "List discovered sources.\n\nToday this endpoint only returns skills. If `type` is omitted, it defaults\nto listing skill sources. Both global and project-scoped skills are included\nwhen `project_dir` is set.",
+ "x-side": "agent",
+ "x-method": "_goose/sources/list"
+ },
+ "ListSourcesResponse": {
+ "type": "object",
+ "properties": {
+ "sources": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/SourceEntry"
+ }
+ }
+ },
+ "required": [
+ "sources"
+ ],
+ "x-side": "agent",
+ "x-method": "_goose/sources/list"
+ },
+ "UpdateSourceRequest": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/$defs/SourceType"
+ },
+ "path": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "content": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "path",
+ "name",
+ "description",
+ "content"
+ ],
+ "description": "Update an existing source's name, description, and content by absolute path.",
+ "x-side": "agent",
+ "x-method": "_goose/sources/update"
+ },
+ "UpdateSourceResponse": {
+ "type": "object",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/SourceEntry"
+ }
+ },
+ "required": [
+ "source"
+ ],
+ "x-side": "agent",
+ "x-method": "_goose/sources/update"
+ },
+ "DeleteSourceRequest": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/$defs/SourceType"
+ },
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "path"
+ ],
+ "description": "Delete a source and its on-disk directory by absolute path.",
+ "x-side": "agent",
+ "x-method": "_goose/sources/delete"
+ },
+ "ExportSourceRequest": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/$defs/SourceType"
+ },
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "path"
+ ],
+ "description": "Export a source at an absolute path as a portable JSON payload.",
+ "x-side": "agent",
+ "x-method": "_goose/sources/export"
+ },
+ "ExportSourceResponse": {
+ "type": "object",
+ "properties": {
+ "json": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "json",
+ "filename"
+ ],
+ "x-side": "agent",
+ "x-method": "_goose/sources/export"
+ },
+ "ImportSourcesRequest": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "string"
+ },
+ "global": {
+ "type": "boolean"
+ },
+ "projectDir": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "data",
+ "global"
+ ],
+ "description": "Import a source from a JSON export payload produced by `_goose/sources/export`.\nThe imported source is written into the explicit target scope; on name\ncollisions a `-imported` suffix is appended.",
+ "x-side": "agent",
+ "x-method": "_goose/sources/import"
+ },
+ "ImportSourcesResponse": {
+ "type": "object",
+ "properties": {
+ "sources": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/SourceEntry"
+ }
+ }
+ },
+ "required": [
+ "sources"
+ ],
+ "x-side": "agent",
+ "x-method": "_goose/sources/import"
+ },
+ "DictationTranscribeRequest": {
+ "type": "object",
+ "properties": {
+ "audio": {
+ "type": "string",
+ "description": "Base64-encoded audio data"
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIME type (e.g. \"audio/wav\", \"audio/webm\")"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Provider to use: \"openai\", \"groq\", \"elevenlabs\", or \"local\""
+ }
+ },
+ "required": [
+ "audio",
+ "mimeType",
+ "provider"
+ ],
+ "description": "Transcribe audio via a dictation provider.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/transcribe"
+ },
+ "DictationTranscribeResponse": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "text"
+ ],
+ "description": "Transcription result.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/transcribe"
+ },
+ "DictationConfigRequest": {
+ "type": "object",
+ "description": "Get the configuration status of all dictation providers.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/config"
+ },
+ "DictationConfigResponse": {
+ "type": "object",
+ "properties": {
+ "providers": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/DictationProviderStatusEntry"
+ }
+ }
+ },
+ "required": [
+ "providers"
+ ],
+ "description": "Dictation config response — map of provider name to status.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/config"
+ },
+ "DictationProviderStatusEntry": {
+ "type": "object",
+ "properties": {
+ "configured": {
+ "type": "boolean"
+ },
+ "host": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": "string"
+ },
+ "usesProviderConfig": {
+ "type": "boolean"
+ },
+ "settingsPath": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "configKey": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "modelConfigKey": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "defaultModel": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "selectedModel": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "availableModels": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/DictationModelOption"
+ },
+ "default": []
+ }
+ },
+ "required": [
+ "configured",
+ "description",
+ "usesProviderConfig"
+ ],
+ "description": "Per-provider configuration status."
+ },
+ "DictationModelOption": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "label",
+ "description"
+ ]
+ },
+ "DictationModelsListRequest": {
+ "type": "object",
+ "description": "List available local Whisper models with their download status.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/models/list"
+ },
+ "DictationModelsListResponse": {
+ "type": "object",
+ "properties": {
+ "models": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/DictationLocalModelStatus"
+ }
+ }
+ },
+ "required": [
+ "models"
+ ],
+ "x-side": "agent",
+ "x-method": "_goose/dictation/models/list"
+ },
+ "DictationLocalModelStatus": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "sizeMb": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "downloaded": {
+ "type": "boolean"
+ },
+ "downloadInProgress": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "label",
+ "description",
+ "sizeMb",
+ "downloaded",
+ "downloadInProgress"
+ ]
+ },
+ "DictationModelDownloadRequest": {
+ "type": "object",
+ "properties": {
+ "modelId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "modelId"
+ ],
+ "description": "Kick off a background download of a local Whisper model.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/models/download"
+ },
+ "DictationModelDownloadProgressRequest": {
+ "type": "object",
+ "properties": {
+ "modelId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "modelId"
+ ],
+ "description": "Poll the progress of an in-flight download.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/models/download/progress"
+ },
+ "DictationModelDownloadProgressResponse": {
+ "type": "object",
+ "properties": {
+ "progress": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/DictationDownloadProgress"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "None when no download is active for this model id."
+ }
+ },
+ "x-side": "agent",
+ "x-method": "_goose/dictation/models/download/progress"
+ },
+ "DictationDownloadProgress": {
+ "type": "object",
+ "properties": {
+ "bytesDownloaded": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "totalBytes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "progressPercent": {
+ "type": "number",
+ "format": "float"
+ },
+ "status": {
+ "type": "string",
+ "description": "serde lowercase of DownloadStatus: \"downloading\" | \"completed\" | \"failed\" | \"cancelled\""
+ },
+ "error": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "bytesDownloaded",
+ "totalBytes",
+ "progressPercent",
+ "status"
+ ]
+ },
+ "DictationModelCancelRequest": {
+ "type": "object",
+ "properties": {
+ "modelId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "modelId"
+ ],
+ "description": "Cancel an in-flight download.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/models/cancel"
+ },
+ "DictationModelDeleteRequest": {
+ "type": "object",
+ "properties": {
+ "modelId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "modelId"
+ ],
+ "description": "Delete a downloaded local Whisper model from disk.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/models/delete"
+ },
+ "DictationModelSelectRequest": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string"
+ },
+ "modelId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "provider",
+ "modelId"
+ ],
+ "description": "Persist the user's model selection for a given provider.",
+ "x-side": "agent",
+ "x-method": "_goose/dictation/model/select"
+ },
+ "ExtRequest": {
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "method": {
+ "type": "string"
+ },
+ "params": {
+ "anyOf": [
+ {
+ "anyOf": [
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/AddExtensionRequest"
+ }
+ ],
+ "description": "Params for _goose/extensions/add",
+ "title": "AddExtensionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/RemoveExtensionRequest"
+ }
+ ],
+ "description": "Params for _goose/extensions/remove",
+ "title": "RemoveExtensionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/GetToolsRequest"
+ }
+ ],
+ "description": "Params for _goose/tools",
+ "title": "GetToolsRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ReadResourceRequest"
+ }
+ ],
+ "description": "Params for _goose/resource/read",
+ "title": "ReadResourceRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/UpdateWorkingDirRequest"
+ }
+ ],
+ "description": "Params for _goose/working_dir/update",
+ "title": "UpdateWorkingDirRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DeleteSessionRequest"
+ }
+ ],
+ "description": "Params for session/delete",
+ "title": "DeleteSessionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/GetExtensionsRequest"
+ }
+ ],
+ "description": "Params for _goose/config/extensions",
+ "title": "GetExtensionsRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/AddConfigExtensionRequest"
+ }
+ ],
+ "description": "Params for _goose/config/extensions/add",
+ "title": "AddConfigExtensionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/RemoveConfigExtensionRequest"
+ }
+ ],
+ "description": "Params for _goose/config/extensions/remove",
+ "title": "RemoveConfigExtensionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ToggleConfigExtensionRequest"
+ }
+ ],
+ "description": "Params for _goose/config/extensions/toggle",
+ "title": "ToggleConfigExtensionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/GetSessionExtensionsRequest"
+ }
+ ],
+ "description": "Params for _goose/session/extensions",
+ "title": "GetSessionExtensionsRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ListProvidersRequest"
+ }
+ ],
+ "description": "Params for _goose/providers/list",
+ "title": "ListProvidersRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/RefreshProviderInventoryRequest"
+ }
+ ],
+ "description": "Params for _goose/providers/inventory/refresh",
+ "title": "RefreshProviderInventoryRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ReadConfigRequest"
+ }
+ ],
+ "description": "Params for _goose/config/read",
+ "title": "ReadConfigRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/UpsertConfigRequest"
+ }
+ ],
+ "description": "Params for _goose/config/upsert",
+ "title": "UpsertConfigRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/RemoveConfigRequest"
+ }
+ ],
+ "description": "Params for _goose/config/remove",
+ "title": "RemoveConfigRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/CheckSecretRequest"
+ }
+ ],
+ "description": "Params for _goose/secret/check",
+ "title": "CheckSecretRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/UpsertSecretRequest"
+ }
+ ],
+ "description": "Params for _goose/secret/upsert",
+ "title": "UpsertSecretRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/RemoveSecretRequest"
+ }
+ ],
+ "description": "Params for _goose/secret/remove",
+ "title": "RemoveSecretRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ExportSessionRequest"
+ }
+ ],
+ "description": "Params for _goose/session/export",
+ "title": "ExportSessionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ImportSessionRequest"
+ }
+ ],
+ "description": "Params for _goose/session/import",
+ "title": "ImportSessionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/UpdateSessionProjectRequest"
+ }
+ ],
+ "description": "Params for _goose/session/update_project",
+ "title": "UpdateSessionProjectRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/RenameSessionRequest"
+ }
+ ],
+ "description": "Params for _goose/session/rename",
+ "title": "RenameSessionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ArchiveSessionRequest"
+ }
+ ],
+ "description": "Params for _goose/session/archive",
+ "title": "ArchiveSessionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/UnarchiveSessionRequest"
+ }
+ ],
+ "description": "Params for _goose/session/unarchive",
+ "title": "UnarchiveSessionRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/CreateSourceRequest"
+ }
+ ],
+ "description": "Params for _goose/sources/create",
+ "title": "CreateSourceRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ListSourcesRequest"
+ }
+ ],
+ "description": "Params for _goose/sources/list",
+ "title": "ListSourcesRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/UpdateSourceRequest"
+ }
+ ],
+ "description": "Params for _goose/sources/update",
+ "title": "UpdateSourceRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DeleteSourceRequest"
+ }
+ ],
+ "description": "Params for _goose/sources/delete",
+ "title": "DeleteSourceRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ExportSourceRequest"
+ }
+ ],
+ "description": "Params for _goose/sources/export",
+ "title": "ExportSourceRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ImportSourcesRequest"
+ }
+ ],
+ "description": "Params for _goose/sources/import",
+ "title": "ImportSourcesRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationTranscribeRequest"
+ }
+ ],
+ "description": "Params for _goose/dictation/transcribe",
+ "title": "DictationTranscribeRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationConfigRequest"
+ }
+ ],
+ "description": "Params for _goose/dictation/config",
+ "title": "DictationConfigRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationModelsListRequest"
+ }
+ ],
+ "description": "Params for _goose/dictation/models/list",
+ "title": "DictationModelsListRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationModelDownloadRequest"
+ }
+ ],
+ "description": "Params for _goose/dictation/models/download",
+ "title": "DictationModelDownloadRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationModelDownloadProgressRequest"
+ }
+ ],
+ "description": "Params for _goose/dictation/models/download/progress",
+ "title": "DictationModelDownloadProgressRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationModelCancelRequest"
+ }
+ ],
+ "description": "Params for _goose/dictation/models/cancel",
+ "title": "DictationModelCancelRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationModelDeleteRequest"
+ }
+ ],
+ "description": "Params for _goose/dictation/models/delete",
+ "title": "DictationModelDeleteRequest"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationModelSelectRequest"
+ }
+ ],
+ "description": "Params for _goose/dictation/model/select",
+ "title": "DictationModelSelectRequest"
+ }
+ ]
+ },
+ {
+ "description": "Untyped params",
+ "type": [
+ "object",
+ "null"
+ ]
+ }
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "method"
+ ],
+ "type": "object",
+ "x-docs-ignore": true
+ },
+ "ExtResponse": {
+ "anyOf": [
+ {
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "result": {
+ "anyOf": [
+ {
+ "anyOf": [
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/EmptyResponse"
+ }
+ ],
+ "title": "EmptyResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/GetToolsResponse"
+ }
+ ],
+ "title": "GetToolsResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ReadResourceResponse"
+ }
+ ],
+ "title": "ReadResourceResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/GetExtensionsResponse"
+ }
+ ],
+ "title": "GetExtensionsResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/GetSessionExtensionsResponse"
+ }
+ ],
+ "title": "GetSessionExtensionsResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ListProvidersResponse"
+ }
+ ],
+ "title": "ListProvidersResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/RefreshProviderInventoryResponse"
+ }
+ ],
+ "title": "RefreshProviderInventoryResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ReadConfigResponse"
+ }
+ ],
+ "title": "ReadConfigResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/CheckSecretResponse"
+ }
+ ],
+ "title": "CheckSecretResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ExportSessionResponse"
+ }
+ ],
+ "title": "ExportSessionResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ImportSessionResponse"
+ }
+ ],
+ "title": "ImportSessionResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/CreateSourceResponse"
+ }
+ ],
+ "title": "CreateSourceResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ListSourcesResponse"
+ }
+ ],
+ "title": "ListSourcesResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/UpdateSourceResponse"
+ }
+ ],
+ "title": "UpdateSourceResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ExportSourceResponse"
+ }
+ ],
+ "title": "ExportSourceResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ImportSourcesResponse"
+ }
+ ],
+ "title": "ImportSourcesResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationTranscribeResponse"
+ }
+ ],
+ "title": "DictationTranscribeResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationConfigResponse"
+ }
+ ],
+ "title": "DictationConfigResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationModelsListResponse"
+ }
+ ],
+ "title": "DictationModelsListResponse"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/DictationModelDownloadProgressResponse"
+ }
+ ],
+ "title": "DictationModelDownloadProgressResponse"
+ }
+ ]
+ },
+ {
+ "description": "Untyped result"
+ }
+ ]
+ }
+ },
+ "required": [
+ "id"
+ ],
+ "title": "Success",
+ "type": "object"
+ },
+ {
+ "properties": {
+ "error": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "data": {}
+ },
+ "required": [
+ "code",
+ "message"
+ ]
+ },
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "error"
+ ],
+ "title": "Error",
+ "type": "object"
+ }
+ ],
+ "x-docs-ignore": true
+ }
+ },
+ "anyOf": [
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ExtRequest"
+ }
+ ],
+ "description": "Extension request (client → agent)",
+ "title": "Request"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/ExtResponse"
+ }
+ ],
+ "description": "Extension response (agent → client)",
+ "title": "Response"
+ }
+ ]
+}
diff --git a/crates/goose-acp/src/adapters.rs b/crates/goose/src/acp/adapters.rs
similarity index 100%
rename from crates/goose-acp/src/adapters.rs
rename to crates/goose/src/acp/adapters.rs
diff --git a/crates/goose-acp/src/fs.rs b/crates/goose/src/acp/fs.rs
similarity index 96%
rename from crates/goose-acp/src/fs.rs
rename to crates/goose/src/acp/fs.rs
index 7677deae3f4e..89381a252f0c 100644
--- a/crates/goose-acp/src/fs.rs
+++ b/crates/goose/src/acp/fs.rs
@@ -1,13 +1,13 @@
-use crate::tools::AcpAwareToolMeta;
+use crate::acp::tools::AcpAwareToolMeta;
+use crate::agents::mcp_client::{Error as McpError, McpClientTrait};
+use crate::agents::platform_extensions::developer::edit::{
+ resolve_path, string_replace, FileEditParams, FileReadParams, FileWriteParams,
+};
+use crate::agents::platform_extensions::developer::shell::{ShellParams, OUTPUT_LIMIT_BYTES};
+use crate::agents::platform_extensions::developer::DeveloperClient;
use agent_client_protocol_schema::TerminalId;
use async_trait::async_trait;
use fs_err as fs;
-use goose::agents::mcp_client::{Error as McpError, McpClientTrait};
-use goose::agents::platform_extensions::developer::edit::{
- resolve_path, string_replace, FileEditParams, FileReadParams, FileWriteParams,
-};
-use goose::agents::platform_extensions::developer::shell::{ShellParams, OUTPUT_LIMIT_BYTES};
-use goose::agents::platform_extensions::developer::DeveloperClient;
use rmcp::model::{CallToolResult, Content as RmcpContent, Tool, ToolAnnotations};
use sacp::schema::{
CreateTerminalRequest, Diff, KillTerminalRequest, ReadTextFileRequest, ReleaseTerminalRequest,
@@ -93,7 +93,7 @@ fn read_tool() -> Tool {
}
impl AcpTools {
- fn update_tool_call(&self, ctx: &goose::agents::ToolCallContext, fields: ToolCallUpdateFields) {
+ fn update_tool_call(&self, ctx: &crate::agents::ToolCallContext, fields: ToolCallUpdateFields) {
if let Some(ref req_id) = ctx.tool_call_request_id {
let _ = self
.cx
@@ -125,7 +125,7 @@ impl AcpTools {
async fn acp_read(
&self,
arguments: Option,
- ctx: &goose::agents::ToolCallContext,
+ ctx: &crate::agents::ToolCallContext,
) -> Result {
let params: FileReadParams = match Self::parse_args(arguments) {
Ok(p) => p,
@@ -150,7 +150,7 @@ impl AcpTools {
async fn acp_write(
&self,
arguments: Option,
- ctx: &goose::agents::ToolCallContext,
+ ctx: &crate::agents::ToolCallContext,
) -> Result {
let params: FileWriteParams = match Self::parse_args(arguments) {
Ok(p) => p,
@@ -187,7 +187,7 @@ impl AcpTools {
async fn acp_edit(
&self,
arguments: Option,
- ctx: &goose::agents::ToolCallContext,
+ ctx: &crate::agents::ToolCallContext,
) -> Result {
let params: FileEditParams = match Self::parse_args(arguments) {
Ok(p) => p,
@@ -240,7 +240,7 @@ impl AcpTools {
async fn acp_shell(
&self,
arguments: Option,
- ctx: &goose::agents::ToolCallContext,
+ ctx: &crate::agents::ToolCallContext,
) -> Result {
let params: ShellParams = match Self::parse_args(arguments) {
Ok(p) => p,
@@ -404,7 +404,7 @@ impl McpClientTrait for AcpTools {
async fn call_tool(
&self,
- ctx: &goose::agents::ToolCallContext,
+ ctx: &crate::agents::ToolCallContext,
name: &str,
arguments: Option,
cancellation_token: CancellationToken,
diff --git a/crates/goose/src/acp/mod.rs b/crates/goose/src/acp/mod.rs
index 6e995948f16b..3887b45b2887 100644
--- a/crates/goose/src/acp/mod.rs
+++ b/crates/goose/src/acp/mod.rs
@@ -1,7 +1,14 @@
+mod adapters;
mod common;
+pub(crate) mod fs;
mod provider;
+pub mod server;
+pub mod server_factory;
+pub(crate) mod tools;
+pub mod transport;
pub use common::{map_permission_response, PermissionDecision};
+pub use goose_sdk::custom_requests;
pub use provider::{
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
};
diff --git a/crates/goose-acp/src/server.rs b/crates/goose/src/acp/server.rs
similarity index 61%
rename from crates/goose-acp/src/server.rs
rename to crates/goose/src/acp/server.rs
index eeeae74df0de..f105412f9757 100644
--- a/crates/goose-acp/src/server.rs
+++ b/crates/goose/src/acp/server.rs
@@ -1,46 +1,58 @@
-use crate::custom_requests::*;
-use crate::fs::AcpTools;
-use crate::tools::AcpAwareToolMeta;
+use crate::acp::custom_requests::*;
+use crate::acp::fs::AcpTools;
+use crate::acp::tools::AcpAwareToolMeta;
+use crate::acp::{PermissionDecision, ACP_CURRENT_MODEL};
+use crate::agents::extension::{Envs, PLATFORM_EXTENSIONS};
+use crate::agents::mcp_client::McpClientTrait;
+use crate::agents::platform_extensions::developer::DeveloperClient;
+use crate::agents::{Agent, AgentConfig, ExtensionConfig, GoosePlatform, SessionConfig};
+use crate::config::base::CONFIG_YAML_NAME;
+use crate::config::extensions::get_enabled_extensions_with_config;
+use crate::config::paths::Paths;
+use crate::config::permission::PermissionManager;
+use crate::config::{Config, GooseMode};
+use crate::conversation::message::{ActionRequiredData, Message, MessageContent};
+#[cfg(feature = "local-inference")]
+use crate::dictation::providers::transcribe_local;
+use crate::dictation::providers::{
+ all_providers, is_configured, transcribe_with_provider, DictationProvider,
+};
+#[cfg(feature = "local-inference")]
+use crate::dictation::whisper;
+use crate::mcp_utils::ToolResult;
+use crate::permission::permission_confirmation::PrincipalType;
+use crate::permission::{Permission, PermissionConfirmation};
+use crate::providers::base::Provider;
+use crate::providers::inventory::{
+ ProviderInventoryEntry, ProviderInventoryService, RefreshSkipReason,
+};
+use crate::session::session_manager::SessionType;
+use crate::session::{EnabledExtensionsState, Session, SessionManager};
+use crate::utils::sanitize_unicode_tags;
use anyhow::Result;
use fs_err as fs;
-use futures::future::BoxFuture;
-use goose::acp::{PermissionDecision, ACP_CURRENT_MODEL};
-use goose::agents::extension::{Envs, PLATFORM_EXTENSIONS};
-use goose::agents::mcp_client::McpClientTrait;
-use goose::agents::platform_extensions::developer::DeveloperClient;
-use goose::agents::{Agent, AgentConfig, ExtensionConfig, GoosePlatform, SessionConfig};
-use goose::builtin_extension::register_builtin_extensions;
-use goose::config::base::CONFIG_YAML_NAME;
-use goose::config::extensions::get_enabled_extensions_with_config;
-use goose::config::paths::Paths;
-use goose::config::permission::PermissionManager;
-use goose::config::{Config, GooseMode};
-use goose::conversation::message::{ActionRequiredData, Message, MessageContent};
-use goose::mcp_utils::ToolResult;
-use goose::permission::permission_confirmation::PrincipalType;
-use goose::permission::{Permission, PermissionConfirmation};
-use goose::providers::base::Provider;
-use goose::session::session_manager::SessionType;
-use goose::session::{EnabledExtensionsState, Session, SessionManager};
+use futures::future::{BoxFuture, Either};
use goose_acp_macros::custom_methods;
-use rmcp::model::{CallToolResult, RawContent, ResourceContents, Role};
+use rmcp::model::{
+ AnnotateAble, CallToolResult, RawContent, RawTextContent, ResourceContents, Role,
+};
use sacp::schema::{
- AgentCapabilities, AuthMethod, AuthMethodAgent, AuthenticateRequest, AuthenticateResponse,
- BlobResourceContents, CancelNotification, CloseSessionRequest, CloseSessionResponse,
- ConfigOptionUpdate, Content, ContentBlock, ContentChunk, CurrentModeUpdate, EmbeddedResource,
- EmbeddedResourceResource, FileSystemCapabilities, ForkSessionRequest, ForkSessionResponse,
- ImageContent, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse,
- LoadSessionRequest, LoadSessionResponse, McpCapabilities, McpServer, Meta, ModelId, ModelInfo,
- NewSessionRequest, NewSessionResponse, PermissionOption, PermissionOptionKind,
- PromptCapabilities, PromptRequest, PromptResponse, RequestPermissionOutcome,
- RequestPermissionRequest, ResourceLink, SessionCapabilities, SessionCloseCapabilities,
- SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption, SessionId,
- SessionInfo, SessionListCapabilities, SessionMode, SessionModeId, SessionModeState,
- SessionModelState, SessionNotification, SessionUpdate, SetSessionConfigOptionRequest,
- SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse,
- SetSessionModelRequest, SetSessionModelResponse, StopReason, TextContent, TextResourceContents,
- ToolCall, ToolCallContent, ToolCallId, ToolCallLocation, ToolCallStatus, ToolCallUpdate,
- ToolCallUpdateFields, ToolKind,
+ AgentCapabilities, Annotations, AuthMethod, AuthMethodAgent, AuthenticateRequest,
+ AuthenticateResponse, BlobResourceContents, CancelNotification, CloseSessionRequest,
+ CloseSessionResponse, ConfigOptionUpdate, Content, ContentBlock, ContentChunk,
+ CurrentModeUpdate, EmbeddedResource, EmbeddedResourceResource, FileSystemCapabilities,
+ ForkSessionRequest, ForkSessionResponse, ImageContent, InitializeRequest, InitializeResponse,
+ ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse,
+ McpCapabilities, McpServer, Meta, ModelId, ModelInfo, NewSessionRequest, NewSessionResponse,
+ PermissionOption, PermissionOptionKind, PromptCapabilities, PromptRequest, PromptResponse,
+ RequestPermissionOutcome, RequestPermissionRequest, ResourceLink, SessionCapabilities,
+ SessionCloseCapabilities, SessionConfigOption, SessionConfigOptionCategory,
+ SessionConfigSelectOption, SessionId, SessionInfo, SessionListCapabilities, SessionMode,
+ SessionModeId, SessionModeState, SessionModelState, SessionNotification, SessionUpdate,
+ SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest,
+ SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse, StopReason,
+ TextContent, TextResourceContents, ToolCall, ToolCallContent, ToolCallId, ToolCallLocation,
+ ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, Usage, UsageUpdate,
};
use sacp::util::MatchDispatchFrom;
use sacp::{
@@ -59,15 +71,49 @@ use url::Url;
pub type AcpProviderFactory = Arc<
dyn Fn(
String,
- goose::model::ModelConfig,
+ crate::model::ModelConfig,
Vec,
) -> BoxFuture<'static, Result>>
+ Send
+ Sync,
>;
+/// Convenience conversions from any `Display` error into an `sacp::Error`.
+///
+/// Replaces the repetitive `.internal_err()`
+/// pattern. Use `.internal_err()?` for server-side failures and `.invalid_params_err()?`
+/// for bad client input. For custom messages use `.internal_err_ctx("context")?`.
+#[allow(dead_code)]
+trait ResultExt {
+ fn internal_err(self) -> Result;
+ fn invalid_params_err(self) -> Result;
+ fn internal_err_ctx(self, context: &str) -> Result;
+ fn invalid_params_err_ctx(self, context: &str) -> Result;
+}
+
+impl ResultExt for Result {
+ fn internal_err(self) -> Result {
+ self.map_err(|e| sacp::Error::internal_error().data(e.to_string()))
+ }
+ fn invalid_params_err(self) -> Result {
+ self.map_err(|e| sacp::Error::invalid_params().data(e.to_string()))
+ }
+ fn internal_err_ctx(self, context: &str) -> Result {
+ self.map_err(|e| sacp::Error::internal_error().data(format!("{context}: {e}")))
+ }
+ fn invalid_params_err_ctx(self, context: &str) -> Result {
+ self.map_err(|e| sacp::Error::invalid_params().data(format!("{context}: {e}")))
+ }
+}
+
const DEFAULT_PROVIDER_ID: &str = "goose";
const DEFAULT_PROVIDER_LABEL: &str = "Goose (Default)";
+const OPENAI_TRANSCRIPTION_MODEL_CONFIG_KEY: &str = "OPENAI_TRANSCRIPTION_MODEL";
+const GROQ_TRANSCRIPTION_MODEL_CONFIG_KEY: &str = "GROQ_TRANSCRIPTION_MODEL";
+const ELEVENLABS_TRANSCRIPTION_MODEL_CONFIG_KEY: &str = "ELEVENLABS_TRANSCRIPTION_MODEL";
+const OPENAI_TRANSCRIPTION_MODEL: &str = "whisper-1";
+const GROQ_TRANSCRIPTION_MODEL: &str = "whisper-large-v3-turbo";
+const ELEVENLABS_TRANSCRIPTION_MODEL: &str = "scribe_v1";
/// In-memory state for an active ACP session.
///
@@ -90,19 +136,33 @@ const DEFAULT_PROVIDER_LABEL: &str = "Goose (Default)";
struct GooseAcpSession {
agent: AgentHandle,
internal_session_id: String,
- tool_requests: HashMap,
+ tool_requests: HashMap,
cancel_token: Option,
/// Working directory set while the agent was still loading.
/// Applied once the agent becomes ready.
pending_working_dir: Option,
}
+/// Progress stages signalled by the background agent setup task via the watch
+/// channel. `ProviderReady` fires as soon as the provider (and goose-mode)
+/// are initialized — before extensions finish loading. `FullyReady` fires
+/// once every extension has been loaded (or failed).
+#[derive(Clone)]
+enum AgentSetupProgress {
+ /// Provider is initialized; extensions are still loading in the background.
+ ProviderReady(Arc),
+ /// Provider *and* all extensions are initialized.
+ FullyReady(Arc),
+}
+
+type AgentSetupSignal = Option>;
+
/// The agent may still be initializing in the background (extension loading,
/// provider setup). Callers that need the live agent (e.g. `on_prompt`) await
/// the handle; callers that only need the session metadata can proceed without it.
enum AgentHandle {
Ready(Arc),
- Loading(tokio::sync::watch::Receiver