037 - Per-User Agents on Demand from Open WebUI¶
User Story¶
As a platform user with agent access, I want Open WebUI to start my own Hermes or OpenClaw agent the first time I address it, isolated from every other user’s agent, so that I work with a personal agent without an administrator provisioning one for me, while administrators decide through RBAC who may use agents at all.
Scope¶
A new role
svc-ai-agent-brokerruns the broker and a restricted container-API proxy.web-app-hermesandweb-app-openclaweach gain the RBAC roleagent-userand keep their existing shared instance unchanged.web-app-openwebuigains a second OpenAI connection pointing at the broker, forwards the caller’s identity and restricts each agent model to its group.Compose and swarm are both supported.
svc-ai-robotstays out of the platform list. It runs the same agent in embodied mode, which means it drops the isolating runtime on purpose and hands the container the host devices it is meant to drive. A broker platform is defined by the opposite:runsc, no host device, no host mount. Serving an embodied agent per user would either break that invariant or ship a robot that cannot reach its hardware.
Design¶
Request flow¶
Open WebUI sends
POST /v1/chat/completionswithmodel: hermesormodel: openclawto the broker, authenticated with the broker key and carrying theX-OpenWebUI-User-*headers (ENABLE_FORWARD_USER_INFO_HEADERS=true).The broker rejects any request whose bearer is not the broker key, and any request without a user id header.
The broker resolves the caller’s groups from Keycloak on every completion and refuses with HTTP 403 when the caller is not in the
agent-usergroup of the requested platform.The broker looks up the caller’s agent for that platform by name. When none exists it creates one; when it is stopped it starts it; it then waits until the agent answers its health endpoint.
The broker forwards the request to the agent with the agent’s own bearer key and streams the answer back unchanged.
GET /v1/modelslists both platforms; Open WebUI shows each only to its group through model access grants.
Model visibility¶
With the broker deployed, Open WebUI runs with BYPASS_MODEL_ACCESS_CONTROL=false. A deploy-time step grants every LiteLLM model public read access and each agent model read access for its agent-user group only. A svc-ai-litellm deploy repeats that step in a running Open WebUI, so a model added to the gateway is visible to every user without an Open WebUI deploy.
Agent container¶
One container (compose) or one single-replica service (swarm) per user and platform, named from the platform and a hash of the user id, labelled with owner and platform.
Runtime is the isolating runtime selected by
svc-virt-kata; the broker stops and refuses an agent that came up underrunc.No privileged mode, no host network, pid, ipc or user namespace, no host bind mount, no container-engine socket.
A dedicated volume per user and platform holds the agent’s state; it survives a stop.
A dedicated network per agent whose only other member is the broker.
A random bearer key per agent, known only to the broker, used in both directions.
The agent holds no model credential: it calls
http://agent-broker:<port>/llm/v1, and the broker relays the call to LiteLLM with the broker’s own virtual key and the owner in the OpenAIuserfield, and logs arelayevent with owner, platform, path and upstream status.CPU, memory and pids limits come from the platform role’s
meta/services.yml.
Lifecycle¶
svc-ai-agent-broker/meta/services.yml carries under agent-broker.agents:
Key |
Meaning |
|---|---|
|
model alias every agent is configured with; defaults to the gateway’s chat model |
|
|
|
idle time before a stop |
|
upper bound of concurrently running agents; a request beyond it gets HTTP 503 |
|
seconds an agent gets to answer its health endpoint |
A stopped agent is started again on the next request with its volume re-attached. An agent serving a request is never stopped, and the idle time is re-read under the agent’s lock before the stop. When a caller has lost the agent-user group, the broker refuses the request and stops that caller’s running agent.
Context window¶
A model that serves agents declares its context window in tokens: context on an AI_LOCAL_MODELS entry or on a services.litellm.remote_models entry. LiteLLM sends it to Ollama as num_ctx and publishes it as model_info.max_input_tokens, and the broker writes it into the agent config as Hermes model.context_length and OpenClaw contextWindow. Without it Ollama truncates the agent’s prompt to its own default window and the agents assume windows of their own.
A platform may refuse a window it considers too small. Hermes requires 64000 tokens and answers HTTP 500 with “below the minimum 64,000 required by Hermes Agent” for anything less, so web-app-hermes declares services.hermes.min_context and the broker withholds any platform whose minimum the selected model misses: it is absent from /v1/models and answers 404, while every other platform keeps serving. Refusing the whole deploy instead would block a pairing that works, since OpenClaw declares no minimum and the broker pulls Hermes into its closure either way. A model that declares no window at all is reported rather than withheld: an agent that does not know its window runs.
Container API¶
The broker reaches the container engine only through a filtered unix socket in a volume it shares with the socket proxy. The proxy serves no TCP port; in compose it also runs with network_mode: none, while a swarm task carries the stack’s default overlay attachment. The proxy admits only the container, service, task, network, volume and image calls the broker makes, and refuses every other method and path, including exec and delete. The image, runtime, limits and mounts of an agent are fixed by the broker, never taken from the request.
The proxy filters methods and paths, not request bodies. POST /volumes/create stays on the allowlist because the broker creates a volume per agent, and a local-driver volume with o=bind,device=/ is a host bind mount that arrives as a volume. Fields such as Privileged, PidMode or CapAdd on containers/create are likewise unfiltered. The isolation therefore holds against a compromised agent, which reaches no socket at all, not against a compromised broker.
Acceptance Criteria¶
Role and configuration¶
[ ]
svc-ai-agent-brokerdeploys in compose and swarm mode and passes the repository lint suite.[x] The
agent-broker.agentskeys reach the running broker frommeta/services.yml, so an inventory override changes its behaviour.[x] The socket proxy refuses
POST /containers/{id}/exec, a container or service create carrying a host bind mount, andDELETE /containers/{id}.[x] The socket proxy serves the engine on the unix socket shared with the broker and publishes no port; in compose it runs with
network_mode: none.
Access control¶
[x]
web-app-hermesandweb-app-openclaweach declare the RBAC roleagent-user, which provisions a Keycloak group.[x] Open WebUI lists
hermesandopenclawonly to a user in the matching group, while every LiteLLM model stays visible to all users.[x] A model added to LiteLLM becomes visible to all Open WebUI users after a
svc-ai-litellmdeploy alone.[x] A direct call to the broker for a user outside the group gets HTTP 403 and creates no container.
[x] A request with a wrong or missing broker key gets HTTP 401.
Agent lifecycle¶
[x] The first prompt of an entitled user creates exactly one agent for that user and platform and returns the agent’s answer in Open WebUI.
[x] A second prompt from the same user reuses the same agent without restarting it.
[x] A stopped agent is started again by the next prompt, keeps its container and its state.
[x] With
idle_stop: truethe sweep stops an agent that has been idle foridle_minutes, and never one that is serving a request or was just used.[x] With
idle_stop: falsethe sweep stops nothing.
Isolation¶
[x] Two users get two different containers, volumes, networks and bearer keys.
[x] An agent inspected on the engine runs under the isolating runtime, not
runc.[x] A file written by one user’s agent is not visible to the other user’s agent.
[x] One agent cannot reach the other agent’s network address.
Tests¶
[x]
web-app-openwebuiships a Playwright spec that logs in asbiberwithoutagent-userand asserts that neither agent model is listed or answers, while every served gateway model is listed.[x] The same spec grants
bibertheagent-usergroups, logs in again and asserts an answer fromhermesand fromopenclawthrough Open WebUI.[x]
svc-ai-agent-brokerships a CLI test that proves the broker-key refusal, the RBAC refusal, two distinct agents under the isolating runtime, file isolation and network isolation on the engine.[x] The same CLI test proves from the broker’s
relayevents that ahermesand anopenclawagent each got a model answer from LiteLLM, independent of what the model wrote.[x] Unit tests cover the idle sweep: it stops an idle agent, skips one that is serving a request or was just used, and does nothing while
idle_stopis false.