feat: add ISSLOP AI usage analysis CLI commands, game router, and politics topic

- Add `cmd_isslop_prune`, `cmd_isslop_clear`, `cmd_isslop_analyze` CLI commands for AI usage analysis job management
- Register `/game` router with `index` and `farm` endpoints for Code Farm idle game
- Add `politics` to allowed TOPICS constant replacing `signals`
- Introduce `ISSLOP_DIR`, `ISSLOP_WORKSPACES_DIR`, `ISSLOP_RUNS_DIR`, `ISSLOP_MEDIA_DIR` config paths
- Add `clear_user_stars` and `clear_user_projects_cache` calls on vote and project create/delete
- Update `make prod` to use `nproc` workers via `DEVPLACE_WEB_WORKERS` env var
- Convert `database.py` and `utils.py` to packages for modular structure
- Add `devplace apikey` and `devplace token` CLI subcommands for API key and access token management
This commit is contained in:
2026-07-06 03:57:47 +00:00
parent f1bdefd834
commit 9a8046ab2a
110 changed files with 1466 additions and 374 deletions
@@ -108,4 +108,58 @@ TOOLS_ACTIONS: tuple[Action, ...] = (
params=(path("uid", "DeepSearch job uid returned by deepsearch."),),
requires_auth=False,
),
Action(
name="isslop",
method="POST",
path="/tools/isslop/run",
summary="Classify a repository or website as AI slop or human work",
description=(
"Queues a background AI Usage Analyzer job and returns {uid, status_url, report_url}. "
"Poll the status with isslop_status until status is 'completed', then share the "
"authenticity grade, category, human/AI split and report_url. Accepts http(s), "
"git and ssh source URLs."
),
params=(
body("url", "Repository or website URL to classify.", required=True),
),
requires_auth=True,
),
Action(
name="isslop_status",
method="GET",
path="/tools/isslop/{uid}",
summary="Check a AI usage analysis and obtain its verdict once finished",
description=(
"Returns the analysis status. When status is 'completed', grade, category, "
"human_percent, ai_percent and report_url are populated; while 'pending' or "
"'running', poll again shortly."
),
params=(path("uid", "Analysis uid returned by isslop."),),
requires_auth=True,
),
Action(
name="isslop_report",
method="GET",
path="/tools/isslop/{uid}/report",
summary="Read a finished AI usage analysis report",
description=(
"Returns the full report for a finished analysis: authenticity grade, human/AI "
"split, markdown findings, per-file scores with signals, the image review and the "
"embeddable badge snippets. Use it after isslop_status reports status 'completed'."
),
params=(path("uid", "Analysis uid returned by isslop."),),
requires_auth=True,
),
Action(
name="isslop_list",
method="GET",
path="/tools/isslop/list",
summary="List the user's AI usage analyses",
description=(
"Returns the signed-in user's analysis history, newest first, each with its grade, "
"category, status and report_url."
),
params=(),
requires_auth=True,
),
)
@@ -55,7 +55,7 @@ CONTAINER_ACTIONS: tuple[Action, ...] = (
),
arg(
"run_as_uid",
"Optional DevPlace user uid whose identity and API key are injected (PRAVDA_API_KEY, PRAVDA_USER_UID). Does NOT change the container OS user, which is always pravda (uid 1000).",
"Optional DevPlace user uid whose identity and API key are injected (DEVPLACE_API_KEY, DEVPLACE_USER_UID). Does NOT change the container OS user, which is always pravda (uid 1000).",
),
arg(
"start_on_boot",
@@ -118,7 +118,7 @@ CONTAINER_ACTIONS: tuple[Action, ...] = (
arg("instance", "Instance name, slug, or uid.", required=True),
arg(
"run_as_uid",
"DevPlace user uid whose identity and API key are injected (PRAVDA_API_KEY, PRAVDA_USER_UID); pass empty to clear. Does NOT change the container OS user (always pravda, uid 1000).",
"DevPlace user uid whose identity and API key are injected (DEVPLACE_API_KEY, DEVPLACE_USER_UID); pass empty to clear. Does NOT change the container OS user (always pravda, uid 1000).",
),
arg("boot_language", "Boot source language: 'none', 'python', or 'bash'."),
arg("boot_script", "Boot source code body run on launch."),
@@ -287,7 +287,7 @@ class Dispatcher:
self._rsearch = RsearchController(settings, owner_kind, owner_id)
from ..container import ContainerController
self._container = ContainerController(client)
self._container = ContainerController(client, owner_id=owner_id)
from ..customization import CustomizationController
self._customization = CustomizationController(owner_kind, owner_id)
@@ -4,7 +4,7 @@ import json
import logging
from typing import Any
from devplacepy.database import get_table, resolve_by_slug
from devplacepy.database import get_table, get_users_by_uids, resolve_by_slug
from devplacepy.services.containers import api, store
from devplacepy.services.containers.api import ContainerError
from devplacepy.services.containers.runtime import get_backend
@@ -15,8 +15,9 @@ logger = logging.getLogger("devii.container")
class ContainerController:
def __init__(self, client: Any = None) -> None:
def __init__(self, client: Any = None, owner_id: str = "") -> None:
self._client = client
self._owner_id = owner_id
def _ingress_url(self, instance: dict):
slug = instance.get("ingress_slug")
@@ -29,11 +30,19 @@ class ContainerController:
def _actor_user(self) -> dict:
username = getattr(self._client, "username", None)
if username:
if username and username != "api-key":
user = get_table("users").find_one(username=username)
if user:
return user
return {"uid": "admin", "username": username or "admin"}
if self._owner_id:
user = get_users_by_uids([self._owner_id]).get(self._owner_id)
if user:
return user
return {
"uid": "admin",
"username": username or "admin",
"role": "Admin",
}
def _project(self, arguments: dict) -> dict:
from devplacepy.content import can_view_project