Commit Graph
4 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 df214dd00a refactor(web-app-moodle): group the provisioning scripts in files/php/provision
provision_ai_gateway.php and provision_mcp.php shared a prefix inside one
directory, which test_prefix_grouped_files rejects. Both move into
files/php/provision/ as ai_gateway.php and mcp.php, the layout
web-app-n8n already uses for its Python provisioning scripts.

The two lookups that feed the scripts to the container follow, and the
unit test moves to its mirrored path as provision/AiGatewayTest.php.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 01:37:03 +02:00
kevinveenbirkenbachandClaude Opus 5.5 3f91dd600e fix(web-app-moodle): point the Moodle 5 AI provider instance at the gateway
Moodle 5 keeps AI provider settings in provider instances, not in the
plugin configuration. The deploy kept writing aiprovider_openai/* through
cfg.php and read the same value back, so its own check stayed green while
no provider existed. The gateway spec then found no settings page and
web-app-moodle#2 failed in run 37646050826.

The deploy now pipes files/php/provision_ai_gateway.php into the
container. It finds the instance by class and name or creates it through
core_ai\manager, points the three text actions at the chat endpoint,
switches image generation off, stores the virtual key and prints what
Moodle stored. The task asserts on that output. An instance an
administrator added is left alone.

The spec finds the instance by MOODLE_AI_PROVIDER_NAME on the providers
page and reads the endpoint from its generate_text action page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 01:27:03 +02:00
kevinveenbirkenbachandClaude Opus 5 65d226d0d9 fix(container): reach every resolver address through a shell, and lint for it
The container_address lookup does not return a container name in swarm. It
returns a shell fragment, "$(/usr/bin/resolve-container-id <stack> <service>)",
which is only a name once an interpreter evaluates the substitution. The shell
module provides one. The command module does not -- it execs the binary directly,
so the fragment arrives at container exec as literal text and the call dies with
"No such container: $(...)".

Compose hides the entire class: there the same lookup yields a bare name and
command: works. The failure exists only in swarm, and no swarm run had touched
these paths -- both were added by feature commits (b23653adf8, 0421ebeb16) that
wrote the same argv shape independently, which is the argument for a rule rather
than six edits.

Six call sites moved to the shell module: the moodle egress task, the three
moodle AI-gateway tasks, and the two wordpress AI-Engine tasks. The wordpress
pair keeps its stdin, so it uses the dict form with cmd:. Moving off argv exposes
the surrounding values to word splitting, so they now go through quote -- the
address itself must NOT, because single quotes would suppress exactly the
substitution this depends on.

test_argv_exec_needs_shell.py closes the gap that let this through.
container-exec-resolver checks that the resolver is used; it has nothing to say
about whether anything can evaluate the result. The new rule checks that other
half. It reuses the sibling's argv parser, which recognises only container/docker
in first position, so an argv list that does start an interpreter
(argv: [bash, -lc, ...], as joomla does) is correctly not matched.

Run against the tree the rule reported exactly the five sites found by hand plus
bigbluebutton, and passed over joomla and the discourse task, whose address is a
config lookup rather than the resolver. Bigbluebutton is pinned to compose via
compose_mode_force and can only ever see a bare name, so it takes a nocheck.

Five container-exec-resolver markers disappear with the argv form: under shell
the resolver is the ordinary path, not an exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 11:39:38 +02:00
kevinveenbirkenbachandClaude Opus 5 b23653adf8 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>
2026-08-24 13:21:00 +02:00