diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-06-03 23:01:39 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-06-03 23:01:39 +0000 |
| commit | 68cf1e24dd2ca2612b72babc4b35280cd3512f92 (patch) | |
| tree | 14b73f21570a2f42ae729ca7e9676de6628d6819 /internal/cli/serve.go | |
| parent | b28cfc6ff288d083f6c8e9c055b69bfcadbceccc (diff) | |
| parent | 7388337d3be5cc7f65ef547e30b2e39884dd165b (diff) | |
merge: integrate oss branch — budget gating, MCP back-channel, events, loopback-only bind
Key features from OSS branch:
- Budget gating: rolling per-provider spend caps with BUDGET_EXCEEDED task state
- Agent MCP back-channel: runners mint tokens; /mcp endpoint resolves them
- Chatbot MCP server at /chatbot/mcp (requires api_token in config)
- Event timeline: unified event stream replacing ad-hoc question/summary flows
- Loopback-only default bind (127.0.0.1:8484); external_bind_allowed=true to expose
- repository_url required on task creation (enforces traceability)
- Auto-checker: spawns verification task after each execution
Conflict resolutions:
- serve.go: keep cfg.Runners.XEnabled() conditional registration from main + agentRegistry from oss
- config.go: keep RunnersConfig from main + BudgetConfig/ExternalBindAllowed from oss
- server.go: handleStaticFiles (base-path rewrite) kept; deduplicated duplicate from both sides
- executor/claude.go: accepted oss deletion (ClaudeRunner replaced by ContainerRunner)
- container_test.go: added noopChannel + AgentChannel parameter to Run call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/cli/serve.go')
| -rw-r--r-- | internal/cli/serve.go | 43 |
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 } - |
