summaryrefslogtreecommitdiff
path: root/internal/cli/serve.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/cli/serve.go')
-rw-r--r--internal/cli/serve.go43
1 files changed, 39 insertions, 4 deletions
diff --git a/internal/cli/serve.go b/internal/cli/serve.go
index ae42780..f687e14 100644
--- a/internal/cli/serve.go
+++ b/internal/cli/serve.go
@@ -11,6 +11,8 @@ import (
"time"
"github.com/thepeterstone/claudomator/internal/api"
+ "github.com/thepeterstone/claudomator/internal/budget"
+ "github.com/thepeterstone/claudomator/internal/config"
"github.com/thepeterstone/claudomator/internal/executor"
"github.com/thepeterstone/claudomator/internal/notify"
"github.com/thepeterstone/claudomator/internal/storage"
@@ -34,7 +36,7 @@ func newServeCmd() *cobra.Command {
},
}
- cmd.Flags().StringVar(&addr, "addr", ":8484", "listen address")
+ cmd.Flags().StringVar(&addr, "addr", "127.0.0.1:8484", "listen address (loopback by default; set external_bind_allowed=true in config to bind externally)")
cmd.Flags().StringVar(&basePath, "base-path", "/claudomator", "URL prefix the UI is served under (e.g. /claudomator-oss)")
cmd.Flags().StringVar(&workspaceRoot, "workspace-root", "/workspace", "root directory for listing workspaces")
cmd.Flags().StringVar(&cfg.ClaudeImage, "claude-image", cfg.ClaudeImage, "docker image for claude agents")
@@ -44,6 +46,9 @@ func newServeCmd() *cobra.Command {
}
func serve(addr, basePath string) error {
+ if err := config.ValidateBindAddr(addr, cfg.ExternalBindAllowed); err != nil {
+ return err
+ }
if err := cfg.EnsureDirs(); err != nil {
return fmt.Errorf("creating dirs: %w", err)
}
@@ -81,6 +86,10 @@ func serve(addr, basePath string) error {
claudeConfigDir := cfg.ClaudeConfigDir
repoDir, _ := os.Getwd()
+
+ // Shared per-task agent MCP token registry: runners mint tokens; the API
+ // server mounts /mcp and resolves them.
+ agentRegistry := executor.NewRegistry()
runners := map[string]executor.Runner{}
// ContainerRunner: binaries are resolved via PATH inside the container image,
@@ -96,6 +105,7 @@ func serve(addr, basePath string) error {
ClaudeConfigDir: claudeConfigDir,
CredentialSyncCmd: filepath.Join(repoDir, "scripts", "sync-credentials"),
Store: store,
+ Registry: agentRegistry,
}
}
if cfg.Runners.GeminiEnabled() {
@@ -109,6 +119,7 @@ func serve(addr, basePath string) error {
ClaudeConfigDir: claudeConfigDir,
CredentialSyncCmd: filepath.Join(repoDir, "scripts", "sync-credentials"),
Store: store,
+ Registry: agentRegistry,
}
}
if cfg.Runners.ContainerEnabled() {
@@ -122,6 +133,7 @@ func serve(addr, basePath string) error {
ClaudeConfigDir: claudeConfigDir,
CredentialSyncCmd: filepath.Join(repoDir, "scripts", "sync-credentials"),
Store: store,
+ Registry: agentRegistry,
}
}
@@ -136,7 +148,6 @@ func serve(addr, basePath string) error {
logger.Info("local runner registered", "endpoint", cfg.LocalModel.Endpoint, "model", cfg.LocalModel.Model)
}
-
pool := executor.NewPool(cfg.MaxConcurrent, runners, store, logger)
pool.Classifier = &executor.Classifier{
LLM: localClient,
@@ -146,6 +157,23 @@ func serve(addr, basePath string) error {
pool.LLM = localClient
}
+ // Budget accountant: gate paid providers against rolling per-provider caps.
+ // With no limits configured this is a no-op.
+ var accountant *budget.Accountant
+ if len(cfg.Budget.Provider5hUSD) > 0 {
+ window := budget.DefaultWindow
+ if cfg.Budget.Window != "" {
+ if d, derr := time.ParseDuration(cfg.Budget.Window); derr == nil {
+ window = d
+ } else {
+ logger.Warn("invalid budget.window; using default", "value", cfg.Budget.Window, "default", window)
+ }
+ }
+ accountant = budget.New(store, budget.Limits{Window: window, PerProvider: cfg.Budget.Provider5hUSD})
+ pool.Budget = accountant
+ logger.Info("budget gating enabled", "window", window, "limits", cfg.Budget.Provider5hUSD)
+ }
+
if err := store.SeedProjects(); err != nil {
logger.Error("failed to seed projects", "error", err)
}
@@ -154,8 +182,11 @@ func serve(addr, basePath string) error {
pool.RecoverStaleQueued(context.Background())
pool.RecoverStaleBlocked()
- srv := api.NewServer(store, pool, logger, cfg.ClaudeBinaryPath, cfg.GeminiBinaryPath)
+ srv := api.NewServer(store, pool, agentRegistry, logger, cfg.ClaudeBinaryPath, cfg.GeminiBinaryPath)
srv.SetBasePath(basePath)
+ if accountant != nil {
+ srv.SetBudget(accountant)
+ }
// Configure notifiers: combine webhook (if set) with web push.
notifiers := []notify.Notifier{}
@@ -185,6 +216,11 @@ func serve(addr, basePath string) error {
}
srv.SetGitHubWebhookConfig(cfg.WebhookSecret, cfg.Projects)
+ if cfg.APIToken != "" {
+ srv.SetAPIToken(cfg.APIToken)
+ logger.Info("API token configured; chatbot MCP endpoint enabled at /chatbot/mcp")
+ }
+
// Register scripts.
wd, _ := os.Getwd()
srv.SetScripts(api.ScriptRegistry{
@@ -238,4 +274,3 @@ func serve(addr, basePath string) error {
}
return nil
}
-