// Package harness talks to the harness runner (design 5.2): proposals, approval, execution, catalogue, scenarios. package harness import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type Client struct { BaseURL string // http://10.20.0.11:8080 HTTP *http.Client } func New(base string) *Client { return &Client{BaseURL: strings.TrimRight(base, "/"), HTTP: &http.Client{Timeout: 11 * time.Minute}} } func (c *Client) call(ctx context.Context, method, path string, body any, out any) (int, error) { var rd io.Reader if body != nil { b, _ := json.Marshal(body) rd = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, rd) if err != nil { return 0, err } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.HTTP.Do(req) if err != nil { return 0, err } defer resp.Body.Close() rb, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) if out != nil && len(rb) > 0 { if err := json.Unmarshal(rb, out); err != nil { return resp.StatusCode, fmt.Errorf("harness %s %s: status %d, body not JSON: %s", method, path, resp.StatusCode, truncate(string(rb), 300)) } } if resp.StatusCode/100 != 2 { return resp.StatusCode, fmt.Errorf("harness %s %s: status %d: %s", method, path, resp.StatusCode, truncate(string(rb), 300)) } return resp.StatusCode, nil } type Proposal struct { Token string `json:"token"` ExpiresIn float64 `json:"expires_in_s"` Approved bool `json:"approved"` } // Propose registers a layer-2 proposal (POST /proposals); the harness records the proposal event and returns the token. func (c *Client) Propose(ctx context.Context, runID, scenarioID *string, actionID string, target, diagnosis map[string]any, ttl time.Duration) (*Proposal, error) { body := map[string]any{"run_id": runID, "scenario_id": scenarioID, "action_id": actionID, "target": target, "diagnosis": diagnosis} if ttl > 0 { body["ttl_seconds"] = int(ttl.Seconds()) } var p Proposal if _, err := c.call(ctx, "POST", "/proposals", body, &p); err != nil { return nil, err } return &p, nil } // Approve marks the proposal approved (POST /proposals/{token}/approve); the harness records the approval event. func (c *Client) Approve(ctx context.Context, token, by string) error { _, err := c.call(ctx, "POST", "/proposals/"+token+"/approve", map[string]any{"approved_by": by}, nil) return err } // Reject records the rejection (POST /proposals/{token}/reject). func (c *Client) Reject(ctx context.Context, token, by, reason string) error { _, err := c.call(ctx, "POST", "/proposals/"+token+"/reject", map[string]any{"rejected_by": by, "reason": reason}, nil) return err } // Execute runs the catalogue action with the approved token (POST /actions/execute). The body is returned as-is for the // Discord message; a non-2xx status is an error with the body text. func (c *Client) Execute(ctx context.Context, token, actionID string, target map[string]any, runID, scenarioID *string, note string) (map[string]any, error) { body := map[string]any{"action_id": actionID, "target": target, "token": token, "run_id": runID, "scenario_id": scenarioID, "note": note} var out map[string]any _, err := c.call(ctx, "POST", "/actions/execute", body, &out) return out, err } type CatalogEntry struct { ActionID string `json:"action_id"` Layer string `json:"layer"` Exec bool `json:"harness_executes"` } func (c *Client) Catalog(ctx context.Context) ([]CatalogEntry, error) { var out []CatalogEntry _, err := c.call(ctx, "GET", "/actions/catalog", nil, &out) return out, err } type Scenario struct { ID string `json:"scenario_id"` Label string `json:"ground_truth_label"` } func (c *Client) Scenarios(ctx context.Context) ([]Scenario, error) { var out []Scenario _, err := c.call(ctx, "GET", "/scenarios", nil, &out) return out, err } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] + "..." }