feat: rebuild as multi-user web agent

This commit is contained in:
wuyang
2026-07-26 06:52:12 +08:00
parent 45792c8fd5
commit 37f83eaac7
634 changed files with 5060 additions and 139619 deletions
+154
View File
@@ -0,0 +1,154 @@
from __future__ import annotations
import json
import time
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any
def _last_user_text(messages: list[dict[str, Any]]) -> str:
for message in reversed(messages):
if message.get("role") != "user":
continue
content = message.get("content", "")
if isinstance(content, str):
return content
if isinstance(content, list):
return " ".join(str(part.get("text", "")) for part in content if isinstance(part, dict))
return ""
def _completion(model: str, message: dict[str, Any], finish_reason: str) -> dict[str, Any]:
return {
"id": f"chatcmpl-stub-{uuid.uuid4().hex}",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [{"index": 0, "message": message, "finish_reason": finish_reason}],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
}
class StubHandler(BaseHTTPRequestHandler):
server_version = "K1412E2EStub/1"
def log_message(self, format: str, *args: Any) -> None:
return
def _json(self, status: int, payload: dict[str, Any]) -> None:
encoded = json.dumps(payload, ensure_ascii=False).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def do_GET(self) -> None:
if self.path == "/health":
self._json(200, {"status": "ok"})
return
self._json(404, {"error": "not found"})
def do_POST(self) -> None:
if self.path != "/v1/chat/completions":
self._json(404, {"error": "not found"})
return
length = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(length) or b"{}")
model = str(payload.get("model", "stub"))
messages = payload.get("messages") or []
tools = payload.get("tools") or []
has_tool_result = any(message.get("role") == "tool" for message in messages)
if tools and not has_tool_result:
names = {str((tool.get("function") or {}).get("name", "")) for tool in tools if isinstance(tool, dict)}
is_work = "update_plan" in names
path = "work-proof.txt" if is_work else "chat-proof.txt"
arguments = json.dumps(
{"path": path, "content": _last_user_text(messages) or "e2e"},
ensure_ascii=False,
)
message = {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_e2e_write",
"type": "function",
"function": {"name": "write_file", "arguments": arguments},
}
],
}
self._respond(payload, _completion(model, message, "tool_calls"))
return
text = "E2E provider completed after tool execution." if has_tool_result else "E2E provider response."
self._respond(payload, _completion(model, {"role": "assistant", "content": text}, "stop"))
def _respond(self, request: dict[str, Any], completion: dict[str, Any]) -> None:
if not request.get("stream"):
self._json(200, completion)
return
message = completion["choices"][0]["message"]
finish_reason = completion["choices"][0]["finish_reason"]
chunks: list[dict[str, Any]] = []
if message.get("tool_calls"):
call = message["tool_calls"][0]
chunks.append(
{
"id": completion["id"],
"object": "chat.completion.chunk",
"created": completion["created"],
"model": completion["model"],
"choices": [
{
"index": 0,
"delta": {
"role": "assistant",
"tool_calls": [{"index": 0, **call}],
},
"finish_reason": None,
}
],
}
)
else:
chunks.append(
{
"id": completion["id"],
"object": "chat.completion.chunk",
"created": completion["created"],
"model": completion["model"],
"choices": [
{
"index": 0,
"delta": {"role": "assistant", "content": message.get("content", "")},
"finish_reason": None,
}
],
}
)
chunks.append(
{
"id": completion["id"],
"object": "chat.completion.chunk",
"created": completion["created"],
"model": completion["model"],
"choices": [{"index": 0, "delta": {}, "finish_reason": finish_reason}],
}
)
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
for chunk in chunks:
self.wfile.write(f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode())
self.wfile.write(b"data: [DONE]\n\n")
self.wfile.flush()
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", 9000), StubHandler).serve_forever() # noqa: S104
+261
View File
@@ -0,0 +1,261 @@
from __future__ import annotations
import atexit
import os
import time
import uuid
from typing import Any
import httpx
import jwt
import docker
from agent_platform.gateway.provider import workspace_ref
BASE_URL = os.getenv("E2E_BASE_URL", "http://127.0.0.1:3000").rstrip("/")
ADMIN_EMAIL = os.getenv("E2E_ADMIN_EMAIL", "admin-e2e@example.invalid")
ADMIN_PASSWORD = os.getenv("E2E_ADMIN_PASSWORD", "e2e-admin-password")
FORWARD_JWT_SECRET = os.getenv(
"E2E_FORWARD_JWT_SECRET",
"e2e-forward-jwt-secret-at-least-32-bytes",
)
INTERNAL_PROVIDER_KEY = os.getenv(
"E2E_INTERNAL_PROVIDER_KEY",
"e2e-provider-secret-at-least-32-bytes",
)
EXPECTED_MODELS = {
"chat-light",
"chat-medium",
"chat-high",
"work-light",
"work-medium",
"work-high",
}
def _auth(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
def _signin(client: httpx.Client, email: str, password: str) -> dict[str, Any]:
response = client.post("/api/v1/auths/signin", json={"email": email, "password": password})
response.raise_for_status()
data = response.json()
assert data.get("token")
return data
def _stream_chat(
client: httpx.Client,
token: str,
*,
model: str,
chat_id: str,
prompt: str,
include_workspace_tools: bool,
) -> tuple[int, str]:
user_message_id = uuid.uuid4().hex
assistant_message_id = uuid.uuid4().hex
payload: dict[str, Any] = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": True,
"chat_id": chat_id,
"id": assistant_message_id,
"assistant_message_id": assistant_message_id,
"user_message": {
"id": user_message_id,
"parentId": None,
"childrenIds": [assistant_message_id],
"role": "user",
"content": prompt,
"timestamp": int(time.time()),
},
"params": {"function_calling": "native"},
"features": {},
}
if include_workspace_tools:
payload["tool_ids"] = ["server:workspace"]
with client.stream(
"POST",
"/api/chat/completions",
headers=_auth(token),
json=payload,
timeout=90,
) as response:
return response.status_code, "".join(response.iter_text())
def _workspace_file(client: docker.DockerClient, user_id: str, path: str) -> str:
container = client.containers.get(workspace_ref(user_id).container_name)
result = container.exec_run(["cat", f"/workspace/{path}"], user="1000:1000")
assert result.exit_code == 0, result.output.decode(errors="replace")
return result.output.decode()
def _cleanup_workspaces(users: list[dict[str, Any]]) -> None:
client = docker.from_env()
for user in users:
ref = workspace_ref(user["id"])
try:
client.containers.get(ref.container_name).remove(force=True)
except docker.errors.NotFound:
pass
try:
client.volumes.get(ref.volume_name).remove(force=True)
except docker.errors.NotFound:
pass
try:
client.networks.get(ref.network_name).remove()
except docker.errors.NotFound:
pass
client.close()
def _runtime_url(client: docker.DockerClient) -> str:
container = client.containers.get("k1412-agent-e2e-runtime-1")
networks = container.attrs["NetworkSettings"]["Networks"]
internal = next(value for name, value in networks.items() if name.endswith("_internal"))
return f"http://{internal['IPAddress']}:8000"
def _runtime_headers(user: dict[str, Any], chat_id: str) -> dict[str, str]:
now = int(time.time())
identity = jwt.encode(
{
"sub": user["id"],
"email": user["email"],
"name": user["name"],
"role": "user",
"iss": "open-webui",
"iat": now,
"exp": now + 60,
},
FORWARD_JWT_SECRET,
algorithm="HS256",
)
return {
"Authorization": f"Bearer {INTERNAL_PROVIDER_KEY}",
"X-OpenWebUI-User-Jwt": identity,
"X-OpenWebUI-Chat-Id": chat_id,
}
def main() -> None:
suffix = uuid.uuid4().hex[:10]
user_specs = [
(f"E2E User A {suffix}", f"e2e-a-{suffix}@example.invalid", "e2e-user-password-a"),
(f"E2E User B {suffix}", f"e2e-b-{suffix}@example.invalid", "e2e-user-password-b"),
]
with httpx.Client(base_url=BASE_URL, timeout=30) as client:
health = client.get("/health")
health.raise_for_status()
assert health.json().get("status") is True
admin = _signin(client, ADMIN_EMAIL, ADMIN_PASSWORD)
admin_token = admin["token"]
config = client.get("/api/v1/auths/admin/config", headers=_auth(admin_token))
config.raise_for_status()
admin_config = config.json()
assert admin_config["ENABLE_SIGNUP"] is True
assert admin_config["DEFAULT_USER_ROLE"] == "pending"
assert admin_config["ENABLE_API_KEYS"] is False
assert admin_config["ENABLE_AUTOMATIONS"] is False
model_response = client.get("/api/models?refresh=true", headers=_auth(admin_token))
model_response.raise_for_status()
model_payload = model_response.json()
model_ids = {item["id"] for item in model_payload["data"]}
assert model_ids == EXPECTED_MODELS, model_ids
assert "ChatGPT-5.6" not in model_response.text
users: list[dict[str, Any]] = []
atexit.register(_cleanup_workspaces, users)
for name, email, password in user_specs:
signup = client.post(
"/api/v1/auths/signup",
json={"name": name, "email": email, "password": password},
)
signup.raise_for_status()
pending = signup.json()
assert pending["role"] == "pending"
denied = client.get("/api/models", headers=_auth(pending["token"]))
assert denied.status_code in {401, 403}
approved = client.post(
f"/api/v1/users/{pending['id']}/update",
headers=_auth(admin_token),
json={"role": "user"},
)
approved.raise_for_status()
assert approved.json()["role"] == "user"
users.append(_signin(client, email, password))
for user in users:
models = client.get("/api/models", headers=_auth(user["token"]))
models.raise_for_status()
user_model_ids = {item["id"] for item in models.json()["data"]}
assert user_model_ids == EXPECTED_MODELS, user_model_ids
assert "ChatGPT-5.6" not in models.text
forbidden = client.get("/api/v1/auths/admin/config", headers=_auth(user["token"]))
assert forbidden.status_code in {401, 403}
chat_ids = [f"local:e2e-{suffix}-a", f"local:e2e-{suffix}-b"]
for index, user in enumerate(users):
marker = f"E2E_CHAT_USER_{index}_{suffix}"
status_code, body = _stream_chat(
client,
user["token"],
model="chat-light",
chat_id=chat_ids[index],
prompt=marker,
include_workspace_tools=True,
)
assert status_code == 200, body
docker_client = docker.from_env()
assert _workspace_file(docker_client, users[0]["id"], "chat-proof.txt") == f"E2E_CHAT_USER_0_{suffix}"
assert _workspace_file(docker_client, users[1]["id"], "chat-proof.txt") == f"E2E_CHAT_USER_1_{suffix}"
for user in users:
ref = workspace_ref(user["id"])
workspace = docker_client.containers.get(ref.container_name)
workspace.reload()
assert set(workspace.attrs["NetworkSettings"]["Networks"]) == {ref.network_name}
work_marker = f"E2E_WORK_USER_0_{suffix}"
status_code, work_body = _stream_chat(
client,
users[0]["token"],
model="work-medium",
chat_id=chat_ids[0],
prompt=work_marker,
include_workspace_tools=False,
)
assert status_code == 200, work_body
assert _workspace_file(docker_client, users[0]["id"], "work-proof.txt") == work_marker
with httpx.Client(base_url=_runtime_url(docker_client), timeout=30, trust_env=False) as runtime:
runtime_headers = _runtime_headers(users[0], chat_ids[0])
events = runtime.get(f"/v1/runs/{chat_ids[0]}", headers=runtime_headers)
events.raise_for_status()
event_types = {event["type"] for event in events.json()["items"]}
assert {"run.created", "tool.completed", "run.completed"} <= event_types
downgrade = runtime.post(
"/v1/chat/completions",
headers=runtime_headers,
json={
"model": "chat-high",
"messages": [{"role": "user", "content": "THIS_DOWNGRADE_MUST_FAIL"}],
"stream": False,
},
)
assert downgrade.status_code == 409, downgrade.text
print("E2E passed: auth approval, six models, Chat tools, Work loop, isolation, and downgrade lock.")
if __name__ == "__main__":
main()