feat(xwiki): declare the AI LLM extension as a gateway consumer

xwiki was listed in requirement 036 as an unverified candidate. Probing the
running instance showed no AI or LLM extension in the extension repository
of the pinned lts-postgres-tomcat image, so there was no surface to point at
the gateway. Rather than excluding the role, wire the extension the same way
mediawiki wires AIEditingAssistant.

The declaration follows the nine existing consumers: a litellm service block
bound to 'svc-ai-litellm' in group_names, a sk_prefixed litellm_api_key, the
gateway pinned on variant 2 as 036 prescribes for three-variant roles, and an
integration hook that pulls the shared consumer contract.

meta/addons/ai-llm.yml feeds the existing installer: xwiki_enabled_install_items
turns enabled addons into the {id, version} list that the Groovy extension
installer consumes.

Two limits, deliberately not papered over. The maven coordinate
org.xwiki.contrib.llm:application-ai-llm-ui at 0.7 is not verified against
the upstream repository, so a wrong coordinate fails loudly on first deploy.
And the spec asserts the extension is installed and the gateway host stays
undotted, not that a prompt is answered: the extension's configuration model
has to be measured on a running container before that wiring can be written
without guessing. xwiki therefore does not yet satisfy criterion 6.

This role has never been deployed; only the gate has run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-28 12:41:55 +02:00
1 parent 90ff555603
commit 2383311b7c
8 files changed
+93 -2

No files matched your search

+5 -2
View File
@@ -15,6 +15,7 @@ The diagram places XWiki in the Infinito.Nexus cosmos: the components it deploys
```mermaid
flowchart LR
subgraph deps [Dependencies]
dep_svc_ai_litellm["svc-ai-litellm 🐳🐝"]
dep_svc_bkp_volume_2_local["svc-bkp-volume-2-local 💻"]
dep_svc_db_openldap["svc-db-openldap 🐳🐝"]
dep_svc_db_postgres["svc-db-postgres 🐳🐝"]
@@ -29,6 +30,7 @@ flowchart LR
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-xwiki 🐳🐝]
svc_litellm["litellm"]
svc_sso["sso"]
svc_logout["logout"]
svc_ldap["ldap ❌"]
@@ -46,6 +48,7 @@ flowchart LR
subgraph dependents [Dependents]
dpt_web_app_nextcloud["web-app-nextcloud 🐳🐝"]
end
dep_svc_ai_litellm -. "0..1" .-> svc_litellm
dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup
dep_svc_db_openldap -- "0..0" --> svc_ldap
dep_svc_db_postgres -. "0..1" .-> svc_postgres
@@ -58,8 +61,8 @@ flowchart LR
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_logout -. "0..1" .-> svc_logout
svc_sso -. "0..1" .-> dpt_web_app_nextcloud
linkStyle 1 stroke:red;
svc_litellm -. "0..1" .-> dpt_web_app_nextcloud
linkStyle 2 stroke:red;
```
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments); red `0..0` edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
@@ -0,0 +1,51 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const {
decodeDotenvQuotedValue,
normalizeBaseUrl,
gotoOnion,
} = require("../personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const aiGatewayBaseUrl = normalizeBaseUrl(process.env.AI_GATEWAY_BASE_URL || "");
const superadminUsername = decodeDotenvQuotedValue(process.env.XWIKI_SUPERADMIN_USERNAME || "");
const superadminPassword = decodeDotenvQuotedValue(process.env.XWIKI_SUPERADMIN_PASSWORD || "");
const EXTENSION_ID = "org.xwiki.contrib.llm:application-ai-llm-ui";
test("ai-llm: the AI LLM Application extension is installed on the wiki", async ({ page }) => {
skipUnlessAddonEnabled("ai-llm");
test.setTimeout(resolveTimeout(120_000));
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
const gatewayHost = new URL(aiGatewayBaseUrl).hostname;
expect(
gatewayHost.includes("."),
`the gateway must be addressed by its bare in-cluster service name; "${gatewayHost}" carries a dot, so the container dns-search suffix completes it and prompts leave the deployment through the public ingress`,
).toBe(false);
expect(superadminUsername, "XWIKI_SUPERADMIN_USERNAME must be set").toBeTruthy();
expect(superadminPassword, "XWIKI_SUPERADMIN_PASSWORD must be set").toBeTruthy();
const installed = await page.request.get(
`${appBaseUrl}/rest/extensions/${encodeURIComponent(EXTENSION_ID)}`,
{
headers: { Accept: "application/json" },
params: { media: "json" },
failOnStatusCode: false,
timeout: resolveTimeout(60_000),
},
);
expect(
installed.status(),
`the deploy installs ${EXTENSION_ID} through the Extension Script Service; a 404 means the addon never reached the wiki, so no AI surface exists to point at the gateway (HTTP ${installed.status()})`,
).toBe(200);
await gotoOnion(page, `${appBaseUrl}/`, { waitUntil: "domcontentloaded" }).catch(() => {});
});
@@ -0,0 +1,11 @@
---
enabled: "{{ lookup('config', 'web-app-xwiki', 'services.litellm.enabled') | bool }}"
required: false
mechanism: extension
source: upstream
bridges:
- litellm
version: "0.7"
group: ai-llm
config:
id: org.xwiki.contrib.llm:application-ai-llm-ui
+5
View File
@@ -1,5 +1,10 @@
---
credentials:
litellm_api_key:
description: Per-consumer virtual key for the shared svc-ai-litellm gateway
algorithm: sk_prefixed
validation:
min_length: 50
superadminpassword:
description: Password for the xwiki superadmin
algorithm: alphanumeric
+4
View File
@@ -1,4 +1,8 @@
---
litellm:
bond: 0.5
enabled: "{{ 'svc-ai-litellm' in group_names }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
# nocheck: seaweedfs-required — object storage not implemented for this role
sso:
bond: 1
+9
View File
@@ -5,6 +5,9 @@
# variant pinning every dynamic services.yml flag to true so the
# matrix exercises the in-group_names branch of web-app-xwiki.
- services:
litellm:
enabled: false
shared: false
container_backup:
enabled: true
dashboard:
@@ -35,6 +38,9 @@
tor:
enabled: true
- services:
litellm:
enabled: false
shared: false
container_backup:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -66,6 +72,9 @@
tor:
enabled: true
- services: # nocheck: variants-auth-isolation
litellm:
enabled: true
shared: true
container_backup:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -0,0 +1,3 @@
---
- name: "🔐 Verify the gateway contract for {{ application_id }}"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/svc-ai-litellm/tasks/utils/consumer_contract.yml') }}"
@@ -2,6 +2,8 @@ ADMIN_PASSWORD={{ lookup('users', 'administrator').password | dotenv_quote }}
ADMIN_USERNAME={{ lookup('users', 'administrator').username | dotenv_quote }}
AI_GATEWAY_BASE_URL={{ LITELLM_OPENAI_BASE_LOCAL_URL | dotenv_quote }}
APP_BASE_URL={{ lookup('tls', application_id, 'url.base') }}
BIBER_PASSWORD={{ lookup('users', 'biber').password | dotenv_quote }}
@@ -16,6 +18,9 @@ EMAIL_SERVICE_ENABLED={{ lookup('config', application_id, 'services.email.enable
# nocheck: playwright-service-gate — gated by the ldap-authenticator addon spec, not the main persona spec
LDAP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.ldap.enabled') | string | lower }}
# nocheck: playwright-service-gate — gated by the ai-llm addon spec, whose addon flag is itself bound to services.litellm.enabled
LITELLM_SERVICE_ENABLED={{ lookup('config', application_id, 'services.litellm.enabled') | string | lower }}
LOGOUT_SERVICE_ENABLED={{ lookup('config', application_id, 'services.logout.enabled') | string | lower }}
MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }}