* ci: consolidate duplicate workflows and merge CodeQL configs Remove 17 duplicate workflow files that were split copies of the combined originals. Each family (CI, CodeQL, Coverage, PR Build, Alpha Release) had the same job duplicated across separate push/pull_request/schedule/manual trigger files. Merge codeql.yml and codescan.yml into a single codeql.yml with a language matrix covering go, javascript-typescript, python, and actions — matching the previous default setup coverage. Remaining workflows (one per family): - ci.yml (push + PR + manual) - codeql.yml (push + PR + schedule, all languages) - coverage.yml (push + PR + manual) - alpha-release.yml (push + manual) - pr-build.yml (PR + manual) - release.yml (tag push) - agent-verify.yml, auto-label.yml, auto-project.yml Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add collect, config, crypt, plugin packages and fix all lint issues Add four new infrastructure packages with CLI commands: - pkg/config: layered configuration (defaults → file → env → flags) - pkg/crypt: crypto primitives (Argon2id, AES-GCM, ChaCha20, HMAC, checksums) - pkg/plugin: plugin system with GitHub-based install/update/remove - pkg/collect: collection subsystem (GitHub, BitcoinTalk, market, papers, excavate) Fix all golangci-lint issues across the entire codebase (~100 errcheck, staticcheck SA1012/SA1019/ST1005, unused, ineffassign fixes) so that `core go qa` passes with 0 issues. Closes #167, #168, #170, #250, #251, #252, #253, #254, #255, #256 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
322 lines
8 KiB
Go
322 lines
8 KiB
Go
package agentic
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/host-uk/core/pkg/log"
|
|
)
|
|
|
|
// Client is the API client for the core-agentic service.
|
|
type Client struct {
|
|
// BaseURL is the base URL of the API server.
|
|
BaseURL string
|
|
// Token is the authentication token.
|
|
Token string
|
|
// HTTPClient is the HTTP client used for requests.
|
|
HTTPClient *http.Client
|
|
// AgentID is the identifier for this agent when claiming tasks.
|
|
AgentID string
|
|
}
|
|
|
|
// NewClient creates a new agentic API client with the given base URL and token.
|
|
func NewClient(baseURL, token string) *Client {
|
|
return &Client{
|
|
BaseURL: strings.TrimSuffix(baseURL, "/"),
|
|
Token: token,
|
|
HTTPClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewClientFromConfig creates a new client from a Config struct.
|
|
func NewClientFromConfig(cfg *Config) *Client {
|
|
client := NewClient(cfg.BaseURL, cfg.Token)
|
|
client.AgentID = cfg.AgentID
|
|
return client
|
|
}
|
|
|
|
// ListTasks retrieves a list of tasks matching the given options.
|
|
func (c *Client) ListTasks(ctx context.Context, opts ListOptions) ([]Task, error) {
|
|
const op = "agentic.Client.ListTasks"
|
|
|
|
// Build query parameters
|
|
params := url.Values{}
|
|
if opts.Status != "" {
|
|
params.Set("status", string(opts.Status))
|
|
}
|
|
if opts.Priority != "" {
|
|
params.Set("priority", string(opts.Priority))
|
|
}
|
|
if opts.Project != "" {
|
|
params.Set("project", opts.Project)
|
|
}
|
|
if opts.ClaimedBy != "" {
|
|
params.Set("claimed_by", opts.ClaimedBy)
|
|
}
|
|
if opts.Limit > 0 {
|
|
params.Set("limit", strconv.Itoa(opts.Limit))
|
|
}
|
|
if len(opts.Labels) > 0 {
|
|
params.Set("labels", strings.Join(opts.Labels, ","))
|
|
}
|
|
|
|
endpoint := c.BaseURL + "/api/tasks"
|
|
if len(params) > 0 {
|
|
endpoint += "?" + params.Encode()
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, log.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, log.E(op, "request failed", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return nil, log.E(op, "API error", err)
|
|
}
|
|
|
|
var tasks []Task
|
|
if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
|
|
return nil, log.E(op, "failed to decode response", err)
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
// GetTask retrieves a single task by its ID.
|
|
func (c *Client) GetTask(ctx context.Context, id string) (*Task, error) {
|
|
const op = "agentic.Client.GetTask"
|
|
|
|
if id == "" {
|
|
return nil, log.E(op, "task ID is required", nil)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s/api/tasks/%s", c.BaseURL, url.PathEscape(id))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, log.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, log.E(op, "request failed", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return nil, log.E(op, "API error", err)
|
|
}
|
|
|
|
var task Task
|
|
if err := json.NewDecoder(resp.Body).Decode(&task); err != nil {
|
|
return nil, log.E(op, "failed to decode response", err)
|
|
}
|
|
|
|
return &task, nil
|
|
}
|
|
|
|
// ClaimTask claims a task for the current agent.
|
|
func (c *Client) ClaimTask(ctx context.Context, id string) (*Task, error) {
|
|
const op = "agentic.Client.ClaimTask"
|
|
|
|
if id == "" {
|
|
return nil, log.E(op, "task ID is required", nil)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s/api/tasks/%s/claim", c.BaseURL, url.PathEscape(id))
|
|
|
|
// Include agent ID in the claim request if available
|
|
var body io.Reader
|
|
if c.AgentID != "" {
|
|
data, _ := json.Marshal(map[string]string{"agent_id": c.AgentID})
|
|
body = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
|
|
if err != nil {
|
|
return nil, log.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, log.E(op, "request failed", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return nil, log.E(op, "API error", err)
|
|
}
|
|
|
|
// Read body once to allow multiple decode attempts
|
|
bodyData, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, log.E(op, "failed to read response", err)
|
|
}
|
|
|
|
// Try decoding as ClaimResponse first
|
|
var result ClaimResponse
|
|
if err := json.Unmarshal(bodyData, &result); err == nil && result.Task != nil {
|
|
return result.Task, nil
|
|
}
|
|
|
|
// Try decoding as just a Task for simpler API responses
|
|
var task Task
|
|
if err := json.Unmarshal(bodyData, &task); err != nil {
|
|
return nil, log.E(op, "failed to decode response", err)
|
|
}
|
|
|
|
return &task, nil
|
|
}
|
|
|
|
// UpdateTask updates a task with new status, progress, or notes.
|
|
func (c *Client) UpdateTask(ctx context.Context, id string, update TaskUpdate) error {
|
|
const op = "agentic.Client.UpdateTask"
|
|
|
|
if id == "" {
|
|
return log.E(op, "task ID is required", nil)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s/api/tasks/%s", c.BaseURL, url.PathEscape(id))
|
|
|
|
data, err := json.Marshal(update)
|
|
if err != nil {
|
|
return log.E(op, "failed to marshal update", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint, bytes.NewReader(data))
|
|
if err != nil {
|
|
return log.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return log.E(op, "request failed", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return log.E(op, "API error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CompleteTask marks a task as completed with the given result.
|
|
func (c *Client) CompleteTask(ctx context.Context, id string, result TaskResult) error {
|
|
const op = "agentic.Client.CompleteTask"
|
|
|
|
if id == "" {
|
|
return log.E(op, "task ID is required", nil)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s/api/tasks/%s/complete", c.BaseURL, url.PathEscape(id))
|
|
|
|
data, err := json.Marshal(result)
|
|
if err != nil {
|
|
return log.E(op, "failed to marshal result", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
|
|
if err != nil {
|
|
return log.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return log.E(op, "request failed", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return log.E(op, "API error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setHeaders adds common headers to the request.
|
|
func (c *Client) setHeaders(req *http.Request) {
|
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", "core-agentic-client/1.0")
|
|
}
|
|
|
|
// checkResponse checks if the response indicates an error.
|
|
func (c *Client) checkResponse(resp *http.Response) error {
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
return nil
|
|
}
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
// Try to parse as APIError
|
|
var apiErr APIError
|
|
if err := json.Unmarshal(body, &apiErr); err == nil && apiErr.Message != "" {
|
|
apiErr.Code = resp.StatusCode
|
|
return &apiErr
|
|
}
|
|
|
|
// Return generic error
|
|
return &APIError{
|
|
Code: resp.StatusCode,
|
|
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, http.StatusText(resp.StatusCode)),
|
|
Details: string(body),
|
|
}
|
|
}
|
|
|
|
// Ping tests the connection to the API server.
|
|
func (c *Client) Ping(ctx context.Context) error {
|
|
const op = "agentic.Client.Ping"
|
|
|
|
endpoint := c.BaseURL + "/api/health"
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return log.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return log.E(op, "request failed", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return log.E(op, fmt.Sprintf("server returned status %d", resp.StatusCode), nil)
|
|
}
|
|
|
|
return nil
|
|
}
|