Migration from Python
API comparison table and Go idioms for developers migrating from the Python Edictum SDK to the Go SDK.
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
| Python | Go |
|---|---|
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
| Python | Go |
|---|---|
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
| Python | Go |
|---|---|
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) -> Decision | rule.Check(ctx, env) -> (Decision, error) |
Options
| Python | Go |
|---|---|
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=sink | guard.WithAuditSink(sink) |
session_id="abc" | guard.WithSessionID("abc") (per-call) |
principal=Principal(...) | guard.WithPrincipal(envelope.NewPrincipal(...)) |
principal_resolver=fn | guard.WithPrincipalResolver(fn) |
Errors
| Python | Go |
|---|---|
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 gettersync.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