v3+
UCS Server - changelog
All notable changes to this project will be documented in this file.
[6.7.0] - 2026-11-13β
π Featuresβ
- (sso) SSO never takes over existing UCS users (ADR 0008)
- Users are matched only by external_id. An existing UCS user of the same
username without external_id logs in via SSO untouched and is skipped by
synchronization (no names, group, role, team, data or phones from AD).
Superusers and users with another external_id are refused. Pair users
explicitly with
ucs-msad external-ids --sql.
- Users are matched only by external_id. An existing UCS user of the same
username without external_id logs in via SSO untouched and is skipped by
synchronization (no names, group, role, team, data or phones from AD).
Superusers and users with another external_id are refused. Pair users
explicitly with
[6.6.0] - 2026-11-13β
π Featuresβ
- (sso) Ucs-msad external-ids pairs synchronized users with UCS users
- For migration of UCS with manually created users: lists users synchronization loads with external ID and status of matching UCS user (ok, set, conflict, superuser, new), matched by synchronized username, sAMAccountName part of username (--match account) or e-mail. --sql prints UPDATE setting external_id of paired users without one, conflicts and superusers are skipped. Changes column shows what synchronization changes on the user: username, active and group, role, team and template user of the matched AD group mapping.
π Bug Fixesβ
- (sso) Principal lookup matches sAMAccountName of realm, ucs-msad sets LDAPSASL_CBINDING
- Principal lookup searched userPrincipalName only, so users whose UPN suffix differs from the realm (kloucek2@cdad.cz vs Kloucek@zap.cd.cz) were not found by MSAD.idp() login and ucs-msad user/attributes. Principal of the provider's realm is Kerberos principal sAMAccountName@REALM (ADR 0006) and now matches userPrincipalName or sAMAccountName, other realms userPrincipalName only.
ucs-msad imported python-ldap before ucs, so LDAPSASL_CBINDING=tls-endpoint set by ucs/init.py was not used and GSSAPI bind over ldaps:// to AD enforcing channel binding failed. ucs-msad sets it by default, environment value wins (none disables it).
[6.5.0] - 2026-11-13β
π Featuresβ
- (sso) Map AD attributes to extension, device, PSTN number and user data
- MSAD SSO settings "attributes" map AD attributes to targets extension, mac, device_type, filter (name or ID, fallback device_type_id / filter_id), pstn (users.number) and user_data key (merged into users.data), device display name template device_displayname.
Synchronization creates extension, softphone, primary extension and device
with line (when MAC is mapped) for users with extension by the same steps as
sync plugin, deletion is limited to msad:
ucs-msad attributes
See decisions/0007-ad-attribute-mapping.md (with login flow diagram). Requires ucs-data 0.103.
π Bug Fixesβ
- (shared) Run change hook when shared data is edited in admin
- Change hooks (e.g. plugin storing cdkc_report as queue metrics) ran only for RPC provisioning.shared (Shared.set). Admin edit (config.shared.edit) stored the data without the hook, so plugins silently did nothing. Hook blocking the data with ProcessingError now rejects the admin edit too.
[6.4.0] - 2026-11-13β
π Featuresβ
- (sso) AD Principal usernames are Kerberos principal sAMAccountName@realm
- MSAD usernames = Principal now makes username sAMAccountName@realm (lowercase)
as in Kerberos ticket instead of userPrincipalName, so synchronized users match
uAuth Kerberos and AD password login with
username: principal. Users of installations with Principal usernames are renamed by next synchronization or login. ADR 0006.
- MSAD usernames = Principal now makes username sAMAccountName@realm (lowercase)
as in Kerberos ticket instead of userPrincipalName, so synchronized users match
uAuth Kerberos and AD password login with
[6.3.2] - 2026-11-13β
π Bug Fixesβ
- (sync) Identity management users from SSO have sync plugin keys
- config.sync.load returned SSO users with load_users() keys only, admin Identity management reads mac, device, number, filter and original of every user. SSO users now have the same keys as declared by the API (no phone, so mac/device/number/filter are None, original is empty).
[6.3.1] - 2026-11-13β
π Bug Fixesβ
- (sso) Send LDAP channel binding token over ldaps (AD LdapErr 80090346)
- AD enforcing LDAP channel binding rejects GSSAPI bind over ldaps:// without channel binding token as invalid credentials (data 80090346). python-ldap has no option for it, so LDAPSASL_CBINDING=tls-endpoint is set by default in ucs/init.py before python-ldap is imported (libldap reads it on import). LDAPS also looks for the RHEL/Fedora system CA bundle.
- (sync) Report unreachable SSO source in identity management
- Identity management showed no users when SSO source failed to load users (e.g. AD not reachable), now it reports error instead of empty list.
[6.3.0] - 2026-11-12β
π Featuresβ
- (sso) Add ucs-msad tool to check AD connection and query users and groups
- Reads MSAD SSO settings via UCS API and queries AD using ucs.sso.msad: list, check, groups, members, users, user (login result) and sync (dry run).
- (sso) Support ldaps:// DC URLs in Active Directory SSO
- DC may be hostname (ldap://) or URL ldap[s]://host[:port]. LDAPS verifies DC certificate against system CAs and sets SASL maxssf=0 (AD refuses GSSAPI sign/seal over TLS). krb5.conf gets hostnames only.
[6.2.0] - 2026-11-12β
π Featuresβ
- (metrics) Queue metrics and external queues
- Queue metric values are stored per queue and date period like user metrics, the queue is referenced by its brainless UUID. Patch 250 adds table queue_metrics. New RPC queues.metric.store/list/delete accepts external queues and UCS inbound queues.
External queues (queues handled by external systems, e.g. OSL) are the first brainless-native config object: no PostgreSQL table, RPC config.external_queues.list/get/add/edit/delete writes the brainless collection directly. Requires ucs-data 0.102.
Metric storage moved to package ucs/metrics: ucs.metrics.agents and ucs.metrics.queues (ucs.agents.metrics removed). ADR 0005 now covers all metrics and external queues.
[6.1.0] - 2026-11-12β
π Featuresβ
- (recording) Allow users to pause and resume call recording
- Agents need to stop recording while a customer dictates sensitive data, for constant recording (trunk incoming route, outgoing filter) as well as recording on demand.
New RPC methods call.record.pause and call.record.resume mute or unmute MixMonitor via AMI MixMonitorMute on every channel of the call carrying the recording. Silence is written while paused, so the recording keeps the call length and stays a single file; conversion and storage are unchanged.
Patch 248 adds role columns recording_pause (permission) and recording_pause_timeout (auto resume after N seconds, empty = never). Auto resume timers are per channel, so they survive call changes on transfer. A newly started MixMonitor is never paused.
Calls and channels expose recording_paused, call updates notify clients.
- (metrics) Store custom user metrics from external systems and plugins
- Admins define metrics (key, unit, rollup, group) and values are stored per user and date period. External systems write them over RPC with the uAuth JWT header, UCS plugins call ucs.agents.metrics.store().
Patch 249 adds tables metrics and user_metrics. Periods of one user and metric never overlap (gist EXCLUDE, btree_gist): writing the same period again overwrites the value, a different overlapping period is rejected.
New RPC methods config.metrics.list/get/add/edit/delete manage definitions, agent.metric.store/list/delete manage values by metric key with optional rollup (sum, avg, min, max, last). See ADR 0005.
π Bug Fixesβ
- (email) Check email source group ACL for source filters
- Filters have no group_id, so add always checked ACL on group 1 and edit/delete failed with KeyError for non-super users. Check the ACL on the group of the filter's email source instead, and on the target source too when a filter is moved.
Dialplanβ
- Record internal calls before entering dispatch workspace
[6.0.0] - 2026-11-09β
π Featuresβ
-
[breaking] UAuth provisioning and refresh of user sessions
-
- auth.lookup has no side effects (unknown user is not created by SSO) and returns id and superuser of the user
-
-
auth.provision creates or updates user from profile read by uAuth from MS AD, using the same mapping as user synchronization (Sync.sync_sso_user)
-
uAuth refreshes sessions of users changed, deleted or renamed in UCS and of users of changed role (rpc.uauth.refresh)
-
Fill user metadata into templates as variables
-
Use aiobp NATS connector and add nats.* API for key-value buckets
- NATS connector is now a thin layer over aiobp.nats keeping the UCS wire format (DecimalEncoder JSON) and the synchronous rpc/bucket access.
-
UCS API is served as rpc.ucs.
to every UCS instance, invalid input errors reply their field errors as before -
every RPC reply code other than 1 raises ProcessingError, error replies with negative codes (e.g. from uAuth) were returned as results before
-
consumer loops stop on shutdown (they ran forever before)
-
NATS events are handled in arrival order across all subjects
New superuser-only API for NATS key-value buckets: nats.buckets, nats.bucket.keys (paging, ordering, values picked from JSON), and nats.bucket.get/set/del with revision checks. Changes emit nats.bucket.update event with old and new value for audit.
Also fixes Json validator crashing on invalid input.
-
OpenVPN configured by ucs-root
- UCS issues the OpenVPN key and certificate and sends the configuration to ucs-root's openvpn.apply without waiting for the result, it runs in the callbacks thread instead of its own one. ucs-root restarts OpenVPN only when something changed, failures are in its journal. HOST changes reprovision OpenVPN too, it's pushed to clients as a route.
-
polkit rule and ConditionFileNotEmpty drop-in removed, also from installed systems by after-install.sh, files given back to root
-
ucs is no longer in openvpn group, polkit dependency removed
-
ucs/root.py: send() copied from ucs-root example client
VPN_SERVICE and UCS_VPN_*_FILENAME parameters are no longer used.
Requires ucs-root with openvpn.apply.
- (teamchat) Colleague chat with groups, receipts and attachments, replaces presence session chat
- (teamchat) Events on conversation subjects routed by ucs-users header
- UCS.notify() and Nats.notify() accept NATS headers, routing_headers() builds ucs-users/ucs-groups. Team chat publishes on teamchat.{conversation_id} with recipients in ucs-users: one publish per message and receipt instead of one per member. client_msg_id is no longer stripped for other members.
- (voicemail) Renumber folders and keep at most 9000 messages
- Asterisk numbers a new message as the highest existing number + 1 and never reuses gaps, so folders managed through UCS ran into its 9999 limit and new messages overwrote msg9999. UCS now removes the oldest messages above 9000 per folder and renumbers the rest from msg0000, keeping voicemail_messages.filename in sync, under Asterisk's folder lock. Runs at startup, daily, when a message gets number 9000 or more, and on demand via voicemail.renumber (superusers).
See decisions/0004-voicemail-renumbering.md.
π Bug Fixesβ
- (brainless) Decode user data stored as varchar JSON
- (recording) Add uniqueid to recording.new event
- The recording.new payload is the recording row, which carries the CDR uniqueid as object_id. Event notifier rules using %uniqueid% got an empty value for it. uniqueid is now added as an alias of object_id.
π Documentationβ
- Add design decisions (ADRs)
- Architecture decision records for decisions spanning UCS components, moved here from the workspace root: NATS routing headers, team chat conversation subjects and snake_case naming for new Python code.
βοΈ Miscellaneous Tasksβ
- Remove keys that we synchronize into Brainess that aren't there
- Make brainless.db public
- Ucs-data 0.101 (NATS routing headers, team chat conversation subjects)
[5.19.0] - 2026-11-05β
π Featuresβ
- Added email feature to mobile_features in roles
- (email) Subject of new e-mail from whole body template
- A whole body template can carry a subject with the same variables as the body. It is set on new e-mails only, replies and forwards keep their subject. %ORIGINAL% in the subject is always empty.
- Link e-mail replies to tasks in queues without task queue
- Thread matching in EmailTasks.delivered() was skipped entirely when the e-mail queue had no new_task_queue_id, so replies to e-mails linked to manually created tasks were never paired. Match the thread for every e-mail queue and only gate creating new tasks (and closing unmatched automatic replies) on new_task_queue_id.
π Bug Fixesβ
- (recording) Transcription_status 0 means never transcribed, 10 waiting for AI driver
π Refactorβ
- (brainless) Store data with ingest() and log invalid values
- Use BrainlessDB 1.0 ingest() instead of setattr()+save(), so one invalid value no longer fails the whole update. Values that could not be stored as given are logged as errors. Fixed security registry attribute names ca_crl_conf and ca_crt_conf.
βοΈ Miscellaneous Tasksβ
- Bump Brainless version to 1.1.0
[5.18.1] - 2026-11-01β
π Bug Fixesβ
- (sms) O2 gateway fails certificate verification on Python 3.13+
- Python 3.13 enabled VERIFY_X509_STRICT in ssl.create_default_context(), which rejects the O2 CA certificate because its Basic Constraints extension is not marked critical. Disable the strict flag for the O2 context, server certificate is still verified against the O2 CA.
[5.18.0] - 2026-11-01β
π Featuresβ
-
Synchronize users from Active Directory SSO provider
- Simpler alternative to the sync plugin built into core. When no sync plugin is installed, sync.synchronize() synchronizes users of every MSAD SSO provider with the new "sync" option enabled:
-
MSAD group mapping extended with group_id, role_id and team_id (user_id template kept for SSO login); mapping is applied also to users created on first SSO login
-
members of mapped groups are loaded via LDAP (paged search, nested membership via LDAP_MATCHING_RULE_IN_CHAIN), first matching group wins
-
users are created/updated (name, e-mail, active, group, role, team), existing users without external ID are paired by username, users no longer member of any mapped group are deactivated (never deleted)
-
external ID is msad:
: -
no groups, extensions or devices are created
-
MSAD login as identity provider with nested group mapping
- SSO login no longer verifies passwords through GSSAPI, MSAD provides identity of already authenticated user from AD (idp()):
-
user is mapped by the first matching AD group mapping, membership counts directly, through primary group and nested (same rule as sync)
-
group mapping sets settings_user_id (inherit provisioning settings) instead of cloning template user_id, "domains" setting is removed
-
fix SID decoding on Python 3 which broke every idp() lookup, primary group is resolved by its SID instead of listing all groups in domain
-
usernames are escaped in LDAP filters
-
krb5.conf domain_realm is written once per realm, not once per DC
-
sync uses every SSO handler with load_users() and sync enabled, frontend or backend
-
gssapi Python dependency and libkrb5-dev dropped
-
tests/msad_check.py tests MSAD against real AD with Kerberos ticket
-
Link e-mails to tasks, return tasks to last agent, automatic task assignment
-
- Link received e-mails to tasks by Message-ID/References threading, reopen recently closed tasks on customer reply, tag and close automatic replies
-
-
Link outgoing replies to task of original e-mail, optionally close task on reply or when e-mail is closed unreplied (forwards are never treated as replies)
-
Return task moved back into queue to its last agent in that queue
-
Automatically assign tasks to agents up to team minimum, capped by maximum
-
E-mail edit API task_id checks task access and records link in both histories
-
E-mail sender option save_original stores sent message as original.msg
-
Background DB patches (patchNNN.background.sql/py) tracked in patches table, run after UCS start with own connection; patch 244 backfills in batches
π Bug Fixesβ
- Always include original e-mail when forwarding
- A forward lost the original e-mail when the queue default template had no %ORIGINAL% placeholder, or when the queue had no default template at all. Add the original after the template content in the first case and use it as the whole body in the second. Replies are unchanged.
- Keep forward when applying template to a forward draft
- template.use took the forward flag only from the client, which does not send it when an agent applies a body template to an existing draft. The original e-mail was then quoted as a reply instead of forwarded. Use the forward flag stored on the draft as well.
- Apply template with the e-mail the draft was created from
- A reply or forward draft found its original e-mail through reply_id, which is set on the first e-mail of the thread. When the agent replied to or forwarded an outbound e-mail, the draft was created from that e-mail, but applying a template later quoted or forwarded the thread's first e-mail instead.
Store the e-mail the agent replied to or forwarded in a new emails.original_id column (DB patch 243) and use it when applying a template. Drafts created before the patch fall back to reply_id.
- Keep original attachments when applying template to a draft
- Applying a body template removes all of the draft's attachments, which on a forward includes the ones copied from the original e-mail. Now that the template puts the original body back in, the forward lost its attachments and its inline images pointed to the original e-mail.
Move copying of the original's attachments from the reply API into Email.copyOriginalAttachments() and call it from useTemplate() whenever the original ends up in the new body: all attachments for a forward, inline images for a reply.
- Copy default template attachments to new e-mails
- A new e-mail took only the HTML of the queue's default template. The template's attachments were never copied and the picture links kept pointing at the template, so pictures were missing in the sent e-mail. Applying the same template manually worked because useTemplate() copies the attachments.
Move the copying into _addTemplateAttachments() and use it in add() as well. reply() applies the template itself afterwards, so it tells add() to skip it.
Picture links are now always pointed at the e-mail's own copy, not only when the attachment IDs differ, and the link to template attachment 1 no longer matches attachment 10.
- Unpair lead user when called don't answer the call so anyone else can try it later
π Documentationβ
- Explain reply_to vs. original_id on reply and forward drafts
π§ͺ Testingβ
- Cover Content-Type name parameter on attachments
- Add uuid and inbound to e-mail test fixtures
- sender.py reads email['uuid'] for Message-ID since 3b39cf1c and api/email.py reply() reads email['inbound'], but the fixtures didn't have them, so 9 tests failed with KeyError.
βοΈ Miscellaneous Tasksβ
- Ignore Claude Code worktrees and local settings
- Log thread utilization to /var/spool/ucs, added cluster create users, removed old docs
[5.17.1] - 2026-10-25β
π Bug Fixesβ
- Give asterisk group access to MOH classes /var/lib/asterisk/moh-
[5.17.0] - 2026-10-25β
π Featuresβ
-
Run UCS as unprivileged ucs user, system configuration by ucs-root
- UCS runs as ucs user (NoNewPrivileges, UMask 0002) and asks ucs-root over /run/ucs/root.socket to apply network, firewall, hostname, NTP, SMTP, DHCP and web certificate configuration (ucs/root.py, copy of ucs-root's example client). provisioning/system.py collects the data from the database as before and sends it as structured parameters.
-
package: ucs user in uauth, asterisk and openvpn groups, owns its log, spool and TFTP directories, depends on ucs-root
-
after-install.sh: ownership for ucs, /etc/ucs/ucs.conf ucs:ucs 0600, web key root:traefik 0640, Asterisk directories group writable, /etc/krb5.conf and OpenVPN files group writable by ucs, DHCP files back to root, old O2 SMS keys removed from /etc/ssl
-
/var/spool/ucs owner is changed in background by ucs-spool-chown (systemd-run, newest e-mails first), it may take hours
-
OpenVPN: polkit lets ucs start/stop/restart openvpn-server@ucs, enabled by the package, a drop-in skips it while its configuration is empty
-
O2 SMS gateway loads certificates from memory (memfd), never from disk
-
keytab made readable by uauth group before start
[5.16.2] - 2026-10-23β
π Bug Fixesβ
- Add name parameter to attachment Content-Type for older mail clients
- Some clients (old Outlook, Lotus Notes, ...) read the attachment name only from the deprecated Content-Type name parameter and show nameless/generic attachments otherwise. Non-ASCII names are sent there as RFC 2047 encoded-words, since those clients don't understand RFC 2231.
- Split postgres initialization and create/upgrade to after install/transaction
[5.16.1] - 2026-10-22β
βοΈ Miscellaneous Tasksβ
- Use .python-version as source of truth
[5.16.0] - 2026-10-21β
π Featuresβ
- UAuth credentials, lookup and impersonation API via NATS
-
- auth.credentials verifies username and password without creating session
-
- auth.lookup checks user authenticated by uAuth is active and returns apps
- auth.impersonate tells uAuth whether its user may impersonate another one
- password verification of sessions.login split to reusable verify()
- password of auth.credentials is masked in request log
- install uAuth login.d/50_ucs.yaml and lookup.yaml
[5.15.3] - 2026-10-21β
π Bug Fixesβ
- Create /var/lib/tftp on Ubuntu too
[5.15.2] - 2026-10-21β
π Bug Fixesβ
- Create /run/postgresql before restarting PostgreSQL on RHEL
- The restart failed with "could not create lock file /var/run/postgresql/.s.PGSQL.5432.lock". The directory comes from tmpfiles.d, which systemd's rpm file trigger applies only after all %posttrans scriptlets, so after-transaction.sh now creates it with systemd-tmpfiles first.
[5.15.1] - 2026-10-21β
π Bug Fixesβ
- Restart PostgreSQL and create database after the dnf transaction
- On RHEL "systemctl restart postgresql" failed inside the dnf transaction. The restart and create.sh move to the packager's after_transaction hook (rpm %posttrans, end of postinst on deb), which also starts ucs.service again now that the database exists. Hook messages use pkg_log/pkg_warn.
Needs the packager with after_transaction and pkg_log (insoft/deploy).
[5.15.0] - 2026-10-20β
π Featuresβ
- Added username to structure.enums user list
π Bug Fixesβ
- Dialer lost lead clid_number when outbound has no numbers
βοΈ Miscellaneous Tasksβ
- Moved to INSOFT Python 3.14
[5.14.8] - 2026-10-19β
π Bug Fixesβ
- Libmysql client has different versions in Ubuntu noble and resolute
[5.14.7] - 2026-10-19β
π Bug Fixesβ
- Typo in package.yaml tftp path
[5.14.6] - 2026-10-19β
π Bug Fixesβ
- Upgrade to aiobp 3.0.1
[5.14.5] - 2026-10-19β
π Bug Fixesβ
- Hold aiobp < 3.0.0
[5.14.4] - 2026-10-17β
π Bug Fixesβ
- (dispatch) Only force call.connect for a leg ucs-ami already reported up
βοΈ Miscellaneous Tasksβ
- Switched to new build system
[5.14.3] - 2026-10-14β
π Bug Fixesβ
- Handle Decimal encoded timestamp in Brainless
- Fallback to context if trunk peer name differs