Edictum
Go SDK

Migration from Python

API comparison table and Go idioms for developers migrating from the Python Edictum SDK to the Go SDK.

AI Assistance

Right page if: you are familiar with the Python Edictum SDK and want to understand the equivalent Go API, or you are porting a Python agent to Go. Wrong page if: you are starting fresh with Go -- see https://docs.edictum.ai/docs/go. For Python docs, see https://docs.edictum.ai/docs/quickstart. Gotcha: Go rulesets use struct literals (Precondition{}, Postcondition{}) instead of Python's dict/class API. The YAML format is identical -- no changes needed.

The Go SDK maintains full feature parity with the Python SDK. The YAML rule format is identical across both SDKs -- you can use the same rules.yaml file without changes. The API surface differs to follow Go idioms.

API Comparison

Guard Construction

PythonGo
Edictum.from_yaml("rules.yaml")guard.FromYAML("rules.yaml")
Edictum.from_yaml_string(content)guard.FromYAMLString(content)
Edictum.from_server(url, api_key, agent_id)guard.FromServer(url, apiKey, agentID)
Edictum(rules=[...])guard.New(guard.WithRules(...))
Edictum(..., mode="observe")guard.New(guard.WithMode("observe"))
Edictum(..., environment="staging")guard.New(guard.WithEnvironment("staging"))

Running Tool Calls

PythonGo
await guard.run("tool", args, fn)guard.Run(ctx, "tool", args, fn)
guard.evaluate("tool", args)guard.Evaluate(ctx, "tool", args)
guard.set_principal(p)guard.SetPrincipal(p)

Rulesets

PythonGo
Precondition(name=..., tool=..., check=fn)rule.Precondition{Name: ..., Tool: ..., Check: fn}
Postcondition(name=..., tool=..., check=fn)rule.Postcondition{Name: ..., Tool: ..., Check: fn}
SessionContract(name=..., check=fn)rule.SessionRule{Name: ..., Check: fn}
rule.check(env) -> Decisionrule.Check(ctx, env) -> (Decision, error)

Options

PythonGo
on_block=lambda env, r, n: ...guard.WithOnBlock(func(call toolcall.ToolCall, reason, name string) { ... })
on_allow=lambda env: ...guard.WithOnAllow(func(call toolcall.ToolCall) { ... })
audit_sink=sinkguard.WithAuditSink(sink)
session_id="abc"guard.WithSessionID("abc") (per-call)
principal=Principal(...)guard.WithPrincipal(envelope.NewPrincipal(...))
principal_resolver=fnguard.WithPrincipalResolver(fn)

Errors

PythonGo
EdictumDenied exception*edictum.DeniedError (use errors.As(err, &blocked))
try/except EdictumDenied as e:var blocked *edictum.DeniedError; if errors.As(err, &blocked) { ... }

Key Go Idioms

context.Context as First Parameter

Every method that does work accepts context.Context as its first parameter. This follows the Go standard library convention and enables cancellation, timeouts, and tracing propagation:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := g.Run(ctx, "read_file", args, toolFn)

Error Returns Instead of Exceptions

Go returns errors instead of raising exceptions. The blocked case is an error type you unwrap:

result, err := g.Run(ctx, "read_file", args, toolFn)
if err != nil {
	var blocked *edictum.DeniedError
	if errors.As(err, &blocked) {
		// Rule blocked the call
		fmt.Println("Blocked:", blocked.Reason)
		return
	}
	// Some other error (network, tool failure, etc.)
	return err
}

Functional Options

Constructor configuration uses the functional options pattern instead of keyword arguments:

// Python: Edictum(mode="observe", environment="staging", on_block=my_fn)
// Go:
g := guard.New(
	guard.WithMode("observe"),
	guard.WithEnvironment("staging"),
	guard.WithOnBlock(myBlockHandler),
)

Invalid options panic at construction time (like regexp.MustCompile). This fails fast and loudly rather than silently ignoring misconfiguration.

Struct Literals for Rulesets

Go rulesets are struct values, not class instances. Fields are set directly in the literal:

rule.Precondition{
	Name: "no-rm-rf",
	Tool: "Bash",
	Check: func(ctx context.Context, call toolcall.ToolCall) (rule.Decision, error) {
		if strings.Contains(call.BashCommand(), "rm -rf") {
			return rule.Fail("Cannot run rm -rf"), nil
		}
		return rule.Pass(), nil
	},
}

Unexported Fields + Getters

Immutable types use unexported fields with getter methods. You cannot modify internal state directly:

call := toolcall.ToolCall{} // fields are set during creation
name := call.ToolName()     // read via getter

sync.Mutex for Shared State

The guard is safe for concurrent use. Internal state is protected with sync.RWMutex. The go test -race detector passes on 660+ tests.

What Stays the Same

  • YAML format: identical across Python, Go, and TypeScript. Same apiVersion, same operators, same effects.
  • Pipeline order: attempt limits, before hooks, preconditions, sandbox, session, execution limits, execute, postconditions, audit.
  • Enforcement semantics: blocked calls never execute. Observe mode logs CALL_WOULD_BLOCK. Fail-closed on every error path.
  • Adapter pattern: thin wrappers around guard.Run(), framework-specific function signature translation.
  • Server protocol: same HTTP API, same SSE events, same Ed25519 verification.

Next Steps

Last updated on

On this page