feat: bounty board, task lifecycle, AI work performer flow, notifications (phase 8)
- 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>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// 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))
|
||||
}
|
||||
Reference in New Issue
Block a user