跳转至

Go 独立消费者

在自己的新应用中使用已发布的 Core 2.8.0,不需要检出 PRAXO-CORE 源码。 Starter 使用确定性模型和内存工具,无需凭据、模型服务或 AxisVela 产品,便能验证安装与授权行为。

是否适用?

任务 适用性与边界
在现有 Go 应用嵌入模型与工具循环 适用;应用负责模型适配器与工具
授权工具调用并验证拒绝路径 适用;应用提供策略与真实身份
接入人工审批或 Checkpoint 恢复 提供接口;持久审批和外部效果对账仍属集成职责
持久图工作流或 exactly-once 效果 Core 单独不提供;需要专门运行时或执行服务
托管沙箱或机器人安全控制器 不提供;Core 提供集成边界,而非这些服务

选型前阅读何时不应选择 Core。

创建应用

使用 Go 1.26.11,即 CI/发布验证工具链。安装需要访问 Registry 或使用已有模块缓存。 全部命令应先设置 GOWORK=off,避免父目录工作区替换为本地依赖。 PowerShell 使用 $env:GOWORK = "off",POSIX Shell 使用 export GOWORK=off。

在新的应用目录运行:

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

应用源码

以下为 main.go。文档站直接从受测 Starter 源码 渲染,不另外维护代码副本。在 GitHub 阅读时可通过该链接查看完整源码。

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

验收测试

以下为 main_test.go,同样从受测源码 渲染。依赖检查拒绝模块替换,并验证精确 Core 版本。

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")
    }
}

验证

添加两个文件后,解析依赖并验证应用:

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

程序预期输出:

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

Fake Model 不依赖策略结果,始终请求工具。显式 AllowedTools 限制工具名,Policy 额外限制目标参数。private 目标被拒绝后,不得增加效果计数。当前 API 的空 allowlist 不代表 deny-all。

治理验收

发布包升级

在源码检出目录运行 python scripts/test_go_upgrade.py -v,会创建一个全新消费者,先安装 tests/consumer/go.mod 固定的历史版本,再在同一应用中升级到包映射里的 Go 推荐版本。 测试拒绝模块替换、核对两个精确版本,并要求应用源码不变。历史阶段只检查基础允许/拒绝; 升级后运行五项治理负向检查、build、vet 和预期程序输出,不声称历史安全基线也通过更强检查。

CI 在 Linux、Windows、macOS 执行该检查。每次运行只证明该发布版本对和 fixture,不证明 未发布候选版本、任意版本升级、真实用户采用或生产留存。

负向检查

发布包测试还包含五项负向检查:

go test -mod=readonly -run Governed -v ./...
检查 必须结果
缺少必需 Policy 应用守卫在运行前拒绝;工具零副作用
工具范围为空 应用守卫拒绝;工具零副作用
Middleware 改变最终参数 最终操作重新授权;拒绝阻止副作用
审批编辑改变参数 编辑后的操作重新授权;拒绝阻止副作用
Policy 服务错误 运行 fail closed;工具零副作用

测试位于受测源码。 守卫属于应用层:Core 保留普通 nil-policy 嵌入模式。这些检查不证明真实身份、持久审批、 效果对账或通用安全 Profile。

这些检查只证明该发布包 fixture,不证明真实模型质量、身份真实性、进程隔离、持久效果 或生产安全。连接真实模型请继续阅读端点快速开始与采用指南, 替换 Fake Model 后仍需保留拒绝测试。