Files
hyperguild/internal/exec/executor.go
Mathias Bergqvist 48d2d7dd73 fix: remove committed binary and stop leaking API key in prompt
Remove cmd/supervisor/supervisor binary from git (was accidentally
committed) and add it to .gitignore. Move LITELLM_API_KEY from the
prompt string into the subprocess env, preventing it from appearing
in error log output when JSON parsing fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 08:49:44 +02:00

101 lines
2.7 KiB
Go

package exec
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
// Config holds executor configuration.
type Config struct {
ClaudeBinary string // path to claude binary, defaults to "claude"
SystemPrompt string // contents of supervisor CLAUDE.md
Timeout time.Duration // per-invocation timeout, default 120s
LiteLLMBaseURL string // passed to Claude so it can delegate to Ollama
LiteLLMAPIKey string // passed to Claude for LiteLLM auth
}
// Request is the input to a single supervisor invocation.
type Request struct {
SkillPrompt string // skill-specific discipline (e.g. tdd.md contents)
TaskPrompt string // the specific task (phase, project_root, spec, model)
Model string // resolved model name, passed in task prompt
Tools string // comma-separated allowed tools, default "Bash,Read,Write"
}
// Executor spawns a claude instance and captures its structured JSON output.
type Executor struct {
cfg Config
}
func New(cfg Config) *Executor {
if cfg.ClaudeBinary == "" {
cfg.ClaudeBinary = "claude"
}
if cfg.Timeout == 0 {
cfg.Timeout = 120 * time.Second
}
return &Executor{cfg: cfg}
}
func (e *Executor) Run(ctx context.Context, req Request) (Result, error) {
ctx, cancel := context.WithTimeout(ctx, e.cfg.Timeout)
defer cancel()
tools := req.Tools
if tools == "" {
tools = "Bash,Read,Write"
}
// Build the full prompt: system rules + skill rules + infra context + task.
// LITELLM_API_KEY is injected as a subprocess env var, not in the prompt,
// to prevent it appearing in error log output.
litellmCtx := fmt.Sprintf("LITELLM_BASE_URL: %s", e.cfg.LiteLLMBaseURL)
prompt := strings.Join([]string{
e.cfg.SystemPrompt,
"---",
req.SkillPrompt,
"---",
litellmCtx,
"---",
req.TaskPrompt,
}, "\n\n")
args := []string{
"--print",
"--bare",
"--permission-mode", "bypassPermissions",
"--tools", tools,
"--json-schema", Schema,
"--output-format", "text",
prompt,
}
cmd := exec.CommandContext(ctx, e.cfg.ClaudeBinary, args...)
cmd.Env = append(os.Environ(), "LITELLM_API_KEY="+e.cfg.LiteLLMAPIKey)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if ctx.Err() != nil {
return Result{}, fmt.Errorf("timeout after %s", e.cfg.Timeout)
}
return Result{}, fmt.Errorf("claude exited with error: %w — stderr: %s", err, stderr.String())
}
var r Result
if err := json.Unmarshal(stdout.Bytes(), &r); err != nil {
return Result{}, fmt.Errorf("parse result JSON: %w — raw output: %s", err, stdout.String())
}
if err := r.Validate(); err != nil {
return Result{}, fmt.Errorf("invalid result: %w", err)
}
return r, nil
}