Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,147 @@ Claude Code finds this file on startup and skips onboarding.
- This approach keeps your workspace self-contained — other developers using the same project are not affected, and your local `~/.claude` directory is not exposed inside the container
- To apply changes to the settings, remove and re-register the workspace: `kortex-cli remove <workspace-id>` then `kortex-cli init` again

### Using Goose Agent with a Model from Vertex AI

This scenario demonstrates how to configure the Goose agent in a kortex-cli workspace using Vertex AI as the backend, covering credential injection, sharing your local gcloud configuration, and pre-configuring the default model.

#### Authenticating with Vertex AI

Goose can use Google Cloud Vertex AI as its backend. Authentication relies on Application Default Credentials (ADC) provided by the `gcloud` CLI. Mount your local `~/.config/gcloud` directory to make your host credentials available inside the workspace, and set the `GCP_PROJECT_ID`, `GCP_LOCATION`, and `GOOSE_PROVIDER` environment variables to tell Goose which project and region to use.

Create or edit `~/.kortex-cli/config/agents.json`:

```json
{
"goose": {
"environment": [
{
"name": "GOOSE_PROVIDER",
"value": "gcp_vertex_ai"
},
{
"name": "GCP_PROJECT_ID",
"value": "my-gcp-project"
},
{
"name": "GCP_LOCATION",
"value": "my-region"
}
],
"mounts": [
{"host": "$HOME/.config/gcloud", "target": "$HOME/.config/gcloud", "ro": true}
]
}
}
```

The `~/.config/gcloud` directory contains your Application Default Credentials and active account configuration. It is mounted read-only so that credentials are available inside the workspace while the host configuration remains unmodified.

Then register and start the workspace:

```bash
# Register a workspace with the Podman runtime and Goose agent
kortex-cli init /path/to/project --runtime podman --agent goose

# Start the workspace
kortex-cli start my-project

# Connect — Goose starts with Vertex AI configured
kortex-cli terminal my-project
```

#### Sharing Local Goose Settings

To reuse your host Goose settings (model preferences, provider configuration, etc.) inside the workspace, mount the `~/.config/goose` directory.

Edit `~/.kortex-cli/config/agents.json` to add the mount alongside the Vertex AI configuration:

```json
{
"goose": {
"environment": [
{
"name": "GOOSE_PROVIDER",
"value": "gcp_vertex_ai"
},
{
"name": "GCP_PROJECT_ID",
"value": "my-gcp-project"
},
{
"name": "GCP_LOCATION",
"value": "my-region"
}
],
"mounts": [
{"host": "$HOME/.config/gcloud", "target": "$HOME/.config/gcloud", "ro": true},
{"host": "$HOME/.config/goose", "target": "$HOME/.config/goose"}
]
}
}
```

The `~/.config/goose` directory contains your Goose configuration (settings, model preferences, etc.). It is mounted read-write so that changes made inside the workspace are persisted back to your host.

#### Using Default Settings

If you want to pre-configure Goose with default settings without exposing your local `~/.config/goose` directory inside the container, create default settings files that are baked into the container image at workspace registration time. This is an alternative to mounting your local Goose settings — use one approach or the other, not both.

**Automatic Onboarding Skip**

When you register a workspace with the Goose agent, kortex-cli automatically sets `GOOSE_TELEMETRY_ENABLED` to `false` in the Goose config file if it is not already defined, so Goose skips its telemetry prompt on first launch.

**Step 1: Create the agent settings directory**

```bash
mkdir -p ~/.kortex-cli/config/goose/.config/goose
```

**Step 2: Write the default Goose settings file**

As an example, you can configure the model and enable telemetry:

```bash
cat > ~/.kortex-cli/config/goose/.config/goose/config.yaml << 'EOF'
GOOSE_MODEL: "claude-sonnet-4-6"
GOOSE_TELEMETRY_ENABLED: true
EOF
```

**Fields:**

- `GOOSE_MODEL` - The model identifier Goose uses for its AI interactions
- `GOOSE_TELEMETRY_ENABLED` - Whether Goose sends usage telemetry; set to `true` to opt in, or omit to have kortex-cli default it to `false`

**Step 3: Register and start the workspace**

```bash
# Register a workspace — the settings file is embedded in the container image
kortex-cli init /path/to/project --runtime podman --agent goose

# Start the workspace
kortex-cli start my-project

# Connect — Goose starts with the configured provider and model
kortex-cli terminal my-project
```

When `init` runs, kortex-cli:
1. Reads all files from `~/.kortex-cli/config/goose/` (e.g., your provider and model settings)
2. Automatically sets `GOOSE_TELEMETRY_ENABLED: false` in `.config/goose/config.yaml` if the key is not already defined
3. Copies the final settings into the container image at `/home/agent/.config/goose/config.yaml`

Goose finds this file on startup and uses the pre-configured settings without prompting.

**Notes:**

- **Telemetry is disabled automatically** — even if you don't create any settings files, kortex-cli ensures Goose starts without the telemetry prompt
- If you prefer to enable telemetry, set `GOOSE_TELEMETRY_ENABLED: true` in `~/.kortex-cli/config/goose/.config/goose/config.yaml`
- The settings are baked into the container image at `init` time, not mounted at runtime — changes to the files on the host require re-registering the workspace to take effect
- Any file placed under `~/.kortex-cli/config/goose/` is copied into the container home directory, preserving the directory structure (e.g., `~/.kortex-cli/config/goose/.config/goose/config.yaml` becomes `/home/agent/.config/goose/config.yaml` inside the container)
- This approach keeps your workspace self-contained — other developers using the same project are not affected, and your local `~/.config/goose` directory is not exposed inside the container
- To apply changes to the settings, remove and re-register the workspace: `kortex-cli remove <workspace-id>` then `kortex-cli init` again

### Using Cursor CLI Agent

This scenario demonstrates how to configure the Cursor agent in a kortex-cli workspace, covering API key injection, sharing your local Cursor settings, and pre-configuring the default model.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.26.1

require (
github.com/fatih/color v1.19.0
github.com/goccy/go-yaml v1.19.2
github.com/kortex-hub/kortex-cli-api/cli/go v0.0.0-20260402113340-592f26f380bc
github.com/kortex-hub/kortex-cli-api/workspace-configuration/go v0.0.0-20260331070743-a7c5f045c21c
github.com/rodaine/table v1.3.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down
81 changes: 81 additions & 0 deletions pkg/agent/goose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**********************************************************************
* Copyright (C) 2026 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
**********************************************************************/

package agent

import (
"fmt"

"github.com/goccy/go-yaml"
)

const (
// GooseConfigPath is the relative path to the Goose configuration file.
GooseConfigPath = ".config/goose/config.yaml"

gooseTelemetryKey = "GOOSE_TELEMETRY_ENABLED"
)

// gooseAgent is the implementation of Agent for Goose.
type gooseAgent struct{}

// Compile-time check to ensure gooseAgent implements Agent interface
var _ Agent = (*gooseAgent)(nil)

// NewGoose creates a new Goose agent implementation.
func NewGoose() Agent {
return &gooseAgent{}
}

// Name returns the agent name.
func (g *gooseAgent) Name() string {
return "goose"
}

// SkipOnboarding modifies Goose settings to disable telemetry prompts.
// It sets GOOSE_TELEMETRY_ENABLED to false in the goose config file if the
// value is not already defined. If the user has already set it in their own
// config file, the existing value is preserved.
func (g *gooseAgent) SkipOnboarding(settings map[string][]byte, _ string) (map[string][]byte, error) {
if settings == nil {
settings = make(map[string][]byte)
}

var config map[string]interface{}
if content, exists := settings[GooseConfigPath]; exists {
if err := yaml.Unmarshal(content, &config); err != nil {
return nil, fmt.Errorf("failed to parse existing %s: %w", GooseConfigPath, err)
}
}
if config == nil {
config = make(map[string]interface{})
}

// Only set telemetry if not already defined by the user
if _, defined := config[gooseTelemetryKey]; !defined {
config[gooseTelemetryKey] = false
}

modifiedContent, err := yaml.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to marshal modified %s: %w", GooseConfigPath, err)
}

settings[GooseConfigPath] = modifiedContent
return settings, nil
}
Loading
Loading