AUD-49: build the Hub skeleton (hear-route-answer)
- Three inbound routes from docs/hub-api.md: POST /ask, POST /approve, GET /health. - Five-step pipeline in src/hub, with packages hear, understand, check, route, answer. - Routing through OmniRoute with the capability list and routing.md. The never-route control check runs before any LLM call. - One real destination: Jervis house TTS, started out of band. - Fake /ask project (src/tests/fakeproject) for the round-trip test. - Append-only, size-capped request log. Bearer auth, source match, rate limit. - Tests: all packages pass, including the fake /ask round trip. Race clean. - Dockerfile and compose updated to mount routing.md and secrets, log on tmpfs. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
85
src/destinations/paperclip.go
Normal file
85
src/destinations/paperclip.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package destinations
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Paperclip files one slow, recorded job after a tap-to-approve
|
||||
// (docs/hub-api.md section 6). The Hub files the job once.
|
||||
type Paperclip struct {
|
||||
URL string
|
||||
APIKey string
|
||||
CompanyID string
|
||||
Timeout time.Duration
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
// NewPaperclip builds the Paperclip client.
|
||||
func NewPaperclip(url, key, companyID string, timeout time.Duration) *Paperclip {
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
return &Paperclip{
|
||||
URL: strings.TrimRight(url, "/"),
|
||||
APIKey: key,
|
||||
CompanyID: companyID,
|
||||
Timeout: timeout,
|
||||
HTTP: &http.Client{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
// FileJob creates one Paperclip issue. It returns the issue id.
|
||||
func (p *Paperclip) FileJob(ctx context.Context, title, description string) (string, error) {
|
||||
if p == nil || p.URL == "" || p.APIKey == "" || p.CompanyID == "" {
|
||||
return "", fmt.Errorf("paperclip: not configured")
|
||||
}
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"title": title,
|
||||
"description": description,
|
||||
"priority": "medium",
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, p.Timeout)
|
||||
defer cancel()
|
||||
|
||||
url := fmt.Sprintf("%s/api/companies/%s/issues", p.URL, p.CompanyID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+p.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := p.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("paperclip: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return "", fmt.Errorf("paperclip: status %d", resp.StatusCode)
|
||||
}
|
||||
var out struct {
|
||||
Issue struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"issue"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := out.Issue.ID
|
||||
if id == "" {
|
||||
id = out.ID
|
||||
}
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("paperclip: reply has no issue id")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
Reference in New Issue
Block a user