Moodle 4.5 ships aiprovider_openai in core, where every action carries its own
endpoint rather than one shared base URL, so the provider is configured per
action. aiprovider_openaicompatible would be the natural fit but requires
Moodle 5.0, which the role does not pin, so the per-action endpoints are the
only surface available on this branch. MOODLE_LITELLM_SETTINGS carries that as
a list of component/name/value triples and also enables the courseassist and
editor placements: a configured provider with no enabled placement exposes no
AI surface to any user, so both halves are required for the feature to exist.
An earlier revision set a global `enableai`, which does not exist in 4.5.
tasks/utils/ai_gateway.yml verifies the shared gateway contract, writes each
setting through admin/cli/cfg.php, then purges caches. Without the purge Moodle
keeps answering from its config cache and the new endpoint appears to have been
ignored.
The egress rule is the half that is easy to miss. Moodle's cURL security helper
blocks private address ranges outright, so a correctly configured in-cluster
endpoint is still refused, and it surfaces as a generic provider error rather
than as a blocked request. 01_manager_ops.yml therefore writes
curlsecurityblockedhosts and curlsecurityallowedport, appending the gateway's
port only while litellm is enabled. This deliberately narrows a security
control: 172.16/12 and 192.168/16 become reachable so the container network
resolves, while 10.0.0.0/8, link-local and loopback stay blocked. The README
records the trade-off.
MOODLE_EGRESS_NEWLINE is double-quoted on purpose. Moodle stores both settings
one entry per line; a single-quoted YAML scalar hands Jinja a literal
backslash-n, the whole list collapses into one unparseable token, and the net
effect is that every port is blocked rather than the intended two. Verified in
the container with negative controls: keycloak and litellm reachable,
10.208.0.5 and 127.0.0.1 still refused.
The spec opens a site-administrator session -- through Keycloak when sso is on,
because 04_oidc.yml rewrites every account to auth: oidc and no password
reaches it then, and through the native form otherwise -- asserts the endpoint
is http on an undotted host that is not Moodle's own domain, then calls
aiplacement_editor_generate_text and requires a non-empty completion. That last
step is a prompt actually answered through the gateway, not a config read-back.
Verified: the deploy-time read-back assertion ran ok in a variant-2 round and
the spec executed rather than skipping. The egress newline defect and its fix
were both measured in-container.
Not verified: I confirmed the spec ran but not that its final assertion passed,
and the cfg.php loop is changed_when: false, so the deploy log cannot show
whether a setting actually moved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Install the protocol plugin during the image build, because a Moodle
plugin must match the pinned release and installing it at runtime would
leave the two free to drift.
Provision the webservice and its token through PHP, and attach only
read-only functions, so the read-only tool policy is enforced by the
service definition rather than declared in metadata.
Address the endpoint as its canonical origin. Moodle compares every
request against wwwroot, so a probe over the internal HTTP origin was
answered with an HTML redirect instead of JSON and every retry hit the
same wall; the verify step now sends the canonical Host and the
forwarded-proto header.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirror the nextcloud playwright layout: orchestrator + _shared.js for
multi-consumer logic, one test-*.js per scenario (baseline, ldap-variant,
profile-readonly, keycloak-scope, lam-writethrough, administrator-persona,
guest-persona). Extract the duplicated Account REST middleName-write
into setMiddleNameViaAccountRest in _shared.js, parametrized by
withRestore so keycloak-scope drives the round-trip+restore path and
lam-writethrough drives the write-only path.
Add LAM_OAUTH2_FRONTED to playwright.env.j2 and gate the LAM UI verifier
on it. With web-app-keycloak deployed, LAM is fronted by oauth2-proxy,
its native /templates/login.php is replaced by the Keycloak login page,
and the LDAP-bind admin password is unusable. The Keycloak-side write
is still verified end-to-end by keycloak-scope's main test.
Net diff -20 lines vs. the original monolith despite the new helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes direct process.env.<X>_SERVICE_ENABLED reads (and bespoke
per-role <ROLE>_OIDC_ENABLED / SERVICE_* / OIDC_ENABLED env flags) in
favor of skipUnlessServiceEnabled/isServiceEnabled from
roles/test-e2e-playwright/files/service-gating.js, per
docs/contributing/artefact/files/role/playwright.specs.js.md service-
gating contract.
Per-role changes:
- discourse, matrix, dashboard: replace process.env.<X>_SERVICE_ENABLED
with the helper; drop now-redundant nocheck markers on the canonical
env flags.
- taiga: drop TAIGA_OAUTH2_ENABLED / TAIGA_OIDC_ENABLED env lines;
consume OAUTH2_SERVICE_ENABLED / OIDC_SERVICE_ENABLED via the helper.
- opentalk, opencloud, nextcloud: drop <ROLE>_OIDC_ENABLED (and
NEXTCLOUD_LDAP_ENABLED) env lines; spec switches to
isServiceEnabled('oidc'|'ldap').
- moodle: drop SERVICE_OIDC / SERVICE_LDAP / SERVICE_LAM in favor of
the canonical OIDC_SERVICE_ENABLED / LDAP_SERVICE_ENABLED plus a new
LAM_SERVICE_ENABLED rendered from 'web-app-lam' group membership.
- decidim: drop OIDC_ENABLED env line; SSO-button-visible scenario
uses skipUnlessServiceEnabled('oidc').
Dashboard asset test split:
The previously bundled "dashboard loads core css, javascript,
simpleicons, and logo assets" test triple-gated (css/cdn/logout)
skipped the entire surface when any single shared service was off.
Split into six focused tests so disabling one service skips only its
specific assertion:
- shared CSS injection (gate: css)
- CDN-served role-local stylesheet (gate: cdn)
- logout.js injection (gate: logout)
- simpleicon-backed cards (gate: simpleicons; nocheck dropped)
- role-core JS modules + logos (ungated, role-core)
- iframe-sync postMessage handler (ungated, role-core)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>