feat(agentic): enforce dispatch entitlement

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 14:25:32 +00:00
parent 460af585ed
commit eab30f578e
2 changed files with 100 additions and 0 deletions

View file

@ -20,11 +20,25 @@ import (
//
// ))
func (s *PrepSubsystem) handleDispatch(ctx context.Context, options core.Options) core.Result {
if s.Core() != nil {
entitlement := s.Core().Entitled("agentic.concurrency", 1)
if !entitlement.Allowed {
reason := core.Trim(entitlement.Reason)
if reason == "" {
reason = "dispatch concurrency not available"
}
return core.Result{Value: core.E("agentic.dispatch", reason, nil), OK: false}
}
}
input := dispatchInputFromOptions(options)
_, out, err := s.dispatch(ctx, nil, input)
if err != nil {
return core.Result{Value: err, OK: false}
}
if s.Core() != nil {
s.Core().RecordUsage("agentic.dispatch")
}
return core.Result{Value: out, OK: true}
}

View file

@ -4,10 +4,14 @@ package agentic
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"dappco.re/go/agent/pkg/lib"
core "dappco.re/go/core"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -22,6 +26,88 @@ func TestActions_HandleDispatch_Good(t *testing.T) {
assert.False(t, r.OK)
}
func TestActions_HandleDispatch_Bad_EntitlementDenied(t *testing.T) {
c := core.New(core.WithService(ProcessRegister))
c.ServiceStartup(context.Background(), nil)
c.SetEntitlementChecker(func(action string, _ int, _ context.Context) core.Entitlement {
if action == "agentic.concurrency" {
return core.Entitlement{Allowed: false, Reason: "dispatch limit reached"}
}
return core.Entitlement{Allowed: true, Unlimited: true}
})
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
r := s.handleDispatch(context.Background(), core.NewOptions(
core.Option{Key: "repo", Value: "go-io"},
core.Option{Key: "task", Value: "fix tests"},
))
assert.False(t, r.OK)
err, ok := r.Value.(error)
require.True(t, ok)
assert.Contains(t, err.Error(), "dispatch limit reached")
}
func TestActions_HandleDispatch_Good_RecordsUsage(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
t.Setenv("CORE_BRAIN_KEY", "")
forgeSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(core.JSONMarshalString(map[string]any{
"title": "Issue",
"body": "Fix",
})))
}))
t.Cleanup(forgeSrv.Close)
s := newPrepWithProcess()
s.Core().SetEntitlementChecker(func(_ string, _ int, _ context.Context) core.Entitlement {
return core.Entitlement{Allowed: true, Unlimited: true}
})
srcRepo := core.JoinPath(t.TempDir(), "core", "go-io")
require.True(t, fs.EnsureDir(srcRepo).OK)
process := s.Core().Process()
require.True(t, process.RunIn(context.Background(), srcRepo, "git", "init", "-b", "main").OK)
require.True(t, process.RunIn(context.Background(), srcRepo, "git", "config", "user.name", "Test").OK)
require.True(t, process.RunIn(context.Background(), srcRepo, "git", "config", "user.email", "test@test.com").OK)
require.True(t, fs.Write(core.JoinPath(srcRepo, "go.mod"), "module test\ngo 1.22\n").OK)
require.True(t, fs.Write(core.JoinPath(srcRepo, "README.md"), "hello\n").OK)
require.True(t, process.RunIn(context.Background(), srcRepo, "git", "add", ".").OK)
require.True(t, process.RunIn(
context.Background(),
srcRepo,
"git",
"commit",
"-m", "initial commit",
).OK)
recorded := 0
s.Core().SetUsageRecorder(func(action string, qty int, _ context.Context) {
if action == "agentic.dispatch" && qty == 1 {
recorded++
}
})
s.forge = forge.NewForge(forgeSrv.URL, "tok")
s.codePath = core.PathDir(core.PathDir(srcRepo))
r := s.handleDispatch(context.Background(), core.NewOptions(
core.Option{Key: "repo", Value: "go-io"},
core.Option{Key: "issue", Value: 42},
core.Option{Key: "task", Value: "fix tests"},
core.Option{Key: "dry-run", Value: true},
))
require.Truef(t, r.OK, "dispatch failed: %#v", r.Value)
assert.Equal(t, 1, recorded)
}
func TestActions_HandleStatus_Good(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
s := newPrepWithProcess()