Control plane for agent execution nodes: deploy one service, scale sandboxed agents with metrics on a shared workspace.
Download
·
Report Bug
·
Go Docs
Beta. dispatch is pre-1.0 and under active development. Interfaces are stabilizing but may change between minor versions; queues are in-memory (at-most-once delivery) and the shared workspace backend is a local directory. Durable brokers and GCS/S3 workspace backends are on the roadmap.
- Deploy a single service and scale its agent execution nodes without changing it
- Producer/consumer execution: tasks flow through a queue; scaling out is adding consumers, locally as goroutines or remotely as Kubernetes pods and serverless containers running
dispatch work - Sandboxed tools: every tool is locked to declared workspace areas and spawn targets; anything not granted is denied, so a compromised tool cannot leak outside its area
- NGAC access control: access is defined in a NIST-style policy machine (users, attributes, associations, prohibitions) and enforced by the sandbox; flat per-tool policies compile into the same graph, full
accessspecs express group grants and overriding denials - Shared workspace: all nodes read and write one storage backend, so state lives in exactly one place
- Self-referential agents: a tool can spawn sub-tasks back into its own deployment (an agent calling another agent), gated by its policy
- Metrics built in: node counts, task throughput, and latency recorded behind a transport-agnostic interface
- Orthogonal interfaces: workspace, sandbox, tool, queue, node, and control plane compose without knowing about each other
curl -fsSL https://raw.githubusercontent.com/urmzd/dispatch/main/install.sh | shDownload a pre-built binary from the releases page.
go get github.com/urmzd/dispatchRun the control plane and exercise it with the built-in sandboxed echo tool:
dispatch serve &
curl -X POST localhost:8484/v1/deployments \
-d '{"name":"echo-service","replicas":3,"policies":[{"tool":"echo","areas":[{"prefix":"echo"}]}]}'
curl -X POST localhost:8484/v1/deployments/echo-service/tasks \
-d '{"tool":"echo","input":"hello"}'
curl -X POST localhost:8484/v1/deployments/echo-service/scale -d '{"replicas":10}'
curl localhost:8484/metricsOr compose the same thing in Go:
// Basic example: compose a control plane from the orthogonal pieces —
// a local workspace, a sandboxed tool, an in-process node factory, and an
// in-memory metrics recorder — then deploy a service, scale it out, submit
// tasks, and print the resulting metrics.
//
// Prerequisites: none. Run with:
//
// go run ./examples/basic/
package main
import (
"bytes"
"context"
"fmt"
"log"
"os"
"sort"
"github.com/urmzd/dispatch/pkg/controlplane"
"github.com/urmzd/dispatch/pkg/metrics"
"github.com/urmzd/dispatch/pkg/node/inproc"
"github.com/urmzd/dispatch/pkg/sandbox"
"github.com/urmzd/dispatch/pkg/task"
"github.com/urmzd/dispatch/pkg/tool"
"github.com/urmzd/dispatch/pkg/workspace"
)
func main() {
ctx := context.Background()
// Shared workspace: every node reads and writes the same backend.
dir, err := os.MkdirTemp("", "dispatch-example-*")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(dir)
ws, err := workspace.NewLocal(dir)
if err != nil {
log.Fatal(err)
}
// One tool. It sees only the workspace view its policy grants below.
registry := tool.NewRegistry()
err = registry.Register(tool.Func("greet", func(ctx context.Context, rt tool.Runtime, in []byte) ([]byte, error) {
out := []byte("hello, " + string(in))
// Allowed: "greetings/..." is inside this tool's area.
if err := rt.Workspace().Write(ctx, "greetings/last", bytes.NewReader(out)); err != nil {
return nil, err
}
// Denied by the sandbox: "secrets/..." is outside the area.
if err := rt.Workspace().Write(ctx, "secrets/steal", bytes.NewReader(out)); err != nil {
fmt.Printf("sandbox blocked the leak: %v\n", err)
}
return out, nil
}))
if err != nil {
log.Fatal(err)
}
// Compose the control plane and deploy one service.
rec := metrics.NewMemory()
plane := controlplane.NewMemory(inproc.NewFactory(registry, ws), rec)
err = plane.Deploy(ctx, controlplane.ServiceSpec{
Name: "greeter",
Replicas: 1,
Policies: []sandbox.Policy{{Tool: "greet", Areas: []sandbox.Area{{Prefix: "greetings"}}}},
})
if err != nil {
log.Fatal(err)
}
// Scale out; submitted tasks are consumed by competing nodes.
if err := plane.Scale(ctx, "greeter", 3); err != nil {
log.Fatal(err)
}
for _, name := range []string{"ada", "grace", "alan"} {
res, err := plane.Submit(ctx, "greeter", task.Task{Tool: "greet", Input: []byte(name)})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s ran on %s: %s\n", name, res.NodeID, res.Output)
}
// Metrics accumulated along the way.
fmt.Println("\nmetrics:")
snap := rec.Snapshot()
keys := make([]string, 0, len(snap))
for k := range snap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf(" %s %g\n", k, snap[k])
}
}See examples/ for more.
Execution scales by replicating consumers, not by touching the control plane. The reference manifests deploy one dispatch serve service and a fleet of dispatch work pods that lease tasks from it over HTTP:
minikube start
eval $(minikube docker-env) && docker build -t dispatch:dev .
kubectl apply -f deploy/k8s/dispatch.yaml
kubectl scale deployment/dispatch-worker --replicas=10Deploy a service with "replicas": -1 (no local nodes) and every task is executed by the worker fleet. An HPA on queue or task metrics gives the same effect for serverless-style autoscaling.
| Example | Description |
|---|---|
basic |
Deploy, scale, submit, and observe with a sandboxed tool |
ngac |
Define access as an NGAC policy graph: attribute grants, prohibitions, spawn objects |
saige |
Run saige AI agents as workloads, including an agent spawning a sub-agent |
Tasks are produced onto per-deployment queues and executed by competing consumers. Each consumer is a node that resolves tools from a registry and confines every call through the deployment's NGAC policy graph: the executing tool is the policy-machine user, workspace keys and spawn targets are the objects, and the default is deny. The packages form a strict one-way dependency graph so each concern can change independently.
See docs/architecture/overview.md for the full architecture guide.
This repo's conventions are available as portable agent skills in skills/.