mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
feat(moodle): point the AI subsystem at the platform gateway
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>
This commit is contained in:
1 parent
19407bad1f
commit
b23653adf8
10 files changed
+308
-1
No files matched your search
@@ -15,6 +15,7 @@ The diagram places Moodle in the Infinito.Nexus cosmos: the components it deploy
|
||||
```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_mariadb["svc-db-mariadb 🐳🐝"]
|
||||
dep_svc_db_openldap["svc-db-openldap 🐳🐝"]
|
||||
@@ -27,6 +28,7 @@ flowchart LR
|
||||
dep_web_svc_logout["web-svc-logout 🐳🐝"]
|
||||
end
|
||||
subgraph role [web-app-moodle 🐳🐝]
|
||||
svc_litellm["litellm"]
|
||||
svc_sso["sso"]
|
||||
svc_logout["logout"]
|
||||
svc_dashboard["dashboard"]
|
||||
@@ -41,6 +43,7 @@ flowchart LR
|
||||
svc_prometheus["prometheus"]
|
||||
svc_container_backup["container_backup"]
|
||||
end
|
||||
dep_svc_ai_litellm -. "0..1" .-> svc_litellm
|
||||
dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup
|
||||
dep_svc_db_mariadb -. "0..1" .-> svc_mariadb
|
||||
dep_svc_db_openldap -. "0..1" .-> svc_ldap
|
||||
@@ -146,6 +149,14 @@ Moodle compares every request against `$CFG->wwwroot`. A probe that reaches the
|
||||
|
||||
Remove the MCP client roles, or pin `mcp.enabled: false` for this role. The web-service token is then not issued and the protocol plugin stays unconfigured.
|
||||
|
||||
## Outbound HTTP policy
|
||||
|
||||
Moodle ships a cURL blocklist that rejects every request to `10.0.0.0/8`, `172.16.0.0/12` and `192.168.0.0/16`, and only permits ports 80 and 443. Both settings gate `\core\files\curl_security_helper`, which `auth_oidc` consults for the Keycloak token exchange and `\core\http_client` consults for the AI provider request. Keycloak answers on `172.16.0.0/12` and the gateway on `192.168.0.0/16`, so with the upstream defaults the OIDC login obtains no token and the AI provider cannot reach the gateway.
|
||||
|
||||
`tasks/01_manager_ops.yml` therefore rewrites `curlsecurityblockedhosts` to `MOODLE_EGRESS_BLOCKED_HOSTS` and extends `curlsecurityallowedport` with the port of `MOODLE_LITELLM_CHAT_ENDPOINT` when the `litellm` service is enabled. Loopback, `0.0.0.0`, the cloud metadata address and `10.0.0.0/8` stay blocked; `172.16.0.0/12` and `192.168.0.0/16` are opened.
|
||||
|
||||
This is an accepted trade-off, not an oversight. Moodle offers no allowlist, so a single internal host cannot be permitted without unblocking the range it sits in. The cost is that any account holding a URL-fetching capability, such as `block/rss_client:myaddinstance` or a `repository/url` instance, can make the server issue requests to sibling containers. Grant those capabilities only to roles you trust with that reach, or keep the upstream blocklist and accept that SSO login and the AI surface do not work.
|
||||
|
||||
## Image source
|
||||
|
||||
This role builds its own Moodle image from upstream Moodle source on top of the official `php:8.3-fpm` base.
|
||||
|
||||
@@ -14,3 +14,4 @@ require("./test-lam-writethrough").register(shared);
|
||||
require("./test-administrator-persona").register(shared);
|
||||
require("./test-guest-persona").register(shared);
|
||||
require("./test-mcp-guest").register(shared);
|
||||
require("./test-litellm-gateway").register(shared);
|
||||
@@ -0,0 +1,163 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { decodeDotenvQuotedValue, performKeycloakLogin, readEnv } = require("./personas");
|
||||
const { skipUnlessServiceEnabled } = require("./service-gating");
|
||||
|
||||
const adminNativePassword = decodeDotenvQuotedValue(process.env.ADMIN_NATIVE_PASSWORD || "");
|
||||
|
||||
const ENDPOINT_FIELD = "#id_s_aiprovider_openai_action_generate_text_endpoint";
|
||||
const USER_MENU = ".usermenu, [data-region='user-menu-toggle'], a[href*='profile.php']";
|
||||
const GATEWAY_PROMPT = "Reply with the single word: pong";
|
||||
|
||||
/**
|
||||
* Open an authenticated site-administrator session.
|
||||
*
|
||||
* `tasks/04_oidc.yml` rewrites `mdl_user.auth` to `oidc` for every account but
|
||||
* `guest`, and `auth_oidc` grants a login only when the request carries an
|
||||
* authorization code, so with the sso service enabled no password reaches the
|
||||
* account through the native form. Without sso that task never runs, the
|
||||
* accounts stay on `auth_manual` and the password `install_database.php`
|
||||
* provisioned is the only way in.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page browser page to authenticate
|
||||
* @param {object} shared role-shared env and persona helpers
|
||||
*/
|
||||
async function loginAsSiteAdmin(page, shared) {
|
||||
if (shared.env.ssoEnabled) {
|
||||
await page.goto(`${shared.env.moodleBaseUrl}/auth/oidc/?source=loginpage`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60_000,
|
||||
});
|
||||
await performKeycloakLogin(
|
||||
page,
|
||||
shared.env.adminUsername,
|
||||
shared.env.adminPassword,
|
||||
readEnv("CANONICAL_DOMAIN"),
|
||||
);
|
||||
} else {
|
||||
expect(
|
||||
adminNativePassword,
|
||||
"ADMIN_NATIVE_PASSWORD must be rendered from credentials.user_password; with the sso service off it is the only secret that authenticates the Moodle site administrator",
|
||||
).toBeTruthy();
|
||||
|
||||
await page.goto(`${shared.env.moodleBaseUrl}/login/index.php`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60_000,
|
||||
});
|
||||
await page
|
||||
.locator("input[name='username'], input#username")
|
||||
.first()
|
||||
.fill(shared.env.adminUsername);
|
||||
|
||||
const passwordInput = page
|
||||
.locator(
|
||||
".toggle-sensitive-wrapper input[name='password'], .toggle-sensitive-wrapper input#password, input[name='password']",
|
||||
)
|
||||
.first();
|
||||
await expect(
|
||||
passwordInput,
|
||||
"the native Moodle login form must expose a password field; its absence means the login page no longer serves the manual auth form",
|
||||
).toBeAttached({ timeout: 30_000 });
|
||||
await expect(async () => {
|
||||
await passwordInput.fill(adminNativePassword);
|
||||
await expect(passwordInput).toHaveValue(adminNativePassword);
|
||||
}).toPass({ timeout: 30_000 });
|
||||
|
||||
await page.locator("#loginbtn, button[type='submit'], input[type='submit']").first().click();
|
||||
await page.waitForLoadState("load");
|
||||
}
|
||||
|
||||
await expect(
|
||||
page.locator(USER_MENU).first(),
|
||||
"the site administrator must reach an authenticated session; a failure here means the auth chain the deploy configured does not admit the administrator account",
|
||||
).toBeVisible({ timeout: 60_000 });
|
||||
}
|
||||
|
||||
exports.register = function (shared) {
|
||||
test("litellm: Moodle answers a prompt through the in-cluster gateway", async ({ page }) => {
|
||||
skipUnlessServiceEnabled("litellm");
|
||||
test.setTimeout(240_000);
|
||||
|
||||
await loginAsSiteAdmin(page, shared);
|
||||
|
||||
await page.goto(
|
||||
`${shared.env.moodleBaseUrl}/admin/settings.php?section=aiprovider_openai_generate_text`,
|
||||
{ waitUntil: "domcontentloaded", timeout: 60_000 },
|
||||
);
|
||||
|
||||
const endpointField = page.locator(ENDPOINT_FIELD);
|
||||
await expect(
|
||||
endpointField,
|
||||
"the aiprovider_openai generate_text settings page must render its API endpoint field; its absence means the provider was never configured or the session lacks moodle/site:config",
|
||||
).toBeVisible({ timeout: 60_000 });
|
||||
|
||||
const configuredEndpoint = ((await endpointField.inputValue()) || "").trim();
|
||||
expect(
|
||||
configuredEndpoint,
|
||||
"aiprovider_openai/action_generate_text_endpoint must be an http(s) URL written by the deploy, not the upstream api.openai.com default",
|
||||
).toMatch(/^https?:\/\/.+/i);
|
||||
|
||||
const endpointHost = new URL(configuredEndpoint).hostname;
|
||||
expect(
|
||||
endpointHost,
|
||||
`the generate_text endpoint host must be a bare in-cluster service name; the dotted host "${endpointHost}" gets completed by the container dns-search suffix and resolves through the public ingress, so the prompt would leave the deployment`,
|
||||
).not.toContain(".");
|
||||
expect(
|
||||
endpointHost,
|
||||
"the generate_text endpoint must address the svc-ai-litellm gateway, not point back at Moodle itself",
|
||||
).not.toBe(new URL(shared.env.moodleBaseUrl).hostname);
|
||||
|
||||
const bootstrap = await page.evaluate(() => ({
|
||||
sesskey: (window.M && window.M.cfg && window.M.cfg.sesskey) || "",
|
||||
contextid: (window.M && window.M.cfg && window.M.cfg.contextid) || 0,
|
||||
}));
|
||||
expect(
|
||||
bootstrap.sesskey,
|
||||
"M.cfg.sesskey must be exposed on the admin page; without it Moodle rejects every /lib/ajax/service.php call",
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
bootstrap.contextid,
|
||||
"M.cfg.contextid must be exposed so the AI placement runs in the context the admin page already validated",
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
const ajaxUrl =
|
||||
`${shared.env.moodleBaseUrl}/lib/ajax/service.php` +
|
||||
`?sesskey=${encodeURIComponent(bootstrap.sesskey)}&info=aiplacement_editor_generate_text`;
|
||||
const response = await page.request.post(ajaxUrl, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
data: [
|
||||
{
|
||||
index: 0,
|
||||
methodname: "aiplacement_editor_generate_text",
|
||||
args: { contextid: bootstrap.contextid, prompttext: GATEWAY_PROMPT },
|
||||
},
|
||||
],
|
||||
timeout: 180_000,
|
||||
});
|
||||
|
||||
expect(
|
||||
response.status(),
|
||||
`the aiplacement_editor web service must answer 200; a non-200 means the placement is disabled or unavailable, so no AI surface reaches the provider (got ${response.status()})`,
|
||||
).toBe(200);
|
||||
|
||||
const payload = await response.json();
|
||||
const entry = Array.isArray(payload) ? payload[0] : null;
|
||||
expect(
|
||||
entry,
|
||||
`aiplacement_editor_generate_text must return exactly one result entry, got ${JSON.stringify(payload).slice(0, 400)}`,
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
entry.error,
|
||||
`the placement call must not raise a Moodle exception, got ${JSON.stringify(entry.exception || entry).slice(0, 400)}`,
|
||||
).toBe(false);
|
||||
|
||||
const result = entry.data;
|
||||
expect(
|
||||
result.success,
|
||||
`the generate_text action must succeed; errorcode carries the gateway HTTP status, so 401 means the virtual key is wrong and 404 or 0 means the endpoint is unreachable (got ${JSON.stringify(result).slice(0, 400)})`,
|
||||
).toBe(true);
|
||||
expect(
|
||||
String(result.generatedcontent || "").trim().length,
|
||||
"the gateway must return a non-empty completion, proving Moodle's AI placement had the prompt answered inside the deployment",
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
user_password:
|
||||
description: Initial password for the Moodle admin user
|
||||
algorithm: sha256
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
prometheus:
|
||||
enabled: true
|
||||
shared: true
|
||||
litellm:
|
||||
enabled: true
|
||||
shared: true
|
||||
|
||||
mcp:
|
||||
enabled: true
|
||||
@@ -71,6 +74,9 @@
|
||||
prometheus:
|
||||
enabled: false
|
||||
shared: false # nocheck: variants-static-override backup shared stays literally true
|
||||
litellm:
|
||||
enabled: false
|
||||
shared: false
|
||||
mcp:
|
||||
enabled: false
|
||||
shared: false
|
||||
@@ -104,6 +110,9 @@
|
||||
prometheus:
|
||||
enabled: false
|
||||
shared: false # nocheck: variants-static-override backup shared stays literally true
|
||||
litellm:
|
||||
enabled: true
|
||||
shared: true
|
||||
mcp:
|
||||
enabled: false
|
||||
shared: false
|
||||
@@ -45,6 +45,23 @@
|
||||
- "'Database tables already present' not in moodle_install.stderr"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🌐 Open Moodle's cURL egress policy for the deployment's own networks"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- --user
|
||||
- "{{ MOODLE_RUNTIME_USER }}"
|
||||
- "{{ MOODLE_CONTAINER_ADDRESS }}" # nocheck: container-exec-resolver
|
||||
- php
|
||||
- "{{ [MOODLE_CLI_DIR, 'cfg.php'] | path_join }}"
|
||||
- "--name={{ item.name }}"
|
||||
- "--set={{ item.value }}"
|
||||
loop: "{{ MOODLE_EGRESS_SETTINGS }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
changed_when: false
|
||||
|
||||
- name: "📂 Include ownership settings tasks for moodle"
|
||||
include_tasks: 03_ownership.yml
|
||||
|
||||
@@ -56,6 +73,10 @@
|
||||
include_tasks: 05_ldap_sync.yml
|
||||
when: MOODLE_LDAP_ENABLED | bool
|
||||
|
||||
- name: "🤖 Bind the native AI surface to the platform gateway"
|
||||
include_tasks: utils/ai_gateway.yml
|
||||
when: MOODLE_LITELLM_ENABLED | bool
|
||||
|
||||
- name: "🔌 Provision the MCP server surface"
|
||||
include_tasks: utils/mcp.yml
|
||||
when: MOODLE_MCP_ENABLED | bool
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
- name: "🔐 Verify the gateway contract for {{ application_id }}"
|
||||
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/svc-ai-litellm/tasks/utils/consumer_contract.yml') }}"
|
||||
|
||||
- name: "🤖 Point the Moodle AI subsystem at the platform gateway"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- --user
|
||||
- "{{ MOODLE_RUNTIME_USER }}"
|
||||
- "{{ MOODLE_CONTAINER_ADDRESS }}" # nocheck: container-exec-resolver
|
||||
- php
|
||||
- "{{ [MOODLE_CLI_DIR, 'cfg.php'] | path_join }}"
|
||||
- "--component={{ item.component }}"
|
||||
- "--name={{ item.name }}"
|
||||
- "--set={{ item.value }}"
|
||||
loop: "{{ MOODLE_LITELLM_SETTINGS }}"
|
||||
loop_control:
|
||||
label: "{{ item.component }}/{{ item.name }}"
|
||||
changed_when: false
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🧹 Purge Moodle caches so the provider picks up its endpoint"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- --user
|
||||
- "{{ MOODLE_RUNTIME_USER }}"
|
||||
- "{{ MOODLE_CONTAINER_ADDRESS }}" # nocheck: container-exec-resolver
|
||||
- php
|
||||
- "{{ [MOODLE_CLI_DIR, 'purge_caches.php'] | path_join }}"
|
||||
changed_when: false
|
||||
|
||||
- name: "🔎 Read back the configured chat endpoint"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- --user
|
||||
- "{{ MOODLE_RUNTIME_USER }}"
|
||||
- "{{ MOODLE_CONTAINER_ADDRESS }}" # nocheck: container-exec-resolver
|
||||
- php
|
||||
- "{{ [MOODLE_CLI_DIR, 'cfg.php'] | path_join }}"
|
||||
- "--component=aiprovider_openai"
|
||||
- "--name=action_generate_text_endpoint"
|
||||
register: moodle_ai_endpoint
|
||||
changed_when: false
|
||||
|
||||
- name: "🛑 Refuse a Moodle AI provider that does not answer through the gateway"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (moodle_ai_endpoint.stdout | trim) == MOODLE_LITELLM_CHAT_ENDPOINT
|
||||
fail_msg: >-
|
||||
aiprovider_openai still resolves generate_text to
|
||||
'{{ moodle_ai_endpoint.stdout | trim }}' instead of
|
||||
{{ MOODLE_LITELLM_CHAT_ENDPOINT }}, so prompts would leave the deployment.
|
||||
@@ -10,6 +10,7 @@ LDAP_ADMIN_PASSWORD={{ LDAP.BIND_CREDENTIAL | dotenv_quote }}
|
||||
{% endif %}
|
||||
ADMIN_USERNAME={{ lookup('users', 'administrator').username | dotenv_quote }}
|
||||
ADMIN_PASSWORD={{ lookup('users', 'administrator').password | dotenv_quote }}
|
||||
ADMIN_NATIVE_PASSWORD={{ lookup('config', application_id, 'credentials.user_password') | dotenv_quote }}
|
||||
BIBER_USERNAME={{ lookup('users', 'biber').username | dotenv_quote }}
|
||||
BIBER_PASSWORD={{ lookup('users', 'biber').password | dotenv_quote }}
|
||||
CANONICAL_DOMAIN={{ lookup('domain', application_id) | dotenv_quote }}
|
||||
@@ -18,6 +19,7 @@ PERSONA_ADMINISTRATOR_BLOCKED=true
|
||||
|
||||
MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }}
|
||||
|
||||
LITELLM_SERVICE_ENABLED={{ lookup('config', application_id, 'services.litellm.enabled') | string | lower }}
|
||||
SSO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.sso.enabled') | string | lower }}
|
||||
LOGOUT_SERVICE_ENABLED={{ lookup('config', application_id, 'services.logout.enabled') | string | lower }}
|
||||
MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }}
|
||||
|
||||
@@ -3,7 +3,6 @@ application_id: "web-app-moodle"
|
||||
entity_name: "{{ application_id | get_entity_name }}"
|
||||
swarm_stack_converge_retries: 600
|
||||
|
||||
MOODLE_DOMAIN: "{{ lookup('domain', application_id) }}"
|
||||
MOODLE_CODE_DIR: "/var/www/html"
|
||||
MOODLE_DATA_DIR: "/var/www/moodledata"
|
||||
MOODLE_LOCAL_CACHE_DIR: "{{ lookup('volume', application_id, 'localcache').mounts[0].target }}"
|
||||
@@ -52,6 +51,40 @@ MOODLE_CRON_CONTAINER: "{{ lookup('config', application_id, 'services.cron.name'
|
||||
MOODLE_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}"
|
||||
MOODLE_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') }}"
|
||||
|
||||
MOODLE_LITELLM_ENABLED: "{{ lookup('config', application_id, 'services.litellm.enabled') | bool }}"
|
||||
MOODLE_LITELLM_API_KEY: "{{ lookup('config', application_id, 'credentials.litellm_api_key') }}"
|
||||
MOODLE_LITELLM_CHAT_ENDPOINT: "{{ LITELLM_OPENAI_BASE_LOCAL_URL }}/chat/completions"
|
||||
MOODLE_LITELLM_IMAGE_ENDPOINT: "{{ LITELLM_OPENAI_BASE_LOCAL_URL }}/images/generations"
|
||||
MOODLE_LITELLM_SETTINGS:
|
||||
- { component: aiprovider_openai, name: enabled, value: "1" }
|
||||
- { component: aiprovider_openai, name: apikey, value: "{{ MOODLE_LITELLM_API_KEY }}" }
|
||||
- { component: aiprovider_openai, name: action_generate_text_endpoint, value: "{{ MOODLE_LITELLM_CHAT_ENDPOINT }}" }
|
||||
- { component: aiprovider_openai, name: action_generate_text_model, value: "{{ LITELLM_CHAT_MODEL }}" }
|
||||
- { component: aiprovider_openai, name: action_summarise_text_endpoint, value: "{{ MOODLE_LITELLM_CHAT_ENDPOINT }}" }
|
||||
- { component: aiprovider_openai, name: action_summarise_text_model, value: "{{ LITELLM_CHAT_MODEL }}" }
|
||||
- { component: aiprovider_openai, name: action_generate_image_endpoint, value: "{{ MOODLE_LITELLM_IMAGE_ENDPOINT }}" }
|
||||
- { component: aiprovider_openai, name: generate_image, value: "0" }
|
||||
- { component: aiplacement_courseassist, name: enabled, value: "1" }
|
||||
- { component: aiplacement_editor, name: enabled, value: "1" }
|
||||
|
||||
MOODLE_EGRESS_BLOCKED_HOSTS:
|
||||
- "0.0.0.0"
|
||||
- "127.0.0.0/8"
|
||||
- "localhost"
|
||||
- "169.254.169.254"
|
||||
- "0000::1"
|
||||
- "10.0.0.0/8"
|
||||
MOODLE_EGRESS_ALLOWED_PORTS:
|
||||
- "80"
|
||||
- "443"
|
||||
MOODLE_EGRESS_NEWLINE: "\n"
|
||||
MOODLE_EGRESS_SETTINGS:
|
||||
- name: curlsecurityblockedhosts
|
||||
value: "{{ MOODLE_EGRESS_BLOCKED_HOSTS | join(MOODLE_EGRESS_NEWLINE) }}"
|
||||
- name: curlsecurityallowedport
|
||||
value: "{{ (MOODLE_EGRESS_ALLOWED_PORTS + ([MOODLE_LITELLM_CHAT_ENDPOINT | urlsplit('port') | string] if MOODLE_LITELLM_ENABLED | bool else [])) | join(MOODLE_EGRESS_NEWLINE)
|
||||
}}"
|
||||
|
||||
MOODLE_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}"
|
||||
MOODLE_MCP_PLUGIN_VERSION: "v0.6.0"
|
||||
MOODLE_MCP_PLUGIN_ARCHIVE_URL: "https://github.com/onbirdev/moodle-webservice_mcp/archive/refs/tags/{{ MOODLE_MCP_PLUGIN_VERSION }}.zip"
|
||||
|
||||
Reference in new issue
Block a user