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

View File

@@ -0,0 +1,13 @@
; Asterisk External Configuration (Realtime)
; Maps PJSIP sorcery objects and voicemail to ODBC backend
[settings]
; PJSIP Realtime Mappings
ps_endpoints => odbc,asterisk,ps_endpoints
ps_auths => odbc,asterisk,ps_auths
ps_aors => odbc,asterisk,ps_aors
ps_endpoint_id_ips => odbc,asterisk,ps_endpoint_id_ips
ps_registrations => odbc,asterisk,ps_registrations
; Voicemail
voicemail => odbc,asterisk,voicemail

View File

@@ -0,0 +1,180 @@
; Asterisk Dynamic Multi-Tenant Dialplan
; DID/route lookups via func_odbc from PostgreSQL
[general]
static=yes
writeprotect=no
clearglobalvars=no
[globals]
; Defaults — overridden per-tenant via DB lookups
DEFAULT_VM_CONTEXT=default
; ============================================================================
; [from-trunk] — Inbound DID calls from providers via Kamailio
; ============================================================================
[from-trunk]
; Inbound call: look up tenant by DID, then route
exten => _+X.,1,NoOp(Inbound trunk call to ${EXTEN})
same => n,Set(TENANT=${ODBC_TENANT_BY_DID(${EXTEN})})
same => n,GotoIf($["${TENANT}" = ""]?no-tenant,${EXTEN},1)
same => n,Set(CDR(accountcode)=${TENANT})
same => n,Set(ROUTE=${ODBC_INBOUND_ROUTE(${EXTEN})})
same => n,GotoIf($["${ROUTE}" = ""]?no-route,${EXTEN},1)
same => n,Set(DEST_TYPE=${CUT(ROUTE,|,1)})
same => n,Set(DEST_ID=${CUT(ROUTE,|,2)})
same => n,Set(DEST_DATA=${CUT(ROUTE,|,3)})
same => n,Goto(route-${DEST_TYPE},${EXTEN},1)
exten => _X.,1,NoOp(Inbound trunk call to ${EXTEN} - no + prefix)
same => n,Set(TENANT=${ODBC_TENANT_BY_DID(+${EXTEN})})
same => n,GotoIf($["${TENANT}" = ""]?no-tenant,${EXTEN},1)
same => n,Set(CDR(accountcode)=${TENANT})
same => n,Set(ROUTE=${ODBC_INBOUND_ROUTE(+${EXTEN})})
same => n,GotoIf($["${ROUTE}" = ""]?no-route,${EXTEN},1)
same => n,Set(DEST_TYPE=${CUT(ROUTE,|,1)})
same => n,Set(DEST_ID=${CUT(ROUTE,|,2)})
same => n,Set(DEST_DATA=${CUT(ROUTE,|,3)})
same => n,Goto(route-${DEST_TYPE},${EXTEN},1)
; ============================================================================
; Destination handlers — routed by destination_type from inbound_routes
; ============================================================================
; Route to extension
[route-extension]
exten => _X.,1,NoOp(Route to extension ${DEST_DATA})
same => n,Dial(PJSIP/${DEST_DATA},30,tTr)
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy)
same => n,VoiceMail(${DEST_DATA}@${TENANT},u)
same => n,Hangup()
same => n(busy),VoiceMail(${DEST_DATA}@${TENANT},b)
same => n,Hangup()
; Route to queue
[route-queue]
exten => _X.,1,NoOp(Route to queue ${DEST_DATA})
same => n,Queue(${DEST_DATA},tTr,,,300)
same => n,Hangup()
; Route to IVR
[route-ivr]
exten => _X.,1,NoOp(Route to IVR ${DEST_DATA})
same => n,Goto(ivr-${DEST_DATA},s,1)
same => n,Hangup()
; Route to voicemail
[route-voicemail]
exten => _X.,1,NoOp(Route to voicemail ${DEST_DATA})
same => n,Answer()
same => n,VoiceMail(${DEST_DATA}@${TENANT},u)
same => n,Hangup()
; Route to ring group
[route-ring_group]
exten => _X.,1,NoOp(Route to ring group ${DEST_DATA})
same => n,Dial(PJSIP/${DEST_DATA},30,tTr)
same => n,Hangup()
; Route to conference
[route-conference]
exten => _X.,1,NoOp(Route to conference ${DEST_DATA})
same => n,Answer()
same => n,ConfBridge(${DEST_DATA})
same => n,Hangup()
; Route to external number
[route-external]
exten => _X.,1,NoOp(Route to external number ${DEST_DATA})
same => n,Dial(PJSIP/${DEST_DATA}@kamailio-out,60,tTr)
same => n,Hangup()
; ============================================================================
; [outbound] — Outbound calls from extensions
; ============================================================================
[outbound]
; Outbound: 9 + number (strip 9, look up trunk by tenant)
exten => _9.,1,NoOp(Outbound call from ${CHANNEL(endpoint)} to ${EXTEN:1})
same => n,Set(CDR(accountcode)=${CHANNEL(accountcode)})
same => n,Set(TRUNK_INFO=${ODBC_OUTBOUND_ROUTE(${CDR(accountcode)},${EXTEN:1})})
same => n,GotoIf($["${TRUNK_INFO}" = ""]?no-trunk,${EXTEN},1)
same => n,Set(TRUNK_ID=${CUT(TRUNK_INFO,|,1)})
same => n,Dial(PJSIP/+${EXTEN:1}@kamailio-out,60,tTr)
same => n,Hangup()
; International: 00 + country code
exten => _900.,1,NoOp(International call to +${EXTEN:3})
same => n,Set(CDR(accountcode)=${CHANNEL(accountcode)})
same => n,Set(TRUNK_INFO=${ODBC_OUTBOUND_ROUTE(${CDR(accountcode)},+${EXTEN:3})})
same => n,GotoIf($["${TRUNK_INFO}" = ""]?no-trunk,${EXTEN},1)
same => n,Dial(PJSIP/+${EXTEN:3}@kamailio-out,60,tTr)
same => n,Hangup()
; ============================================================================
; [internal] — Internal extension-to-extension calls
; ============================================================================
[internal]
; Local extensions (4-digit)
exten => _XXXX,1,NoOp(Internal call to ${EXTEN})
same => n,Dial(PJSIP/${EXTEN},30,tTr)
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy)
same => n,VoiceMail(${EXTEN}@${CHANNEL(accountcode)},u)
same => n,Hangup()
same => n(busy),VoiceMail(${EXTEN}@${CHANNEL(accountcode)},b)
same => n,Hangup()
; Cross-server extension calls via Kamailio
exten => _XXXXX,1,NoOp(Cross-server call to ${EXTEN})
same => n,Dial(PJSIP/${EXTEN}@kamailio-out,30,tTr)
same => n,Hangup()
; Echo test
exten => 600,1,Answer()
same => n,Echo()
same => n,Hangup()
; Music on hold test
exten => 601,1,Answer()
same => n,MusicOnHold(default,60)
same => n,Hangup()
; Check voicemail
exten => *97,1,Answer()
same => n,VoiceMailMain(${CALLERID(num)}@${CHANNEL(accountcode)})
same => n,Hangup()
; Voicemail direct deposit
exten => *98,1,Answer()
same => n,VoiceMailMain(${EXTEN}@${CHANNEL(accountcode)},s)
same => n,Hangup()
; Include outbound dialing
include => outbound
; ============================================================================
; [from-kamailio] — Entry point for Kamailio-routed calls
; ============================================================================
[from-kamailio]
include => internal
include => from-trunk
; ============================================================================
; Error handlers
; ============================================================================
[no-tenant]
exten => _X.,1,NoOp(No tenant found for DID ${EXTEN})
same => n,Log(WARNING,No tenant found for DID ${EXTEN})
same => n,Playback(ss-noservice)
same => n,Hangup()
[no-route]
exten => _X.,1,NoOp(No route configured for DID ${EXTEN})
same => n,Log(WARNING,No inbound route for DID ${EXTEN} in tenant ${TENANT})
same => n,Playback(ss-noservice)
same => n,Hangup()
[no-trunk]
exten => _X.,1,NoOp(No outbound trunk found)
same => n,Log(WARNING,No outbound trunk for ${EXTEN} in tenant ${CDR(accountcode)})
same => n,Playback(ss-noservice)
same => n,Hangup()

View File

@@ -0,0 +1,25 @@
; Asterisk func_odbc Configuration
; Custom ODBC functions for multi-tenant DID routing
; TENANT_BY_DID - Lookup tenant code by DID number
; Usage: ${ODBC_TENANT_BY_DID(+3726026553)}
[TENANT_BY_DID]
dsn=asterisk
readsql=SELECT tenant_code FROM did_tenant_lookup WHERE did_number='${SQL_ESC(${ARG1})}' LIMIT 1
synopsis=Lookup tenant code by DID number
; INBOUND_ROUTE - Lookup inbound destination by DID and tenant
; Usage: ${ODBC_INBOUND_ROUTE(+3726026553)}
; Returns: destination_type|destination_id|destination_data
[INBOUND_ROUTE]
dsn=asterisk
readsql=SELECT destination_type || '|' || COALESCE(destination_id::text, '') || '|' || COALESCE(destination_data, '') FROM inbound_route_lookup WHERE did_number='${SQL_ESC(${ARG1})}' ORDER BY priority LIMIT 1
synopsis=Lookup inbound route by DID number
; OUTBOUND_ROUTE - Lookup outbound trunk for dialing
; Usage: ${ODBC_OUTBOUND_ROUTE(tenant_code,dialed_number)}
; Returns: trunk_id|trunk_host|trunk_port
[OUTBOUND_ROUTE]
dsn=asterisk
readsql=SELECT trunk_id || '|' || trunk_host || '|' || trunk_port FROM outbound_route_lookup WHERE tenant_code='${SQL_ESC(${ARG1})}' AND '${SQL_ESC(${ARG2})}' ~ ANY(dial_patterns) ORDER BY route_priority, trunk_priority LIMIT 1
synopsis=Lookup outbound trunk by tenant and dial pattern

11
configs/asterisk/odbc.ini Normal file
View File

@@ -0,0 +1,11 @@
[asterisk]
Description = Asterisk PostgreSQL Database
Driver = /usr/lib64/psqlodbcw.so
Servername = 172.17.3.14
Port = 5432
Database = asterisk
UserName = asterisk_reader
Password = gxiXVWr5t6anOw3Kzf5I66I84MZv6YuZ
ReadOnly = No
Debug = 0
CommLog = 0

View File

@@ -0,0 +1,75 @@
; PJSIP Configuration — Transport, Global, and ACL only
; All endpoints/auths/AORs are loaded from PostgreSQL via ODBC Realtime
[global]
type=global
user_agent=GoSec-PBX
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060
; ACL to allow only Kamailio servers
[kamailio-acl]
type=acl
permit=172.17.6.42/32
permit=172.17.6.43/32
permit=172.17.6.1/32
; === Kamailio Dispatcher Health Checks ===
; These remain static — Kamailio needs them for OPTIONS pings
[dispatcher]
type=endpoint
context=from-kamailio
disallow=all
allow=ulaw
allow=alaw
allow=g729
direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
aors=dispatcher-aor
[dispatcher-aor]
type=aor
qualify_frequency=0
[identify-kamailio1]
type=identify
endpoint=dispatcher
match=172.17.6.42/32
[identify-kamailio2]
type=identify
endpoint=dispatcher
match=172.17.6.43/32
[identify-kamailio-vip]
type=identify
endpoint=dispatcher
match=172.17.6.1/32
; === Kamailio Outbound Endpoint ===
; Static — used by dialplan for outbound calls via Kamailio VIP
[kamailio-out]
type=endpoint
context=from-kamailio
disallow=all
allow=ulaw
allow=alaw
allow=g729
direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
trust_id_inbound=yes
send_rpid=yes
acl=kamailio-acl
aors=kamailio-out-aor
[kamailio-out-aor]
type=aor
contact=sip:172.17.6.1:5060
qualify_frequency=30

View File

@@ -0,0 +1,30 @@
# Postfix satellite relay configuration for Asterisk voicemail-to-email
# Deployed to /etc/postfix/main.cf on pb01 (172.17.6.40) and pb02 (172.17.6.41)
# General
myhostname = HOSTNAME_PLACEHOLDER.gosec.internal
mydomain = gosec.internal
myorigin = gosec.cloud
mydestination =
mynetworks = 127.0.0.0/8
# Relay through mail server
relayhost = [172.17.4.11]:25
# Security
inet_interfaces = loopback-only
inet_protocols = ipv4
# TLS (optional — internal relay)
smtp_tls_security_level = none
# Aliases
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
# Limits
message_size_limit = 10485760
mailbox_size_limit = 0
# Logging
syslog_name = postfix-voicemail

View File

@@ -0,0 +1,13 @@
; Asterisk ODBC Resource Configuration
; Connects to PostgreSQL VIP for realtime config
[ENV]
[asterisk]
enabled => yes
dsn => asterisk
pre-connect => yes
max_connections => 10
username => asterisk_reader
password => PLACEHOLDER_FROM_INFISICAL
logging => no

View File

@@ -0,0 +1,23 @@
; Asterisk Sorcery Configuration
; Defines object-to-backend mappings for PJSIP
[res_pjsip]
; Static endpoints from pjsip.conf (dispatcher, kamailio-out, ACLs)
endpoint=config,pjsip.conf,criteria=type=endpoint
auth=config,pjsip.conf,criteria=type=auth
aor=config,pjsip.conf,criteria=type=aor
; Dynamic endpoints from PostgreSQL via ODBC realtime
endpoint=realtime,ps_endpoints
auth=realtime,ps_auths
aor=realtime,ps_aors
domain_alias=config,pjsip.conf,criteria=type=domain_alias
contact=astdb,registrar
; IP-based endpoint identification — static + realtime
[res_pjsip_endpoint_identifier_ip]
identify=config,pjsip.conf,criteria=type=identify
identify=realtime,ps_endpoint_id_ips
; Outbound registrations
[res_pjsip_outbound_registration]
registration=realtime,ps_registrations

View File

@@ -0,0 +1,45 @@
; Asterisk Voicemail Configuration
; Mailbox data loaded from PostgreSQL via ODBC Realtime (extconfig.conf)
; Email delivery via local Postfix satellite relay
[general]
format=wav49|wav
serveremail=voicemail@gosec.cloud
attach=yes
skipms=3000
maxsilence=10
silencethreshold=128
maxlogins=3
maxmsg=100
maxsecs=180
minsecs=3
saycid=yes
sayduration=yes
saydurationm=2
sendvoicemail=yes
review=yes
operator=no
envelope=yes
delete=no
nextaftercmd=yes
forcename=no
forcegreetings=no
; Email settings
emailsubject=New voicemail from ${VM_CALLERID} (${VM_DUR})
emailbody=Dear ${VM_NAME},\n\nYou have a new voicemail:\n\n From: ${VM_CALLERID}\n Date: ${VM_DATE}\n Duration: ${VM_DUR}\n Message: ${VM_MSGNUM}\n\nDial *97 to listen.\n\n--\nGoSec Cloud PBX
emaildateformat=%A, %B %d, %Y at %r
pagerdateformat=%A, %B %d, %Y at %r
mailcmd=/usr/sbin/sendmail -t
; Timezone
tz=utc
[zonemessages]
utc=UTC|'vm-received' q 'digits/at' H N
european=Europe/Helsinki|'vm-received' a d b 'digits/at' HM
eastern=America/New_York|'vm-received' Q 'digits/at' IMp
; Default context — mailbox data from realtime DB
; No static mailboxes needed; all loaded from pbx_voicemail_boxes via ODBC
[default]