Add community node protocol spec, Agent Skills, and make the failing tests hermetic
DevPlace CI / test (push) Failing after 31m42s

Community nodes (Phase 1 of plan.md): the protocol specification in
services/nodes/spec.py as the single source of truth, GET /nodes/spec.json,
a generated protocol docs page, the Community nodes API docs group, and the
implementation plan plus the residential proxy research. The no-open-proxy rule
is enforced in the spec (open_proxy cannot be set).

Agent Skills: register the Skills API docs group, cascade skills on account
deletion, add the nested CLAUDE.md, README and routes docs, and cover the
parser, the fetcher (end to end against a mock origin), the data layer and the
routes with 73 unit and 16 api tests.

Tests made hermetic with the repo's existing patterns instead of ambient
environment: embeddings tests set OPENROUTER_API_KEY via monkeypatch, the
workspace tests install FakeBackend via runtime.set_backend, and the skills
install tests use run_async. DockerCliBackend now returns 127 (command not
found) when the CLI is missing instead of raising FileNotFoundError, so a host
without docker answers with a clean error rather than a 500. The Providers tab
empty state no longer names the Model routes tab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 07:13:25 +02:00
co-authored by Claude Sonnet 5.5
parent cae139ead9
commit f55c7ee5f4
102 changed files with 7414 additions and 65 deletions
+31
View File
@@ -194,3 +194,34 @@ def test_provider_edit_page_404_for_missing_provider(seeded_db):
admin.post(f"{BASE_URL}/admin/gateway/providers/{missing}/delete").status_code
== 404
)
def test_provider_protocol_round_trip(seeded_db):
admin = admin_session(seeded_db)
name = _unique_gateway("protop").lower()
created = admin.post(
f"{BASE_URL}/admin/gateway/providers",
json={
"name": name,
"base_url": "https://api.meta.ai/v1/responses",
"api_key": "sk-test",
"is_active": True,
"protocol": "responses",
},
)
assert created.status_code == 200, created.text[:300]
assert created.json()["provider"]["protocol"] == "responses"
listed = admin.get(f"{BASE_URL}/admin/gateway/providers").json()
match = next((p for p in listed["providers"] if p["name"] == name), None)
assert match is not None
assert match["protocol"] == "responses"
bad = admin.post(
f"{BASE_URL}/admin/gateway/providers",
json={"name": _unique_gateway("protobad").lower(), "protocol": "soap"},
)
assert bad.status_code in (400, 422)
assert admin.delete(f"{BASE_URL}/admin/gateway/providers/{name}").status_code == 200
+47
View File
@@ -225,3 +225,50 @@ def test_an_explicitly_disabled_account_still_cannot_log_in(seeded_db):
allow_redirects=False,
)
assert response.status_code == 401
def test_login_json_returns_api_key_and_member_flag(seeded_db):
_, name = _member()
row = _db_user(name)
response = requests.post(
f"{BASE_URL}/auth/login",
headers=JSON_audit_log,
data={"email": f"{name}@t.dev", "password": "secret123"},
allow_redirects=False,
)
assert response.status_code == 200, response.text[:300]
body = response.json()
assert body["ok"] is True
assert body["data"]["username"] == name
assert body["data"]["email"] == f"{name}@t.dev"
assert body["data"]["api_key"] == row["api_key"]
assert body["data"]["is_admin"] is False
def test_login_json_returns_api_key_and_admin_flag(seeded_db):
response = requests.post(
f"{BASE_URL}/auth/login",
headers=JSON_audit_log,
data={
"email": seeded_db["alice"]["email"],
"password": seeded_db["alice"]["password"],
},
allow_redirects=False,
)
assert response.status_code == 200, response.text[:300]
body = response.json()
assert body["data"]["username"] == "alice_test"
assert body["data"]["email"] == seeded_db["alice"]["email"]
assert body["data"]["api_key"] == _db_user("alice_test")["api_key"]
assert body["data"]["is_admin"] is True
def test_login_failure_json_leaks_no_key(app_server):
response = requests.post(
f"{BASE_URL}/auth/login",
headers=JSON_audit_log,
data={"email": f"{_unique('aunokey')}@t.dev", "password": "wrongpassword"},
allow_redirects=False,
)
assert response.status_code == 401
assert "api_key" not in response.text
+38
View File
@@ -36,3 +36,41 @@ def test_signup_short_username_rerenders_with_message(app_server):
)
assert r.status_code == 400
assert "Username must be between 3 and 32 characters" in r.text
def test_signup_json_returns_api_key_and_admin_flag(seeded_db):
from devplacepy.database import get_table, refresh_snapshot
name = f"sgk{int(time.time() * 1000000)}"
r = requests.post(
f"{BASE_URL}/auth/signup",
headers={"Accept": "application/json"},
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=False,
)
assert r.status_code == 200, r.text[:300]
body = r.json()
assert body["ok"] is True
assert body["data"]["username"] == name
assert body["data"]["email"] == f"{name}@t.dev"
assert body["data"]["is_admin"] is False
refresh_snapshot()
row = get_table("users").find_one(username=name)
assert row is not None
assert body["data"]["api_key"] == row["api_key"]
me = requests.get(
f"{BASE_URL}/profile",
headers={
"Accept": "application/json",
"X-API-KEY": body["data"]["api_key"],
},
)
assert me.status_code == 200
assert me.json()["profile_user"]["username"] == name
+1
View File
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
+39
View File
@@ -0,0 +1,39 @@
# retoor <retoor@molodetz.nl>
import requests
from devplacepy.services.nodes.spec import SPEC
from tests.conftest import BASE_URL
def test_spec_json_is_public_and_matches_the_spec(app_server):
response = requests.get(f"{BASE_URL}/nodes/spec.json")
assert response.status_code == 200
assert response.headers["content-type"].startswith("application/json")
assert response.json() == SPEC.describe()
def test_spec_json_never_describes_an_open_proxy(app_server):
body = requests.get(f"{BASE_URL}/nodes/spec.json").json()
assert body["open_proxy"] is False
assert body["allowed_ports"] == [80, 443]
def test_protocol_docs_page_renders_every_spec_value(app_server):
response = requests.get(f"{BASE_URL}/docs/nodes-protocol.html")
assert response.status_code == 200
page = response.text
for frame in SPEC.frames:
assert f"<code>{frame.type}</code>" in page
for destination in SPEC.destinations:
assert destination.suffix in page
for refusal in SPEC.refusals:
assert refusal.code in page
for name in SPEC.limits.describe():
assert name in page
def test_nodes_api_docs_group_lists_the_spec_endpoint(app_server):
response = requests.get(f"{BASE_URL}/docs/nodes.html")
assert response.status_code == 200
assert "/nodes/spec.json" in response.text
+55
View File
@@ -707,4 +707,59 @@ def test_the_owner_gets_the_privacy_controls(app_server):
assert "Only the account holder can change this." not in html
def _profile_json_body(session, username):
return session.get(
f"{BASE_URL}/profile/{username}",
headers={"Accept": "application/json"},
).json()
def test_owner_sees_own_key_and_member_flag(app_server, seeded_db):
from devplacepy.database import get_table, refresh_snapshot
session, name = _privacy_member("keyo")
refresh_snapshot()
expected = get_table("users").find_one(username=name)["api_key"]
body = _profile_json_body(session, name)
assert body["profile_is_admin"] is False
assert body["viewer_is_admin"] is False
assert body["is_owner"] is True
assert body["can_view_api_key"] is True
assert body["api_key"] == expected
def test_admin_sees_member_key_and_flags(app_server, seeded_db):
from devplacepy.database import get_table, refresh_snapshot
_, name = _privacy_member("keya")
refresh_snapshot()
expected = get_table("users").find_one(username=name)["api_key"]
body = _profile_json_body(_admin_session(), name)
assert body["profile_is_admin"] is False
assert body["viewer_is_admin"] is True
assert body["is_owner"] is False
assert body["can_view_api_key"] is True
assert body["api_key"] == expected
def test_admin_profile_marks_admin_flag_for_guest(app_server, seeded_db):
body = requests.get(
f"{BASE_URL}/profile/alice_test",
headers={"Accept": "application/json"},
).json()
assert body["profile_is_admin"] is True
assert body["viewer_is_admin"] is False
assert body["can_view_api_key"] is False
assert body["api_key"] is None
def test_stranger_gets_no_key(app_server, seeded_db):
_, name = _privacy_member("keys")
stranger, _ = _privacy_member("keyv")
body = _profile_json_body(stranger, name)
assert body["profile_is_admin"] is False
assert body["can_view_api_key"] is False
assert body["api_key"] is None
+4 -1
View File
@@ -7,7 +7,8 @@ import pytest
from devplacepy.content import can_manage_workspace, can_open_workspace
from devplacepy.database import get_table, init_db, set_setting
from devplacepy.services.containers import activity, api, store
from devplacepy.services.containers import activity, api, runtime, store
from devplacepy.services.containers.backend.fake import FakeBackend
from devplacepy.services.containers.workspace import (
editor,
flags,
@@ -29,7 +30,9 @@ HTTP_TIMEOUT = 30
def _workspace_db():
init_db()
set_setting("workspace_enabled", "1")
runtime.set_backend(FakeBackend())
yield
runtime.set_backend(None)
for table in (
"instances",
"tunnels",
+1
View File
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
+185
View File
@@ -0,0 +1,185 @@
# retoor <retoor@molodetz.nl>
import json
import uuid
import requests
from devplacepy.database import get_table
from devplacepy.database.skills import get_skill_row, upsert_skill
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
def _user_skills(name):
return get_table("users").find_one(username=name)
def _member_skills():
name = f"skl{uuid.uuid4().hex[:10]}"
requests.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
row = _user_skills(name)
session = requests.Session()
session.headers.update({"X-API-KEY": row["api_key"], **JSON})
return session, row
def _admin_skills(seeded_db):
session = requests.Session()
session.headers.update({"X-API-KEY": _user_skills("alice_test")["api_key"], **JSON})
return session
def _seed_skills(user_uid, name, description="Does a thing."):
row, _ = upsert_skill(
user_uid,
{
"name": name,
"description": description,
"body": "# Body\n\nSteps.",
"files": json.dumps({"scripts/run.py": "print(1)"}),
"source_url": f"https://example.org/{name}/SKILL.md",
"license": "",
"compatibility": "",
"metadata": json.dumps({}),
"allowed_tools": "",
"sha256": "0" * 64,
"skipped": json.dumps([]),
"warnings": json.dumps([]),
},
)
return row
def test_skills_require_a_signed_in_user(app_server):
assert requests.get(f"{BASE_URL}/skills", headers=JSON, allow_redirects=False).status_code == 401
assert requests.post(
f"{BASE_URL}/skills/install",
data={"url": "https://example.org"},
headers=JSON,
allow_redirects=False,
).status_code == 401
assert requests.get(f"{BASE_URL}/skills/anything", headers=JSON, allow_redirects=False).status_code == 401
def test_list_and_detail_json(app_server):
session, user = _member_skills()
_seed_skills(user["uid"], "listed", "Listed skill.")
listing = session.get(f"{BASE_URL}/skills").json()
assert listing["skill_total"] == 1
assert listing["skill_limit"] == 50
assert [item["name"] for item in listing["items"]] == ["listed"]
assert "body" not in listing["items"][0]
detail = session.get(f"{BASE_URL}/skills/listed").json()["skill"]
assert detail["body"] == "# Body\n\nSteps."
assert detail["files"] == {"scripts/run.py": "print(1)"}
def test_html_pages_render_and_are_noindex(app_server):
session, user = _member_skills()
_seed_skills(user["uid"], "paged", "Paged skill.")
html_headers = {"Accept": "text/html"}
listing = session.get(f"{BASE_URL}/skills", headers=html_headers)
assert listing.status_code == 200
assert "paged" in listing.text
assert "noindex" in listing.text
detail = session.get(f"{BASE_URL}/skills/paged", headers=html_headers)
assert detail.status_code == 200
assert "noindex" in detail.text
def test_skills_are_invisible_to_other_users(app_server):
owner_session, owner = _member_skills()
other_session, _ = _member_skills()
_seed_skills(owner["uid"], "private-one")
assert other_session.get(f"{BASE_URL}/skills/private-one").status_code == 404
assert other_session.get(f"{BASE_URL}/skills").json()["items"] == []
assert other_session.post(f"{BASE_URL}/skills/private-one/enabled", data={"enabled": "false"}).status_code == 404
assert other_session.post(f"{BASE_URL}/skills/private-one/delete").status_code == 404
assert get_skill_row(owner["uid"], "private-one") is not None
def test_toggle_flips_and_sets_explicitly(app_server):
session, user = _member_skills()
_seed_skills(user["uid"], "toggled")
assert session.post(f"{BASE_URL}/skills/toggled/enabled").json() == {"name": "toggled", "enabled": False}
assert session.post(f"{BASE_URL}/skills/toggled/enabled").json() == {"name": "toggled", "enabled": True}
assert session.post(f"{BASE_URL}/skills/toggled/enabled", data={"enabled": "false"}).json()["enabled"] is False
assert get_skill_row(user["uid"], "toggled")["enabled"] == 0
assert session.post(f"{BASE_URL}/skills/nope/enabled").status_code == 404
def test_delete_is_soft_and_audited(app_server):
session, user = _member_skills()
row = _seed_skills(user["uid"], "doomed")
assert session.post(f"{BASE_URL}/skills/doomed/delete").json() == {"deleted": True, "name": "doomed"}
assert get_skill_row(user["uid"], "doomed") is None
physical = get_table("skills").find_one(uid=row["uid"])
assert physical["deleted_at"] is not None and physical["deleted_by"] == user["uid"]
assert session.get(f"{BASE_URL}/skills/doomed").status_code == 404
keys = {event["event_key"] for event in get_table("audit_log").find(target_uid=row["uid"])}
assert "skill.delete" in keys
def test_a_member_cannot_delete_another_users_skill(app_server):
member, _ = _member_skills()
_, owner = _member_skills()
_seed_skills(owner["uid"], "protected")
response = member.post(f"{BASE_URL}/skills/protected/delete", data={"owner": owner["username"]})
assert response.status_code == 403
assert get_skill_row(owner["uid"], "protected") is not None
def test_an_administrator_can_delete_another_users_skill(seeded_db):
_, owner = _member_skills()
_seed_skills(owner["uid"], "admin-removed")
admin = _admin_skills(seeded_db)
assert admin.post(
f"{BASE_URL}/skills/admin-removed/delete", data={"owner": owner["username"]}
).status_code == 200
assert get_skill_row(owner["uid"], "admin-removed") is None
missing = admin.post(f"{BASE_URL}/skills/x/delete", data={"owner": "no_such_user_xyz"})
assert missing.status_code == 404
def test_install_failures_are_400_with_a_reason(app_server):
session, _ = _member_skills()
for url in (
"ftp://example.org/SKILL.md",
"https://user:pw@example.org/SKILL.md",
"http://127.0.0.1:1/SKILL.md",
"http://169.254.169.254/SKILL.md",
):
response = session.post(f"{BASE_URL}/skills/install", data={"url": url})
assert response.status_code == 400, url
assert response.json()["error"]["message"], url
assert session.get(f"{BASE_URL}/skills").json()["skill_total"] == 0
def test_install_failure_from_a_browser_redirects_with_the_error(app_server):
session, _ = _member_skills()
response = session.post(
f"{BASE_URL}/skills/install",
data={"url": "ftp://example.org/SKILL.md"},
headers={"Accept": "text/html"},
allow_redirects=False,
)
assert response.status_code == 302
assert response.headers["location"].startswith("/skills?error=")
def test_install_validates_the_form(app_server):
session, _ = _member_skills()
assert session.post(f"{BASE_URL}/skills/install", data={"url": ""}).status_code in (400, 422)
+157
View File
@@ -0,0 +1,157 @@
# retoor <retoor@molodetz.nl>
import uuid
import pytest
import requests
from starlette.testclient import TestClient
import devplacepy.main as m
from devplacepy.database import get_table
from devplacepy.database.skills import get_skill_row
from devplacepy.routers import skills as skills_router
from devplacepy.services.skills import limits
from devplacepy.services.skills.fetcher import FetchedSkill, SkillFetchError, SkippedFile
from devplacepy.services.skills.parser import parse_skill_md
from devplacepy.services.skills.prompt import skill_index_lines
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
def _signup_install():
name = f"ins{uuid.uuid4().hex[:10]}"
requests.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
return get_table("users").find_one(username=name)
def _fetched(name="installed-skill", description="Installed through the route."):
parsed = parse_skill_md(f"---\nname: {name}\ndescription: {description}\n---\n# Body\n\n`scripts/run.py`")
return FetchedSkill(
parsed=parsed,
source_url=f"https://skills.example/{name}/SKILL.md",
files={"scripts/run.py": "print(1)"},
skipped=[SkippedFile("assets/logo.png", "binary or unsupported file type; only text files are stored")],
warnings=["a warning"],
)
@pytest.fixture
def client(app_server):
return TestClient(m.app)
def _headers(user):
return {"X-API-KEY": user["api_key"], **JSON}
def test_install_through_the_route_stores_audits_and_feeds_the_prompt(client, monkeypatch):
user = _signup_install()
async def fake_fetch(url):
return _fetched()
monkeypatch.setattr(skills_router, "fetch_skill", fake_fetch)
response = client.post("/skills/install", data={"url": "https://skills.example/x"}, headers=_headers(user))
assert response.status_code == 200
body = response.json()
assert body["action"] == "install"
skill = body["skill"]
assert skill["name"] == "installed-skill"
assert skill["files"] == {"scripts/run.py": "print(1)"}
assert skill["skipped"] == [
{"path": "assets/logo.png", "reason": "binary or unsupported file type; only text files are stored"}
]
assert skill["warnings"] == ["a warning"]
assert len(skill["sha256"]) == 64
assert get_skill_row(user["uid"], "installed-skill")["enabled"] == 1
assert "installed-skill: Installed through the route." in skill_index_lines(user["uid"])
audit = [e for e in get_table("audit_log").find(target_uid=skill["uid"]) if e["event_key"] == "skill.install"]
assert len(audit) == 1 and audit[0]["result"] == "success"
def test_reinstalling_updates_in_place_and_keeps_the_uid(client, monkeypatch):
user = _signup_install()
calls = {"description": "First."}
async def fake_fetch(url):
return _fetched(description=calls["description"])
monkeypatch.setattr(skills_router, "fetch_skill", fake_fetch)
first = client.post("/skills/install", data={"url": "https://skills.example/x"}, headers=_headers(user)).json()
calls["description"] = "Second."
second = client.post("/skills/install", data={"url": "https://skills.example/x"}, headers=_headers(user)).json()
assert first["action"] == "install" and second["action"] == "update"
assert first["skill"]["uid"] == second["skill"]["uid"]
assert second["skill"]["description"] == "Second."
listing = client.get("/skills", headers=_headers(user)).json()
assert listing["skill_total"] == 1
keys = [e["event_key"] for e in get_table("audit_log").find(target_uid=first["skill"]["uid"])]
assert "skill.install" in keys and "skill.update" in keys
def test_a_failed_fetch_is_audited_as_a_failure_and_stores_nothing(client, monkeypatch):
user = _signup_install()
async def failing_fetch(url):
raise SkillFetchError("the server returned 500")
monkeypatch.setattr(skills_router, "fetch_skill", failing_fetch)
response = client.post("/skills/install", data={"url": "https://skills.example/x"}, headers=_headers(user))
assert response.status_code == 400
assert response.json()["error"]["message"] == "the server returned 500"
assert client.get("/skills", headers=_headers(user)).json()["skill_total"] == 0
failures = [
e
for e in get_table("audit_log").find(actor_uid=user["uid"])
if e["event_key"] == "skill.install" and e["result"] == "failure"
]
assert len(failures) == 1
def test_the_per_user_cap_refuses_a_new_install_but_allows_an_update(client, monkeypatch):
user = _signup_install()
current = {"name": "cap-0"}
async def fake_fetch(url):
return _fetched(name=current["name"])
monkeypatch.setattr(skills_router, "fetch_skill", fake_fetch)
for index in range(limits.SKILLS_PER_USER_MAX):
current["name"] = f"cap-{index}"
assert client.post("/skills/install", data={"url": "https://s.example/x"}, headers=_headers(user)).status_code == 200
current["name"] = "one-too-many"
refused = client.post("/skills/install", data={"url": "https://s.example/x"}, headers=_headers(user))
assert refused.status_code == 400
assert "at most" in refused.json()["error"]["message"]
current["name"] = "cap-0"
assert client.post("/skills/install", data={"url": "https://s.example/x"}, headers=_headers(user)).json()["action"] == "update"
def test_a_browser_install_redirects_to_the_list(client, monkeypatch):
user = _signup_install()
async def fake_fetch(url):
return _fetched(name="browser-skill")
monkeypatch.setattr(skills_router, "fetch_skill", fake_fetch)
response = client.post(
"/skills/install",
data={"url": "https://skills.example/x"},
headers={"X-API-KEY": user["api_key"], "Accept": "text/html"},
follow_redirects=False,
)
assert response.status_code == 302
assert response.headers["location"] == "/skills"
assert get_skill_row(user["uid"], "browser-skill") is not None
+34
View File
@@ -0,0 +1,34 @@
# retoor <retoor@molodetz.nl>
import requests
from tests.api.uploads.uploadurl import _key, _upload_settings # noqa: F401
from tests.conftest import BASE_URL
def test_generate_image_requires_auth():
response = requests.post(f"{BASE_URL}/uploads/generate-image", data={"prompt": "a cat"})
assert response.status_code == 401
def test_generate_image_rejects_empty_prompt():
response = requests.post(
f"{BASE_URL}/uploads/generate-image",
headers={"X-API-KEY": _key()},
data={"prompt": ""},
allow_redirects=False,
)
assert response.status_code != 201
def test_generate_image_upstream_failure_is_a_clean_json_error():
response = requests.post(
f"{BASE_URL}/uploads/generate-image",
headers={"X-API-KEY": _key()},
data={"prompt": "a lighthouse at dusk"},
)
assert response.status_code in (201, 502)
body = response.json()
if response.status_code == 502:
assert body["error"]
else:
assert body["uid"]