controlzero

package module
v1.7.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

README

controlzero (Go)

AI agent governance for Go. Policies, audit, and observability for tool calls. Works locally with no signup.

Hello World

package main

import (
    "fmt"
    "log"

    "controlzero.ai/sdk/go"
)

func main() {
    cz, err := controlzero.New(controlzero.WithPolicy(map[string]any{
        "rules": []any{
            map[string]any{"deny": "delete_*", "reason": "Hello World: deletes are blocked"},
            map[string]any{"allow": "*", "reason": "Hello World: everything else is fine"},
        },
    }))
    if err != nil {
        log.Fatal(err)
    }
    defer cz.Close()

    d, _ := cz.Guard("delete_file", controlzero.GuardOptions{
        Args: map[string]any{"path": "/tmp/foo"},
    })
    fmt.Println(d.Decision()) // "deny"

    d, _ = cz.Guard("read_file", controlzero.GuardOptions{
        Args: map[string]any{"path": "/tmp/foo"},
    })
    fmt.Println(d.Decision()) // "allow"
}

No API key. No signup. Run it.

Install

go get controlzero.ai/sdk/go@v1.6.1

Pin to a specific version for reproducible builds. The version source of truth is VERSION; bumping that file triggers an auto-tagged release on the public mirror repo (handled by the monorepo sync workflow).

Quickstart with the CLI

go install controlzero.ai/sdk/go/cmd/controlzero@latest

controlzero init
controlzero validate
controlzero test delete_file

The generated controlzero.yaml is the tutorial. It ships with annotated rules covering allow lists, deny lists, wildcards, and the catch-all.

Templates available:

  • controlzero init - Hello World template (default)
  • controlzero init -t rag - RAG agent template (block exfiltration)
  • controlzero init -t mcp - MCP server template
  • controlzero init -t cost-cap - model allow-listing and cost guards

Loading a policy

Three ways:

// From a Go map
cz, _ := controlzero.New(controlzero.WithPolicy(map[string]any{
    "rules": []any{
        map[string]any{"deny": "delete_*"},
        map[string]any{"allow": "read_*"},
    },
}))

// From a YAML file
cz, _ := controlzero.New(controlzero.WithPolicyFile("./controlzero.yaml"))

// From an environment variable
// (set CONTROLZERO_POLICY_FILE=./controlzero.yaml)
cz, _ := controlzero.New()

If ./controlzero.yaml exists in the current directory, it is picked up automatically. No env var needed.

Policy schema

version: '1'
rules:
  - deny: 'delete_*'
    reason: 'Deletes need human approval'
  - allow: 'search'
  - allow: 'read_*'
  - allow: 'github:list_*'
  - deny: 'github:delete_repo'
  - deny: '*'
    reason: 'Default deny'

Rules are evaluated top to bottom. The first match wins. If no rule matches, the call is denied (fail-closed).

Local audit log

When running without an API key, every decision is written to ./controlzero.log. Configure rotation via options:

cz, _ := controlzero.New(
    controlzero.WithPolicyFile("./controlzero.yaml"),
    controlzero.WithLogPath("./logs/controlzero.log"),
    controlzero.WithLogRotation(10, 5, 30, true), // 10MB, 5 backups, 30 days, gzip
    controlzero.WithLogFormat("json"),            // or "pretty"
)

When CONTROLZERO_API_KEY is set, audit ships to the remote dashboard and log options are ignored with a warning.

Hybrid mode

If you set both an API key AND pass a local policy, the local policy overrides the dashboard policy and you get a loud WARN log on init. For prod, use WithStrictHosted() to return an error instead:

cz, err := controlzero.New(
    controlzero.WithAPIKey("cz_live_..."),
    controlzero.WithPolicy(localPolicy),
    controlzero.WithStrictHosted(),
)
// err is ErrHybridMode

Hosted mode

Hosted mode ships dashboard-managed signed policy bundles and remote audit. Pass your project API key; New hits /v1/sdk/bootstrap, pulls the signed .czpolicy bundle, verifies the signature, decrypts locally, and enforces every call against the dashboard policy. Audit streams to the remote trail.

client, err := controlzero.New(controlzero.WithAPIKey("cz_live_..."))
if err != nil {
    log.Fatal(err)
}
defer client.Close()

Use NewWithContext(ctx, ...) to pass your own context. Bootstrap keys and the bundle are cached under ~/.controlzero/cache/ so restarts work offline.

Framework examples

Full integration guides at docs.controlzero.ai/sdk/integrations.

License

Apache 2.0

Documentation

Overview

Package controlzero is the Go SDK for Control Zero AI agent governance.

It provides policies, audit, and observability for tool calls. Works locally with no signup.

Package controlzero exposes API key redaction + leak detection helpers shared by the host-side CLI and the runtime SDK. Mirrors the Python `controlzero.cli._secrets` module so all three SDKs agree on what counts as a leak.

Tier 0a hotfix (#174). See docs/security/key-leak-postmortem.md.

Package controlzero exports the canonical Version of the SDK so runtime callers (audit_remote.go, conformance fixtures, debug bundle) can read it without parsing go.mod or guessing.

#495 / v1 (2026-05-14): introduced alongside the controlzero_sdk_version audit-log column. The wire-format helper builds the normalized "go@<version>" string the backend ingest accepts.

CI drift guard: scripts/ci/check-go-sdk-version-drift.sh asserts Version matches the latest git tag matching `sdks/go/controlzero/v*`. A tag bump without updating this constant fails the check. The reverse (constant ahead of tag) is fine -- the next tag will catch up.

Index

Constants

View Source
const (
	DefaultPolicyAction    = "deny"
	DefaultPolicyOnMissing = "deny"
	DefaultPolicyOnTamper  = "warn"
)

Canonical default values when the bundle or YAML does not carry the knobs. Matches the backend's canonical defaults (see bundle_handler.go: DefaultBundleAction / DefaultBundleOnMissing / DefaultBundleOnTamper) and the hard-coded Python/Node SDK behaviour pre-#228 Phase 2 so upgrading is non-breaking in both directions.

View Source
const (
	// ReasonCodeRuleMatch is emitted when a user-authored policy rule
	// matched and its effect (allow/deny/warn) is the decision.
	ReasonCodeRuleMatch = "RULE_MATCH"

	// ReasonCodeNoRuleMatch is emitted when the bundle loaded cleanly
	// but no rule matched the call. The surface then applies
	// default_action.
	ReasonCodeNoRuleMatch = "NO_RULE_MATCH"

	// ReasonCodeNoActivePolicies is emitted when the bundle is
	// structurally empty (zero attached policies). Synthetic deny
	// rather than a missing-bundle error so the dashboard can
	// distinguish "nothing attached" from "bundle broken."
	ReasonCodeNoActivePolicies = "NO_ACTIVE_POLICIES"

	// ReasonCodeBundleMissing is emitted when the client is enrolled
	// but has no cached bundle and cannot pull a fresh one. The
	// surface then applies default_on_missing.
	ReasonCodeBundleMissing = "BUNDLE_MISSING"

	// ReasonCodeBundleTampered is emitted when bundle verification
	// fails (bad signature, checksum mismatch, unwrap error).
	ReasonCodeBundleTampered = "BUNDLE_TAMPERED"

	// ReasonCodeMachineQuarantined is emitted when the machine is
	// locally quarantined (see tamper.go) and any tool call is denied
	// until recovery.
	ReasonCodeMachineQuarantined = "MACHINE_QUARANTINED"

	// ReasonCodeNetworkError is emitted when the backend is
	// unreachable (DNS, connect, timeout) and no cached bundle is
	// available. The surface then applies default_on_missing.
	ReasonCodeNetworkError = "NETWORK_ERROR"

	// ReasonCodeDLPBlocked is emitted when a DLP rule with
	// action="block" matched the tool arguments and overrode a
	// would-be allow decision.
	ReasonCodeDLPBlocked = "DLP_BLOCKED"

	// ReasonCodeHITLSDKTimeout is emitted when the SDK waited the
	// configured timeout for an approver verdict and gave up.
	ReasonCodeHITLSDKTimeout = "HITL_SDK_TIMEOUT"

	// ReasonCodeHITLSLAExpired is emitted when the backend's SLA
	// timer expired before any approver acted on the request.
	ReasonCodeHITLSLAExpired = "HITL_SLA_EXPIRED"

	// ReasonCodeHITLBackendUnreachable is emitted when the SDK could
	// not reach the backend to enqueue the approval request.
	ReasonCodeHITLBackendUnreachable = "HITL_BACKEND_UNREACHABLE"

	// ReasonCodeHITLPolicyVersionConflict is emitted when the policy
	// bundle changed between request and verdict, invalidating the
	// pending approval.
	ReasonCodeHITLPolicyVersionConflict = "HITL_POLICY_VERSION_CONFLICT"

	// ReasonCodeHITLNoApproverAvailable is emitted when no eligible
	// approver was online or reachable at request time.
	ReasonCodeHITLNoApproverAvailable = "HITL_NO_APPROVER_AVAILABLE"

	// ReasonCodeHITLIdentityNotInOrg is emitted when the approving
	// identity is not a member of the request's org.
	ReasonCodeHITLIdentityNotInOrg = "HITL_IDENTITY_NOT_IN_ORG"

	// ReasonCodeHITLIdentityRequired is emitted when the request
	// requires an authenticated identity claim and none was provided.
	ReasonCodeHITLIdentityRequired = "HITL_IDENTITY_REQUIRED"

	// ReasonCodeHITLIdentityClaimRejected is emitted when the
	// identity claim attached to the request failed verification.
	ReasonCodeHITLIdentityClaimRejected = "HITL_IDENTITY_CLAIM_REJECTED"

	// ReasonCodeHITLArgsHashMismatch is emitted when the args hash
	// at verdict time differs from the args hash at request time,
	// indicating the call payload mutated mid-approval.
	ReasonCodeHITLArgsHashMismatch = "HITL_ARGS_HASH_MISMATCH"
)

Canonical reason_code enum values. Stable, machine-readable labels so integrations + dashboards can branch on decision provenance without regex-matching the human-readable `reason` string.

Added in #228 Phase 2 to bring the Go SDK to parity with the Python SDK. The enum is cross-language: the exact same eight strings appear on Python, Node, the compiled policy engine, the Gateway, and the backend audit envelope. New values are additive; downstream consumers MUST tolerate unknown codes.

See docs/behavior-matrix.md section S6 for the canonical list and the canonical mapping from surface event -> reason_code.

View Source
const (
	SyntheticPolicyIDPrefix        = "synthetic:"
	SyntheticPolicyIDNoRuleMatch   = "synthetic:NO_RULE_MATCH"
	SyntheticPolicyIDNoActive      = "synthetic:NO_ACTIVE_POLICIES"
	SyntheticPolicyIDBundleMiss    = "synthetic:BUNDLE_MISSING"
	SyntheticPolicyIDResGateSkip   = "synthetic:RESOURCE_GATE_SKIP"
	SyntheticPolicyIDQuarantine    = "synthetic:QUARANTINE"
	SyntheticPolicyIDEngineUnavail = "synthetic:ENGINE_UNAVAILABLE"
)

Synthetic policy_id sentinels (T79 / the deny-deny postmortem, 2026-05-11). When a deny is emitted by anything OTHER than a user-authored rule, the SDK stamps the audit row's PolicyID with one of these `synthetic:*` values so the audit dashboard can render a recognizable chip and link it to the right troubleshooting anchor. Without this, four very different bug classes (stale bundle, missing resource gate, vocabulary mismatch, genuine no-match) all looked identical in the Policy column (blank placeholder + Decision=Deny + reason_code=NO_RULE_MATCH).

Keep these in lockstep with the Python (`SYNTHETIC_*`) and Node (`SYNTHETIC_*`) constants; the audit dashboard matches on the exact strings.

View Source
const AliasTool = "database"

AliasTool is the only tool covered by this alias table today. Other tools were added post-#350 and always emitted canonical names.

View Source
const Version = "v1.7.6"

Version is the package version of the controlzero Go SDK. Bump this in lockstep with the git tag in sdks/go/controlzero. The drift CI guard enforces equality on every PR that touches the package.

Variables

View Source
var ErrBundleSignatureInvalid = errors.New("policy bundle signature verification failed")

Sentinel errors.

View Source
var ErrHostedModeNotImplemented = errors.New(
	"controlzero: hosted mode (dashboard policies + remote audit) is not yet implemented in this package.\n" +
		"  - For local mode, provide a policy: controlzero.New(controlzero.WithPolicy(...))\n" +
		"  - For hosted mode today, use the legacy SDK at sdks/go/control-zero\n" +
		"  - Hosted mode in this package is coming in a future release.",
)

HostedModeNotImplemented is returned when an API key is set but no local policy is provided. Hosted mode is not yet implemented in this slim package; users should provide a local policy or use the legacy SDK.

View Source
var ErrHybridMode = errors.New(
	"controlzero: explicit local policy overrides the hosted bundle (strict_hosted=true)",
)

ErrHybridMode is returned when an API key and local policy are both provided AND WithStrictHosted was set. T103: message reworded from "manual policy override detected" to "explicit local policy overrides the hosted bundle". Semantics unchanged.

View Source
var ValidDefaultActions = map[string]bool{
	"deny":  true,
	"allow": true,
	"warn":  true,
}

ValidDefaultActions is the canonical enum for settings.default_action and bundle-level default_action. See S1.

View Source
var ValidOnMissing = map[string]bool{
	"deny":  true,
	"allow": true,
}

ValidOnMissing is the canonical enum for settings.default_on_missing and bundle-level default_on_missing. See S2. `last-known-good` is deferred to Phase 3.

ValidReasonCodes is the full enum, exposed for tests + downstream validators. Unknown codes from a newer SDK version are still permitted on the wire; this set is informational, not a gate.

ValidSyntheticPolicyIDs is the full set, exposed for tests and for downstream consumers that want to recognize a synthetic deny without string-prefix matching.

View Source
var ValidTamperBehaviors = map[string]bool{
	"warn":       true,
	"deny":       true,
	"deny-all":   true,
	"quarantine": true,
}

ValidTamperBehaviors mirrors the Python + Node enum so policies authored once validate identically across all SDKs. Source of truth: docs/behavior-matrix.md section S3.

Functions

func AliasTableJSON added in v1.7.0

func AliasTableJSON() string

AliasTableJSON returns a deterministic JSON dump of the alias table for parity testing against the cross-SDK fixture. Excludes the dev- only "comment" key from the on-disk fixture so each SDK's hash check compares apples to apples.

func ApplyTamperBehavior added in v1.6.0

func ApplyTamperBehavior(stateDir string, settings PolicySettings, reason, source string) bool

ApplyTamperBehavior applies the configured default_on_tamper mode when tamper is detected. Semantics match the Python + Node SDKs so cross-language behaviour is consistent:

"warn"        -> only log (no quarantine, no state change).
"deny"        -> caller should deny this one call; no quarantine.
"deny-all"    -> enter quarantine, deny everything until cleared.
"quarantine"  -> same as deny-all (alias).
unknown / ""  -> treat as "warn" (safest forward-compat).

Returns true if the machine entered quarantine (so the caller can log / surface the state transition). No-op if stateDir is empty.

func ClearQuarantine

func ClearQuarantine(stateDir string)

ClearQuarantine removes the quarantine file. Best-effort, never errors.

func ComputeFingerprint

func ComputeFingerprint() string

ComputeFingerprint returns a stable 32-char hex hash of host attributes. Re-running enroll on the same machine converges on the same fingerprint, which lets the backend's UNIQUE(org_id, fingerprint_hint) upsert return the existing machine_id rather than minting a new one.

func DefaultStateDir

func DefaultStateDir() string

DefaultStateDir returns ~/.controlzero (or the override env var).

func EnterQuarantine

func EnterQuarantine(stateDir, reason, source string) error

EnterQuarantine creates a quarantine state file with the given reason and source.

func ExpandCandidateActions added in v1.7.0

func ExpandCandidateActions(actions []string) []string

ExpandCandidateActions expands an input slice of candidate actions to include every known alias in both directions (legacy -> canonical AND canonical -> legacy). Originals stay first in stable input order; expansions follow in deterministic order.

func ExtractTextFromArgs

func ExtractTextFromArgs(args interface{}) string

ExtractTextFromArgs flattens nested args into a single scannable string.

func GenerateKeypair

func GenerateKeypair() (ed25519.PrivateKey, string, error)

GenerateKeypair mints a fresh signing keypair and returns the private key bytes plus the PEM-encoded public key suitable for the /api/enroll machine_pubkey field.

func GetAPIURL

func GetAPIURL() string

GetAPIURL resolves the Control Zero API base URL from env var, or falls back to the SaaS default.

func GetFindingsForAudit

func GetFindingsForAudit(matches []DLPMatch) []map[string]interface{}

GetFindingsForAudit converts matches to audit-safe findings.

For secret-category matches, only count is included (no text). For other categories, the matched text is included.

func HasBlockingMatch

func HasBlockingMatch(matches []DLPMatch) bool

HasBlockingMatch reports whether any match has Action="block".

func IsQuarantined

func IsQuarantined(stateDir string) bool

IsQuarantined checks whether the quarantine state is set without loading the full struct. Returns false if the file does not exist or is malformed.

func LoadDLPRulesFromPolicy

func LoadDLPRulesFromPolicy(policyData map[string]interface{}) []map[string]interface{}

LoadDLPRulesFromPolicy extracts DLP rules from a policy dict's dlp_rules section. Returns nil if no dlp_rules section exists.

func LoadPrivateKey

func LoadPrivateKey(stateDir string) (ed25519.PrivateKey, error)

LoadPrivateKey reads the private key from <stateDir>/machine.key.

func RedactKey added in v1.7.2

func RedactKey(key string) string

RedactKey converts a full `cz_live_<64hex>` key to `cz_live_***<last5>`. Returns the input verbatim if it does not match the expected shape, so caller-side `fmt.Println(RedactKey(maybe))` is always safe.

func RedactText added in v1.7.2

func RedactText(text string) string

RedactText applies RedactKey to every `cz_(live|test)_*` match inside an arbitrary string.

func SavePrivateKey

func SavePrivateKey(priv ed25519.PrivateKey, stateDir string) (string, error)

SavePrivateKey writes the private key to <stateDir>/machine.key with mode 0600 via an atomic tmp + rename.

func SaveState

func SaveState(state *EnrollmentState, stateDir string) (string, error)

SaveState persists the enrollment state to <stateDir>/enrollment.json.

func SaveTamperState

func SaveTamperState(stateDir string, ts TamperState) error

SaveTamperState writes the quarantine state to the given directory. Uses tmp + rename for atomicity. Permissions are set to 0600.

func SignRequest

func SignRequest(state *EnrollmentState, method, path string, body []byte, stateDir string, now int64) (map[string]string, error)

SignRequest builds the X-CZ-* headers for a signed machine request. Canonical signed string format matches the backend exactly:

machine_id + "\n" + ts + "\n" + METHOD + "\n" + path + "\n" + sha256_hex(body)

Types

type AuditEntry

type AuditEntry struct {
	ID             string `json:"id"`
	User           string `json:"user,omitempty"`
	ToolName       string `json:"tool_name"`
	Decision       string `json:"decision"`
	PolicyID       string `json:"policy_id,omitempty"`
	RuleID         string `json:"rule_id,omitempty"`
	Reason         string `json:"reason,omitempty"`
	Hostname       string `json:"hostname,omitempty"`
	Mode           string `json:"mode,omitempty"`
	Ts             string `json:"ts,omitempty"`
	VerbosityLevel string `json:"verbosity_level,omitempty"`
}

AuditEntry is one decision record streamed via /api/audit.

type AuditIngestResponse

type AuditIngestResponse struct {
	Accepted int `json:"accepted"`
	Dropped  int `json:"dropped"`
}

AuditIngestResponse is the body of POST /api/audit.

func SendAuditBatch

func SendAuditBatch(ctx context.Context, state *EnrollmentState, stateDir string, entries []AuditEntry) (*AuditIngestResponse, error)

SendAuditBatch streams up to 500 entries to /api/audit. The caller is responsible for batching larger flushes.

type BearerAuditOptions

type BearerAuditOptions struct {
	APIURL string
	APIKey string
}

BearerAuditOptions configures a BearerAuditSink.

type BearerAuditSink

type BearerAuditSink struct {
	// contains filtered or unexported fields
}

BearerAuditSink is the hosted-mode audit shipper.

func NewBearerAuditSink

func NewBearerAuditSink(opts BearerAuditOptions) *BearerAuditSink

NewBearerAuditSink constructs a hosted audit sink.

func (*BearerAuditSink) Close

func (s *BearerAuditSink) Close() error

Close flushes pending entries and stops the background goroutine.

func (*BearerAuditSink) IsDisabled

func (s *BearerAuditSink) IsDisabled() bool

IsDisabled reports whether the sink has permanently disabled itself (e.g. due to 401 from the backend).

func (*BearerAuditSink) Log

func (s *BearerAuditSink) Log(entry map[string]any)

Log appends an entry to the buffer. Non-blocking.

type BundleFormatError

type BundleFormatError struct{ Msg string }

BundleFormatError wraps a bundle wire-format problem surfaced to the user. Distinct from bundle signature/crypto failures.

func (*BundleFormatError) Error

func (e *BundleFormatError) Error() string

type BundleSignatureError

type BundleSignatureError struct{ Msg string }

BundleSignatureError wraps a signature/AEAD failure surfaced to the user. Fails closed -- the bundle is untrusted.

func (*BundleSignatureError) Error

func (e *BundleSignatureError) Error() string

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the user-facing entrypoint.

func New

func New(opts ...Option) (*Client, error)

New constructs a Client with the given options. Returns an error rather than panicking on invalid configuration.

This is the synchronous entry point. Hosted mode (WithAPIKey with no local policy) performs a network fetch of the signed policy bundle during construction. Use NewWithContext to pass your own context.

func NewWithContext

func NewWithContext(ctx context.Context, opts ...Option) (*Client, error)

NewWithContext is New with a caller-supplied context. The context governs the hosted-mode bootstrap + bundle-pull HTTP calls.

func (*Client) AgentName added in v1.6.0

func (c *Client) AgentName() string

AgentName returns the agent identity attached to audit events for this client. Resolution order: WithAgentName option > CZ_AGENT_NAME env var > "default-agent".

func (*Client) Close

func (c *Client) Close() error

Close flushes and closes the local audit log sink.

func (*Client) Guard

func (c *Client) Guard(tool string, opts GuardOptions) (PolicyDecision, error)

Guard evaluates a tool call against the loaded policy.

Returns a PolicyDecision and an optional error. The error is non-nil only when RaiseOnDeny was true AND the decision is deny: in that case the returned error is *PolicyDeniedError.

func (*Client) PolicySettings added in v1.6.0

func (c *Client) PolicySettings() PolicySettings

PolicySettings returns the effective settings block (default_action, default_on_missing, default_on_tamper, tamper_behavior) that the client was constructed with. Useful for `controlzero status` style introspection and for tests. Returns the canonical defaults if no local policy was configured.

type DLPMatch

type DLPMatch struct {
	RuleID      string
	RuleName    string
	Category    string
	Action      string
	MatchedText string // plaintext for pii/financial, SHA-256 hash for secret
	Offset      int
	Count       int
}

DLPMatch is a single DLP match result.

type DLPRule

type DLPRule struct {
	ID       string
	Name     string
	Pattern  string
	Category string // "pii", "financial", "secret", "healthcare"
	Action   string // "detect", "block", "mask"
	Region   string
	// contains filtered or unexported fields
}

DLPRule is a single DLP scanning rule with a precompiled regex.

type DLPScanner

type DLPScanner struct {
	// contains filtered or unexported fields
}

DLPScanner scans text for DLP pattern matches.

Loads built-in patterns on creation. Custom rules can be added via AddRules or passed during construction with NewDLPScannerWithRules.

func NewDLPScanner

func NewDLPScanner() *DLPScanner

NewDLPScanner creates a scanner with all built-in patterns loaded.

func NewDLPScannerNoBuiltins

func NewDLPScannerNoBuiltins() *DLPScanner

NewDLPScannerNoBuiltins creates a scanner with NO built-in patterns. Only custom rules added via AddRules will be active.

func NewDLPScannerWithRules

func NewDLPScannerWithRules(customRules []map[string]interface{}) *DLPScanner

NewDLPScannerWithRules creates a scanner with built-in patterns plus the given custom rules.

func (*DLPScanner) AddRules

func (s *DLPScanner) AddRules(rawRules []map[string]interface{})

AddRules adds custom rules on top of existing ones. Each map must contain at least a "pattern" key. Invalid regex patterns are silently skipped.

func (*DLPScanner) RuleCount

func (s *DLPScanner) RuleCount() int

RuleCount returns the number of loaded rules.

func (*DLPScanner) Scan

func (s *DLPScanner) Scan(text string) []DLPMatch

Scan scans text against all loaded DLP rules.

For secret-category rules, MatchedText is a SHA-256 hash of the actual matched text. For all other categories, MatchedText contains the literal matched string.

type EnrollOptions

type EnrollOptions struct {
	APIURL          string
	Token           string
	Hostname        string            // optional override
	FingerprintHint string            // optional override
	PluginVersions  map[string]string // optional override
	StateDir        string            // optional override (default: ~/.controlzero)
	UserEmail       string            // optional
	Timeout         time.Duration     // optional, default 30s
}

EnrollOptions is the input to Enroll.

type EnrollmentError

type EnrollmentError struct{ Msg string }

EnrollmentError is the typed error for everything in this file.

func (*EnrollmentError) Error

func (e *EnrollmentError) Error() string

type EnrollmentPolicyRule

type EnrollmentPolicyRule struct {
	ID       string   `json:"id"`
	Name     string   `json:"name"`
	Pattern  string   `json:"pattern"`
	Category string   `json:"category"`
	Action   string   `json:"action"`
	Scopes   []string `json:"scopes"`
}

EnrollmentPolicyRule is one entry inside a PolicyBundle. Slim projection of the admin DLPRule shape -- no lifecycle / approver fields.

type EnrollmentState

type EnrollmentState struct {
	MachineID           string            `json:"machine_id"`
	OrgID               string            `json:"org_id"`
	APIURL              string            `json:"api_url"`
	Hostname            string            `json:"hostname"`
	FingerprintHint     string            `json:"fingerprint_hint"`
	MachinePubkeyPEM    string            `json:"machine_pubkey_pem"`
	EnrolledAt          string            `json:"enrolled_at"`
	PolicyVersion       int               `json:"policy_version"`
	TamperBehavior      string            `json:"tamper_behavior"`
	OrgSigningPubkeyPEM string            `json:"org_signing_pubkey_pem"`
	PluginVersions      map[string]string `json:"plugin_versions"`
}

EnrollmentState is everything the SDK needs to authenticate signed requests after a successful enroll round-trip.

func Enroll

func Enroll(ctx context.Context, opts EnrollOptions) (*EnrollmentState, error)

Enroll runs the round-trip and persists state + private key. Idempotent on the same machine because the backend dedups by (org_id, fingerprint_hint).

func LoadState

func LoadState(stateDir string) (*EnrollmentState, error)

LoadState reads the persisted enrollment state. Returns (nil, nil) when no state file exists (i.e. machine not yet enrolled).

type EvalContext

type EvalContext struct {
	Resource string
	Tags     map[string]string
}

EvalContext is optional context for resource-level matching.

type GuardOptions

type GuardOptions struct {
	Args        map[string]any
	Method      string
	RaiseOnDeny bool
	Context     *EvalContext
}

GuardOptions configures a single Guard call.

type HeartbeatResponse

type HeartbeatResponse struct {
	ServerTime    string `json:"server_time"`
	PolicyVersion int    `json:"policy_version"`
}

HeartbeatResponse mirrors the JSON returned by /api/heartbeat.

func Heartbeat

func Heartbeat(ctx context.Context, state *EnrollmentState, stateDir string, pluginVersions map[string]string) (*HeartbeatResponse, error)

Heartbeat sends a single signed POST /api/heartbeat. The optional pluginVersions update is merged server-side.

type HostedAuthError

type HostedAuthError struct{ Msg string }

HostedAuthError is returned when the project API key is rejected by the backend (401/403). Permanent: retrying with the same key will not help.

func (*HostedAuthError) Error

func (e *HostedAuthError) Error() string

type HostedBootstrapError

type HostedBootstrapError struct{ Msg string }

HostedBootstrapError is returned when hosted mode cannot initialize (backend unreachable, malformed response, etc.) AND no cached bundle is available. The SDK fails closed: without a valid policy, New() refuses to construct.

func (*HostedBootstrapError) Error

func (e *HostedBootstrapError) Error() string

type KeyMatch added in v1.7.2

type KeyMatch struct {
	Start      int
	End        int
	Key        string
	LineNumber int // 1-indexed
}

KeyMatch describes one occurrence of a `cz_(live|test)_*` substring inside arbitrary text.

func FindKeyLeaks added in v1.7.2

func FindKeyLeaks(text string) []KeyMatch

FindKeyLeaks returns every `cz_(live|test)_*` substring inside `text`. Safe on arbitrary untrusted input (linear regex).

type LocalAuditLogger

type LocalAuditLogger struct {
	// contains filtered or unexported fields
}

LocalAuditLogger writes JSON Lines audit entries to a local file with optional size-based rotation via lumberjack.

Why lumberjack?

It's the boring choice for log rotation in Go. Single dependency, mature,
zero ceremony, plays well with the standard io.Writer interface.

func NewLocalAuditLogger

func NewLocalAuditLogger(opts LocalAuditOptions) *LocalAuditLogger

NewLocalAuditLogger constructs a sink. If the path is unwritable, all subsequent writes go to stderr instead.

func (*LocalAuditLogger) Close

func (l *LocalAuditLogger) Close() error

Close flushes and closes the underlying file. Safe to call multiple times.

func (*LocalAuditLogger) Log

func (l *LocalAuditLogger) Log(entry map[string]any)

Log writes a single audit entry.

type LocalAuditOptions

type LocalAuditOptions struct {
	LogPath    string
	MaxSizeMB  int    // size-based rotation; 0 = no rotation
	MaxBackups int    // how many rotated files to keep
	MaxAgeDays int    // delete rotated files older than N days
	Compress   bool   // gzip rotated files
	Format     string // "json" (default) or "pretty"
}

LocalAuditOptions configures the local audit sink.

type NotEnrolledError

type NotEnrolledError struct{ Msg string }

NotEnrolledError fires when a method that needs local state is called before Enroll has run successfully.

func (*NotEnrolledError) Error

func (e *NotEnrolledError) Error() string

type Option

type Option func(*clientConfig)

Option configures a Client at construction time.

func WithAPIKey

func WithAPIKey(key string) Option

func WithAgentName added in v1.6.0

func WithAgentName(name string) Option

WithAgentName sets the agent identity attached to audit events. Falls back to CZ_AGENT_NAME env var, then to "default-agent".

func WithLogFormat

func WithLogFormat(format string) Option

func WithLogPath

func WithLogPath(path string) Option

func WithLogRotation

func WithLogRotation(maxSizeMB, maxBackups, maxAgeDays int, compress bool) Option

func WithPolicy

func WithPolicy(policy map[string]any) Option

func WithPolicyFile

func WithPolicyFile(path string) Option

func WithStrictHosted

func WithStrictHosted() Option

type ParsedPolicy added in v1.6.0

type ParsedPolicy struct {
	Rules    []PolicyRule
	Settings PolicySettings
}

ParsedPolicy is the full result of loading a policy source. Matches Python's ParsedPolicy and Node's ParsedPolicy one-to-one so fixtures + cross-language parity tests stay consistent.

func LoadPolicyFull added in v1.6.0

func LoadPolicyFull(source any) (ParsedPolicy, error)

LoadPolicyFull is LoadPolicy but also returns the parsed settings block. Introduced in #228 Phase 2. Prefer this in new code so the evaluator can honour default_action / default_on_missing / default_on_tamper.

type PolicyBundle

type PolicyBundle struct {
	PolicyVersion int                    `json:"policy_version"`
	Rules         []EnrollmentPolicyRule `json:"rules"`
}

PolicyBundle is the SDK projection returned by GET /api/policy.

func PullPolicy

func PullPolicy(ctx context.Context, state *EnrollmentState, stateDir string) (*PolicyBundle, error)

PullPolicy issues a signed GET /api/policy with If-None-Match using the cached version. Returns nil bundle on 304 (caller keeps the existing local copy). On 200, also persists the new policy_version to the state file.

type PolicyDecision

type PolicyDecision struct {
	Effect         string // "allow", "deny", "warn", "audit"
	PolicyID       string
	Reason         string
	ReasonCode     string
	EvaluatedRules int
}

PolicyDecision is the result of evaluating a tool call against a policy.

ReasonCode is a machine-readable, cross-language enum value drawn from the constants in reason_codes.go (RULE_MATCH, NO_RULE_MATCH, NO_ACTIVE_POLICIES, BUNDLE_MISSING, BUNDLE_TAMPERED, MACHINE_QUARANTINED, NETWORK_ERROR, DLP_BLOCKED). Automation should branch on ReasonCode, not on Reason -- Reason is free text that may be re-worded between releases. Empty string when the decision predates #228 Phase 2 or when a user-authored rule did not declare a reason_code of its own.

func (PolicyDecision) Allowed

func (d PolicyDecision) Allowed() bool

Allowed reports whether the call was allowed.

func (PolicyDecision) Decision

func (d PolicyDecision) Decision() string

Decision returns Effect. Reads more naturally in user code.

func (PolicyDecision) Denied

func (d PolicyDecision) Denied() bool

Denied reports whether the call was denied.

type PolicyDeniedError

type PolicyDeniedError struct {
	Decision PolicyDecision
}

PolicyDeniedError is returned by Guard when a deny decision is reached AND the caller passed RaiseOnDeny.

func (*PolicyDeniedError) Error

func (e *PolicyDeniedError) Error() string

type PolicyEvaluator

type PolicyEvaluator struct {
	// contains filtered or unexported fields
}

PolicyEvaluator runs in-process. Fail-closed by default: if no rule matches AND no default_action was provided, the call is denied.

Pattern matching uses path.Match, which is Go's stdlib equivalent of fnmatch (case-sensitive shell glob). Friendly schema like "delete_*" is canonicalized to "delete_*:*" in policy_loader.go BEFORE rules reach this evaluator. By the time rules arrive here, every action is in canonical "tool:method" form.

defaultAction controls the no-match path (added in #228 Phase 2):

""     -> fall back to "deny" with ReasonCodeNoRuleMatch (legacy /
           fail-closed contract; matches pre-Phase-2 behaviour).
"deny" -> deny + ReasonCodeNoRuleMatch.
"allow" -> allow + ReasonCodeNoRuleMatch.
"warn" -> warn + ReasonCodeNoRuleMatch (effect "warn"; caller
           decides whether to fire).
any other value -> treated as empty; fall back to deny. This is
defensive: a corrupt or future-version bundle MUST NOT flip an
org from deny to allow through a typo.

func NewPolicyEvaluator

func NewPolicyEvaluator(rules []PolicyRule) *PolicyEvaluator

NewPolicyEvaluator constructs an evaluator with an optional initial rule set. defaultAction is left empty, which keeps the legacy fail-closed contract (deny on no-match). Use NewPolicyEvaluatorWithSettings to honour a bundle's default_action.

func NewPolicyEvaluatorWithSettings added in v1.6.0

func NewPolicyEvaluatorWithSettings(rules []PolicyRule, settings PolicySettings) *PolicyEvaluator

NewPolicyEvaluatorWithSettings constructs an evaluator that honours the per-bundle settings block. If settings.DefaultAction is empty or unknown, the evaluator falls back to deny (fail-closed contract).

func (*PolicyEvaluator) Evaluate

func (e *PolicyEvaluator) Evaluate(tool, method string, ctx *EvalContext) PolicyDecision

Evaluate returns a PolicyDecision for the given tool/method. Always returns; never panics.

func (*PolicyEvaluator) Load

func (e *PolicyEvaluator) Load(rules []PolicyRule)

Load replaces the rule set.

func (*PolicyEvaluator) SetDefaultAction added in v1.6.0

func (e *PolicyEvaluator) SetDefaultAction(action string)

SetDefaultAction sets the no-match default. Accepts "deny" / "allow" / "warn"; any other value (including empty) reverts to the fail-closed contract.

type PolicyLoadError

type PolicyLoadError struct {
	Message string
	Source  string
	Cause   error
}

PolicyLoadError is returned when a policy file cannot be loaded.

func (*PolicyLoadError) Error

func (e *PolicyLoadError) Error() string

func (*PolicyLoadError) Unwrap

func (e *PolicyLoadError) Unwrap() error

type PolicyRule

type PolicyRule struct {
	ID         string
	Name       string
	Effect     string // "allow" or "deny"
	Actions    []string
	Resources  []string
	Conditions map[string]any
	Reason     string // Human-readable explanation, surfaced in audit + denies
	ReasonCode string // Machine-readable code (see reason_codes.go)

	// EscalateOnDeny is the HITL escalation tag (HITL-5c, gh#540):
	// when true and the rule's effect is `deny`, future SDK versions
	// will mark the resulting PolicyDecision as hitl_eligible=true.
	// The actual approval-request flow ships in v1.8.0 (HITL-6a,
	// gh#542); v1.7.6 just acknowledges the field so a customer
	// pre-tagging rules for HITL does not crash an old client.
	EscalateOnDeny bool
}

PolicyRule is the canonical internal representation of a single policy rule. The user-facing schema (see policy_loader.go) is friendlier; this is what the evaluator consumes after translation.

ReasonCode is an optional rule-level override of the emitted reason_code. Today the backend bundle translator stamps it on synthetic empty-bundle denies (NO_ACTIVE_POLICIES); user-authored rules usually leave it empty and the evaluator fills it with RULE_MATCH on match.

func LoadPolicy

func LoadPolicy(source any) ([]PolicyRule, error)

LoadPolicy parses a policy from a map[string]any (in-memory) or a path (string ending in .yaml/.yml/.json). Returns the canonicalized rule list or a *PolicyValidationError / *PolicyLoadError on failure.

Backwards-compatible signature: historic callers treat the first return as a []PolicyRule which is still correct. New callers should prefer LoadPolicyFull for access to PolicySettings.

type PolicySettings added in v1.6.0

type PolicySettings struct {
	// DefaultAction is the decision applied when the evaluator loads
	// rules but none match. Values: deny (default), allow, warn.
	DefaultAction string

	// DefaultOnMissing is the decision applied when the client cannot
	// load a bundle at all. Values: deny (default), allow.
	DefaultOnMissing string

	// DefaultOnTamper is the mode used when tamper is detected. Values:
	// warn (default), deny, deny-all, quarantine.
	DefaultOnTamper string

	// TamperBehavior is the legacy alias for DefaultOnTamper. Kept for
	// one release so operators with existing YAML files do not break.
	// Populated from settings.tamper_behavior if present.
	TamperBehavior string
}

PolicySettings carries the three enforcement-default knobs defined in docs/behavior-matrix.md:

  • DefaultAction deny | allow | warn fired when the bundle has rules but nothing matches.
  • DefaultOnMissing deny | allow fired when the client is enrolled but no bundle could be loaded.
  • DefaultOnTamper warn | deny | deny-all | quarantine mirrors the existing tamper_behavior field. Both names are accepted in YAML; tamper_behavior is the legacy name.

The zero value of this struct is the canonical fallback (deny/deny/ warn). This matches every SDK's hard-coded pre-#228 behaviour, so a caller that never reads settings keeps the old behaviour.

func DefaultPolicySettings added in v1.6.0

func DefaultPolicySettings() PolicySettings

DefaultPolicySettings returns a settings block pre-populated with the canonical defaults.

func (PolicySettings) EffectiveTamperBehavior added in v1.6.0

func (s PolicySettings) EffectiveTamperBehavior() string

EffectiveTamperBehavior returns the effective tamper mode, preferring the new DefaultOnTamper field but falling back to the legacy TamperBehavior alias and finally to the canonical default.

type PolicyValidationError

type PolicyValidationError struct {
	Errors []string
	Source string
}

PolicyValidationError is returned when a policy file or map fails schema validation.

func (*PolicyValidationError) Error

func (e *PolicyValidationError) Error() string

type TamperState

type TamperState struct {
	Quarantined bool   `json:"quarantined"`
	Reason      string `json:"reason"`
	DetectedAt  string `json:"detected_at"`
	Source      string `json:"source"` // "policy_hmac" | "audit_chain" | "bundle_signature"
}

TamperState is the persisted quarantine state for mandatory tamper enforcement. Fields match the Python/Node SDKs exactly.

func LoadTamperState

func LoadTamperState(stateDir string) TamperState

LoadTamperState reads the quarantine state from the given directory. Returns a zero-value TamperState if the file does not exist or is malformed (never returns an error).

type TokenError

type TokenError struct{ Msg string }

TokenError is a more specific subtype for enroll-token rejections.

func (*TokenError) Error

func (e *TokenError) Error() string

Directories

Path Synopsis
cmd
controlzero command
Command controlzero is the CLI for the Go SDK.
Command controlzero is the CLI for the Go SDK.
parity-parse command
Parse the parity fixture with the Go SDK bundle parser and print canonical JSON of the translated policy.
Parse the parity fixture with the Go SDK bundle parser and print canonical JSON of the translated policy.
Hello World: no API key, no signup.
Hello World: no API key, no signup.
integrations
langchaingo
Package langchaingo wraps LangChainGo tools with Control Zero policy enforcement (issue #96).
Package langchaingo wraps LangChainGo tools with Control Zero policy enforcement (issue #96).
internal
bundle
Package bundle parses the .czpolicy signed binary bundle format.
Package bundle parses the .czpolicy signed binary bundle format.
Package templates exposes the policy templates as embedded files so the `controlzero` binary is self-contained.
Package templates exposes the policy templates as embedded files so the `controlzero` binary is self-contained.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL