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.