// ccl-delegate is a thin CLI wrapper around internal/secutools. It exists // because the dispatcher that scans agent-queue inboxes is written in // bash (dev-management/agent-orchestrator/dispatcher.sh). Rather than // duplicate the HTTP contract in bash + curl + jq, we shell out to this // binary for every delegation action. // // Subcommands: // // ccl-delegate submit --prompt=... [--preferred-ai=gpu] [--priority=default] // Prints JSON to stdout: {"job_id": "...", "status": "pending"}. // // ccl-delegate get --job-id=xyz // Prints JSON to stdout: {"job_id":"...","status":"...","provider":"..."} // // ccl-delegate result --job-id=xyz [--timeout=5m] // Waits for completion (timeout-bounded) and prints result JSON. // // Auth: reads CCL_SECUTOOLS_API_KEY first, then SECUTOOLS_API_KEY. URL // override via CCL_SECUTOOLS_URL (default https://api.secutools.secuaas.ovh), // or CCL_SECUTOOLS_MOCK_URL for tests. package main import ( "context" "encoding/json" "flag" "fmt" "os" "time" "forge.secuaas.ovh/olivier/claude-failover/internal/secutools" ) const defaultURL = "https://api.secutools.secuaas.ovh" func main() { if len(os.Args) < 2 { usage() os.Exit(2) } sub := os.Args[1] args := os.Args[2:] switch sub { case "submit": runSubmit(args) case "get": runGet(args) case "result": runResult(args) case "decide": runDecide(args) case "-h", "--help", "help": usage() os.Exit(0) default: fmt.Fprintf(os.Stderr, "ccl-delegate: unknown subcommand %q\n", sub) usage() os.Exit(2) } } func usage() { fmt.Fprint(os.Stderr, `ccl-delegate — submit/poll secutools jobs from bash usage: ccl-delegate submit --prompt=... [--preferred-ai=gpu] [--priority=default] [--source=...] ccl-delegate get --job-id=xyz ccl-delegate result --job-id=xyz [--timeout=5m] ccl-delegate decide --frontmatter=path/to/task.md env: CCL_SECUTOOLS_API_KEY (preferred) or SECUTOOLS_API_KEY CCL_SECUTOOLS_URL (default https://api.secutools.secuaas.ovh) CCL_SECUTOOLS_MOCK_URL (overrides CCL_SECUTOOLS_URL, for tests) `) } func newClient() *secutools.HTTPClient { url := os.Getenv("CCL_SECUTOOLS_MOCK_URL") if url == "" { url = os.Getenv("CCL_SECUTOOLS_URL") } if url == "" { url = defaultURL } key := os.Getenv("CCL_SECUTOOLS_API_KEY") if key == "" { key = os.Getenv("SECUTOOLS_API_KEY") } if key == "" { fmt.Fprintln(os.Stderr, "ccl-delegate: missing CCL_SECUTOOLS_API_KEY / SECUTOOLS_API_KEY") os.Exit(4) } return secutools.NewHTTPClient(url, key, nil) } func runSubmit(args []string) { fs := flag.NewFlagSet("submit", flag.ExitOnError) prompt := fs.String("prompt", "", "prompt text (required)") preferred := fs.String("preferred-ai", "", "preferred provider (gpu|gemini|claude-api|claude-opus|claude-sonnet|claude-haiku|auto)") priority := fs.String("priority", "default", "priority (critical|high|default|low)") jobType := fs.String("type", string(secutools.TypeAnalyze), "job type (ai:analyze|ai:batch|ai:report|ai:correlate)") source := fs.String("source", "ccl-delegate", "source identifier") maxTokens := fs.Int("max-tokens", 0, "max tokens (0 = server default)") _ = fs.Parse(args) if *prompt == "" { fmt.Fprintln(os.Stderr, "ccl-delegate submit: --prompt is required") os.Exit(2) } c := newClient() ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() resp, err := c.SubmitJob(ctx, &secutools.JobRequest{ Type: secutools.JobType(*jobType), Priority: secutools.Priority(*priority), Prompt: *prompt, PreferredAI: *preferred, Source: *source, MaxTokens: *maxTokens, }) if err != nil { fmt.Fprintf(os.Stderr, "submit: %v\n", err) os.Exit(1) } writeJSON(resp) } func runGet(args []string) { fs := flag.NewFlagSet("get", flag.ExitOnError) id := fs.String("job-id", "", "job id (required)") _ = fs.Parse(args) if *id == "" { fmt.Fprintln(os.Stderr, "ccl-delegate get: --job-id required") os.Exit(2) } c := newClient() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() st, err := c.GetJob(ctx, *id) if err != nil { fmt.Fprintf(os.Stderr, "get: %v\n", err) os.Exit(1) } writeJSON(st) } func runResult(args []string) { fs := flag.NewFlagSet("result", flag.ExitOnError) id := fs.String("job-id", "", "job id (required)") timeout := fs.Duration("timeout", 5*time.Minute, "wait timeout") _ = fs.Parse(args) if *id == "" { fmt.Fprintln(os.Stderr, "ccl-delegate result: --job-id required") os.Exit(2) } c := newClient() ctx := context.Background() res, err := c.WaitForResult(ctx, *id, *timeout) if err != nil { fmt.Fprintf(os.Stderr, "result: %v\n", err) os.Exit(1) } writeJSON(res) } func runDecide(args []string) { fs := flag.NewFlagSet("decide", flag.ExitOnError) frontmatterPath := fs.String("frontmatter", "", "path to task .md with YAML frontmatter") _ = fs.Parse(args) if *frontmatterPath == "" { fmt.Fprintln(os.Stderr, "ccl-delegate decide: --frontmatter required") os.Exit(2) } data, err := os.ReadFile(*frontmatterPath) if err != nil { fmt.Fprintf(os.Stderr, "decide: read %s: %v\n", *frontmatterPath, err) os.Exit(1) } fm := parseFrontmatterMap(data) fmt.Println(secutools.DecideProvider(fm)) } // parseFrontmatterMap extracts a flat key/value map from a YAML frontmatter // block delimited by "---". Only top-level scalars; nested structures are // ignored. Intentionally dependency-free (no yaml unmarshal) so this CLI // stays tiny and predictable. func parseFrontmatterMap(content []byte) map[string]any { out := map[string]any{} s := string(content) if len(s) < 4 || s[:4] != "---\n" { return out } end := -1 for i := 4; i < len(s)-3; i++ { if s[i] == '\n' && s[i+1:i+4] == "---" && (i+4 == len(s) || s[i+4] == '\n') { end = i break } } if end < 0 { return out } body := s[4:end] start := 0 for i := 0; i <= len(body); i++ { if i == len(body) || body[i] == '\n' { line := body[start:i] start = i + 1 // find "key: value" colon := -1 for j := 0; j < len(line); j++ { if line[j] == ':' { colon = j break } } if colon <= 0 { continue } key := trim(line[:colon]) val := trim(line[colon+1:]) if key == "" { continue } // strip surrounding quotes if len(val) >= 2 && ((val[0] == '"' && val[len(val)-1] == '"') || (val[0] == '\'' && val[len(val)-1] == '\'')) { val = val[1 : len(val)-1] } // bool coercion switch val { case "true": out[key] = true case "false": out[key] = false default: out[key] = val } } } return out } func trim(s string) string { start := 0 for start < len(s) && (s[start] == ' ' || s[start] == '\t') { start++ } end := len(s) for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\r') { end-- } return s[start:end] } func writeJSON(v any) { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if err := enc.Encode(v); err != nil { fmt.Fprintf(os.Stderr, "encode: %v\n", err) os.Exit(1) } }