Commit Graph
26 Commits
Author SHA1 Message Date
evangelostsak 5092aab4c2 feat(web-app-openclaw): let a deployment put its hosts into meshes 2026-10-16 03:40:19 +02:00
kevinveenbirkenbachandClaude Opus 5 328113e7c3 refactor(ai): declare the litellm consumer contract once
Fourteen roles repeated the same two lines to say they use the gateway: a
`shared` flag gated on group_names, and an `enabled` flag conjoining that gate
with LITELLM_CHAT_MODEL_SERVED. Both are now single-sourced.

`shared` becomes literal true. The flag means the provider is a shared instance
rather than one deployed per consumer, and a central gateway is always that;
the dynamic form only said "shared if deployed", which `enabled` already
decides. The 50 shared pins under `litellm:` leave meta/variants.yml with it.
The tree had already noticed: 20 of those 50 lines carried
`# nocheck: variants-static-override`, two of them with copy-pasted text about
container_backup. This narrows e5de4e085a for one service key rather than
reversing it; every other dynamic flag keeps the group_names form.

`enabled` becomes `{{ LITELLM_USABLE }}`, defined in group_vars/all/16_ai.yml.

That shorthand is only possible because is_explicit_truth learns about it.
The resolver reads a dependency out of the flag TEXT — "in group_names" in
value — so a constant hides the role id and the provider silently leaves the
static closure of every role referencing it. Measured: the old expression
resolves True, `{{ LITELLM_USABLE }}` resolves False. GROUP_GATED_CONSTANTS
names the constants whose own definition carries the gate, and the docstring
states the obligation: a SPOT of this kind that is not listed there drops its
provider without a word.

The wider fix, accepting any Jinja as dynamic, was measured and rejected: 35
other flags carry a Jinja value without the marker (MCP sidecars, javascript
and SSO gates, recaptcha), and they would all enter closures they are not in
today.

A literal shared flag is no longer dynamic, so test_dynamic_flags wants the
`# nocheck: dynamic-flag` its own message documents for legitimate exceptions;
where a marker already existed the rule list is extended rather than a second
comment added.

Behaviour is unchanged where it counts. The closure keeps litellm with the same
app counts (wordpress 26, openwebui 23, hermes 23, mediawiki 22, flowise 23),
unrelated Jinja still resolves False, and the truth table of the new enabled
form matches the old one in all four served/deployed combinations. Three roles
resolve without litellm, and `git show HEAD:` proves that predates this change:
zammad pins enabled false in variant 0 at line 39, nextcloud at line 97.

Six unit tests cover the resolver, including one asserting that every
registered constant is actually defined in group_vars, so an entry cannot
become a dead promise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 19:23:59 +02:00
kevinveenbirkenbachandClaude Opus 5 4ac07edaea refactor(meta): move the info-media exemption to the file it speaks about
The marker names the reason a role has no upstream homepage or demo video, and
it sat in meta/main.yml because a comment-only info.yml parses to an empty
document, which the schema lint rejected outright. So the exemption lived one
file away from the two fields it explains, and a reader looking for the video
found neither the field nor the reason it was missing.

The schema lint now accepts an empty document exactly when it carries the
marker, and that is what allows the move. The media lint reads the marker from
info.yml rather than main.yml, and its failure message names that same file, so
the fix it suggests lands where the absent field would have gone.

A hundred and two roles follow. Every occurrence was a standalone comment on
the second line of main.yml, directly under the document start, so the move is
a line delete and an insert at the same offset. Seventy-three roles already had
an info.yml and gain the line; twenty-nine get one whose whole content is the
document start and the marker. All hundred and two reasons carry over verbatim,
including the thirty-five bespoke ones.

Nothing downstream sees a change. Of the four readers of meta/info.yml, three
coerce a null parse through `or {}` behind an isinstance guard, and
utils/cache/applications.py skips a falsy parse before assignment, so
applications.<role>.info stays absent for the twenty-nine exactly as it was
while the file did not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 22:26:38 +02:00
kevinveenbirkenbachandClaude Opus 5 e0048f157b fix(litellm): gate consumers on a served model, not on the gateway being deployed
The gateway publishes only models it has a backend for and renders model_list: [] when neither ollama nor lmstudio is deployed and no OpenRouter key is set. LITELLM_CHAT_MODEL fell back to openrouter/auto without checking that key, so consumers were configured for a model the gateway does not carry and every prompt failed with a 400.

Observed in the Mattermost#1 CI job: litellm.log carries 12 occurrences of "Invalid model name passed in model=openrouter/auto" next to 400 Bad Request, and tests/test-litellm-agent.js failed on every attempt while the rest of the suite passed.

Make the fallback conditional on the key so the name is empty when nothing is served, promote LITELLM_CHAT_MODEL_SERVED from the role vars to group_vars where consumers can see it (it was the third copy of this logic and the only correct one), and switch the 14 consumers from "litellm is deployed" to "litellm serves a model". The Playwright service flag reads the same lookup, so the spec skips together with the Ansible gate. The bool filter carries weight: the folded scalar yields the string False, which Jinja treats as truthy.

A new unit test renders both expressions against Jinja and pins the invariant that a model name exists exactly when a model is served, including the case where ollama preloads embedding models only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 19:48:32 +02:00
kevinveenbirkenbachandClaude Opus 5 9fb8f63286 fix(test-e2e-cli): exec on the node that holds the container
hermes_hermes.1 ran on a worker while run_one.yml ran its script on the loop
host. resolve_container_id.sh finds nothing node-locally on the manager, falls
through to the cluster-wide branch and returns the task's ContainerID, which
only the owning node's daemon knows, so `container exec` answered
`No such container: 65801ca259a1` twenty times over 116 seconds. Run
33785198875, job 100756596935.

Resolve the owning node and run there. meta/tests.yml gains
container_service_key for the two roles that exec into their own container; a
role without the key keeps the loop host, and role_tests returns the default
for a missing path, so absence is a polarity rather than an error.

The reset at run_one.yml:27-30 is unconditional and precedes every
delegate_to. Without it the first loop iteration dies with
'CLI_HOST_NODE' is undefined, because _cli_apps starts with a role that has no
key; the same task closes the fact leak that include_tasks leaves open between
iterations. The resolve include is gated on an explicit length check rather
than truthiness, or a keyless role would hang in resolve_host_cid's converge
wait, and it caps that wait at 30 x 2s instead of inheriting 600 x 2s: the
deploy is long finished by the time the CLI stage runs, so failing fast beats
waiting twenty minutes.

Staging, render and run all delegate. Delegating only the run would turn
`No such container` into `No such file or directory`, since the assets would
stay on the loop host. Both templates now carry CLI_LOCAL_CID: with the exec
on the owning node the id is node-local, so container_address's deferred
`$( ... )` is not needed, and it never survived `set -a && . test.env` anyway,
which evaluates it once at source time.

The stage directory is removed on the delegate too. tasks/main.yml clears it
without delegation, i.e. only on the loop host, and the task's own name calls
its contents decodable DB secrets.

Measured locally rather than argued: self-delegation leaves file, copy with
content, copy with src and shell byte-identical, including mode; the loop
reset gives keyless roles an empty container and keeps CLI_HOST_NODE on the
loop host across five iterations; and container_service_key resolves to
hermes, openclaw and empty strings through the same walk cli.timeout already
uses in production.

Not fixed, and named so it is not forgotten: web-app-joomla, web-app-yourls
and web-app-penpot exec from an IS_STACK_HOST gate without resolve_host_cid,
so a task of theirs landing on a worker hits the same error from the same
source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 02:55:08 +02:00
kevinveenbirkenbachandClaude Opus 5 346ad875c6 refactor(mcp): drop the two provenance fields nothing reads
supported_version and minimum_version sat in 24 roles and were read by no template, no task and no lookup. Their only consumer was the audit report version column, which turned false the moment an automated image bump ran: qdrant reported a verified version nobody had verified, two releases long. Both leave MCP_KEYS, so a role that declares them again fails the schema lint instead of carrying a field with no effect.

Requirement 025 mandated minimum_version as a MUST and 035 carried it in an example; both are corrected here, otherwise the repository would contradict its own code.

The gate then surfaced five dead docs.mattermost.com links. Mattermost moved to /administration-guide/ and /deployment-guide/, and every replacement was probed for 200 rather than guessed. The MCP page is gone with no successor, so mattermost now points at mcpserver/README.md in the Agents repository, the source rather than a rendered mirror.

Both concerns share three files - mattermost's meta/mcp.yml, requirement 025, and the generated audit.md, which derives from a source of each - so splitting them would commit an artifact that does not match its sources.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 19:31:08 +02:00
kevinveenbirkenbachandClaude Opus 5 4164f37981 fix(mcp): give every MCP round an identity provider to sign in with
An MCP client spec signs in through the client's own OIDC button, which
redirects to Keycloak. Every provider put its MCP pairing in the lean variant,
and that variant pinned sso off - so the round that exists to prove the pairing
was the one round without an identity provider. Run 33574056480 shows what that
costs: openwebui's MCP specs reached _shared.js:131 and died on "openwebui OIDC
sign-in button must be visible on the auth page".

The rule now demands two things of the variant that carries the consumers: the
provider pins sso on, and each admitted consumer, in the variant that round
actually pulls in, does not pin it off. The second half matters because a round
deploys every dependency at its own index and falls back to variant 0 only when
the dependency has fewer, so the client shipping alongside is that one and not
the client's variant 0. A role that declares no sso, or pins it to a literal
false because upstream speaks no OIDC, is exempt from a flag it cannot have.

Twenty roles moved to satisfy it. Ten already had a second sso: false pin and
only needed the flip. Six could host the pairing in variant 0, which already
has sso on, and dropped back to two variants. Six kept a third lean variant to
carry the false polarity variants-coverage wants. moodle and mattermost took
swaps instead: moodle's variant 1 is the mandated LDAP-only branch, and
mattermost was already at the three-variant cap with sso on in its variant 2.
nextcloud also turns context_agent on in that round, the ExApp that serves the
endpoint - with it off the round proved nothing it was built to prove.

The rule moves to tests/lint/ansible/services/ because it reads meta YAML and
executes nothing, which is where integration.md says such a check belongs, and
its suppression key joins the catalog it was never listed in.

nextcloud_exapp_reregister becomes nextcloud_exapp_renew: mcp-secret-masking
matches `register:` unanchored, so the substring in the old name read as a task
that prints a secret. register.yml already masks the two tasks that carry one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 15:51:24 +02:00
kevinveenbirkenbachandClaude Opus 5 0b13fb73d0 style: apply make autoformat
Two leftovers of my own. The sweep that removed the reverse client entries
appended a newline where it cut a block, which at end of file leaves a blank
line after the last key in four services.yml. And the two cache-read nocheck
comments in the bond editor were written past the line limit, so the
formatter breaks their read_text calls across three lines and carries the
comment to the closing paren.

The wrap does not cost the suppression: test_no_uncached_file_reads matches
the statement, not the line the directive sits on, and passes on this form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 01:24:47 +02:00
kevinveenbirkenbachandClaude Opus 5 1e6e343268 refactor(mcp): declare a client-provider pairing once, on the provider
Four MCP clients each carried a services entry for homeassistant, and
openwebui and flowise one for baserow, with matching run_after lines. Those
predate the admission refactor: since then the co-deploy edge lives on the
provider, where mcp-consumer-services-entry enforces it, and homeassistant
and baserow already carry an entry for every client they admit. The client
copies stated the same relation a second time.

The asymmetry gives them away. Open WebUI consumes twenty MCP servers and
declared three of them; the other seventeen work because the bridge is built
from MCP_DISCOVERED_SERVERS, never from the client's own services map.

Drop the run_after lines in the same move: a run_after target must be backed
by a services entry, so one cannot go without the other. Hermes and OpenClaw
lose the whole key, homeassistant being its only entry, because a bare
run_after parses as null rather than an empty list.

Keep qdrant on flowise, which renders QDRANT_URL and QDRANT_API_KEY into the
container and is a real dependency, and keep prometheus everywhere as the
ordinary metrics edge.

Open WebUI stops dragging both providers into every round it joins, which
brings moodle variant 2 from 64.75 GB back to 62.75 GB, under the budget the
preceding commit pushed it over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 01:03:22 +02:00
kevinveenbirkenbachandClaude Opus 5 791eee7c9f refactor(mcp)!: prove the endpoint contract with a CLI test, not a deploy probe
The contract check ran as an Ansible task in the middle of every provider deploy, and the two client checks ran the same way. A test that is part of the deploy cannot be run without deploying, cannot be re-run on its own, and reports its verdict as a deploy failure. Twenty-seven task files lose 491 lines to this.

It is now what it always was. Each provider ships files/test/test.sh, templates/test.env.j2 and meta/tests.yml, and test-e2e-cli stages one shared harness beside it: run.sh drives contract.sh in a throwaway container on the provider's own network, then isolation.sh for adapter-fronted providers. The read probe each provider offers moved from the calling task into meta/mcp.yml next to the allowlist it must satisfy, and the schema now rejects a read probe naming a tool outside that allowlist.

The bearer the test presents is the one a client resolves through mcp.credential, via the new mcp_client_bearer lookup over resolve_credential, which moved to utils.roles.applications.mcp so it is the same function mcp_servers renders every client connection with. The probe presented whatever the calling role passed, which for the fifteen adapter-fronted providers is a different secret than any client holds.

Sidecar isolation is read from the engine rather than measured inside the container, so it no longer needs a delegation to the node holding the task: attachment is compared by network name, which drops the gwbridge special case, and a swarm worker says it cannot measure instead of skipping silently.

Retrying is bounded by wall clock against MCP_DEADLINE_SECONDS, which scales through the same lookup('timeout') as the harness budget it has to stay under. A flag that is neither true nor false aborts rather than reading as off.

mcp-provider-probe becomes mcp-provider-cli-test: an enabled provider must ship the test rather than include the task. mcp-probe-bearer becomes mcp-adapter-bearer, tying ADAPTER_BEARER to the declared credential key now that no probe include carries it. A new integration test renders the shared env template for every provider through Ansible's own templar, so a later change to it breaks visibly instead of poisoning twenty deploys quietly.

Hermes and OpenClaw keep their client-side proof as CLI tests of their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 20:46:26 +02:00
kevinveenbirkenbachandClaude Opus 5 ffdef28da3 refactor(mcp)!: derive a surface's presence from its peers
enabled and shared were hand-maintained in 24 roles and wrong in both
directions. Both now resolve from the mcp_consumer edges already declared
in meta/services.yml: a server takes the consumers it admits, a client
takes the servers that admit it, and the pair renders as "'peer' in
group_names or ...", or False when no peer is admitted at all. setdefault
keeps meta/mcp.yml and meta/variants.yml as the override channel, so
pinning either key still wins.

The resolved block replaces the mapping instead of being written into it.
_build_role_base_config parks the YAML cache's own object under mcp, so an
in-place setdefault injected the derived keys into every later raw read and
made test_mcp_schema pass alone but fail in a directory run.

The derivation keys on peer presence, never on a peer's own value: a
re-entrant read under _RENDER_GUARD returns the unrendered dict.

Four lints now ask the resolver instead of reading meta/mcp.yml, which they
could not do while the key was hand-written. That is what exposed the eleven
missing hostile-path specs the previous commit closed.

Carries one change that belongs to the WordPress probe but shares a file
with this one: the tool allowlist moves to the hyphenated names the adapter
actually serves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 16:13:01 +02:00
kevinveenbirkenbachandClaude Opus 5 83c1c729de refactor(mcp)!: derive admission from the client
allowed_consumers was twenty hand-kept lists of which fourteen were identical, so every new client meant an edit in every provider. A client now declares services.<own-entity>.mcp_consumer: true once and a provider refuses one with mcp_consumer: false, which is also what tells a deliberate exclusion apart from a forgotten entry.

The group_names-gated consumer entries give the inventory resolver the co-deploy edge meta/mcp.yml never had: one of sixty-nine matrix rounds carried an MCP client before, twenty-three role/round pairs do now. bond 0.5 keeps the clients out of round 0 so the host memory budget still holds.

BREAKING CHANGE: meta/mcp.yml no longer accepts allowed_consumers; declare services.<entity>.mcp_consumer on the client instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 18:33:22 +02:00
kevinveenbirkenbachandClaude Opus 5 ead858f481 feat(agents): let each sandboxed agent act as its own platform account
The platform reserves a user key per role, so hermes and openclaw existed as non-privileged accounts before either role claimed one. Claiming the key is what turns a reservation into an identity the agent may authenticate as, and gives the account its description and its mailbox. Neither role declares a password: provisioning stores one for every declared account, and a role whose registration rejects the shell-safe alphabet says so in its own meta/users.yml rather than carrying a second credential beside the account it already owns.

The lint derives the agent set from the sandbox-runtime scan rather than a list and holds three properties: the role claims its own reserved key, the claimed account carries no role because an agent employee is never an administrator, and no second credential sits beside it. Pointing an agent at an account that already exists needs no code, since the inventory users variable overrides the same key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 00:23:20 +02:00
kevinveenbirkenbachandClaude Opus 5 30f24bb346 docs(csp): record why openclaw allows unsafe-eval and unsafe-inline
Both flags gain the rationale the lint requires, in the same form as
896b72428. Neither is a justification: both state that the flag was set when
the role first deployed with no console error recorded against it, and carry
a TODO to re-verify against the running Control UI.

unsafe-eval is the sharper of the two, permitting eval and its relatives. A
WebSocket SPA plausibly needs it; that it does has not been established.

Three roles now carry this unanswered TODO: hermes, litellm and openclaw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 13:14:37 +02:00
kevinveenbirkenbachandClaude Opus 5 cd1f07a91e feat(roles): onboard hermes, homeassistant, litellm and openclaw to tor
Four roles gained a canonical domain without a tor service block. Declare it
the way 84b22b37a8 did for n8n: bond 0.5, enabled on 'svc-net-tor' in
group_names, shared, with the variant pins alongside.

The nocheck directives carry the two exemptions: the service has no persona
surface for the playwright flag, and the sso-variant-tor combination requires
the onion in every variant, so the false polarity has no variant left to
live in.

hermes also moves its local http port from 8089 to 8092, because 8089
belongs to web-app-socialhome. That is a separate concern riding along in the
same file; splitting it would need interactive staging.

This is merge work rather than authored here: it follows the established
pattern and the port collision is real, but none of it has been deployed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 12:48:10 +02:00
kevinveenbirkenbachandClaude Opus 5 481666bdb0 refactor(roles): finish renaming meta/schema.yml to meta/secrets.yml
Eleven roles still declared their credentials in meta/schema.yml while the
rest of the repository had moved to meta/secrets.yml. The name is not
cosmetic: the inventory resolves credentials at that path, as
d86913225b and c541e7c22f record, so a role left on the old name drops out
of that resolution.

All eleven are pure renames with no content change.

Also correct the four remaining places that still named the old path in
prose: the credentials reset CLI help, two test docstrings and the espocrm
README. None of them resolved a file, but they misdescribe where credentials
live, which is exactly what sent an earlier reader to the wrong location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 12:13:48 +02:00
kevinveenbirkenbachandClaude Opus 5 575b9a463c fix(web-app-openclaw): keep one writer on the agent's shared state
The service declared no replica count, so it took the topology default of
three and spread them over two nodes while its state directory is one NFS
export shared by all of them. The agent keeps that state in SQLite, and the
swarm run shows what concurrent writers do to it: one replica exits with the
database reported as not a database, another cannot even create the state
directory.

Those exits are what started the restart storm the run then failed on, and
they are runtime independent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:30:31 +02:00
kevinveenbirkenbach 6923c5f361 fix(openclaw): allow eval in script-src so the CSP health check can pass
The shipped frontend bundle calls eval() from its runtime config chunk,
which script-src blocks, so the CSP health unit failed twenty-five times
in a row and aborted the deploy. The application itself answers 200 and
loads every asset.

This widens the policy rather than fixing the cause, which sits in the
upstream bundle; four other roles carry the same allowance for the same
reason. The failure was masked while the adapter build failed earlier in
the same job.
2026-08-10 13:31:48 +02:00
kevinveenbirkenbach b57a728320 feat(openclaw): admit the mcp group at the SSO gate
The agent drives MCP clients, so holders of its application-scoped mcp
role must reach it, which completes the set of three client roles whose
gate now admits that group. The README follows the renamed assert task.
2026-08-10 00:07:58 +02:00
kevinveenbirkenbach 67feed6332 feat(rbac): declare the mcp role on every agent that carries clients
An mcp grant lets its holder drive clients that reach admitted providers
with the credentials the deployment issued, not the caller's own, so the
role is declared per application and never spans them.

The unit test pins that boundary directly: a scoped grant reaches only
the named application, an unscoped one reaches nothing, and an ordinary
role still spans everything. The Keycloak realm builder, the LDAP entry
builder and the Open WebUI reconciler all resolve membership through the
one module this covers.
2026-08-09 23:53:41 +02:00
kevinveenbirkenbach 06bc856c32 refactor(mcp): make MCP a role topic instead of a service entry
The block sat under services but never behaved like a service. A service has
exactly one providing role, declared with provides, so the registry can map the
key to that role and consumers bind their flag to its presence. No role declares
provides: mcp. There are seventeen providers and the pairing is done by
allowed_consumers inside the block itself.

Every rule that enforces the service model therefore needed an exemption for it:
a shared-provider suppression in fourteen roles, a playwright-service-flag
suppression, and two freshly added exemptions in the dynamic-flag and
variants-static-override checks. Four independent rules, four escape hatches,
one cause. They are all gone now, together with a bond key that only the
resource budgeter reads and only for entries under services, and a wordpress
addon bridge that pointed at a service block that no longer exists.

The block moves to meta/mcp.yml. The applications loader maps meta/<name>.yml to
applications.<app>.<name> generically, so the move needs no wiring; the file
takes the same shape as meta/rbac.yml. Every config path follows, the variant
overrides are lifted out of their services map, and the vocabulary module leaves
the services package because it no longer describes one. roles_with_service
gains an explicit topic parameter rather than an implicit fallback, so nothing
silently resolves against the wrong container.

Files that are otherwise unrelated to this move are included where the committed
tree would otherwise keep pointing at a path that no longer resolves: thirteen
vars files, eleven playwright env templates and one task file.
2026-08-06 20:36:00 +02:00
kevinveenbirkenbachandClaude Opus 5 e9af031215 feat(hermes,openclaw): consume the discovered MCP servers
Render every discovered server into the agent's own config, filtered to
the auth schemes a client can actually present, so no entry carries a
credential the server would reject.

Render hermes' config after the stack exists and before the handlers
flush; the other order fired compose up while the config was still
missing.

Own the config file as the container user. Rendered as root it stays
unreadable to the agent, which then falls back to its built-in defaults
silently and reports only that the server is unknown, pointing the reader
at the wrong layer.

Assert what these clients expose: neither ships an admin surface listing
its configured servers, so the specs hold the API to refusing an
unauthenticated caller, which is what keeps the per-server bearers out of
reach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 22:26:32 +02:00
kevinveenbirkenbach f39073c95c Merge remote-tracking branch 'fork/feature/swarm-nfs' into feature/ai-agent-employees
# Conflicts:
#	roles/web-app-flowise/meta/services.yml
2026-07-30 15:39:29 +02:00
kevinveenbirkenbachandClaude Opus 5 e3481b7745 feat(openclaw): reach the declared MCP servers and prove it
MCP_DISCOVERED_SERVERS resolves every server-direction role to its internal URL, the bearer the server role minted, and its transport, dropping any server without a token so no client renders a connection it cannot authenticate.

OpenClaw moves from 2026.3.11 to 2026.7.1 because the pinned version has no configuration surface for outbound MCP servers at all: its validator rejects mcpServers, the shipped bundle carries no bundle-mcp, and the ACP path refuses per-session servers while pointing at a gateway config that does not exist there. 2026.7.1 reads mcp.servers, which the template now renders. The gateway command gains its run subcommand because the bare gateway no longer starts anything.

Reachability was the missing half: openclaw sat on its own networks only, so the hub name resolved through the DNS wildcard to the ingress instead of the container. The hub overlay now declares an mcp consumer key, which attaches every role carrying services.mcp.

A probe task runs openclaw mcp probe after the handlers flush and fails the deploy when a configured server does not answer. Without it the previous run reported failed=0 while the client could not reach anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:40:19 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8b5312445b docs(roles): write the AI and runtime role documentation
The eight new roles shipped with the README template unfilled: every one
of them described itself as "[Name](https://example.com/) is an
application" with a single "Describe a capability" feature. They were
implemented, deployed and tested; their documentation was placeholder
text.

Each role now carries a Description, Overview and Features written
against its own services, tasks and templates, with TODO.md as the
binding list of what is not wired. That caught a false claim: OpenClaw
advertised routing through the shared gateway, but it reads model
providers from openclaw.json rather than the environment, so the gateway
only shapes its deploy topology. The claim is gone.

The overview table derives its Name column from each README H1, and all
eight had kept the humanised fallback. Worst of these, both LiteLLM roles
rendered as "Litellm" on adjacent rows, so a reference in the Integrated
with column could not say which one it meant; they are now the Gateway
and the Admin UI. The rest take their official spelling.

The galaxy descriptions all repeated the product name the Name column
already carries, one named a role id in prose and one used a hyphen as a
dash. svc-virt-kata also claimed to install Kata, which it does not: it
installs gVisor and registers an already-present Kata shim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 20:21:25 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bcd32463a2 feat(web-app-openclaw): deploy the OpenClaw agent behind an SSO proxy
The second agent of requirement 032, pinned to the same isolating runtime
as hermes. Unlike hermes it exposes a browser Control UI rather than a
bearer API, so it takes the oauth2 proxy flavour with its own sso port;
a proxy in front of a browser surface is correct where it would break
OpenAI-compatible clients.

Three deploy failures shaped this role. OpenClaw maps no environment
variables onto config keys, so it restart-looped on 'Missing config' and
then on 'non-loopback Control UI requires allowedOrigins'; both now come
from a rendered openclaw.json mounted as a config. That render must run
after sys-stk-full, because it notifies compose up and the handler would
otherwise fire before the compose file exists. And the process runs as
root: OpenClaw refuses a config file not owned by the running uid and
writes cron state into its home, neither of which a non-root user can do
against the platform's root-owned volume and config mount. gVisor is the
isolation boundary here, not the in-guest uid.

The administrator persona is blocked: the Control UI is a WebSocket SPA
behind gateway device pairing with no DOM logout control for the shared
helper. The SSO round-trip itself is covered by test-oidc-login.js, which
drives the full unauthenticated to Keycloak to authenticated path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 19:09:57 +02:00