Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions scenarios/policy-enforcement/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module example.com/policy-enforcement

go 1.26.0

require (
github.com/AI-agent-assembly/go-sdk v0.0.1-alpha.2
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
53 changes: 53 additions & 0 deletions scenarios/policy-enforcement/go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
github.com/AI-agent-assembly/go-sdk v0.0.1-alpha.2 h1:0o+8dqLpBi3cIKNu8mXI0vP4RdgyYfgkKqtfI6B8u1A=
github.com/AI-agent-assembly/go-sdk v0.0.1-alpha.2/go.mod h1:vsRXutpz1R1zFmIn08agGluNdSBB/2kiqSHtu63LxoU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
82 changes: 82 additions & 0 deletions scenarios/policy-enforcement/go/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"context"
"errors"
"fmt"

"github.com/AI-agent-assembly/go-sdk/assembly"
)

const policyFile = "../policy.yaml"

func main() {
fmt.Printf("%s\n", repeat("=", 62))
fmt.Printf(" Agent Assembly — Policy Enforcement Scenario (Go)\n")
fmt.Printf("%s\n\n", repeat("=", 62))

cfg, err := loadPolicyConfig(policyFile)
if err != nil {
fmt.Printf("ERROR: %v\n", err)
return
}

fmt.Printf("Policy loaded from policy.yaml (%d rules, default: %s)\n", len(cfg.Rules), cfg.DefaultAction)
for _, r := range cfg.Rules {
icon := "ALLOW"
if r.Action == "deny" {
icon = "DENY "
}
fmt.Printf(" %s %-14s — %s\n", icon, r.Tool, r.Reason)
}
fmt.Println()

client := newPolicyClient(cfg)
tools := assembly.WrapTools(
[]assembly.Tool{
&readConfigTool{},
&listAgentsTool{},
&deleteAgentTool{},
&sendEmailTool{},
},
client,
)

inputs := map[string]string{
"read_config": "database.host",
"list_agents": "",
"delete_agent": "agent-001",
"send_email": "to=admin@example.com subject=Hello",
}

fmt.Println("Running governed tool calls:")
fmt.Printf("%s\n", repeat("-", 44))
allowed, denied := 0, 0
for _, tool := range tools {
input := inputs[tool.Name()]
fmt.Printf(" → %s(%q)\n", tool.Name(), input)
result, err := tool.Call(context.Background(), input)
if err != nil {
var pve *assembly.PolicyViolationError
if errors.As(err, &pve) {
fmt.Printf(" ❌ DENIED — %v\n", pve)
denied++
} else {
fmt.Printf(" ⚠️ ERROR — %v\n", err)
}
} else {
fmt.Printf(" ✅ ALLOWED — %s\n", result)
allowed++
}
fmt.Println()
}
fmt.Printf("%d tool calls: %d allowed, %d denied.\n", allowed+denied, allowed, denied)
}

func repeat(s string, n int) string {
result := ""
for i := 0; i < n; i++ {
result += s
}
return result
}
119 changes: 119 additions & 0 deletions scenarios/policy-enforcement/go/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"context"
"errors"
"testing"

"github.com/AI-agent-assembly/go-sdk/assembly"
)

const testPolicyFile = "../policy.yaml"

func loadTestClient(t *testing.T) *policyClient {
t.Helper()
cfg, err := loadPolicyConfig(testPolicyFile)
if err != nil {
t.Fatalf("loadPolicyConfig: %v", err)
}
return newPolicyClient(cfg)
}

func TestReadConfigIsAllowed(t *testing.T) {
t.Parallel()
client := loadTestClient(t)
tools := assembly.WrapTools([]assembly.Tool{&readConfigTool{}}, client)

result, err := tools[0].Call(context.Background(), "database.host")
if err != nil {
t.Fatalf("expected allowed, got error: %v", err)
}
if result != "localhost:5432" {
t.Fatalf("unexpected result: %q", result)
}
}

func TestListAgentsIsAllowed(t *testing.T) {
t.Parallel()
client := loadTestClient(t)
tools := assembly.WrapTools([]assembly.Tool{&listAgentsTool{}}, client)

result, err := tools[0].Call(context.Background(), "")
if err != nil {
t.Fatalf("expected allowed, got error: %v", err)
}
if result == "" {
t.Fatal("expected non-empty agent list")
}
}

func TestDeleteAgentIsDenied(t *testing.T) {
t.Parallel()
client := loadTestClient(t)
tools := assembly.WrapTools([]assembly.Tool{&deleteAgentTool{}}, client)

_, err := tools[0].Call(context.Background(), "agent-001")
if err == nil {
t.Fatal("expected denial error, got nil")
}
var pve *assembly.PolicyViolationError
if !errors.As(err, &pve) {
t.Fatalf("expected PolicyViolationError, got %T: %v", err, err)
}
if pve.ToolName != "delete_agent" {
t.Fatalf("expected ToolName %q, got %q", "delete_agent", pve.ToolName)
}
}

func TestSendEmailIsDenied(t *testing.T) {
t.Parallel()
client := loadTestClient(t)
tools := assembly.WrapTools([]assembly.Tool{&sendEmailTool{}}, client)

_, err := tools[0].Call(context.Background(), "admin@example.com")
if err == nil {
t.Fatal("expected denial error, got nil")
}
var pve *assembly.PolicyViolationError
if !errors.As(err, &pve) {
t.Fatalf("expected PolicyViolationError, got %T: %v", err, err)
}
}

func TestUnknownToolIsDeniedByDefault(t *testing.T) {
t.Parallel()
client := loadTestClient(t)

stubTool := &namedStub{name: "execute_shell"}
tools := assembly.WrapTools([]assembly.Tool{stubTool}, client)

_, err := tools[0].Call(context.Background(), "rm -rf /")
if err == nil {
t.Fatal("expected default-deny for unknown tool, got nil")
}
var pve *assembly.PolicyViolationError
if !errors.As(err, &pve) {
t.Fatalf("expected PolicyViolationError, got %T: %v", err, err)
}
}

func TestLoadPolicyConfigRules(t *testing.T) {
t.Parallel()
cfg, err := loadPolicyConfig(testPolicyFile)
if err != nil {
t.Fatalf("loadPolicyConfig: %v", err)
}
if len(cfg.Rules) < 4 {
t.Fatalf("expected at least 4 rules, got %d", len(cfg.Rules))
}
if cfg.DefaultAction != "deny" {
t.Fatalf("expected default_action=deny, got %q", cfg.DefaultAction)
}
}

// namedStub is a stub tool that always returns "ok" when called.
type namedStub struct{ name string }

func (n *namedStub) Name() string { return n.name }
func (n *namedStub) Description() string { return "stub" }
func (n *namedStub) Call(_ context.Context, _ string) (string, error) { return "ok", nil }
78 changes: 78 additions & 0 deletions scenarios/policy-enforcement/go/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"context"
"fmt"
"os"

"github.com/AI-agent-assembly/go-sdk/assembly"
"gopkg.in/yaml.v3"
)

// policyRule mirrors a rule in policy.yaml.
type policyRule struct {
Tool string `yaml:"tool"`
Action string `yaml:"action"`
Reason string `yaml:"reason"`
}

// policyConfig is the top-level policy.yaml structure.
type policyConfig struct {
Rules []policyRule `yaml:"rules"`
DefaultAction string `yaml:"default_action"`
DefaultReason string `yaml:"default_reason"`
}

// loadPolicyConfig reads and parses ../policy.yaml relative to the working directory.
func loadPolicyConfig(path string) (*policyConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read policy file: %w", err)
}
var cfg policyConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse policy file: %w", err)
}
return &cfg, nil
}

// policyClient enforces per-tool allow/deny rules loaded from policy.yaml.
// It implements assembly.GovernanceClient.
type policyClient struct {
rules map[string]policyRule
defaultAction string
defaultReason string
}

func newPolicyClient(cfg *policyConfig) *policyClient {
rules := make(map[string]policyRule, len(cfg.Rules))
for _, r := range cfg.Rules {
rules[r.Tool] = r
}
return &policyClient{
rules: rules,
defaultAction: cfg.DefaultAction,
defaultReason: cfg.DefaultReason,
}
}

func (p *policyClient) Check(_ context.Context, req assembly.CheckRequest) (assembly.Decision, error) {
if rule, ok := p.rules[req.ToolName]; ok {
if rule.Action == "deny" {
fmt.Printf("[policy] DENIED tool=%s reason=%q\n", req.ToolName, rule.Reason)
return assembly.Decision{Denied: true, Reason: rule.Reason}, nil
}
fmt.Printf("[policy] ALLOWED tool=%s\n", req.ToolName)
return assembly.Decision{Denied: false}, nil
}
// Default: deny unknown tools (fail-closed).
fmt.Printf("[policy] DENIED tool=%s reason=%q\n", req.ToolName, p.defaultReason)
return assembly.Decision{Denied: true, Reason: p.defaultReason}, nil
}

func (p *policyClient) WaitForApproval(_ context.Context, _ assembly.ApprovalRequest) (assembly.Decision, error) {
return assembly.Decision{Denied: false}, nil
}

func (p *policyClient) RecordResult(_ context.Context, _ assembly.RecordRequest) error { return nil }
func (p *policyClient) Close() error { return nil }
Loading
Loading