Skip to main content

Go SDK

The Control Zero Go SDK provides policy enforcement for AI agents running in Go environments.

Installation

go get github.com/controlzero/controlzero-go

Requirements:

  • Go 1.23 or later

Configuration

Basic Configuration

package main

import (
"log"

controlzero "github.com/controlzero/controlzero-go"
)

func main() {
client, err := controlzero.NewClient("cz_live_your_api_key_here",
controlzero.WithProjectID("proj_abc123"),
)
if err != nil {
log.Fatalf("failed to initialize controlzero: %v", err)
}
defer client.Close()
}

Environment Variables

You can configure the SDK using environment variables:

export CONTROLZERO_API_KEY="cz_live_your_api_key_here"
export CONTROLZERO_PROJECT_ID="proj_abc123"
// Read API key from environment variable
apiKey := os.Getenv("CONTROLZERO_API_KEY")
client, err := controlzero.NewClient(apiKey,
controlzero.WithProjectID(os.Getenv("CONTROLZERO_PROJECT_ID")),
)
if err != nil {
log.Fatalf("failed to initialize controlzero: %v", err)
}
defer client.Close()

Configuration Options

Option FunctionEnvironment VariableTypeDefaultDescription
(positional arg)CONTROLZERO_API_KEYstring--Your project API key. Required. Passed as first argument to NewClient().
WithProjectIDCONTROLZERO_PROJECT_IDstring--Your project ID. Required.
WithBaseURLCONTROLZERO_BASE_URLstringhttps://api.controlzero.devThe Control Zero API base URL.
WithCacheDirCONTROLZERO_CACHE_DIRstring~/.controlzero/cacheDirectory for caching policy bundles.
WithRefreshIntervalCONTROLZERO_REFRESH_INTERVALtime.Duration60sDuration between policy bundle refresh checks.
WithLogDecisionsCONTROLZERO_LOG_DECISIONSbooltrueWhether to send decision logs to the server.
WithFailOpenCONTROLZERO_FAIL_OPENboolfalseIf true, allow actions when policy evaluation fails.
WithSecretsCONTROLZERO_SECRETS_ENABLEDboolfalseEnable encrypted secrets vault access. See Secrets Management.

Advanced Configuration

client, err := controlzero.NewClient("cz_live_your_api_key_here",
controlzero.WithProjectID("proj_abc123"),
controlzero.WithCacheDir("/var/cache/controlzero"),
controlzero.WithRefreshInterval(30 * time.Second),
controlzero.WithLogDecisions(true),
controlzero.WithFailOpen(false),
)

Basic Usage

Checking an Action

The primary method is Check(), which evaluates an action against your policies:

ctx := context.Background()

decision, err := client.Check(ctx, controlzero.CheckRequest{
Action: "mcp.tool.call",
Resource: "mcp://filesystem/read_file",
Context: map[string]string{
"agent_id": "agent-001",
"session_id": "sess_xyz",
"user_id": "user-42",
},
})
if err != nil {
log.Fatalf("policy check failed: %v", err)
}

if decision.Allowed {
fmt.Println("Action is permitted")
// Proceed with the action
} else {
fmt.Printf("Action denied: %s\n", decision.Reason)
fmt.Printf("Matched policy: %s\n", decision.PolicyID)
}

Enforcing an Action

The Enforce() method works like Check() but returns an error if the action is denied:

err := client.Enforce(ctx, controlzero.CheckRequest{
Action: "mcp.tool.call",
Resource: "mcp://filesystem/write_file",
Context: map[string]string{
"agent_id": "agent-001",
},
})
if err != nil {
var denied *controlzero.PolicyDeniedError
if errors.As(err, &denied) {
log.Printf("Blocked: %s (policy: %s)", denied.Message, denied.PolicyID)
return
}
// Some other error (network, config, etc.)
log.Fatalf("enforcement error: %v", err)
}

// Action is allowed -- proceed
writeFile("/data/output.csv", data)

Batch Checking

Check multiple actions in a single call:

decisions, err := client.CheckBatch(ctx, []controlzero.CheckRequest{
{
Action: "mcp.tool.call",
Resource: "mcp://filesystem/read_file",
Context: map[string]string{"agent_id": "agent-001"},
},
{
Action: "api.request",
Resource: "https://api.example.com/v1/data",
Context: map[string]string{"agent_id": "agent-001"},
},
})
if err != nil {
log.Fatalf("batch check failed: %v", err)
}

for _, d := range decisions {
status := "allowed"
if !d.Allowed {
status = "denied"
}
fmt.Printf("%s: %s\n", d.Resource, status)
}

Manual Policy Refresh

Force an immediate refresh of the policy bundle:

err := client.RefreshPolicies(ctx)
if err != nil {
log.Printf("policy refresh failed: %v", err)
}

API Reference

Client

The main client type.

NewClient(apiKey string, opts ...Option) (*Client, error)

Creates a new Control Zero client. The API key is a required positional argument. See Configuration Options for available option functions.

(*Client) Check(ctx context.Context, req CheckRequest) (*Decision, error)

Evaluates an action against the active policies.

(*Client) Enforce(ctx context.Context, req CheckRequest) error

Like Check(), but returns a PolicyDeniedError if the action is denied.

(*Client) CheckBatch(ctx context.Context, reqs []CheckRequest) ([]*Decision, error)

Evaluates multiple actions in a single call.

(*Client) RefreshPolicies(ctx context.Context) error

Forces an immediate download and activation of the latest policy bundle.

(*Client) Close() error

Shuts down the client and releases resources. Stops the background refresh goroutine.

CheckRequest

type CheckRequest struct {
// Action is the action being attempted (e.g., "mcp.tool.call").
Action string

// Resource is the target resource URI (e.g., "mcp://filesystem/read_file").
Resource string

// Context contains additional key-value pairs for condition evaluation.
Context map[string]string
}

Decision

type Decision struct {
// Allowed indicates whether the action is permitted.
Allowed bool

// Reason is a human-readable explanation of the decision.
Reason string

// PolicyID is the ID of the policy that matched, if any.
PolicyID string

// RuleID is the ID of the specific rule that matched, if any.
RuleID string

// Resource is the resource that was evaluated.
Resource string

// Timestamp is when the decision was made.
Timestamp time.Time
}

PolicyDeniedError

type PolicyDeniedError struct {
// Action is the action that was denied.
Action string

// Resource is the resource that was denied.
Resource string

// PolicyID is the ID of the matching policy.
PolicyID string

// Message explains why the action was denied.
Message string
}

func (e *PolicyDeniedError) Error() string

Usage with MCP

Control Zero integrates with the Model Context Protocol. Here is an example of wrapping MCP tool calls with policy enforcement:

func callMCPTool(ctx context.Context, server, tool string, args map[string]any) (map[string]any, error) {
resource := fmt.Sprintf("mcp://%s/%s", server, tool)

// Enforce the policy before calling the tool
err := czClient.Enforce(ctx, controlzero.CheckRequest{
Action: "mcp.tool.call",
Resource: resource,
Context: map[string]string{
"agent_id": currentAgentID(ctx),
},
})
if err != nil {
return nil, fmt.Errorf("policy enforcement failed: %w", err)
}

// Policy check passed -- call the tool
return mcpClient.CallTool(ctx, server, tool, args)
}

Error Handling

client, err := controlzero.NewClient(os.Getenv("CONTROLZERO_API_KEY"))
if err != nil {
// ConfigurationError: invalid API key, project ID, etc.
log.Fatal(err)
}

decision, err := client.Check(ctx, req)
if err != nil {
switch {
case errors.Is(err, controlzero.ErrConnectionFailed):
// Cannot reach Control Zero server.
// Behavior depends on WithFailOpen setting.
case errors.Is(err, controlzero.ErrPolicyTampered):
// Policy bundle failed signature verification.
// The SDK continues using the previous valid bundle.
default:
// Unexpected error
}
}

Secrets Management

Optional

Secrets management is entirely optional. By default, the SDK operates in policy-only mode -- you manage your own LLM provider keys through environment variables, a secrets manager, or any method you prefer. Enable the vault only if you want Control Zero to handle key storage and injection.

Policy-Only Mode (Default)

In the default mode, you manage your own provider keys. The SDK enforces policies without touching your keys:

// You manage your own keys -- Control Zero only enforces policies
client, err := controlzero.NewClient("cz_live_your_api_key_here",
controlzero.WithProjectID("proj_abc123"),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()

// Your key, your way
openaiKey := os.Getenv("OPENAI_API_KEY")

// Control Zero enforces the policy before the call
err = client.Enforce(ctx, controlzero.CheckRequest{
Action: "llm.generate",
Resource: "model/gpt-4",
Context: map[string]string{"agent_id": "agent-001"},
})
if err != nil {
log.Fatalf("action denied: %v", err)
}
// Proceed with your own key

Vault Mode (Optional)

Enable the vault to let the SDK retrieve your provider keys from Control Zero's encrypted storage. Keys are fetched once during Initialize() and cached in memory. Subsequent GetSecret() calls are in-memory lookups with no network overhead.

client, err := controlzero.NewClient("cz_live_your_api_key_here",
controlzero.WithProjectID("proj_abc123"),
controlzero.WithSecrets(), // opt into vault access
)
if err != nil {
log.Fatal(err)
}
defer client.Close()

if err := client.Initialize(ctx); err != nil {
log.Fatalf("initialization failed: %v", err)
}

// Retrieve the decrypted key from memory (no network call)
openaiKey, err := client.GetSecret(controlzero.ProviderOpenAI)
if err != nil {
log.Fatalf("secret retrieval failed: %v", err)
}

// Policy enforcement works the same way in both modes
err = client.Enforce(ctx, controlzero.CheckRequest{
Action: "llm.generate",
Resource: "model/gpt-4",
Context: map[string]string{"agent_id": "agent-001"},
})

Secrets API Reference

(*Client) GetSecret(provider ProviderType) (string, error)

Returns the decrypted plaintext value for an LLM provider. This is an in-memory lookup -- no network call is made.

Parameters:

NameTypeDescription
providerProviderTypeThe provider constant (e.g., controlzero.ProviderOpenAI).

Returns: The decrypted plaintext API key, or an error.

Errors: Returns ErrSecretNotFound if secrets are not enabled or no matching secret exists.

(*Client) GetSecretByName(name string) (string, error)

Returns the decrypted plaintext value for a secret identified by its name (as stored in the dashboard).

Parameters:

NameTypeDescription
namestringThe human-readable name of the secret.

Returns: The decrypted plaintext secret value, or an error.

Errors: Returns ErrSecretNotFound if secrets are not enabled or no matching secret exists.

Provider Constants

const (
ProviderOpenAI ProviderType = "openai"
ProviderAnthropic ProviderType = "anthropic"
ProviderGoogle ProviderType = "google"
ProviderCohere ProviderType = "cohere"
ProviderCustom ProviderType = "custom"
)

For more details on how secrets are stored and secured, see Secrets Management.

Thread Safety

The Control Zero Go client is safe for concurrent use. You can share a single *Client instance across multiple goroutines. The background policy refresh runs in its own goroutine and uses appropriate synchronization.

// Create one client at application startup
client, err := controlzero.NewClient(os.Getenv("CONTROLZERO_API_KEY"))
if err != nil {
log.Fatal(err)
}
defer client.Close()

// Use from multiple goroutines
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(agentID string) {
defer wg.Done()
decision, err := client.Check(ctx, controlzero.CheckRequest{
Action: "mcp.tool.call",
Resource: "mcp://filesystem/read_file",
Context: map[string]string{"agent_id": agentID},
})
// handle decision
}(fmt.Sprintf("agent-%d", i))
}
wg.Wait()