Skip to content
Closed
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export ANTHROPIC_API_KEY=your_api_key_here
export GOOGLE_API_KEY=your_api_key_here
```

### Docker Desktop Integration

`cagent` includes optional Docker Desktop integration for enhanced AI Gateway features. For minimal deployments or environments without Docker Desktop, you can build without this dependency by using "no_docker_desktop" Go build tag.

### Run Agents!

```bash
Expand Down
32 changes: 32 additions & 0 deletions pkg/desktop/client_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//go:build no_docker_desktop

package desktop

import (
"context"
"errors"
"net"
)

// Stub client when Docker Desktop is not available
var ClientBackend = newStubClient()

type RawClient struct {
// Empty implementation
}

func newStubClient() *RawClient {
return &RawClient{}
}

func (c *RawClient) Get(ctx context.Context, endpoint string, v any) error {
return errors.New("Docker Desktop is not available (built with no_docker_desktop tag)")
}

func dialBackend(ctx context.Context) (net.Conn, error) {
return nil, errors.New("Docker Desktop is not available (built with no_docker_desktop tag)")
}

func dial(ctx context.Context, path string) (net.Conn, error) {
return nil, errors.New("Docker Desktop is not available (built with no_docker_desktop tag)")
}
4 changes: 2 additions & 2 deletions pkg/desktop/connection_other.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//go:build !windows
// +build !windows
//go:build !no_docker_desktop && !windows
// +build !no_docker_desktop,!windows

package desktop

Expand Down
3 changes: 3 additions & 0 deletions pkg/desktop/connection_windows.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build !no_docker_desktop && windows
// +build !no_docker_desktop,windows

package desktop

import (
Expand Down
2 changes: 2 additions & 0 deletions pkg/desktop/login.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !no_docker_desktop

package desktop

import (
Expand Down
27 changes: 27 additions & 0 deletions pkg/desktop/login_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build no_docker_desktop

package desktop

import (
"context"
"os"
)

type DockerHubInfo struct {
Email string `json:"email,omitempty"`
Organizations []string `json:"organizations,omitempty"`
PlanName string `json:"planName"`
}

// GetToken returns empty string when Docker Desktop is not available
func GetToken(ctx context.Context) string {
// Allow the user to override the token via an environment variable.
// This is e.g. useful when talking to a gateway on staging.
manualToken := os.Getenv("DOCKER_TOKEN")
if manualToken != "" {
return manualToken
}

// Return empty string when Docker Desktop is not available
return ""
}
2 changes: 2 additions & 0 deletions pkg/desktop/paths.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !no_docker_desktop

package desktop

import "sync"
Expand Down
23 changes: 23 additions & 0 deletions pkg/desktop/paths_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build no_docker_desktop

package desktop

import (
"errors"
"sync"
)

type DockerDesktopPaths struct {
BackendSocket string
}

var Paths = sync.OnceValue(func() DockerDesktopPaths {
// Return empty paths when Docker Desktop is not available
return DockerDesktopPaths{
BackendSocket: "",
}
})

func getDockerDesktopPaths() (DockerDesktopPaths, error) {
return DockerDesktopPaths{}, errors.New("Docker Desktop is not available (built with no_docker_desktop tag)")
}
2 changes: 2 additions & 0 deletions pkg/desktop/raw_client.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !no_docker_desktop

package desktop

import (
Expand Down
3 changes: 3 additions & 0 deletions pkg/desktop/sockets_darwin.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build !no_docker_desktop && darwin
// +build !no_docker_desktop,darwin

package desktop

import (
Expand Down
3 changes: 3 additions & 0 deletions pkg/desktop/sockets_linux.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build !no_docker_desktop && linux
// +build !no_docker_desktop,linux

package desktop

import (
Expand Down
3 changes: 3 additions & 0 deletions pkg/desktop/sockets_windows.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build !no_docker_desktop && windows
// +build !no_docker_desktop,windows

package desktop

import (
Expand Down
45 changes: 45 additions & 0 deletions pkg/desktop/stub_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//go:build no_docker_desktop

package desktop

import (
"context"
"testing"
)

func TestGetTokenWithoutDockerDesktop(t *testing.T) {
ctx := context.Background()
token := GetToken(ctx)

// When built with no_docker_desktop tag, GetToken should return empty string
// (unless DOCKER_TOKEN env var is set)
if token != "" && token != "test-token" {
t.Errorf("Expected empty token or test-token, got: %s", token)
}
}

func TestPathsWithoutDockerDesktop(t *testing.T) {
paths := Paths()

// When built with no_docker_desktop tag, BackendSocket should be empty
if paths.BackendSocket != "" {
t.Errorf("Expected empty BackendSocket, got: %s", paths.BackendSocket)
}
}

func TestClientBackendWithoutDockerDesktop(t *testing.T) {
ctx := context.Background()
var result string

err := ClientBackend.Get(ctx, "/test", &result)

// When built with no_docker_desktop tag, should return error
if err == nil {
t.Error("Expected error from ClientBackend.Get, but got nil")
}

expectedErrMsg := "Docker Desktop is not available (built with no_docker_desktop tag)"
if err.Error() != expectedErrMsg {
t.Errorf("Expected error message '%s', got: %s", expectedErrMsg, err.Error())
}
}
2 changes: 1 addition & 1 deletion pkg/model/provider/anthropic/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
authToken := desktop.GetToken(ctx)
if authToken == "" {
slog.Error("Anthropic client creation failed", "error", "failed to get Docker Desktop's authentication token")
return nil, errors.New("sorry, you first need to sign in Docker Desktop to use the Docker AI Gateway")
return nil, errors.New("Docker AI Gateway requires Docker Desktop to be running and signed in")
}

slog.Debug("Docker Desktop's authentication token found, creating client")
Expand Down
4 changes: 2 additions & 2 deletions pkg/model/provider/gemini/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
// genai client requires a non-empty API key
apiKey = desktop.GetToken(ctx)
if apiKey == "" {
return nil, errors.New("sorry, you first need to sign in Docker Desktop to use the Docker AI Gateway")
return nil, errors.New("Docker AI Gateway requires Docker Desktop to be running and signed in")
}
httpOptions.BaseURL = gateway
httpOptions.Headers = make(http.Header)
Expand Down Expand Up @@ -87,7 +87,7 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
func (c *Client) newGatewayClient(ctx context.Context) (*genai.Client, error) {
token := desktop.GetToken(ctx)
if token == "" {
return nil, errors.New("failed to get Docker Desktop token for gateway")
return nil, errors.New("Docker AI Gateway requires Docker Desktop to be running and signed in")
}
httpOptions := genai.HTTPOptions{
BaseURL: c.gatewayBaseURL,
Expand Down
2 changes: 1 addition & 1 deletion pkg/model/provider/openai/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
authToken := desktop.GetToken(ctx)
if authToken == "" {
slog.Error("OpenAI client creation failed", "error", "failed to get Docker Desktop's authentication token")
return nil, errors.New("sorry, you first need to sign in Docker Desktop to use the Docker AI Gateway")
return nil, errors.New("Docker AI Gateway requires Docker Desktop to be running and signed in")
}

openaiConfig = openai.DefaultConfig(authToken)
Expand Down