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>
This commit is contained in:
162
internal/client/hockeypuck.go
Normal file
162
internal/client/hockeypuck.go
Normal file
@@ -0,0 +1,162 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user