#!/usr/bin/env python3 """Agent settings through the dashboard API: save and load, masked keys, rights, cache, system status, form markup.""" import json import tempfile from support import Client, Site, write_json BRAVE = "BSA-brave-http-secret-9876543210" PEXELS = "pexels-http-secret-ZYXWVUTS987654" with tempfile.TemporaryDirectory(prefix="raiju-agent-settings-") as folder: site = Site(folder) root = site.root write_json(root / "content/settings.json", {"modules": {"ai": True}, "privacy": {"analyticsEnabled": False}}) site.start() admin = Client(site).login("admin") editor = Client(site).login("editor") visitor = Client(site) def load(client): status, _, body = client.request("/admin/api.php?action=load-ai-settings") return status, json.loads(body) def save(client, settings): return client.api("save-ai-settings", settings=json.dumps(settings)) try: # ---- Load ---------------------------------------------------------------------------------------------------------------------------- status, payload = load(admin) settings = payload["data"]["settings"] assert status == 200 and settings["agent"]["enabled"] is False and settings["research"]["hasBraveApiKey"] is False and settings["stock"]["hasPexelsApiKey"] is False, settings assert settings["agentStatus"]["gd"] in (True, False) and settings["agentStatus"]["toolCalling"] in (True, False), "admins see what the agent needs" status, payload = load(editor) assert status == 200 and "agentStatus" not in payload["data"]["settings"], "editors do not get the server status" # ---- Save ---------------------------------------------------------------------------------------------------------------------------- form = {"enabled": True, "provider": "openai-compatible", "baseUrl": "http://127.0.0.1:9", "allowLocalProvider": True, "apiKey": "provider-http-secret-0001", "chatModel": "gpt-4.1-mini", "features": {"backendAssistant": True, "seoTextGeneration": True, "imageGeneration": True}, "agent": {"enabled": True, "limits": {"runsPerHour": 7, "maxRuntimeSeconds": 600}, "roles": {"editor": {"publish": False, "uploadImages": True}}, "mediaFolder": "agent-images", "creditInNews": True, "sourcesInNews": False}, "research": {"braveApiKey": BRAVE, "allowDomains": "example.org\nexample.net", "denyDomains": "", "respectRobots": True}, "stock": {"pexelsApiKey": PEXELS}} status, payload = save(admin, form) assert status == 200 and payload["success"], payload saved = payload["data"]["settings"] assert saved["agent"]["limits"]["runsPerHour"] == 7 and saved["agent"]["mediaFolder"] == "agent-images" and saved["agent"]["sourcesInNews"] is False, saved["agent"] assert saved["research"]["hasBraveApiKey"] and saved["stock"]["hasPexelsApiKey"] and saved["research"]["allowDomains"] == ["example.org", "example.net"], saved assert "agentStatus" in saved stored = (root / "content/ai-settings.json").read_text() secrets = (root / "content/ai-secrets.json").read_text() assert BRAVE in secrets and PEXELS in secrets and "provider-http-secret" in secrets, "the server keeps the keys in the secrets file" assert BRAVE not in stored and PEXELS not in stored and "provider-http-secret" not in stored and '"apiKey"' not in stored, "and none of them in the settings file, which is safe to commit" assert oct((root / "content/ai-secrets.json").stat().st_mode & 0o777) == "0o600", "the secrets file is for the owner only" for text in [json.dumps(payload), json.dumps(load(admin)[1]), json.dumps(load(editor)[1])]: assert BRAVE not in text and PEXELS not in text and "provider-http-secret" not in text, "no key in any response" assert visitor.request("/content/ai-settings.json")[0] != 200 and visitor.request("/content/ai-secrets.json")[0] != 200, "neither file is downloadable" # keys survive a save without keys, and are cleared on request status, payload = save(admin, {**form, "apiKey": "", "research": {"braveApiKey": "", "clearBraveApiKey": False}, "stock": {"pexelsApiKey": ""}}) assert payload["success"] and BRAVE in (root / "content/ai-secrets.json").read_text() and PEXELS in (root / "content/ai-secrets.json").read_text() status, payload = save(admin, {**form, "apiKey": "", "research": {"clearBraveApiKey": True}, "stock": {"clearPexelsApiKey": True}}) assert payload["success"] and BRAVE not in (root / "content/ai-secrets.json").read_text() and PEXELS not in (root / "content/ai-secrets.json").read_text() and not payload["data"]["settings"]["research"]["hasBraveApiKey"] # refusals for bad in [{"research": {"braveApiKey": "short"}}, {"stock": {"pexelsApiKey": "a key with spaces 12345"}}, {"agent": {"mediaFolder": "../up"}}]: status, payload = save(admin, {**form, "apiKey": "", **bad}) assert payload["success"] is False and status != 200 or payload["success"] is False, (bad, payload) # ---- Warnings travel with the usage numbers --------------------------------------------------------------------------------- import time usage = {"days": {time.strftime("%Y-%m-%d", time.gmtime()): {"requests": 290, "textRequests": 10, "imageRequests": 0, "inputTokens": 0, "outputTokens": 0, "estimatedCostCents": 0}}, "months": {time.strftime("%Y-%m", time.gmtime()): {"requests": 290, "textRequests": 10, "imageRequests": 0, "inputTokens": 0, "outputTokens": 0, "estimatedCostCents": 1000}}} write_json(root / "content/ai-usage.json", usage) save(admin, {**form, "apiKey": "", "limits": {"monthlyBudgetCents": 1000, "dailyRequests": 300, "dailyTextRequests": 250, "dailyImageRequests": 10, "maxInputTokens": 64000, "maxOutputTokens": 16000, "requestTimeoutSeconds": 300}}) for client in (admin, editor): kinds = {w["kind"]: w["level"] for w in load(client)[1]["data"]["usage"]["warnings"]} assert kinds == {"month": "reached", "requests": "warning"}, kinds status, _, body = admin.request("/admin/api.php", {"action": "ai-copilot-context", "csrf_token": admin.csrf, "contentPage": "en_home"}) assert {w["kind"] for w in json.loads(body)["data"]["limitWarnings"]} == {"month", "requests"}, "the assistant gets them with its context" write_json(root / "content/ai-usage.json", {"days": {}, "months": {}}) # ---- Who may change them --------------------------------------------------------------------------------------------------------- status, payload = save(editor, form) assert status == 403 and payload["success"] is False, (status, payload) status, _, body = admin.request("/admin/api.php", {"action": "save-ai-settings", "settings": json.dumps(form)}) assert status == 403, "no CSRF token, no change" write_json(root / "content/ai-settings.json", {**json.loads((root / "content/ai-settings.json").read_text()), "agent": {"enabled": True}}) # ---- Clear the page scan cache --------------------------------------------------------------------------------------------------- cache = root / "content/ai-agent/cache" cache.mkdir(parents=True, exist_ok=True) for name in ["cap-aaaa.json", "cap-bbbb.json"]: (cache / name).write_text("{}") (cache / "keep.txt").write_text("not a scan") assert editor.api("ai-agent-clear-cache")[0] == 403, "editors cannot clear it" assert admin.request("/admin/api.php?action=ai-agent-clear-cache")[0] == 405, "not over GET" assert admin.request("/admin/api.php", {"action": "ai-agent-clear-cache"})[0] == 403, "not without the CSRF token" assert len(list(cache.glob("cap-*.json"))) == 2 status, payload = admin.api("ai-agent-clear-cache") assert status == 200 and payload["data"]["removed"] == 2 and not list(cache.glob("cap-*.json")) and (cache / "keep.txt").exists(), payload # ---- System status --------------------------------------------------------------------------------------------------------------- save(admin, {**form, "apiKey": ""}) status, _, body = admin.request("/admin/api.php?action=system-status&scheme=http") data = json.loads(body)["data"] assert set(data["agent"]) == {"enabled", "toolCalling", "gd", "webp", "research", "stock", "storageWritable"} and data["agent"]["enabled"] is True and data["agent"]["research"] is True and data["agent"]["stock"] is True, data["agent"] assert BRAVE not in body.decode() and PEXELS not in body.decode(), "the status never contains a key" assert editor.request("/admin/api.php?action=system-status&scheme=http")[0] in (403, 200) and BRAVE not in editor.request("/admin/api.php?action=system-status&scheme=http")[2].decode() # ---- The form ----------------------------------------------------------------------------------------------------------------------- status, _, body = admin.request("/admin/dashboard.php") html = body.decode() for needle in ["id=\"aiAgentSection\"", "id=\"aiAgentEnabled\"", "id=\"aiBraveApiKey\"", "id=\"aiPexelsApiKey\"", "id=\"aiAgentClearCache\"", "data-agent-right=\"publish\"", "data-agent-right=\"editSharedContent\"", "id=\"aiAgentMediaFolder\""]: assert needle in html, needle assert BRAVE not in html and PEXELS not in html and "provider-http-secret" not in html, "keys are never rendered into the dashboard" assert "aiAgentSection" not in editor.request("/admin/dashboard.php")[2].decode(), "editors do not get the section" # ---- A settings file of an older version: the keys move out when an administrator loads the settings --------------------------------- legacy = json.loads((root / "content/ai-settings.json").read_text()) legacy["apiKey"] = "legacy-http-key" legacy["providerCredentials"]["openai-compatible"]["apiKey"] = "legacy-http-key" legacy["research"]["brave"] = {"apiKey": "BSA-legacy-http-brave-1234"} (root / "content/ai-secrets.json").unlink(missing_ok=True) write_json(root / "content/ai-settings.json", legacy) status, payload = load(editor) assert "legacy-http-key" in (root / "content/ai-settings.json").read_text(), "an editor loading the settings moves nothing" status, payload = load(admin) assert status == 200 and payload["success"] and payload["data"]["settings"]["hasApiKey"] and payload["data"]["settings"]["research"]["hasBraveApiKey"], payload moved = (root / "content/ai-settings.json").read_text() assert "legacy-http-key" not in moved and "BSA-legacy-http" not in moved and '"apiKey"' not in moved, "the settings file holds no key afterwards" kept = json.loads((root / "content/ai-secrets.json").read_text()) assert kept["providerCredentials"]["openai-compatible"]["apiKey"] == "legacy-http-key" and kept["research"]["brave"]["apiKey"] == "BSA-legacy-http-brave-1234", kept assert set(kept) == {"providerCredentials", "research"}, "only keys are in the secrets file: " + json.dumps(list(kept)) print("PASS agent settings over HTTP: load and save, keys never returned and kept, clear flags, refusals, admin only, CSRF, cache, system status, form markup") finally: site.close()