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:
Claude (gsc-ops-api init)
2026-05-03 20:06:02 +02:00
commit 3847eb2036
68 changed files with 12982 additions and 0 deletions

377
internal/config/config.go Normal file
View File

@@ -0,0 +1,377 @@
package config
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Databases map[string]DatabaseConfig `yaml:"databases"`
TLS TLSConfig `yaml:"tls"`
LDAP LDAPConfig `yaml:"ldap"`
PowerDNS PowerDNSConfig `yaml:"powerdns"`
EJBCA EJBCAConfig `yaml:"ejbca"`
Hockeypuck HockeypuckConfig `yaml:"hockeypuck"`
Infisical InfisicalConfig `yaml:"infisical"`
CardDAV CardDAVConfig `yaml:"carddav"`
Asterisk AsteriskConfig `yaml:"asterisk"`
Kamailio KamailioConfig `yaml:"kamailio"`
Auth AuthConfig `yaml:"auth"`
Logging LoggingConfig `yaml:"logging"`
}
type ServerConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
ReadTimeout time.Duration `yaml:"readTimeout"`
WriteTimeout time.Duration `yaml:"writeTimeout"`
}
type DatabaseConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Database string `yaml:"database"`
Schema string `yaml:"schema"`
User string `yaml:"user"`
Password string `yaml:"password"`
SSLMode string `yaml:"sslMode"`
MaxConns int `yaml:"maxConns"`
MinConns int `yaml:"minConns"`
}
type TLSConfig struct {
CertFile string `yaml:"certFile"`
KeyFile string `yaml:"keyFile"`
CAFile string `yaml:"caFile"`
}
type LDAPConfig struct {
Servers []string `yaml:"servers"`
BaseDN string `yaml:"baseDn"`
BindDN string `yaml:"bindDn"`
BindPass string `yaml:"bindPassword"`
PoolSize int `yaml:"poolSize"`
UseTLS bool `yaml:"useTls"`
CAFile string `yaml:"caFile"`
}
type PowerDNSConfig struct {
BaseURL string `yaml:"baseUrl"`
APIKey string `yaml:"apiKey"`
ServerID string `yaml:"serverId"`
}
type EJBCAConfig struct {
BaseURL string `yaml:"baseUrl"`
CertFile string `yaml:"certFile"`
KeyFile string `yaml:"keyFile"`
CAFile string `yaml:"caFile"`
}
type HockeypuckConfig struct {
Servers []string `yaml:"servers"`
}
type CardDAVConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Database string `yaml:"database"`
User string `yaml:"user"`
Password string `yaml:"password"`
SSLMode string `yaml:"sslMode"`
}
type AsteriskServerConfig struct {
Host string `yaml:"host"`
AMIPort int `yaml:"amiPort"`
}
type AsteriskConfig struct {
Servers []AsteriskServerConfig `yaml:"servers"`
AMIUser string `yaml:"amiUser"`
AMISecret string `yaml:"amiSecret"`
}
type KamailioConfig struct {
Servers []string `yaml:"servers"`
SSHUser string `yaml:"sshUser"`
SSHKey string `yaml:"sshKeyFile"`
}
type InfisicalConfig struct {
Host string `yaml:"host"`
ProjectID string `yaml:"projectId"`
Environment string `yaml:"environment"`
TokenFile string `yaml:"tokenFile"`
SecretPath string `yaml:"secretPath"`
}
type AuthConfig struct {
APIKeys []string `yaml:"apiKeys"`
}
type LoggingConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
cfg.setDefaults()
return &cfg, nil
}
func (c *Config) setDefaults() {
if c.Server.Host == "" {
c.Server.Host = "0.0.0.0"
}
if c.Server.Port == 0 {
c.Server.Port = 8443
}
if c.Server.ReadTimeout == 0 {
c.Server.ReadTimeout = 30 * time.Second
}
if c.Server.WriteTimeout == 0 {
c.Server.WriteTimeout = 30 * time.Second
}
// Legacy single database config
if c.Database.Port == 0 {
c.Database.Port = 5432
}
if c.Database.SSLMode == "" {
c.Database.SSLMode = "require"
}
if c.Database.MaxConns == 0 {
c.Database.MaxConns = 25
}
if c.Database.MinConns == 0 {
c.Database.MinConns = 5
}
// Multi-database defaults
if c.Databases == nil {
c.Databases = make(map[string]DatabaseConfig)
}
for name, db := range c.Databases {
if db.Port == 0 {
db.Port = 5432
}
if db.SSLMode == "" {
db.SSLMode = c.Database.SSLMode
}
if db.Host == "" {
db.Host = c.Database.Host
}
if db.MaxConns == 0 {
db.MaxConns = 10
}
if db.MinConns == 0 {
db.MinConns = 2
}
c.Databases[name] = db
}
if c.LDAP.PoolSize == 0 {
c.LDAP.PoolSize = 10
}
if c.PowerDNS.ServerID == "" {
c.PowerDNS.ServerID = "localhost"
}
if c.CardDAV.Port == 0 {
c.CardDAV.Port = 5432
}
if c.CardDAV.SSLMode == "" {
c.CardDAV.SSLMode = "disable"
}
if c.Asterisk.AMIUser == "" {
c.Asterisk.AMIUser = "gsc-ops-api"
}
for i := range c.Asterisk.Servers {
if c.Asterisk.Servers[i].AMIPort == 0 {
c.Asterisk.Servers[i].AMIPort = 5038
}
}
if c.Kamailio.SSHUser == "" {
c.Kamailio.SSHUser = "root"
}
if c.Logging.Level == "" {
c.Logging.Level = "info"
}
if c.Logging.Format == "" {
c.Logging.Format = "json"
}
if c.Infisical.SecretPath == "" {
c.Infisical.SecretPath = "/gsc-ops-api"
}
}
// LoadSecretsFromInfisical fetches secrets from the Infisical API
func (c *Config) LoadSecretsFromInfisical() error {
if c.Infisical.Host == "" {
return nil
}
tokenFile := c.Infisical.TokenFile
if tokenFile == "" {
tokenFile = "/etc/gsc-ops-api/.infisical"
}
tokenData, err := os.ReadFile(tokenFile)
if err != nil {
return fmt.Errorf("failed to read Infisical token: %w", err)
}
token := strings.TrimSpace(string(tokenData))
// Legacy single database credentials
if v, err := c.fetchSecret(token, "GSC_OPS_DB_USER"); err == nil && v != "" {
c.Database.User = v
}
if v, err := c.fetchSecret(token, "GSC_OPS_DB_PASSWORD"); err == nil && v != "" {
c.Database.Password = v
}
// Per-database credentials: inherit from primary DB user/password
for name, db := range c.Databases {
if db.User == "" {
db.User = c.Database.User
}
if db.Password == "" {
db.Password = c.Database.Password
}
c.Databases[name] = db
}
// LDAP credentials
if v, err := c.fetchSecret(token, "GSC_OPS_LDAP_BIND_DN"); err == nil && v != "" {
c.LDAP.BindDN = v
}
if v, err := c.fetchSecret(token, "GSC_OPS_LDAP_BIND_PASSWORD"); err == nil && v != "" {
c.LDAP.BindPass = v
}
// PowerDNS API key
if v, err := c.fetchSecret(token, "GSC_OPS_PDNS_API_KEY"); err == nil && v != "" {
c.PowerDNS.APIKey = v
}
// CardDAV DB password
if v, err := c.fetchSecret(token, "GSC_OPS_CARDDAV_DB_PASSWORD"); err == nil && v != "" {
c.CardDAV.Password = v
}
// API keys for authorized clients
if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_SKILL_SERVER"); err == nil && v != "" {
c.Auth.APIKeys = append(c.Auth.APIKeys, v)
}
if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_VOICE_AGENT"); err == nil && v != "" {
c.Auth.APIKeys = append(c.Auth.APIKeys, v)
}
if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_GSC_MY"); err == nil && v != "" {
c.Auth.APIKeys = append(c.Auth.APIKeys, v)
}
if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_SYNAPSE_HUB"); err == nil && v != "" {
c.Auth.APIKeys = append(c.Auth.APIKeys, v)
}
// Asterisk AMI secret
if v, err := c.fetchSecret(token, "GSC_OPS_ASTERISK_AMI_SECRET"); err == nil && v != "" {
c.Asterisk.AMISecret = v
}
return nil
}
func (c *Config) fetchSecret(token, secretName string) (string, error) {
url := fmt.Sprintf("%s/api/v3/secrets/raw/%s?workspaceId=%s&environment=%s&secretPath=%s",
c.Infisical.Host, secretName, c.Infisical.ProjectID, c.Infisical.Environment, c.Infisical.SecretPath)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Infisical returned status %d for %s", resp.StatusCode, secretName)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var result struct {
Secret struct {
SecretValue string `json:"secretValue"`
} `json:"secret"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", err
}
return result.Secret.SecretValue, nil
}
// DatabaseDSN returns the legacy database connection string
func (c *Config) DatabaseDSN() string {
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
c.Database.Host, c.Database.Port, c.Database.Database,
c.Database.User, c.Database.Password, c.Database.SSLMode)
}
// NamedDatabaseDSN returns the connection string for a named database
func (c *Config) NamedDatabaseDSN(name string) string {
db, ok := c.Databases[name]
if !ok {
return c.DatabaseDSN()
}
dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
db.Host, db.Port, db.Database,
db.User, db.Password, db.SSLMode)
if db.Schema != "" {
dsn += fmt.Sprintf(" search_path=%s,public", db.Schema)
}
return dsn
}
// HasDatabase returns true if a named database is configured
func (c *Config) HasDatabase(name string) bool {
_, ok := c.Databases[name]
return ok
}
// CardDAVDSN returns the CardDAV database connection string
func (c *Config) CardDAVDSN() string {
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
c.CardDAV.Host, c.CardDAV.Port, c.CardDAV.Database,
c.CardDAV.User, c.CardDAV.Password, c.CardDAV.SSLMode)
}