70a813edfa
- whitelist HTML sanitizer (stdlib tokenizer) with XSS vector tests - developer board: pool-scoped visibility, customer/search/minBounty/sort filters, stale-task age badges, competing-claims visibility setting, saved filters in profile extras - claims: request/withdraw (developer), approve/decline (consultant) with notifications to winners and losers; unassign/abandon back to board - work tracking: start, sanitized comments with @mention notifications, time logging, submit for review - review queue + review with per-AC checklist stored on the timeline; approve writes the immutable bountyAwards row (human assignees only) - assign-to-AI: §5.2 job submission, HMAC-verified callback endpoint, idempotent by jobId, artifacts downloaded into GridFS, failure path keeps the task assigned with timeline + notification - notifications API + bell with unread badge, dropdown, page, WS toasts - pages: bounty board, my-tasks kanban, task detail (role-driven actions, review dialog, AI dialog), review queue, developer pool Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
3.3 KiB
Go
113 lines
3.3 KiB
Go
// Package workperform is the app-side client of the Work Performer Service
|
|
// (§5.2) — fully independent from the atomizer: separate base URL, token,
|
|
// and circuit breaker.
|
|
package workperform
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"time"
|
|
|
|
"bountyboard/internal/extsvc"
|
|
)
|
|
|
|
type Attachment struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
MimeType string `json:"mimeType"`
|
|
}
|
|
|
|
type JobContext struct {
|
|
RepositoryURL string `json:"repositoryUrl,omitempty"`
|
|
Branch string `json:"branch,omitempty"`
|
|
Instructions string `json:"instructions,omitempty"`
|
|
}
|
|
|
|
type JobRequest struct {
|
|
TaskID string `json:"taskId"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
AcceptanceCriteria []string `json:"acceptanceCriteria"`
|
|
Attachments []Attachment `json:"attachments"`
|
|
Links []string `json:"links"`
|
|
Context JobContext `json:"context"`
|
|
CallbackURL string `json:"callbackUrl"`
|
|
}
|
|
|
|
type JobAccepted struct {
|
|
JobID string `json:"jobId"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type JobStatus struct {
|
|
JobID string `json:"jobId"`
|
|
Status string `json:"status"` // queued | running | succeeded | failed
|
|
StartedAt string `json:"startedAt"`
|
|
FinishedAt string `json:"finishedAt"`
|
|
}
|
|
|
|
// Callback is the §5.2 service→app result payload.
|
|
type Callback struct {
|
|
JobID string `json:"jobId"`
|
|
TaskID string `json:"taskId"`
|
|
Status string `json:"status"` // succeeded | failed
|
|
Summary string `json:"summary"`
|
|
Artifacts []struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
} `json:"artifacts"`
|
|
Log string `json:"log"`
|
|
}
|
|
|
|
type Client struct {
|
|
c *extsvc.Client
|
|
token string
|
|
}
|
|
|
|
func New(baseURL func() string, token string, timeout time.Duration) *Client {
|
|
return &Client{c: extsvc.NewClient("work-performer", baseURL, token, timeout), token: token}
|
|
}
|
|
|
|
func (w *Client) BreakerState() string { return w.c.BreakerState() }
|
|
func (w *Client) Healthy(ctx context.Context) error { return w.c.Healthy(ctx) }
|
|
|
|
func (w *Client) Submit(ctx context.Context, req JobRequest) (*JobAccepted, error) {
|
|
var out JobAccepted
|
|
if err := w.c.Call(ctx, http.MethodPost, "/v1/jobs", req, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
func (w *Client) Status(ctx context.Context, jobID string) (*JobStatus, error) {
|
|
var out JobStatus
|
|
if err := w.c.Call(ctx, http.MethodGet, "/v1/jobs/"+jobID, nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// Cancel is best effort (§5.2).
|
|
func (w *Client) Cancel(ctx context.Context, jobID string) error {
|
|
return w.c.Call(ctx, http.MethodDelete, "/v1/jobs/"+jobID, nil, nil)
|
|
}
|
|
|
|
// VerifySignature checks the §5.2 callback HMAC:
|
|
// X-Signature = hex(hmac-sha256(body, WORK_PERFORMER_TOKEN)).
|
|
func VerifySignature(token string, body []byte, signature string) bool {
|
|
mac := hmac.New(sha256.New, []byte(token))
|
|
mac.Write(body)
|
|
want := hex.EncodeToString(mac.Sum(nil))
|
|
return hmac.Equal([]byte(want), []byte(signature))
|
|
}
|
|
|
|
// Sign produces the callback signature (used by tests and the mock service).
|
|
func Sign(token string, body []byte) string {
|
|
mac := hmac.New(sha256.New, []byte(token))
|
|
mac.Write(body)
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|