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,533 @@
-- PBX Multi-Tenant Realtime Views for Asterisk ODBC
-- Executed against the 'asterisk' database on PostgreSQL VIP 172.17.3.14
-- Prerequisites: postgres_fdw extension, asterisk_reader user
-- ============================================================================
-- Step 1: Create asterisk_reader user (run as superuser on primary)
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'asterisk_reader') THEN
CREATE ROLE asterisk_reader WITH LOGIN PASSWORD 'PLACEHOLDER_CHANGE_ME';
END IF;
END $$;
-- ============================================================================
-- Step 2: Install extensions
-- ============================================================================
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- ============================================================================
-- Step 3: Create foreign server pointing to gsc_admin database
-- ============================================================================
DROP SERVER IF EXISTS gsc_admin_server CASCADE;
CREATE SERVER gsc_admin_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '127.0.0.1', port '5432', dbname 'gsc_admin');
-- User mapping: asterisk DB superuser -> gsc_admin reader
CREATE USER MAPPING IF NOT EXISTS FOR CURRENT_USER
SERVER gsc_admin_server
OPTIONS (user 'asterisk_reader', password 'PLACEHOLDER_CHANGE_ME');
-- ============================================================================
-- Step 4: Create foreign tables mirroring gsc_admin pbx_* tables
-- ============================================================================
DROP SCHEMA IF EXISTS gsc CASCADE;
CREATE SCHEMA gsc;
-- pbx_trunks
CREATE FOREIGN TABLE gsc.pbx_trunks (
id uuid,
tenant_id uuid,
provider_id uuid,
name varchar(128),
trunk_type varchar(16),
status varchar(16),
host varchar(256),
port int,
transport varchar(8),
username varchar(128),
password_encrypted bytea,
auth_realm varchar(128),
from_domain varchar(256),
from_user varchar(128),
register boolean,
register_frequency int,
codecs text[],
dtmf_mode varchar(16),
nat_mode varchar(16),
max_channels int,
context varchar(64),
trust_id_inbound boolean,
trust_id_outbound boolean,
send_pai boolean,
send_rpid boolean,
send_diversion boolean,
direct_media boolean,
rtp_symmetric boolean,
rewrite_contact boolean,
ice_support boolean,
force_rport boolean,
timers varchar(16),
timers_min_se int,
timers_sess_expires int,
media_encryption varchar(16),
media_encryption_optimistic boolean,
max_contacts int,
default_expiration int,
minimum_expiration int,
maximum_expiration int,
qualify_timeout numeric(5,2),
match_by_ip boolean,
match_ips text[],
outbound_caller_id_name varchar(64),
outbound_caller_id_number varchar(32),
t38_udptl boolean,
t38_udptl_ec varchar(16),
t38_udptl_maxdatagram int,
t38_udptl_nat boolean,
fax_detect boolean,
rtp_timeout int,
rtp_timeout_hold int,
rtp_keepalive int,
inband_progress boolean,
contact_user varchar(128),
line boolean,
client_uri varchar(256),
server_uri varchar(256),
outbound_proxy varchar(256),
registration_expiration int,
registration_retry_interval int,
pjsip_options jsonb,
is_active boolean,
created_at timestamptz,
updated_at timestamptz
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_trunks');
-- pbx_trunk_dids
CREATE FOREIGN TABLE gsc.pbx_trunk_dids (
id uuid,
trunk_id uuid,
tenant_id uuid,
did_number varchar(32),
description varchar(256),
destination_type varchar(32),
destination_id uuid,
is_active boolean
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_trunk_dids');
-- pbx_extensions
CREATE FOREIGN TABLE gsc.pbx_extensions (
id uuid,
tenant_id uuid,
user_id uuid,
extension varchar(16),
name varchar(128),
extension_type varchar(16),
caller_id_name varchar(64),
caller_id_number varchar(32),
outbound_caller_id_number varchar(32),
sip_username varchar(64),
sip_password_encrypted bytea,
transport varchar(8),
codecs text[],
dtmf_mode varchar(16),
nat_mode varchar(16),
ring_timeout int,
call_waiting boolean,
dnd_enabled boolean,
forward_all_enabled boolean,
forward_all_destination varchar(64),
forward_busy_enabled boolean,
forward_busy_destination varchar(64),
forward_no_answer_enabled boolean,
forward_no_answer_destination varchar(64),
forward_no_answer_timeout int,
voicemail_enabled boolean,
voicemail_id uuid,
recording_mode varchar(16),
pjsip_options jsonb,
is_active boolean,
created_at timestamptz,
updated_at timestamptz
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_extensions');
-- pbx_voicemail_boxes
CREATE FOREIGN TABLE gsc.pbx_voicemail_boxes (
id uuid,
tenant_id uuid,
extension_id uuid,
mailbox varchar(16),
name varchar(128),
pin_encrypted bytea,
email varchar(256),
email_attachment boolean,
email_delete_after_send boolean,
pager_email varchar(256),
max_message_length int,
max_messages int,
say_caller_id boolean,
say_duration boolean,
review_enabled boolean,
operator_enabled boolean,
envelope_enabled boolean,
timezone varchar(64),
mwi_enabled boolean,
is_active boolean,
created_at timestamptz
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_voicemail_boxes');
-- pbx_inbound_routes
CREATE FOREIGN TABLE gsc.pbx_inbound_routes (
id uuid,
tenant_id uuid,
name varchar(128),
did_number varchar(32),
caller_id_pattern varchar(64),
destination_type varchar(32),
destination_id uuid,
destination_data varchar(256),
priority int,
is_active boolean,
trunk_id uuid
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_inbound_routes');
-- pbx_outbound_routes
CREATE FOREIGN TABLE gsc.pbx_outbound_routes (
id uuid,
tenant_id uuid,
name varchar(128),
priority int,
dial_patterns text[],
prepend varchar(16),
prefix varchar(16),
strip_digits int,
override_caller_id boolean,
caller_id_name varchar(64),
caller_id_number varchar(32),
is_emergency boolean,
is_active boolean
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_outbound_routes');
-- pbx_outbound_route_trunks
CREATE FOREIGN TABLE gsc.pbx_outbound_route_trunks (
id uuid,
outbound_route_id uuid,
trunk_id uuid,
priority int
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_outbound_route_trunks');
-- pbx_tenant_settings
CREATE FOREIGN TABLE gsc.pbx_tenant_settings (
id uuid,
tenant_id uuid,
extension_length int,
extension_start int,
default_codecs text[],
default_outbound_cid_name varchar(64),
default_outbound_cid_number varchar(32),
default_recording_mode varchar(16),
voicemail_email_from varchar(256),
default_moh_class varchar(64),
default_language varchar(8),
timezone varchar(64),
max_extensions int,
max_trunks int,
max_concurrent_calls int
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_tenant_settings');
-- pbx_queues
CREATE FOREIGN TABLE gsc.pbx_queues (
id uuid,
tenant_id uuid,
extension varchar(16),
name varchar(128),
strategy varchar(32),
timeout int,
retry int,
wrapup_time int,
max_wait_time int,
max_callers int,
moh_class varchar(64),
is_active boolean
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_queues');
-- pbx_queue_members
CREATE FOREIGN TABLE gsc.pbx_queue_members (
id uuid,
queue_id uuid,
extension_id uuid,
penalty int,
paused boolean,
dynamic boolean,
membername varchar(128),
interface varchar(128),
state_interface varchar(128)
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'pbx_queue_members');
-- tenants (for name resolution)
CREATE FOREIGN TABLE gsc.tenants (
id uuid,
code varchar(32),
name varchar(128),
is_active boolean
) SERVER gsc_admin_server
OPTIONS (schema_name 'public', table_name 'tenants');
-- ============================================================================
-- Step 5: Create PJSIP Sorcery Views (Asterisk Realtime)
-- ============================================================================
-- ps_endpoints: PJSIP endpoint objects for trunks
CREATE OR REPLACE VIEW public.ps_endpoints AS
-- Trunk endpoints
SELECT
t.id::text AS id,
'endpoint' AS type,
COALESCE(t.transport, 'udp')::text AS transport,
COALESCE(t.context, 'from-trunk-' || ten.code) AS context,
CASE
WHEN array_length(t.codecs, 1) > 0
THEN array_to_string(t.codecs, ',')
ELSE 'ulaw,alaw'
END AS allow,
'all' AS disallow,
t.username AS auth,
t.id::text AS aors,
COALESCE(t.outbound_caller_id_name, '') || ' <' || COALESCE(t.outbound_caller_id_number, '') || '>' AS callerid,
CASE WHEN t.direct_media THEN 'yes' ELSE 'no' END AS direct_media,
CASE WHEN COALESCE(t.rtp_symmetric, true) THEN 'yes' ELSE 'no' END AS rtp_symmetric,
CASE WHEN COALESCE(t.force_rport, true) THEN 'yes' ELSE 'no' END AS force_rport,
CASE WHEN COALESCE(t.rewrite_contact, true) THEN 'yes' ELSE 'no' END AS rewrite_contact,
CASE WHEN t.trust_id_inbound THEN 'yes' ELSE 'no' END AS trust_id_inbound,
CASE WHEN t.send_rpid THEN 'yes' ELSE 'no' END AS send_rpid,
CASE WHEN t.send_pai THEN 'yes' ELSE 'no' END AS send_pai,
COALESCE(t.dtmf_mode, 'rfc4733')::text AS dtmf_mode,
CASE WHEN t.ice_support THEN 'yes' ELSE 'no' END AS ice_support,
CASE WHEN t.t38_udptl THEN 'yes' ELSE 'no' END AS t38_udptl,
COALESCE(t.t38_udptl_ec, 'none')::text AS t38_udptl_ec,
COALESCE(t.t38_udptl_maxdatagram, 0) AS t38_udptl_maxdatagram,
CASE WHEN t.t38_udptl_nat THEN 'yes' ELSE 'no' END AS t38_udptl_nat,
CASE WHEN COALESCE(t.inband_progress, false) THEN 'yes' ELSE 'no' END AS inband_progress,
COALESCE(t.timers, 'yes')::text AS timers,
COALESCE(t.timers_min_se, 90) AS timers_min_se,
COALESCE(t.timers_sess_expires, 1800) AS timers_sess_expires,
COALESCE(t.rtp_timeout, 60) AS rtp_timeout,
COALESCE(t.rtp_timeout_hold, 300) AS rtp_timeout_hold,
COALESCE(t.rtp_keepalive, 0) AS rtp_keepalive,
COALESCE(t.media_encryption, 'no')::text AS media_encryption,
CASE WHEN t.media_encryption_optimistic THEN 'yes' ELSE 'no' END AS media_encryption_optimistic,
t.tenant_id
FROM gsc.pbx_trunks t
JOIN gsc.tenants ten ON ten.id = t.tenant_id
WHERE t.is_active = true AND t.status = 'active';
-- ps_auths: PJSIP auth objects for trunks
CREATE OR REPLACE VIEW public.ps_auths AS
SELECT
t.id::text AS id,
'auth' AS type,
'userpass' AS auth_type,
t.username,
-- Password: stored as AES-256-GCM encrypted bytea in gsc_admin.
-- For ODBC realtime we store a plaintext copy in a separate field.
-- This view returns the username; actual password set via ops-api sync.
''::text AS password,
COALESCE(t.auth_realm, t.from_domain, t.host, '')::text AS realm,
t.tenant_id
FROM gsc.pbx_trunks t
JOIN gsc.tenants ten ON ten.id = t.tenant_id
WHERE t.is_active = true AND t.status = 'active'
AND t.username IS NOT NULL AND t.username != '';
-- ps_aors: PJSIP AOR (Address of Record) objects for trunks
CREATE OR REPLACE VIEW public.ps_aors AS
SELECT
t.id::text AS id,
'aor' AS type,
'sip:' || t.host || ':' || COALESCE(t.port, 5060) AS contact,
COALESCE(t.max_contacts, 1) AS max_contacts,
COALESCE(t.qualify_timeout, 3.0)::numeric(5,2) AS qualify_timeout,
30 AS qualify_frequency,
COALESCE(t.default_expiration, 3600) AS default_expiration,
COALESCE(t.minimum_expiration, 60) AS minimum_expiration,
COALESCE(t.maximum_expiration, 7200) AS maximum_expiration,
t.tenant_id
FROM gsc.pbx_trunks t
WHERE t.is_active = true AND t.status = 'active';
-- ps_endpoint_id_ips: IP-based endpoint identification for trunks
CREATE OR REPLACE VIEW public.ps_endpoint_id_ips AS
SELECT
t.id::text AS endpoint,
ip AS match,
t.tenant_id
FROM gsc.pbx_trunks t,
LATERAL unnest(t.match_ips) AS ip
WHERE t.is_active = true AND t.status = 'active'
AND t.match_by_ip = true
AND t.match_ips IS NOT NULL
AND array_length(t.match_ips, 1) > 0;
-- ps_registrations: outbound registration objects for trunks that register
CREATE OR REPLACE VIEW public.ps_registrations AS
SELECT
t.id::text AS id,
'registration' AS type,
COALESCE(t.transport, 'udp')::text AS transport,
COALESCE(t.outbound_proxy, '')::text AS outbound_proxy,
COALESCE(t.server_uri, 'sip:' || t.host || ':' || COALESCE(t.port, 5060))::text AS server_uri,
COALESCE(t.client_uri, 'sip:' || t.username || '@' || COALESCE(t.from_domain, t.host))::text AS client_uri,
COALESCE(t.contact_user, t.username, '')::text AS contact_user,
COALESCE(t.registration_expiration, 3600) AS expiration,
COALESCE(t.registration_retry_interval, 60) AS retry_interval,
CASE WHEN COALESCE(t.line, false) THEN 'yes' ELSE 'no' END AS line,
t.id::text AS outbound_auth,
t.tenant_id
FROM gsc.pbx_trunks t
WHERE t.is_active = true AND t.status = 'active'
AND t.register = true;
-- voicemail: Asterisk voicemail boxes
CREATE OR REPLACE VIEW public.voicemail AS
SELECT
vm.id::text AS uniqueid,
ten.code AS context,
vm.mailbox,
''::text AS password,
vm.name AS fullname,
COALESCE(vm.email, '') AS email,
COALESCE(vm.pager_email, '') AS pager,
CASE WHEN COALESCE(vm.email_attachment, true) THEN 'yes' ELSE 'no' END AS attach,
CASE WHEN COALESCE(vm.email_delete_after_send, false) THEN 'yes' ELSE 'no' END AS "delete",
COALESCE(vm.max_message_length, 180) AS maxsecs,
COALESCE(vm.max_messages, 100) AS maxmsg,
COALESCE(vm.timezone, 'UTC') AS tz,
CASE WHEN COALESCE(vm.say_caller_id, true) THEN 'yes' ELSE 'no' END AS saycid,
CASE WHEN COALESCE(vm.say_duration, true) THEN 'yes' ELSE 'no' END AS sayduration,
CASE WHEN COALESCE(vm.review_enabled, true) THEN 'yes' ELSE 'no' END AS review,
CASE WHEN COALESCE(vm.operator_enabled, true) THEN 'yes' ELSE 'no' END AS operator,
CASE WHEN COALESCE(vm.envelope_enabled, true) THEN 'yes' ELSE 'no' END AS envelope,
vm.tenant_id
FROM gsc.pbx_voicemail_boxes vm
JOIN gsc.tenants ten ON ten.id = vm.tenant_id
WHERE vm.is_active = true;
-- ============================================================================
-- Step 6: func_odbc lookup views
-- ============================================================================
-- Tenant lookup by DID number
CREATE OR REPLACE VIEW public.did_tenant_lookup AS
SELECT
td.did_number,
td.tenant_id,
ten.code AS tenant_code
FROM gsc.pbx_trunk_dids td
JOIN gsc.tenants ten ON ten.id = td.tenant_id
WHERE td.is_active = true;
-- Inbound route lookup by DID
CREATE OR REPLACE VIEW public.inbound_route_lookup AS
SELECT
ir.did_number,
ir.tenant_id,
ten.code AS tenant_code,
ir.destination_type,
ir.destination_id,
ir.destination_data,
ir.priority
FROM gsc.pbx_inbound_routes ir
JOIN gsc.tenants ten ON ten.id = ir.tenant_id
WHERE ir.is_active = true
ORDER BY ir.priority ASC;
-- Outbound route lookup with trunk priority
CREATE OR REPLACE VIEW public.outbound_route_lookup AS
SELECT
obr.id AS route_id,
obr.tenant_id,
ten.code AS tenant_code,
obr.dial_patterns,
obr.prepend,
obr.prefix,
obr.strip_digits,
obr.priority AS route_priority,
obr.override_caller_id,
obr.caller_id_name,
obr.caller_id_number,
obr.is_emergency,
ort.trunk_id,
ort.priority AS trunk_priority,
t.host AS trunk_host,
t.port AS trunk_port,
t.username AS trunk_username
FROM gsc.pbx_outbound_routes obr
JOIN gsc.tenants ten ON ten.id = obr.tenant_id
LEFT JOIN gsc.pbx_outbound_route_trunks ort ON ort.outbound_route_id = obr.id
LEFT JOIN gsc.pbx_trunks t ON t.id = ort.trunk_id
WHERE obr.is_active = true
ORDER BY obr.priority ASC, ort.priority ASC;
-- ============================================================================
-- Step 7: SIP password sync table (plaintext for Asterisk ODBC)
-- ============================================================================
-- Asterisk cannot decrypt AES-256-GCM from the DB. We keep a sync table
-- that ops-api populates with plaintext passwords.
CREATE TABLE IF NOT EXISTS public.sip_passwords (
entity_id uuid PRIMARY KEY,
entity_type varchar(16) NOT NULL DEFAULT 'trunk', -- 'trunk' or 'extension'
password text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT NOW()
);
-- Update ps_auths to use sip_passwords
CREATE OR REPLACE VIEW public.ps_auths AS
SELECT
t.id::text AS id,
'auth' AS type,
'userpass' AS auth_type,
t.username,
COALESCE(sp.password, '')::text AS password,
COALESCE(t.auth_realm, t.from_domain, t.host, '')::text AS realm,
t.tenant_id
FROM gsc.pbx_trunks t
JOIN gsc.tenants ten ON ten.id = t.tenant_id
LEFT JOIN public.sip_passwords sp ON sp.entity_id = t.id AND sp.entity_type = 'trunk'
WHERE t.is_active = true AND t.status = 'active'
AND t.username IS NOT NULL AND t.username != '';
-- ============================================================================
-- Step 8: Grant permissions
-- ============================================================================
GRANT USAGE ON SCHEMA public TO asterisk_reader;
GRANT USAGE ON SCHEMA gsc TO asterisk_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO asterisk_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA gsc TO asterisk_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO asterisk_reader;
-- Allow asterisk_reader to read sip_passwords (and in future, insert/update via ops-api)
GRANT SELECT, INSERT, UPDATE, DELETE ON public.sip_passwords TO asterisk_reader;
-- ============================================================================
-- Step 9: pg_hba.conf additions (must be done via Patroni config)
-- Add these lines to patroni.yml postgresql.pg_hba section:
-- - host asterisk asterisk_reader 172.17.6.40/32 scram-sha-256
-- - host asterisk asterisk_reader 172.17.6.41/32 scram-sha-256
-- - host asterisk asterisk_reader 172.17.8.20/32 scram-sha-256
-- - host gsc_admin asterisk_reader 172.17.3.11/32 scram-sha-256
-- - host gsc_admin asterisk_reader 172.17.3.12/32 scram-sha-256
-- - host gsc_admin asterisk_reader 172.17.3.13/32 scram-sha-256
-- ============================================================================