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 contracts 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 contract format is identical across both SDKs -- you can use the same contracts.yaml file without changes. The API surface differs to follow Go idioms.
API Comparison
Guard Construction
| Python | Go |
|---|---|
Edictum.from_yaml("contracts.yaml") | guard.FromYAML("contracts.yaml") |
Edictum.from_yaml_string(content) | guard.FromYAMLString(content) |
Edictum.from_server(url, api_key, agent_id) | guard.FromServer(url, apiKey, agentID) |
Edictum(contracts=[...]) | guard.New(guard.WithContracts(...)) |
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) |
Contracts
| Python | Go |
|---|---|
Precondition(name=..., tool=..., check=fn) | contract.Precondition{Name: ..., Tool: ..., Check: fn} |
Postcondition(name=..., tool=..., check=fn) | contract.Postcondition{Name: ..., Tool: ..., Check: fn} |
SessionContract(name=..., check=fn) | contract.SessionContract{Name: ..., Check: fn} |
contract.check(env) -> Verdict | contract.Check(ctx, env) -> (Verdict, error) |
Options
| Python | Go |
|---|---|
on_deny=lambda env, r, n: ... | guard.WithOnDeny(func(env, reason, name string) { ... }) |
on_allow=lambda env: ... | guard.WithOnAllow(func(env envelope.ToolEnvelope) { ... }) |
audit_sink=sink | guard.WithAuditSink(sink) |
session_id="abc" | guard.WithSessionID("abc") (per-call) |
principal=Principal(...) | guard.WithPrincipal(&envelope.Principal{...}) |
principal_resolver=fn | guard.WithPrincipalResolver(fn) |
Errors
| Python | Go |
|---|---|
EdictumDenied exception | *edictum.DeniedError (use edictum.AsDenied(err)) |
try/except EdictumDenied as e: | if denied, ok := edictum.AsDenied(err); ok { ... } |
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 denied case is an error type you unwrap:
result, err := g.Run(ctx, "read_file", args, toolFn)
if err != nil {
if denied, ok := edictum.AsDenied(err); ok {
// Contract denied the call
fmt.Println("Denied:", denied.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_deny=my_fn)
// Go:
g := guard.New(
guard.WithMode("observe"),
guard.WithEnvironment("staging"),
guard.WithOnDeny(myDenyHandler),
)Invalid options panic at construction time (like regexp.MustCompile). This fails fast and loudly rather than silently ignoring misconfiguration.
Struct Literals for Contracts
Go contracts are struct values, not class instances. Fields are set directly in the literal:
contract.Precondition{
Name: "no-rm-rf",
Tool: "Bash",
Check: func(ctx context.Context, env envelope.ToolEnvelope) (contract.Verdict, error) {
if strings.Contains(env.BashCommand(), "rm -rf") {
return contract.Fail("Cannot run rm -rf"), nil
}
return contract.Pass(), nil
},
}Unexported Fields + Getters
Immutable types use unexported fields with getter methods. You cannot modify internal state directly:
env := envelope.ToolEnvelope{} // fields are set during creation
name := env.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 all 562 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: denied calls never execute. Observe mode logs
CALL_WOULD_DENY. 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