Files
gsc-ops-api/internal/client/hockeypuck.go
Claude (gsc-ops-api init) 3847eb2036 Initial import — snapshot from admin host /srv/gosec/gsc-ops-api
This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.

The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.

Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.

Future changes go through PRs against this repo — no more bin-only
deploys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:06:02 +02:00

163 lines
3.6 KiB
Go

package client
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/gosec/gsc-ops-api/internal/config"
)
// HockeypuckClient implements the HKP (HTTP Keyserver Protocol) client
type HockeypuckClient struct {
servers []string
client *http.Client
logger zerolog.Logger
}
// NewHockeypuckClient creates a new Hockeypuck HKP client
func NewHockeypuckClient(cfg config.HockeypuckConfig, logger zerolog.Logger) *HockeypuckClient {
return &HockeypuckClient{
servers: cfg.Servers,
client: &http.Client{Timeout: 30 * time.Second},
logger: logger.With().Str("component", "hockeypuck").Logger(),
}
}
// SearchKeys searches for PGP keys by query string (email, name, or key ID)
func (c *HockeypuckClient) SearchKeys(query string) (string, error) {
params := url.Values{
"search": {query},
"op": {"index"},
"options": {"mr"},
}
for _, server := range c.servers {
u := fmt.Sprintf("%s/pks/lookup?%s", server, params.Encode())
resp, err := c.client.Get(u)
if err != nil {
c.logger.Warn().Err(err).Str("server", server).Msg("HKP search failed")
continue
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return "", nil
}
if resp.StatusCode != http.StatusOK {
continue
}
body, err := io.ReadAll(resp.Body)
if err != nil {
continue
}
return string(body), nil
}
return "", fmt.Errorf("all HKP servers failed")
}
// GetKey retrieves a PGP key by key ID
func (c *HockeypuckClient) GetKey(keyID string) (string, error) {
// Ensure keyID has 0x prefix
if !strings.HasPrefix(keyID, "0x") {
keyID = "0x" + keyID
}
params := url.Values{
"search": {keyID},
"op": {"get"},
"options": {"mr"},
}
for _, server := range c.servers {
u := fmt.Sprintf("%s/pks/lookup?%s", server, params.Encode())
resp, err := c.client.Get(u)
if err != nil {
continue
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return "", nil
}
if resp.StatusCode != http.StatusOK {
continue
}
body, err := io.ReadAll(resp.Body)
if err != nil {
continue
}
return string(body), nil
}
return "", fmt.Errorf("all HKP servers failed")
}
// UploadKey uploads a PGP public key
func (c *HockeypuckClient) UploadKey(armoredKey string) error {
form := url.Values{
"keytext": {armoredKey},
}
for _, server := range c.servers {
u := fmt.Sprintf("%s/pks/add", server)
resp, err := c.client.PostForm(u, form)
if err != nil {
c.logger.Warn().Err(err).Str("server", server).Msg("HKP upload failed")
continue
}
resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
}
return fmt.Errorf("failed to upload key to any HKP server")
}
// DeleteKey deletes a PGP key (Hockeypuck-specific API, not standard HKP)
func (c *HockeypuckClient) DeleteKey(keyID string) error {
if !strings.HasPrefix(keyID, "0x") {
keyID = "0x" + keyID
}
for _, server := range c.servers {
u := fmt.Sprintf("%s/pks/delete?search=%s", server, url.QueryEscape(keyID))
req, err := http.NewRequest("DELETE", u, nil)
if err != nil {
continue
}
resp, err := c.client.Do(req)
if err != nil {
continue
}
resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
}
return fmt.Errorf("failed to delete key from any HKP server")
}
// Health checks HKP server connectivity
func (c *HockeypuckClient) Health() error {
for _, server := range c.servers {
resp, err := c.client.Get(server + "/pks/lookup?op=stats")
if err != nil {
continue
}
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return nil
}
}
return fmt.Errorf("no HKP servers reachable")
}