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 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

PythonGo
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

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)

Contracts

PythonGo
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) -> Verdictcontract.Check(ctx, env) -> (Verdict, error)

Options

PythonGo
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=sinkguard.WithAuditSink(sink)
session_id="abc"guard.WithSessionID("abc") (per-call)
principal=Principal(...)guard.WithPrincipal(&envelope.Principal{...})
principal_resolver=fnguard.WithPrincipalResolver(fn)

Errors

PythonGo
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 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 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

On this page