Adds the project_create tool to the routing pod that automates the
"new project" bootstrap end-to-end from claude.ai. Gitea-first
architecture: GitHub receives the repo only via push-mirror, never
via a direct GitHub API call from this server.
Four sequential calls to the gitea-mcp server (configured via
GITEA_MCP_URL):
1. create_project_from_template — Gitea repo from
template-go-{agent,web} per the 'stack' arg
2. repo_mirror_push (action=add) — push-mirror to
github.com/<GITHUB_OWNER>/<name>.git, interval 8h, sync_on_commit
3. file_write_branch — k3s/staging/<name>/namespace.yaml committed
on a staging/<name> branch in the infra repo
4. issue_create — experiment brief (hypothesis + description + stack
+ provisioning log) on the new repo, returns the issue_url
Returns gitea_url, github_url, issue_url, next_steps. The next_steps
string is the exact shell sequence the operator runs locally to
clone, scaffold via local-dev 'task new-project', and push.
Idempotency: create_project_from_template + repo_mirror_push +
file_write_branch all return JSON-RPC code -32003 (Conflict) when
their target already exists; the orchestrator swallows the conflict
and continues. Re-running on an existing repo restates the brief in
a fresh issue.
Error handling: on any non-conflict downstream failure the response
returns {reached: ["<step>",...], failed_step: "<step>"} alongside
a JSON-RPC error. No rollback — partial state stays so the operator
can resume manually.
New env vars (all optional except GITEA_MCP_URL):
GITEA_MCP_URL enables the tool
GITEA_MCP_TOKEN bearer auth for gitea-mcp
GITEA_OWNER default mathias
GITHUB_OWNER default mathiasb
INFRA_REPO default infra
GITHUB_PAT repo scope, used as mirror remote_password; never logged
Without GITEA_MCP_URL set, the tool is not registered and the
routing pod starts normally (degrades open).
internal/mcpclient/: new minimal JSON-RPC tools/call client with
bearer auth, used by project_create. Unwraps MCP's
content[0].text envelope and surfaces typed errors via mcpclient.Error.
Tests: table-driven against an httptest fake gitea-mcp covering happy
path (4-step success + correct PATCH-style arg shapes), idempotent
repo-exists, mirror failure (partial-success response with reached=
[create_repo] + failed_step=mirror), infra-commit failure (reached up
to mirror + failed_step=infra_commit), and validation errors.
Closes #10
102 lines
3.6 KiB
Go
102 lines
3.6 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
// RoutingConfig holds the runtime configuration for the routing pod.
|
|
// Separate from Config because the routing pod's surface differs from the supervisor's.
|
|
type RoutingConfig struct {
|
|
Port string // ROUTING_PORT, default 3210
|
|
MCPAuthToken string // ROUTING_MCP_TOKEN, optional bearer token
|
|
LiteLLMBaseURL string // LITELLM_BASE_URL, default http://piguard:4000
|
|
LiteLLMAPIKey string // LITELLM_API_KEY
|
|
BrainURL string // BRAIN_URL, default http://ingestion.supervisor:3300
|
|
FastModel string // HYPERGUILD_FAST_MODEL, default koala/qwen35-9b-fast
|
|
ThinkingModel string // HYPERGUILD_THINKING_MODEL, default iguana/gemma4-26b
|
|
// RouteLocalFloor and RouteLocalCeil intentionally invert the usual
|
|
// floor < ceil mathematical convention: Floor (default 0.90) is the
|
|
// UPPER boundary — at/above it, always route local; Ceil (default 0.70)
|
|
// is the LOWER boundary — below it, always route Claude. The band in
|
|
// between is the 50/50 sample zone. The naming follows the spec's policy
|
|
// vocabulary; see internal/routing/policy.go for the consumer.
|
|
RouteLocalFloor float64 // HYPERGUILD_ROUTE_LOCAL_FLOOR, default 0.90
|
|
RouteLocalCeil float64 // HYPERGUILD_ROUTE_LOCAL_CEIL, default 0.70
|
|
PassRateTTLSeconds int // HYPERGUILD_PASS_RATE_TTL_SECONDS, default 60
|
|
|
|
// project_create configuration. Empty GiteaMCPURL disables the
|
|
// project_create tool registration so the routing pod still starts
|
|
// in environments where it's not wired up.
|
|
GiteaMCPURL string // GITEA_MCP_URL, e.g. http://koala:30340/mcp
|
|
GiteaMCPToken string // GITEA_MCP_TOKEN, bearer for gitea-mcp
|
|
GiteaOwner string // GITEA_OWNER, default mathias
|
|
GitHubOwner string // GITHUB_OWNER, default mathiasb
|
|
InfraRepo string // INFRA_REPO, default infra
|
|
GitHubPAT string // GITHUB_PAT, repo scope; never logged
|
|
}
|
|
|
|
func LoadRouting() (RoutingConfig, error) {
|
|
cfg := RoutingConfig{
|
|
Port: envOr("ROUTING_PORT", "3210"),
|
|
MCPAuthToken: os.Getenv("ROUTING_MCP_TOKEN"),
|
|
LiteLLMBaseURL: envOr("LITELLM_BASE_URL", "http://piguard:4000"),
|
|
LiteLLMAPIKey: os.Getenv("LITELLM_API_KEY"),
|
|
BrainURL: envOr("BRAIN_URL", "http://ingestion.supervisor:3300"),
|
|
FastModel: envOr("HYPERGUILD_FAST_MODEL", "koala/qwen35-9b-fast"),
|
|
ThinkingModel: envOr("HYPERGUILD_THINKING_MODEL", "iguana/gemma4-26b"),
|
|
}
|
|
|
|
floor, err := parseFloatEnv("HYPERGUILD_ROUTE_LOCAL_FLOOR", 0.90)
|
|
if err != nil {
|
|
return RoutingConfig{}, err
|
|
}
|
|
cfg.RouteLocalFloor = floor
|
|
|
|
ceil, err := parseFloatEnv("HYPERGUILD_ROUTE_LOCAL_CEIL", 0.70)
|
|
if err != nil {
|
|
return RoutingConfig{}, err
|
|
}
|
|
cfg.RouteLocalCeil = ceil
|
|
|
|
ttl, err := parseIntEnv("HYPERGUILD_PASS_RATE_TTL_SECONDS", 60)
|
|
if err != nil {
|
|
return RoutingConfig{}, err
|
|
}
|
|
cfg.PassRateTTLSeconds = ttl
|
|
|
|
cfg.GiteaMCPURL = os.Getenv("GITEA_MCP_URL")
|
|
cfg.GiteaMCPToken = os.Getenv("GITEA_MCP_TOKEN")
|
|
cfg.GiteaOwner = envOr("GITEA_OWNER", "mathias")
|
|
cfg.GitHubOwner = envOr("GITHUB_OWNER", "mathiasb")
|
|
cfg.InfraRepo = envOr("INFRA_REPO", "infra")
|
|
cfg.GitHubPAT = os.Getenv("GITHUB_PAT")
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func parseFloatEnv(key string, def float64) (float64, error) {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
return def, nil
|
|
}
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("config: %s: %w", key, err)
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func parseIntEnv(key string, def int) (int, error) {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
return def, nil
|
|
}
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("config: %s: %w", key, err)
|
|
}
|
|
return n, nil
|
|
}
|