Add route declarations and automatic port allocation to MCP agent

Service definitions can now declare routes per component instead of
manual port mappings:

  [[components.routes]]
  name = "rest"
  port = 8443
  mode = "l4"

The agent allocates free host ports at deploy time and injects
$PORT/$PORT_<NAME> env vars into containers. Backward compatible:
components with old-style ports= work unchanged.

Changes:
- Proto: RouteSpec message, routes + env fields on ComponentSpec
- Servicedef: RouteDef parsing and validation from TOML
- Registry: component_routes table with host_port tracking
- Runtime: Env field on ContainerSpec, -e flag in BuildRunArgs
- Agent: PortAllocator (random 10000-60000, availability check),
  deploy wiring for route→port mapping and env injection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 01:04:47 -07:00
parent 503c52dc26
commit 777ba8a0e1
14 changed files with 1101 additions and 222 deletions

View File

@@ -0,0 +1,65 @@
package agent
import (
"testing"
)
func TestPortAllocator_Allocate(t *testing.T) {
pa := NewPortAllocator()
port, err := pa.Allocate()
if err != nil {
t.Fatalf("allocate: %v", err)
}
if port < portRangeMin || port >= portRangeMax {
t.Fatalf("port %d out of range [%d, %d)", port, portRangeMin, portRangeMax)
}
}
func TestPortAllocator_NoDuplicates(t *testing.T) {
pa := NewPortAllocator()
ports := make(map[int]bool)
for range 20 {
port, err := pa.Allocate()
if err != nil {
t.Fatalf("allocate: %v", err)
}
if ports[port] {
t.Fatalf("duplicate port allocated: %d", port)
}
ports[port] = true
}
}
func TestPortAllocator_Release(t *testing.T) {
pa := NewPortAllocator()
port, err := pa.Allocate()
if err != nil {
t.Fatalf("allocate: %v", err)
}
pa.Release(port)
// After release, the port should no longer be tracked as allocated.
pa.mu.Lock()
if pa.allocated[port] {
t.Fatal("port should not be tracked after release")
}
pa.mu.Unlock()
}
func TestPortAllocator_PortIsFree(t *testing.T) {
pa := NewPortAllocator()
port, err := pa.Allocate()
if err != nil {
t.Fatalf("allocate: %v", err)
}
// The port should be free (we only track it, we don't hold the listener).
if !isPortFree(port) {
t.Fatalf("allocated port %d should be free on the system", port)
}
}