Skip to content

Independent Go Consumer

Build a new application using published Core 2.8.0, without checking out PRAXO-CORE. This starter uses a deterministic model and an in-memory tool so installation and authorization behavior can be checked without credentials, a model service, or AxisVela products.

Is This a Fit?

Task Fit and boundary
Embed a model/tool loop in an existing Go application Good fit; your application owns the model adapter and tools
Authorize tool calls and test a denial path Good fit; your application supplies policy and authentic identity
Add human approval or checkpoint recovery Supported seams; persistent approvals and external effect reconciliation remain integration work
Run durable graph workflows or guarantee exactly-once effects Not supplied by Core alone; use a dedicated runtime or execution service
Obtain a hosted sandbox or robot safety controller Not supplied; Core exposes integration boundaries, not those services

Review when not to use Core before choosing it.

Create the Application

Use Go 1.26.11, the tested CI/release toolchain. Installation needs registry access or a populated module cache. Set GOWORK=off for all commands in this guide so a parent workspace cannot substitute local dependencies. In PowerShell use $env:GOWORK = "off"; in a POSIX shell use export GOWORK=off.

In a new application directory, run:

go mod init example.com/praxo-starter
go get github.com/axisvela/praxo-core/v2@v2.8.0

Application Source

Use the following as main.go. On the documentation site this code is rendered directly from the tested starter source, not a separately maintained copy. GitHub readers can follow that source link.

package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "os"
    "strings"

    "github.com/axisvela/praxo-core/v2/pkg/agent"
    "github.com/axisvela/praxo-core/v2/pkg/model"
    "github.com/axisvela/praxo-core/v2/pkg/policy"
    "github.com/axisvela/praxo-core/v2/pkg/tool"
)

type fixtureModel struct{ target string }

func (m fixtureModel) Generate(_ context.Context, req model.Request) (model.Response, error) {
    last := req.Messages[len(req.Messages)-1]
    if last.Role == model.RoleTool {
        return model.Response{Message: model.Message{Role: model.RoleAssistant, Content: "recorded"}}, nil
    }
    return model.Response{Message: model.Message{
        Role: model.RoleAssistant,
        ToolCalls: []model.ToolCall{{
            ID: "record-1", Name: "record", Arguments: map[string]any{"target": m.target},
        }},
    }}, nil
}

type recordTool struct{ calls int }

func (*recordTool) Name() string        { return "record" }
func (*recordTool) Description() string { return "Record a target in an in-memory counter." }

func (*recordTool) Spec() model.ToolSpec {
    return model.ToolSpec{
        Name: "record", Description: "Record a target in an in-memory counter.",
        Parameters: map[string]any{
            "type": "object", "required": []string{"target"},
            "properties": map[string]any{"target": map[string]any{"type": "string"}},
        },
    }
}

func (t *recordTool) Invoke(context.Context, map[string]any) (any, error) {
    t.calls++
    return "recorded", nil
}

func runScenario(ctx context.Context, target string) (string, int, error) {
    impl := &recordTool{}
    tools := tool.NewRegistry()
    tools.Register(impl)
    ag, err := agent.New(agent.Options{
        Model: fixtureModel{target: target}, Tools: tools, MaxSteps: 3,
        AllowedTools: []string{"record"},
        Policy: policy.FuncHook(func(_ context.Context, action policy.Action) (policy.Decision, error) {
            switch action.Kind {
            case "model.generate":
                return policy.Decision{Allowed: true}, nil
            case "tool.invoke":
                return policy.Decision{
                    Allowed: action.ToolName == "record" && action.Arguments["target"] == "public",
                    Reason:  "only the public target is permitted",
                }, nil
            default:
                return policy.Decision{Reason: "unsupported action"}, nil
            }
        }),
    })
    if err != nil {
        return "", impl.calls, err
    }
    result, err := ag.Run(ctx, agent.Session{ID: "starter-" + target}, "Record the requested target.")
    if err != nil {
        return "", impl.calls, err
    }
    return result.Output, impl.calls, nil
}

func run(ctx context.Context, out io.Writer) error {
    output, calls, err := runScenario(ctx, "public")
    if err != nil || output != "recorded" || calls != 1 {
        return fmt.Errorf("allow verification failed: output=%q effects=%d error=%v", output, calls, err)
    }
    if _, err := fmt.Fprintln(out, "allow: recorded; effects=1"); err != nil {
        return err
    }
    _, calls, err = runScenario(ctx, "private")
    if err == nil || !strings.Contains(err.Error(), "tool.invoke denied") || calls != 0 {
        return fmt.Errorf("deny verification failed: effects=%d error=%v", calls, err)
    }
    _, err = fmt.Fprintln(out, "deny: policy rejected; effects=0")
    return err
}

func main() {
    if err := run(context.Background(), os.Stdout); err != nil {
        log.Fatal(err)
    }
}

Acceptance Tests

Use the following as main_test.go, also rendered from the tested source. The dependency check rejects replacements and verifies the exact Core version.

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "io"
    "os"
    "os/exec"
    "strings"
    "testing"
)

func TestAllowedTool(t *testing.T) {
    output, calls, err := runScenario(context.Background(), "public")
    if err != nil || output != "recorded" || calls != 1 {
        t.Fatalf("output=%q effects=%d error=%v", output, calls, err)
    }
}

func TestDeniedToolHasNoEffect(t *testing.T) {
    output, calls, err := runScenario(context.Background(), "private")
    if err == nil || !strings.Contains(err.Error(), "tool.invoke denied") {
        t.Fatalf("expected policy denial, got %v", err)
    }
    if calls != 0 || output != "" {
        t.Fatalf("denied call: output=%q effects=%d", output, calls)
    }
}

func TestStarterOutput(t *testing.T) {
    var out bytes.Buffer
    if err := run(context.Background(), &out); err != nil {
        t.Fatal(err)
    }
    if want := "allow: recorded; effects=1\ndeny: policy rejected; effects=0\n"; out.String() != want {
        t.Fatalf("output=%q, want %q", out.String(), want)
    }
}

func TestPublishedDependency(t *testing.T) {
    cmd := exec.Command("go", "list", "-mod=readonly", "-m", "-json", "all")
    cmd.Env = append(os.Environ(), "GOWORK=off")
    output, err := cmd.Output()
    if err != nil {
        t.Fatal(err)
    }
    decoder := json.NewDecoder(bytes.NewReader(output))
    found := false
    for {
        var dependency struct {
            Path    string
            Version string
            Replace json.RawMessage
        }
        if err := decoder.Decode(&dependency); err == io.EOF {
            break
        } else if err != nil {
            t.Fatal(err)
        }
        if len(dependency.Replace) != 0 {
            t.Fatalf("local replacement for %s", dependency.Path)
        }
        if dependency.Path == "github.com/axisvela/praxo-core/v2" {
            found = true
            if dependency.Version != "v2.8.0" {
                t.Fatalf("Core version=%q, want v2.8.0", dependency.Version)
            }
        }
    }
    if !found {
        t.Fatal("published Core dependency missing")
    }
}

Verify

After adding both files, resolve their dependencies and verify the application:

go mod tidy
go test -mod=readonly ./...
go vet -mod=readonly ./...
go build -mod=readonly ./...
go run -mod=readonly .

Expected program output:

allow: recorded; effects=1
deny: policy rejected; effects=0

The fake model requests the tool regardless of policy. The explicit AllowedTools list limits the tool name; the policy additionally restricts its target argument. A rejected private target must never increment the effect counter. An empty allowlist is not a deny-all configuration in the current API.

Governed Acceptance

Published-Package Upgrades

From a source checkout, python scripts/test_go_upgrade.py -v creates one fresh consumer, installs the historical version pinned by tests/consumer/go.mod, then upgrades that same application to the Go version in the package map. It rejects module replacements, checks both exact dependency versions, and requires unchanged application source. The historical phase checks basic allow/deny behavior; the upgraded phase runs all five governed negative checks, build, vet and the expected program output. It does not claim those stronger checks passed on the historical security baseline.

CI runs this check on Linux, Windows and macOS. A run proves only its named published-version pair and fixture, not an unpublished release candidate, arbitrary version upgrades, user adoption or production retention.

Negative Checks

The published-package tests also include five negative checks:

go test -mod=readonly -run Governed -v ./...
Check Required outcome
Missing mandatory policy application guard rejects before a run; zero tool effects
Empty tool scope application guard rejects; zero tool effects
Middleware changes final arguments final operation is reauthorized; denial prevents the effect
Approval edit changes arguments edited operation is reauthorized; denial prevents the effect
Policy service error run fails closed; zero tool effects

The tests are in the tested source. The guard is application-level: Core keeps its ordinary nil-policy embeddable behavior. These checks do not prove authentic identity, durable approvals, effect reconciliation, or a general security profile.

These checks prove this published-package fixture only, not live-model quality, identity authenticity, process isolation, durable effects, or production safety. To connect a real model, continue with the endpoint quickstart and the adoption guide. Keep denial tests when replacing the fake.