Add the trust and safety subsystem and the App Store compliance work

Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.

Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.

Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.

Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.

Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.

Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.

Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
This commit is contained in:
2026-08-09 00:18:20 +02:00
parent 68c2bbe387
commit 8e9d3fad98
348 changed files with 10633 additions and 238 deletions
+1
View File
@@ -21,6 +21,7 @@ def _cli_user(role="Member"):
{
"uid": generate_uid(),
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"api_key": generate_uid(),
"role": role,
+69
View File
@@ -36,6 +36,7 @@ def env(tmp_path, monkeypatch):
{
"uid": user["uid"],
"username": user["username"],
"terms_version": "1",
"email": "ctestadmin@example.com",
"api_key": generate_uid(),
"password_hash": "",
@@ -296,6 +297,74 @@ def test_run_as_uid_rejects_unknown_user(env):
)
def _other_member(uid="ctest-u2", username="ctestmember"):
users = get_table("users")
if not users.find_one(uid=uid):
users.insert(
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@example.com",
"api_key": generate_uid(),
"password_hash": "",
"role": "Member",
"is_active": True,
"created_at": "2020-01-01T00:00:00+00:00",
}
)
refresh_snapshot()
return uid
def test_run_as_another_member_needs_their_credential_consent(env):
from devplacepy.database import set_consent
other = _other_member()
set_consent("user", other, "container_credentials", False)
with pytest.raises(api.ContainerError) as refused:
run_async(
api.create_instance(
env["project"],
name="noconsent",
run_as_uid=other,
autostart=False,
actor=("user", env["user"]["uid"]),
)
)
assert "consent" in str(refused.value).lower()
def test_run_as_another_member_is_allowed_once_they_consent(env):
from devplacepy.database import set_consent
other = _other_member()
set_consent("user", other, "container_credentials", True)
instance = run_async(
api.create_instance(
env["project"],
name="withconsent",
run_as_uid=other,
autostart=False,
actor=("user", env["user"]["uid"]),
)
)
assert instance["run_as_uid"] == other
def test_run_as_yourself_never_needs_a_consent(env):
instance = run_async(
api.create_instance(
env["project"],
name="selfrunas",
run_as_uid=env["user"]["uid"],
autostart=False,
actor=("user", env["user"]["uid"]),
)
)
assert instance["run_as_uid"] == env["user"]["uid"]
def test_start_on_boot_pass_forces_running(env):
inst = _ready_instance(env, name="boot", start_on_boot=True, autostart=False)
assert inst["desired_state"] == store.DESIRED_STOPPED
+1
View File
@@ -17,6 +17,7 @@ def _seed_dbapi_schema(local_db):
{
"uid": generate_uid(),
"username": "dbapi_seed",
"terms_version": "1",
"email": "seed@dbapi.test",
"role": "Member",
"xp": 0,
+1
View File
@@ -52,6 +52,7 @@ def _make_user_devii_quota(role="Member"):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"api_key": generate_uid(),
"role": role,
@@ -20,6 +20,7 @@ def _make_user_at(role, created_at):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"role": role,
"created_at": created_at.isoformat(),
+2
View File
@@ -87,6 +87,7 @@ def test_scheduled_tasks_only_ever_resolve_a_main_channel_session(local_db):
{
"uid": "sess-docs-sched",
"username": "sched-owner",
"terms_version": "1",
"role": "Admin",
"api_key": "k",
"deleted_at": None,
@@ -144,6 +145,7 @@ def _task_session(local_db, uid, role):
{
"uid": uid,
"username": uid,
"terms_version": "1",
"role": role,
"api_key": "k",
"deleted_at": None,
@@ -17,6 +17,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"ctx-{uid[-10:]}",
"terms_version": "1",
"role": role,
"deleted_at": None,
"created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
@@ -33,6 +33,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"guard-{uid[-10:]}",
"terms_version": "1",
"role": role,
"deleted_at": None,
"created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
@@ -16,6 +16,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"lim-{uid[-10:]}",
"terms_version": "1",
"role": role,
"deleted_at": None,
"created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
@@ -28,6 +28,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"sched-{uid[-10:]}",
"terms_version": "1",
"role": role,
"api_key": "k",
"deleted_at": None,
+1
View File
@@ -26,6 +26,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"store-{uid[-10:]}",
"terms_version": "1",
"role": role,
"deleted_at": None,
"created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
+1
View File
@@ -17,6 +17,7 @@ def _user(username):
{
"uid": f"uid-{username}",
"username": username,
"terms_version": "1",
"role": "Member",
"created_at": datetime.now(timezone.utc).isoformat(),
}
+1
View File
@@ -98,6 +98,7 @@ def _make_user_issues_gitea():
{
"uid": uid,
"username": username,
"terms_version": "1",
"role": "member",
"xp": 0,
"level": 1,
@@ -96,6 +96,7 @@ def _user(prefix):
{
"uid": uid,
"username": f"{prefix}_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@t.dev",
"api_key": generate_uid(),
"created_at": datetime.now(timezone.utc).isoformat(),
+1
View File
@@ -13,6 +13,7 @@ def _user_uid():
{
"uid": uid,
"username": f"tkt_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@tkt.dev",
"password_hash": "x",
"role": "Member",
+131
View File
@@ -0,0 +1,131 @@
# retoor <retoor@molodetz.nl>
import pytest
from devplacepy.services.moderation import filter as content_filter
from devplacepy.services.moderation.rules import (
ALWAYS_BLOCK_CATEGORIES,
FILTER_MODES,
RULES,
matches,
score_for,
)
MODE_ORDER = content_filter.MODE_ORDER
VERDICT_ORDER = content_filter.VERDICT_ORDER
DEVELOPER_PROSE = [
"Here is a proof of concept for CVE-2024-1234, the exploit overwrites the stack.",
"The kernel panic dump shows a segmentation fault in the parser.",
"Reverse engineering this malware sample took two evenings of disassembly.",
"I hate this framework and everyone who defends it should read the source.",
"Traceback (most recent call last): RuntimeError: process was killed",
"Our red team ran a penetration test against the staging cluster.",
"This weapon of a regex kills my CPU: (a+)+$",
"The build failed with 'fatal error: cannot execute'.",
]
PROHIBITED_PROSE = [
("i will kill you when i find you", "violence"),
("go kill yourself, nobody wants you here", "harassment"),
("selling stolen credit cards and bank logs here", "illegal"),
("how to build a pipe bomb at home", "weapons"),
]
def test_every_rule_has_a_positive_weight_and_a_known_category():
from devplacepy.database.moderation import REPORT_REASONS
for rule in RULES:
assert rule.weight > 0
assert rule.category in REPORT_REASONS
@pytest.mark.parametrize("text", DEVELOPER_PROSE)
def test_developer_prose_is_never_flagged(text, local_db):
result = content_filter.classify(text, "review")
assert result.verdict == "allow", f"{text!r} -> {result}"
@pytest.mark.parametrize("text,category", PROHIBITED_PROSE)
def test_prohibited_prose_is_flagged_with_its_category(text, category, local_db):
result = content_filter.classify(text, "review")
assert result.flagged
assert category in result.categories
def test_the_always_block_categories_block_in_every_active_mode(local_db):
text = "selling child porn here"
for mode in ("review", "block"):
assert content_filter.classify(text, mode).verdict == "block"
assert content_filter.classify(text, "off").verdict == "allow"
def test_score_is_monotonic_in_the_number_of_matched_rules():
previous = 0
accumulated = ""
for text, _ in PROHIBITED_PROSE:
accumulated = f"{accumulated}\n{text}"
current = score_for(accumulated, matches(accumulated))
assert current >= previous
previous = current
def test_technical_context_only_ever_lowers_the_score():
for text, _ in PROHIBITED_PROSE:
plain = score_for(text, matches(text))
technical = f"{text}\nThis is a malware analysis write-up."
discounted = score_for(technical, matches(technical))
assert discounted <= plain
def test_a_verdict_never_weakens_as_the_mode_strengthens():
samples = DEVELOPER_PROSE + [text for text, _ in PROHIBITED_PROSE]
for text in samples:
previous = -1
for mode in sorted(FILTER_MODES, key=lambda name: MODE_ORDER[name]):
current = VERDICT_ORDER[content_filter.classify(text, mode).verdict]
assert current >= previous, f"{text!r} weakened at mode {mode}"
previous = current
def test_resolve_verdict_is_total_over_every_mode_and_verdict():
for mode in FILTER_MODES:
for verdict in VERDICT_ORDER:
resolved = content_filter.resolve_verdict(verdict, mode)
assert resolved in VERDICT_ORDER
def test_classification_failure_falls_back_to_review(monkeypatch, local_db):
def explode(text):
raise RuntimeError("rule engine unavailable")
monkeypatch.setattr(content_filter, "matches", explode)
result = content_filter.classify("anything at all", "review")
assert result.verdict == "review"
assert result.failed is True
assert result.detail
def test_empty_and_blank_text_is_allowed(local_db):
for text in ("", " ", "\n\t"):
assert content_filter.classify(text, "block").verdict == "allow"
def test_screen_reports_the_worst_field_and_names_every_flagged_one(local_db):
screening = content_filter.screen(
{
"title": "a normal title",
"content": "i will kill you when i find you",
"extra": "buy now 100% free money",
},
"review",
)
assert screening.flagged
assert "content" in screening.fields
assert "extra" in screening.fields
assert "title" not in screening.fields
def test_always_block_categories_are_a_subset_of_the_rule_categories():
assert ALWAYS_BLOCK_CATEGORIES <= {rule.category for rule in RULES}
@@ -26,6 +26,7 @@ def _make_user_ai_usage_profile(role="Member"):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"api_key": api_key,
"role": role,
@@ -83,6 +83,7 @@ def _make_admin_openai_gateway(role="Admin"):
{
"uid": generate_uid(),
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"api_key": api_key,
"role": role,
+32 -1
View File
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
import uuid_utils
from devplacepy.config import PRESENCE_TIMEOUT_SECONDS, PRESENCE_WRITE_SECONDS
from devplacepy.database import get_table
from devplacepy.database import get_table, set_consent
from devplacepy.services import presence
@@ -57,10 +57,12 @@ def test_touch_writes_once_then_throttles(local_db):
{
"uid": uid,
"username": f"presence_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@presence.test",
"last_seen": None,
}
)
set_consent("user", uid, "activity_recording", True)
presence._last_write.pop(uid, None)
presence.touch(uid)
@@ -182,3 +184,32 @@ def test_online_candidates_tracks_beyond_the_display_limit(local_db):
made.append(uid)
tracked = {u["uid"] for u in presence.online_candidates()}
assert set(made) <= tracked
def test_touch_is_silent_without_activity_recording_consent(local_db):
uid = str(uuid_utils.uuid7())
users = get_table("users")
users.insert(
{
"uid": uid,
"username": f"presence_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@presence.test",
"last_seen": None,
}
)
presence._last_write.pop(uid, None)
presence.touch(uid)
assert users.find_one(uid=uid)["last_seen"] is None
set_consent("user", uid, "activity_recording", True)
presence._last_write.pop(uid, None)
presence.touch(uid)
assert users.find_one(uid=uid)["last_seen"]
set_consent("user", uid, "activity_recording", False)
presence._last_write.pop(uid, None)
written = users.find_one(uid=uid)["last_seen"]
presence.touch(uid)
assert users.find_one(uid=uid)["last_seen"] == written
+1
View File
@@ -44,6 +44,7 @@ def _user(prefix):
{
"uid": uid,
"username": f"{prefix}{_counter[0]}",
"terms_version": "1",
"email": f"{prefix}{_counter[0]}@t.dev",
"role": "Member",
"api_key": generate_uid(),
+1
View File
@@ -16,6 +16,7 @@ def _user(prefix):
{
"uid": uid,
"username": f"{prefix}{_counter[0]}",
"terms_version": "1",
"email": f"{prefix}{_counter[0]}@t.dev",
"role": "Member",
"api_key": generate_uid(),
+1
View File
@@ -21,6 +21,7 @@ def _user(prefix):
{
"uid": uid,
"username": f"{prefix}{_counter[0]}",
"terms_version": "1",
"email": f"{prefix}{_counter[0]}@t.dev",
"role": "Member",
"api_key": generate_uid(),