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:
191
internal/client/asterisk.go
Normal file
191
internal/client/asterisk.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// AsteriskServer defines an Asterisk AMI endpoint
|
||||
type AsteriskServer struct {
|
||||
Host string `yaml:"host"`
|
||||
AMIPort int `yaml:"amiPort"`
|
||||
}
|
||||
|
||||
// AsteriskClient manages AMI connections to Asterisk servers
|
||||
type AsteriskClient struct {
|
||||
servers []AsteriskServer
|
||||
user string
|
||||
secret string
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// NewAsteriskClient creates a new Asterisk AMI client
|
||||
func NewAsteriskClient(servers []AsteriskServer, user, secret string, logger zerolog.Logger) *AsteriskClient {
|
||||
return &AsteriskClient{
|
||||
servers: servers,
|
||||
user: user,
|
||||
secret: secret,
|
||||
logger: logger.With().Str("client", "asterisk").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadPJSIP sends a PJSIP reload command to all Asterisk servers
|
||||
func (c *AsteriskClient) ReloadPJSIP() []ServerResult {
|
||||
return c.runOnAll("pjsip reload")
|
||||
}
|
||||
|
||||
// ReloadDialplan sends a dialplan reload to all Asterisk servers
|
||||
func (c *AsteriskClient) ReloadDialplan() []ServerResult {
|
||||
return c.runOnAll("dialplan reload")
|
||||
}
|
||||
|
||||
// ReloadAll reloads both PJSIP and dialplan on all servers
|
||||
func (c *AsteriskClient) ReloadAll() []ServerResult {
|
||||
results := c.runOnAll("core reload")
|
||||
return results
|
||||
}
|
||||
|
||||
// GetChannelCount returns active channel count from each server
|
||||
func (c *AsteriskClient) GetChannelCount() []ServerResult {
|
||||
return c.runOnAll("core show channels count")
|
||||
}
|
||||
|
||||
// GetUptime returns uptime from each server
|
||||
func (c *AsteriskClient) GetUptime() []ServerResult {
|
||||
return c.runOnAll("core show uptime")
|
||||
}
|
||||
|
||||
// ServerResult holds the result from an AMI command on a single server
|
||||
type ServerResult struct {
|
||||
Host string `json:"host"`
|
||||
Success bool `json:"success"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// runOnAll executes an AMI command on all Asterisk servers in parallel
|
||||
func (c *AsteriskClient) runOnAll(command string) []ServerResult {
|
||||
var wg sync.WaitGroup
|
||||
results := make([]ServerResult, len(c.servers))
|
||||
|
||||
for i, srv := range c.servers {
|
||||
wg.Add(1)
|
||||
go func(idx int, server AsteriskServer) {
|
||||
defer wg.Done()
|
||||
results[idx] = c.execAMI(server, command)
|
||||
}(i, srv)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// execAMI connects to a single Asterisk AMI server and executes a command
|
||||
func (c *AsteriskClient) execAMI(server AsteriskServer, command string) ServerResult {
|
||||
addr := fmt.Sprintf("%s:%d", server.Host, server.AMIPort)
|
||||
result := ServerResult{Host: server.Host}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("connection failed: %v", err)
|
||||
c.logger.Warn().Str("host", server.Host).Err(err).Msg("AMI connection failed")
|
||||
return result
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
// Read AMI banner
|
||||
banner, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("failed to read banner: %v", err)
|
||||
return result
|
||||
}
|
||||
if !strings.HasPrefix(banner, "Asterisk Call Manager") {
|
||||
result.Error = fmt.Sprintf("unexpected banner: %s", strings.TrimSpace(banner))
|
||||
return result
|
||||
}
|
||||
|
||||
// Login
|
||||
loginMsg := fmt.Sprintf("Action: Login\r\nUsername: %s\r\nSecret: %s\r\n\r\n",
|
||||
c.user, c.secret)
|
||||
if _, err := conn.Write([]byte(loginMsg)); err != nil {
|
||||
result.Error = fmt.Sprintf("login write failed: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
// Read login response
|
||||
loginResp, err := readAMIResponse(reader)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("login response failed: %v", err)
|
||||
return result
|
||||
}
|
||||
if !strings.Contains(loginResp, "Success") {
|
||||
result.Error = fmt.Sprintf("login failed: %s", loginResp)
|
||||
return result
|
||||
}
|
||||
|
||||
// Execute command
|
||||
cmdMsg := fmt.Sprintf("Action: Command\r\nCommand: %s\r\n\r\n", command)
|
||||
if _, err := conn.Write([]byte(cmdMsg)); err != nil {
|
||||
result.Error = fmt.Sprintf("command write failed: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
// Read command response
|
||||
cmdResp, err := readAMIResponse(reader)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("command response failed: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
// Logoff
|
||||
conn.Write([]byte("Action: Logoff\r\n\r\n"))
|
||||
|
||||
result.Success = true
|
||||
result.Output = cmdResp
|
||||
c.logger.Debug().Str("host", server.Host).Str("command", command).Msg("AMI command executed")
|
||||
return result
|
||||
}
|
||||
|
||||
// readAMIResponse reads a complete AMI response (terminated by blank line)
|
||||
func readAMIResponse(reader *bufio.Reader) (string, error) {
|
||||
var lines []string
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return strings.Join(lines, "\n"), err
|
||||
}
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
break
|
||||
}
|
||||
lines = append(lines, trimmed)
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
// Health checks connectivity to all Asterisk servers
|
||||
func (c *AsteriskClient) Health() error {
|
||||
for _, srv := range c.servers {
|
||||
addr := fmt.Sprintf("%s:%d", srv.Host, srv.AMIPort)
|
||||
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("asterisk %s unreachable: %w", srv.Host, err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Servers returns the configured server list
|
||||
func (c *AsteriskClient) Servers() []AsteriskServer {
|
||||
return c.servers
|
||||
}
|
||||
Reference in New Issue
Block a user