№ A · 06 / Intelligence architecture
One toggle in git,
a complete AI workspace in a customer's cluster.
ADR-12 makes Intelligence look like a single boolean: intelligence: true in the customer YAML, everything else automatic. Underneath, the chart spins up the Onyx CE workload, a Keycloak service-account broker, a Univention portal tile, a model-list and per-user-connector reconciliation loop, and reuses the tenant's existing Postgres, Redis, MinIO and Keycloak so it never asks an operator to provision a second copy of anything. This page documents what each component does, where it lives, and how the wiring is structured.
№ I1 / Component anatomy
Onyx isn't one process.
It's a small system, plus a cluster-shared gateway, plus EU-hosted APIs.
The popular reading of "an AI workspace" is one container with a chat box. The reality, both upstream and in our deployment, is a small system: an API, a UI, a worker pool, two model servers (one for indexing and one for queries), a hybrid search index, a relational database and a cache. Plus Skynet on the cluster for live captions, and two external APIs at Mistral for the heavy lifting Mistral is good at. Each row below names a real Kubernetes object you would see with kubectl get.
Component
Role
Where it runs
api_server
FastAPI backend. Serves the chat API, the admin API, the connector API and the OIDC callback.
New Deployment, 1 to 2 replicas, sits behind the Intelligence vhost.
web_server
Next.js UI. The workspace the user actually sees.
New Deployment, 1 to 2 replicas, served through the same ingress as the API.
celery workers (4 pools)
Pull from each connector on a schedule, chunk and embed documents, push them into Vespa. Pools: primary, docfetching, docprocessing, light.
Four Deployments, 1 replica each. No public ingress; talk to Vespa and the indexer.
indexing_model_server
HuggingFace embedding model on the indexing path. Turns document chunks into vectors as they arrive.
New Deployment. CPU-only by default; can opt into the Mistral embeddings API per-tenant.
inference_model_server
Same model class on the query path. Turns the user's question into a vector at search time.
New Deployment, kept hot so queries don't pay cold-start latency.
vespa
Hybrid vector + BM25 search index. The actual retrieval engine that Onyx queries on every chat turn.
Dedicated StatefulSet, JVM, PVC backed by the tenant storage class. Heavier than the rest combined.
postgres (intelligence schema)
Users, chats, connector configs, ACL state, audit log.
Reuses the tenant Postgres statefulset from core-base. Onyx gets a dedicated database <slug>_onyx; no shared tables.
redis (logical DB)
Celery broker, in-memory cache, plus the credentials provider lock used during in-flight bearer-token refresh.
Reuses the tenant Redis. Onyx gets a dedicated logical DB index; eviction is isolated.
keycloak-broker-bootstrap
Pre-install Helm Job. Creates the kiwistack-intelligence-broker service-account client in the tenant Keycloak realm, grants it fine-grained token-exchange permission on each target client (opendesk-nextcloud, opendesk-caldav, opendesk-xwiki, opendesk-openproject), writes the broker secret to a per-release K8s Secret. Idempotent.
One-shot Job in <slug>-intelligence, runs at install and on every helm upgrade.
portal-tile-bootstrap
Pre-install Helm Job. Calls the tenant UDM REST API to UPSERT a portals/entry for the Intelligence tile and append it to the od.applications category. Sparkle glyph in pop-coral, white background.
One-shot Job, runs after the broker bootstrap and before the Onyx seed Job.
onyx-seed
Post-install Helm Job. UPSERTs the llm_provider row, the model_configuration rows (and DELETEs same-provider rows not in the desired list, so the admin UI never grows ghost models), anchors the built-in Assistant persona to the chart's default model, and seeds the SearXNG web search provider.
One-shot Job in <slug>-intelligence at install + every upgrade.
onyx-user-reconcile (CronJob)
Every 5 minutes, walks members of the realm group intelligence-users and UPSERTs one (connector, credential, PRIVATE cc_pair, user_group user:<onyx-uuid>) per enabled per-user data source: Nextcloud, CalDAV, XWiki, OpenProject. Skips users who have not yet logged into Onyx. Never deletes (orphan cleanup is a v0.2 workstream).
CronJob in <slug>-intelligence; concurrencyPolicy=Forbid.
Skynet
faster-whisper module that streams live captions during a Jitsi call.
Already deployed in core-base for every Core tenant (see ADR-11). Intelligence flips the engine on per-tenant; no new pods.
LiteLLM gateway
Routes Onyx's chat-completion + embedding traffic to Mistral La Plateforme. Cluster-shared, one instance per K3S cluster (not per tenant). Holds the master key; mints per-tenant virtual keys with a model allow-list.
Deployment in the cluster-shared kiwi-llm-gateway namespace.
SearXNG (web search)
Self-hosted metasearch aggregator. Used by Onyx's WebSearchTool for live web search citations.
Deployment in the cluster-shared kiwi-search-gateway namespace.
Voxtral (Mistral La Plateforme)
Post-call audio to a diarised transcript with word-level timestamps.
External API, EU-hosted. No on-cluster component, audio is uploaded once per call.
Mistral La Plateforme (chat)
Chat completion endpoint, reached through the cluster LiteLLM gateway above.
External API, EU-hosted, Paris.
On the fork, and on Vespa
We do maintain a small fork of Onyx CE, KiwiStack/onyx-kiwistack, branch kiwistack/main. It carries five surgical patches: the Nextcloud connector with Keycloak token exchange and OpenDocument text extraction, the Mistral function-call sanitiser, the IMAP synthetic-id fallback, the S3 endpoint_url support, and the visual rebrand. The branch rebases weekly against upstream; the patches are intentionally small so the rebase stays a chore, not a project. Vespa stays as-is, because hybrid vector + BM25 in one engine is what the citation UX actually relies on. The cost is one JVM StatefulSet per tenant; the benefit is that bumping Onyx CE is a one-line tag change.
№ I2 / What it reuses, what it adds
Reuse where reuse is safe.
Dedicated where it isn't.
The Core tier already runs Postgres, Redis, MinIO and Keycloak per tenant. Spinning up a second copy of each just to host Onyx would double the operational surface for no isolation benefit. So Intelligence reuses those, with a dedicated database, a dedicated bucket, a dedicated Redis index and a dedicated OIDC client. The pieces that actually carry Intelligence-specific load (Vespa, the model servers, the workers, the API, the UI) are net new and live in their own namespace.
Component
Source
Strategy
Postgres
tenant statefulset (core-base)
New database <slug>_onyx. Onyx never touches OX, Nextcloud or Keycloak schemas.
Redis
tenant Redis (core-base)
New logical DB index. Cache eviction and Celery broker are isolated by index.
MinIO
tenant MinIO (core-base)
New bucket <slug>-onyx for Voxtral transcripts and Onyx uploads only. File search no longer touches MinIO since the Files connector reads through Nextcloud directly.
Keycloak (Nubus)
tenant realm (core-base)
Two new OIDC clients. The login client intelligence-<slug> for OIDC sign-in. The broker client kiwistack-intelligence-broker (service-account only) with fine-grained token-exchange permission on each per-user data source. Reconciliation reads the intelligence-users realm group via the Admin API.
OpenLDAP + UDM REST
tenant directory (core-base)
Read-write for the portal-tile-bootstrap Job (one entry under cn=entry,cn=portals,...). Read-only for the user-reconcile CronJob (Admin API users list).
Univention portal (Nubus)
tenant portal (core-base)
Intelligence tile appended to the od.applications category. White background, coral sparkle glyph. ums-portal-consumer downstream syncs the entry to the live portal.
Ingress + cert-manager
cluster-shared
New vhost intelligence.<slug>.od.kiwistack.io. TLS via the existing wildcard.
LiteLLM gateway
cluster-shared (kiwi-llm-gateway)
Onyx is wired to litellm.kiwi-llm-gateway.svc as its OpenAI-compatible endpoint. One LiteLLM per cluster; per-tenant virtual key with a model allow-list.
SearXNG
cluster-shared (kiwi-search-gateway)
Web search backend for the WebSearchTool. JSON format enabled in settings.yml.
Vespa + model servers + api / web / celery
intelligence-base (new)
All dedicated to Intelligence. Live entirely inside the <slug>-intelligence namespace.
NetworkPolicy + ResourceQuota + LimitRange + PodSecurity
core-base
Applied to <slug>-intelligence the same way every other tenant namespace gets them (gitops § G3).
Same per-tenant isolation primitives as every other app in core-base. The <slug>-intelligence namespace cannot reach <other-slug>-*; cross-customer egress fails at the NetworkPolicy. Mesh and Fleet customers run the same chart in their dedicated k3s; the only difference is the namespace pattern drops the slug prefix (see /architecture/gitops § G3 for the convention).
№ I2b / Deployment topology
Three rings.
Two cluster-shared,
one per tenant,
one off-cluster.
Two services run cluster-wide and are reused by every tenant: the LiteLLM gateway that routes all model traffic to Mistral La Plateforme, and a self-hosted SearXNG for the WebSearchTool. Everything else lives in two per-tenant namespaces: cust-<slug> holds the OpenDesk identity stack plus the shared services Intelligence reuses (Postgres, Redis, MinIO, Nextcloud, the Univention portal); kiwistack-intel-<slug> holds the Onyx workload itself. The two pre-install Jobs reach across the namespace boundary (UDM REST, Keycloak Admin) to wire the customer experience.
┌─────────────────────────────────────────────────────┐
│ Cluster-shared (one instance per K3S cluster) │
│ ┌───────────────────┐ ┌────────────────────┐ │
│ │ kiwi-llm-gateway │ │ kiwi-search-gateway│ │
│ │ LiteLLM → Mistral │ │ SearXNG (self-host)│ │
│ └─────────┬─────────┘ └─────────┬──────────┘ │
└────────────┼────────────────────────┼───────────────┘
│ /v1/chat │ /search
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Per-customer (cust-<slug> + kiwistack-intel-<slug>) │
│ │
│ ┌─────────────────────────┐ ┌────────────────────────────┐ │
│ │ cust-<slug> │ │ kiwistack-intel-<slug> │ │
│ │ Keycloak (OIDC IdP) │◄───┤ Onyx API + Web │ │
│ │ OpenLDAP + UDM REST │◄───┤ Celery workers (4 pools) │ │
│ │ Nextcloud (user_oidc) │◄───┤ Model servers (CPU embed)│ │
│ │ Univention portal │◄───┤ Vespa hybrid index │ │
│ │ PostgreSQL, Redis, │◄───┤ keycloak-broker bootstrap│ │
│ │ MinIO, Dovecot, ... │ │ portal-tile bootstrap │ │
│ └─────────────────────────┘ │ onyx-seed │ │
│ ▲ │ onyx-user-reconcile cron │ │
│ │ UDM REST: add tile └────────────┬───────────────┘ │
│ │ KC Admin: list users │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
▲
│ Mistral La Plateforme (Paris, EU)
│ Voxtral API (Paris, EU)
┌────────────┴────────────┐
│ External AI providers │
└─────────────────────────┘ Mesh and Fleet customers get their own K3S clusters, so the "cluster-shared" ring is in fact also per-customer at those tiers. The split above is the per-tier-Core deployment, where one shared cluster hosts many cust-<slug> namespaces.
№ I3 / The six connectors
Six wires
into the rest of the suite.
ADR-12 promises six out-of-the-box connectors. Each one talks the protocol the source app already exposes: IMAP for mail, WebDAV plus OCS for files (with per-user Keycloak bearer tokens, no shared service account), REST APIs for the apps that have them, CalDAV for the calendar, and an S3 prefix where the Voxtral pipeline drops transcripts. Only Nextcloud and the broker logic are KiwiStack-fork additions; the other five connectors are upstream Onyx CE wired through the chart's seed Job and the onyx-user-reconcile CronJob.
→ OX App Suite / Stalwart
Protocol
IMAP4 SSL on :993
Auth
Per-tenant read-only service account, credentials in SOPS
Indexed as
One document per message. ACL respects the user's Keycloak group through Onyx connector permissions.
Files
→ Nextcloud
Protocol
WebDAV PROPFIND + GET (per user), plus OCS /cloud/user for the owner email
Auth
Keycloak OIDC token exchange (RFC 8693). The broker SA client mints a per-user bearer token for each request; Nextcloud's user_oidc app validates it and runs the call as the user. No shared service account, no app-passwords.
Indexed as
One Document per file. external_user_emails={owner_email} on every Document, so Onyx's apply_document_access_filter scopes retrieval per logged-in user. Office bodies (.pdf/.docx/.pptx/.xlsx) extracted via Onyx's bundled reader; OpenDocument (.odt/.odp/.ods) via a stdlib ZIP+XML helper in the fork. Citation URLs use the public files.<domain> hostname, not the cluster-internal WebDAV one.
Wiki
→ XWiki
Protocol
REST API on xwiki/rest/wikis/<wiki>/spaces/<space>/pages/<page>
Auth
Keycloak OIDC token exchange (RFC 8693). The broker SA mints a per-user bearer token for each request; XWiki's OIDC auth module validates it and runs the call as the user. Same pattern as Files.
Indexed as
One Document per page. Per-user reconcile (the onyx-user-reconcile CronJob UPSERTs one PRIVATE cc_pair per user) means each user sees only the pages they have Read on. Author, space, syntax and version metadata preserved as Onyx filters.
Projects
→ OpenProject
Protocol
REST API on /api/v3/projects, /api/v3/work_packages and /api/v3/work_packages/<id>/activities
Auth
Keycloak OIDC token exchange (RFC 8693). The broker SA mints a per-user bearer token; OpenProject's OIDC plugin validates it and answers as the user. Same pattern as Files.
Indexed as
One Document per work package, with activity comments inline. Per-user reconcile (PRIVATE cc_pair per Keycloak user) means each user only indexes the work packages OpenProject would return for them. Type, status, priority, assignee, due date and project preserved as Onyx filters.
Calendar
→ Tenant calendar (OX or Nubus)
Protocol
CalDAV PROPFIND on the user principal plus calendar-multiget REPORT for VEVENTs in a bounded time window
Auth
Keycloak OIDC token exchange (RFC 8693). The broker SA mints a per-user bearer token; the CalDAV server validates it via its OIDC auth module and returns only the user's home calendars. Same pattern as Files.
Indexed as
One Document per VEVENT. external_user_emails = union of attendees + organiser, so an event indexed under one user's home is retrievable by every attendee through Onyx's per-user ACL filter.
Meetings
→ Voxtral transcripts
Protocol
S3 connector against <slug>-onyx/meetings/ where the Voxtral pipeline writes
Auth
Bucket-scoped, internal to the cluster
Indexed as
One document per meeting, with speaker labels and timestamps that link back to the recording.
What's not wired (and why)
Notes (CryptPad) is end-to-end encrypted: the server never sees plaintext, so no AI can read it. This is a property of CryptPad, not a gap we'll close (see /office/notes). Chat (Matrix / Synapse) has no upstream Onyx connector today. A custom bridge is feasible but adds a fork to maintain. We defer it to v2 once the upstream connector landscape settles.
№ I4 / Identity, RBAC, portal
One login.
One tile in the portal.
Onyx authenticates the same way every other app in the suite does: OIDC against the tenant's Nubus Keycloak. The customer does not see a second sign-in screen, manage a second password, or enroll a second MFA factor. Keycloak groups drive Onyx roles; the portal tile is added and removed by the chart itself.
Two OIDC clients
The intelligence-<slug> login client (standard confidential OIDC, scopes openid email profile groups, redirect on the Intelligence vhost) handles user sign-in. The kiwistack-intelligence-broker client is service-account only, with fine-grained token-exchange permission on each per-user data source (opendesk-nextcloud, opendesk-caldav, opendesk-xwiki, opendesk-openproject). Connector pods and the kiwi-broker MCP server read its credentials from env via the <release>-keycloak-broker Secret; rotation is a Secret update plus a pod restart.
Per-user reconcile, not roles
ACL is per-user, not per-group: each Document is tagged external_user_emails={owner_email} at index time (calendar events use the union of attendees + organiser), and Onyx's retrieval filter scopes results to the authenticated user automatically. The Keycloak group intelligence-users only decides who is in scope for the reconcile loop: every 5 minutes the onyx-user-reconcile CronJob walks that group, looks each user up by email in the Onyx DB (skips users who have not signed in yet), and UPSERTs the per-user (connector, credential, PRIVATE cc_pair, user:<onyx-uuid> group) tuple per enabled per-user data source: Nextcloud, CalDAV, XWiki, OpenProject.
Portal tile
The portal-tile-bootstrap Job UPSERTs a portals/entry in the tenant UDM directory and appends it to the od.applications category, alongside Files, Mail, Calendar. Title Intelligence; sparkle glyph filled in #FF7043 (KiwiStack pop-coral) on a white background, so it reads as Intelligence at a glance next to the Core tier. URL is the per-tenant ingress. The tile only appears when intelligence: true.
№ I5 / Voxtral pipeline
From a Jitsi call
to a searchable
meeting record.
Meetings are the connector with the most moving parts because the source data is audio, not text. The pipeline below combines a piece of core-base (Jitsi + Skynet, already deployed for every Core tenant) with two pieces of intelligence-base (Voxtral and the Onyx S3 connector). The engine choice itself was made in ADR-11.
01
Record
Jitsi records the call. Already deployed by core-base for every Core tenant. No new component for Intelligence.
02
Live captions
Skynet's faster-whisper module streams captions in the Jitsi UI in real time. Runs on the tenant cluster, no API call.
03
Audio upload
Post-call, Jigasi uploads the audio to Voxtral on Mistral La Plateforme (EU).
04
Diarised transcript
Voxtral returns the transcript with speaker labels and word-level timestamps. Typical latency: 1 to 3 minutes for an hour-long call.
05
Persist
The pipeline writes the transcript to <slug>-onyx/meetings/<date>/<roomid>/transcript.md on the tenant MinIO. The raw audio is dropped unless the customer opts into retention.
06
Index
Onyx's S3 connector picks it up on the next pass (every 15 minutes by default).
07
Searchable
The meeting is now in the workspace, with citations that link back to the moment in the recording.
Live captions are not the same engine as the post-call transcript. Skynet runs faster-whisper on the cluster because real-time captioning needs sub-second latency; Voxtral runs on Mistral La Plateforme because diarisation and speaker labels need a bigger model than is worth standing up per tenant. The split is invisible to the user.
№ I6 / Helm chart structure
One umbrella,
five thin overlays
over upstream Onyx.
The intelligence-base chart lives in od-platform alongside core-base and mesh-base. It bundles upstream Onyx with five KiwiStack-owned overlay subcharts: the Keycloak client, the theme, the connectors, the portal tile, and a small Keycloak group seed. Nothing in upstream Onyx is forked, so upstream upgrades are a version bump in Chart.yaml, not a rebase.
od-platform/
├── charts/intelligence-base/
│ ├── Chart.yaml # current: 0.8.0
│ ├── values.yaml # keycloak.broker, connectors.{mail,files,calendar,wiki,projects,meetings}, broker.*, portal.tile
│ └── templates/
│ ├── keycloak-broker-bootstrap-job.yaml # pre-install hook (weight -10)
│ ├── portal-tile-bootstrap-job.yaml # pre-install hook (weight -5)
│ ├── onyx-seed-job.yaml # post-install hook (incl. mcp_server row, Meetings cc_pair)
│ ├── onyx-seed-configmap.yaml # reconcile SQL (llm_provider, models, persona, web search, meetings, mcp_server)
│ ├── kiwi-broker-deployment.yaml # unified actions MCP server
│ ├── kiwi-broker-service.yaml # ClusterIP :8080 (Onyx reaches the broker by Service DNS)
│ ├── user-reconcile-cronjob.yaml # every 5 min, per-user UPSERTs across Nextcloud/CalDAV/XWiki/OpenProject
│ └── ingress.yaml
├── services/
│ ├── keycloak-bootstrap/ # stdlib Python, alpine image
│ ├── portal-tile-bootstrap/ # stdlib Python, alpine image
│ ├── onyx-user-reconcile/ # Python + psycopg; iterates source descriptors
│ └── kiwi-broker/ # FastMCP server hosting every action tool
│ ├── main.py # bootstrap only
│ └── tools/
│ ├── _app.py # shared FastMCP + Redis + config
│ ├── _auth.py # OIDC bearer decode + JWT body parse
│ ├── _drafts.py # Redis draft store (per element_type + user_sub)
│ ├── mail.py # prepare_email / send_prepared_email
│ ├── calendar.py # prepare_calendar_event / send_calendar_event / list_upcoming_events
│ ├── wiki.py # prepare_wiki_page / publish_wiki_page / update_wiki_page
│ └── projects.py # prepare_work_package / create_work_package / comment_on_work_package
└── customers/<slug>.yaml # per-tenant values + Secret refs
KiwiStack/onyx-kiwistack/ (fork of onyx-dot-app/onyx, kiwistack/main branch)
├── backend/onyx/connectors/_kiwistack/ # fork-only primitives
│ ├── keycloak_delegated.py # bearer-token mixin (RFC 8693)
│ └── odf_text.py # stdlib .odt/.odp/.ods text extractor
├── backend/onyx/connectors/nextcloud/ # fork-only WebDAV+OCS connector
├── backend/onyx/connectors/caldav/ # fork-only CalDAV connector
├── backend/onyx/connectors/xwiki/ # fork-only XWiki REST connector
├── backend/onyx/connectors/openproject/ # fork-only OpenProject REST connector
├── backend/onyx/llm/multi_llm.py # two-pass Mistral sanitiser
└── web/src/kiwistack/custom-elements/ # action UI cards
├── registry.tsx # element_type → React component
├── parser.ts # extracts custom-element packets from the stream
└── elements/ # EmailDraft, CalendarEventDraft, WikiPageDraft, WorkPackageDraft Subchart
Owner
Role
onyx (forked)
KiwiStack/onyx-kiwistack
Forked from onyx-dot-app/onyx CE, branch kiwistack/main. Image tag kiwistack.<sha>. Brings the api / web / celery / model servers / Vespa. Carries five maintained fork patches (see I6b below); rebases against upstream main weekly.
keycloak-broker-bootstrap (Job)
KiwiStack/od-platform
Pre-install + pre-upgrade Helm hook (weight -10). Creates the kiwistack-intelligence-broker service-account client in the tenant realm, grants it fine-grained token-exchange permission on each target client (opendesk-nextcloud, opendesk-caldav, opendesk-xwiki, opendesk-openproject). Writes the broker secret to <release>-keycloak-broker. Idempotent.
portal-tile-bootstrap (Job)
KiwiStack/od-platform
Pre-install + pre-upgrade Helm hook (weight -5). UPSERTs a portals/entry in the tenant UDM directory with a sparkle glyph (#FF7043 on white) and appends it to the od.applications category. Idempotent.
onyx-seed (Job)
KiwiStack/od-platform
Post-install Helm hook. Reconciles the llm_provider + model_configuration rows (INSERTs from values, DELETEs same-provider rows not in the desired set), seeds the SearXNG web search provider, seeds the Meetings S3 cc_pair (tenant-internal, no per-user reconcile), registers the kiwi-broker MCP server, and anchors the built-in Assistant persona's default_model_configuration_id.
onyx-user-reconcile (CronJob)
KiwiStack/od-platform
Runs every 5 minutes. Walks members of the realm group intelligence-users via the Keycloak Admin API. For each user with an Onyx account already (skipped if not yet logged in), UPSERTs one (connector, credential, PRIVATE cc_pair, user_group user:<onyx-uuid>) per enabled per-user data source: Nextcloud, CalDAV, XWiki, OpenProject.
kiwi-broker (Deployment)
KiwiStack/od-platform
In-cluster FastMCP server hosting every action tool (prepare/send pairs for mail, calendar, wiki and projects) behind one Service. Auth is pass-through: tools decode the user's Keycloak access token from the inbound MCP request and forward it as Bearer to the target app for writes.
intelligence-base values
KiwiStack/od-platform
Per-customer values.yaml flips intelligence: true and sets the public hostname plus references to per-tenant Secrets (onyx-postgres, onyx-redis, onyx-objectstorage, keycloak-admin, ums-administrator). One-line PR in the customer state repo.
№ I6b / Onyx CE fork patches
Five surgical patches.
One weekly rebase.
The fork tag kiwistack.<sha> is what the chart pulls. Each patch below is small, surgical, and tagged [kiwistack-fork] in its commit, so the weekly rebase against upstream/main stays mechanical.
Patch
What it does
frontend: visual rebrand
Kiwi mark replaces the Onyx mark across the chat header (no wordmark), the agent avatar in streaming messages, and the left navigation tint (kiwi-green). Subtitle text reads 'Intelligence' alone, no KiwiStack prefix.
frontend: custom-element framework + actions UI
New web/src/kiwistack/custom-elements/ registry that renders MCP tool results returning response_type=custom_element as inline interactive cards (EmailDraft, CalendarEventDraft, WikiPageDraft, WorkPackageDraft). Send/Create/Post buttons POST to /api/chat/mcp-tool-invoke (a new chat-backend endpoint) so the action lands without an LLM round-trip.
connector: Keycloak-delegated bearer mixin
Shared _kiwistack/keycloak_delegated.py implements RFC 8693 subject-token-exchange: the SA broker mints a per-user bearer token, caches it in the credential row with a TTL-aware refresh path. Reused by every per-user connector below.
connector: Nextcloud + ODF text
Nextcloud connector module that walks WebDAV PROPFIND with the bearer mixin and extracts text from office (PDF/docx/pptx/xlsx) and OpenDocument (odt/odp/ods) files. Drops the legacy S3-against-MinIO Files path entirely.
connector: CalDAV
New CalDAV connector that does PROPFIND on the user's principal path, REPORTs VEVENTs in [now-90d, now+365d], and parses ICS with a stdlib scanner. external_user_emails is the union of attendees + organiser so each invitee can retrieve the event in their own chat.
connector: XWiki
New XWiki connector that walks /xwiki/rest/wikis/<wiki>/spaces/.../pages/<page> with the bearer mixin and cleans xwiki/2.1 macros (or passes markdown/1.0 through) before indexing.
connector: OpenProject
New OpenProject connector that pages /api/v3/projects and /api/v3/projects/<id>/work_packages, then fetches /work_packages/<id>/activities to fold inline comments into each work-package Document.
llm: Mistral function-call sanitiser
Two-pass cleaner in multi_llm.py. Trims unmatched assistant.tool_calls and orphan role=tool messages before the API call, so Mistral's strict pairing rule never 400s when Onyx's agentic loop produces an unbalanced list (typical on empty search results).
connector: IMAP synthetic Message-ID + Date fallback
Onyx CE indexes one Document per email but crashes when an upstream message lacks a Message-ID header. The fork synthesises a stable id from a SHA-256 of headers + body, and falls back to UTC now() for missing Date.
connector: S3 endpoint_url support
Onyx CE's S3 connector hardcodes the AWS endpoint. The fork reads s3_endpoint_url from the credential, threads it through boto3, and rewrites the citation URL so a custom MinIO works as a legacy file source for customers who do not use the per-user Nextcloud connector.
Wiring it in
template-core declares it,
cust-<slug> toggles it.
The umbrella is referenced as a conditional Helm dependency in template-core; the customer's state repo carries the flag plus any per-tenant overrides. This matches the existing pattern for core-base and mesh-base (see /architecture/gitops § G1, G2).
template-core / Chart.yaml
dependencies:
- name: core-base
version: 1.4.2
repository: oci://ghcr.io/kiwistack/charts
- name: intelligence-base
version: 0.8.0
repository: oci://ghcr.io/kiwistack/charts
condition: intelligence.enabled cust-<slug> / values / intelligence.yaml
intelligence:
enabled: true
hostname: intelligence.od.<customer>
llmProvider:
name: mistral-via-litellm
models: [mistral-large, mistral-medium, mistral-small]
defaultModel: mistral-medium
keycloak:
realm: opendesk
broker:
targetClients:
- opendesk-nextcloud
- opendesk-caldav
- opendesk-xwiki
- opendesk-openproject
reconcile:
schedule: "*/5 * * * *"
group: intelligence-users
connectors:
files:
mode: keycloak-delegated
baseUrl: http://opendesk-nextcloud-aio.cust-<slug>.svc.cluster.local
publicBaseUrl: https://files.od.<customer>
calendar:
enabled: true
baseUrl: http://opendesk-caldav.cust-<slug>.svc.cluster.local
publicBaseUrl: https://calendar.od.<customer>
wiki:
enabled: true
baseUrl: http://opendesk-xwiki.cust-<slug>.svc.cluster.local
publicBaseUrl: https://wiki.od.<customer>
projects:
enabled: true
baseUrl: http://opendesk-openproject.cust-<slug>.svc.cluster.local
publicBaseUrl: https://projects.od.<customer>
meetings:
enabled: true
prefix: meetings/
portal:
enabled: true
tile: { title: Intelligence, color: "#FF7043" } The OpenDesk pattern, applied
This is the same overlay shape OpenDesk applies to OX App Suite, Nextcloud, XWiki and OpenProject. The upstream app stays upstream; KiwiStack owns five thin overlay subcharts that wire it into the suite (OIDC, theme, portal tile, connectors, Keycloak client). Nothing in Onyx itself is forked, so the version contract with upstream is just a Helm version: field. The same is true of Nextcloud (Helm chart in core-base) and XWiki today, which is why upgrades on the rest of the suite are also one-line PRs.
№ I7 / Sovereignty and data flow
Most of it never leaves
the customer's cluster.
Two external API calls; everything else stays on the tenant cluster. The DPA addendum on /office/intelligence lists exactly what's sent, when, and how long Mistral retains it on its side (zero, per the EU AI Act-aligned terms). The audit log in Onyx records every external call per-user.
Stays on tenant cluster
- → Documents, vectors, embeddings, audit logs
- → User accounts, chats, custom assistants, prompts
- → Transcripts (after upload from Voxtral)
- → Skynet live captions: faster-whisper on cluster, audio never leaves
- → All indexing pipelines (connectors run inside the namespace)
External API call (EU)
- → Voxtral: audio uploads for post-call transcription (Mistral La Plateforme, France)
- → Mistral chat: prompt + retrieved chunks for the answer, never the full corpus
- → Mistral embeddings (optional, per-tenant opt-in): turns chunks and queries into vectors. By default this stays on the cluster.
Cross-references: /security for the full data-flow story across the whole suite; /office/intelligence for the DPA addendum specific to AI; ADR-11 for the Voxtral engine choice; ADR-12 for the toggle contract this page builds on.
№ I8 / Lifecycle
Turn on,
upgrade,
turn off,
churn.
Same four lifecycle moments every other app in the suite has. The flow below is the operator-facing version; the customer-facing version is just "tick a box in the signup form, or email us to flip it on mid-flight."
Turn on
PR flips intelligence: true in customers/<slug>.yaml inside od-platform. Argo reconciles. The chart installs in about 5 minutes. Connectors warm up over 2 to 6 hours for the initial index, depending on corpus size.
Upgrade
Renovate proposes a bump to intelligence-base@vX+1 in the customer state repo. Merge. Argo rolls forward. Same flow as core-base upgrades (gitops § G2).
Turn off
PR flips to false. Argo prunes the Deployments. Namespace, PVCs and the Onyx Postgres database are kept by default (toggle intelligence.purgeOnDisable: true wipes immediately, otherwise a 30-day grace window applies before automatic purge).
Churn
Same as every other app (gitops § G9). Transcripts and Onyx documents ride the standard backup egress; users, chats and configs are exportable as JSON via the Onyx admin API.
"Around 5 minutes" assumes the upstream Onyx images are already cached on the cluster (they are, after the first customer turns it on). Cold-start on the very first tenant is closer to 15 minutes because the model server downloads its weights from HuggingFace on first boot.
Where this fits in the rest of the docs
The strategic decisions (engine choice, toggle contract, naming) live in /architecture/decisions (ADR-11, ADR-12). The repo-topology and reconciliation flow that ships this chart into a cluster lives in /architecture/gitops. The user-facing capabilities and DPA addendum live in /office/intelligence. The OSS upstream choices and any future EE positions live in /architecture/oss-vs-ee. This page is the operator's complete picture of what the chart actually deploys.