feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
# retoor <retoor@molodetz.nl>
from __future__ import annotations
from . spec import Action , Catalog , Param
def path ( name : str , description : str , required : bool = True ) - > Param :
return Param ( name = name , location = " path " , description = description , required = required )
def query ( name : str , description : str , required : bool = False ) - > Param :
return Param ( name = name , location = " query " , description = description , required = required )
def body ( name : str , description : str , required : bool = False ) - > Param :
return Param ( name = name , location = " body " , description = description , required = required )
def upload ( name : str , description : str ) - > Param :
return Param ( name = name , location = " file " , description = description , required = True )
ATTACHMENTS = " Comma separated attachment uids returned by upload_file. "
TARGET_TYPE = " Target type, e.g. post, comment, project, gist. "
ACTIONS : tuple [ Action , . . . ] = (
Action (
name = " auth_status " ,
method = " GET " ,
path = " " ,
summary = " Report whether the current session is authenticated " ,
handler = " status " ,
requires_auth = False ,
) ,
Action (
name = " login " ,
method = " POST " ,
path = " /auth/login " ,
summary = " Authenticate with email and password and start a session " ,
handler = " login " ,
requires_auth = False ,
params = (
body ( " email " , " Account email address. " , required = True ) ,
body ( " password " , " Account password. " , required = True ) ,
body ( " remember_me " , " Keep the session alive longer ( ' on ' or ' ' ). " ) ,
) ,
) ,
Action (
name = " logout " ,
method = " GET " ,
path = " /auth/logout " ,
summary = " End the current session " ,
handler = " logout " ,
requires_auth = True ,
) ,
Action (
name = " signup " ,
method = " POST " ,
path = " /auth/signup " ,
summary = " Create a new account " ,
requires_auth = False ,
params = (
body ( " username " , " Desired username. " , required = True ) ,
body ( " email " , " Email address. " , required = True ) ,
body ( " password " , " Password (minimum six characters). " , required = True ) ,
body ( " confirm_password " , " Password confirmation. " , required = True ) ,
) ,
) ,
Action (
name = " forgot_password " ,
method = " POST " ,
path = " /auth/forgot-password " ,
summary = " Request a password reset email " ,
requires_auth = False ,
params = ( body ( " email " , " Account email address. " , required = True ) , ) ,
) ,
Action (
name = " reset_password " ,
method = " POST " ,
path = " /auth/reset-password/ {token} " ,
summary = " Reset a password using a reset token " ,
requires_auth = False ,
params = (
path ( " token " , " Reset token from the email link. " ) ,
body ( " password " , " New password. " , required = True ) ,
body ( " confirm_password " , " New password confirmation. " , required = True ) ,
) ,
) ,
Action (
name = " view_feed " ,
method = " GET " ,
path = " /feed " ,
summary = " View the activity feed " ,
params = (
query ( " tab " , " Feed tab to view. " ) ,
query ( " topic " , " Filter by topic. " ) ,
query ( " before " , " Pagination cursor. " ) ,
) ,
) ,
Action (
name = " create_post " ,
method = " POST " ,
path = " /posts/create " ,
summary = " Create a new post " ,
params = (
body ( " content " , " Post body text. " , required = True ) ,
body ( " title " , " Optional post title. " ) ,
body ( " topic " , " Optional topic. " ) ,
body ( " project_uid " , " Attach the post to a project uid. " ) ,
body ( " attachment_uids " , ATTACHMENTS ) ,
body ( " poll_question " , " Optional poll question. " ) ,
body ( " poll_options " , " Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created. " ) ,
) ,
) ,
Action (
name = " view_post " ,
method = " GET " ,
path = " /posts/ {post_slug} " ,
summary = " View a single post by slug " ,
params = ( path ( " post_slug " , " Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title. " ) , ) ,
) ,
Action (
name = " edit_post " ,
method = " POST " ,
path = " /posts/edit/ {post_slug} " ,
summary = " Edit an existing post " ,
params = (
path ( " post_slug " , " Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title. " ) ,
body ( " content " , " Updated post body. " , required = True ) ,
body ( " title " , " Updated title. " ) ,
body ( " topic " , " Updated topic. " ) ,
body ( " poll_question " , " Optional poll question. Adds a poll to a post that does not already have one. " ) ,
body ( " poll_options " , " Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created. " ) ,
) ,
) ,
Action (
name = " delete_post " ,
method = " POST " ,
path = " /posts/delete/ {post_slug} " ,
summary = " Delete a post " ,
params = ( path ( " post_slug " , " Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title. " ) , ) ,
) ,
Action (
name = " create_comment " ,
method = " POST " ,
path = " /comments/create " ,
summary = " Create a comment on a post or other target " ,
params = (
body ( " content " , " Comment body. " , required = True ) ,
body ( " post_uid " , " Uid of the post being commented on. " ) ,
body ( " target_uid " , " Uid of the target when not a post. " ) ,
body ( " target_type " , TARGET_TYPE ) ,
body ( " parent_uid " , " Parent comment uid for replies. " ) ,
body ( " attachment_uids " , ATTACHMENTS ) ,
) ,
) ,
Action (
name = " delete_comment " ,
method = " POST " ,
path = " /comments/delete/ {comment_uid} " ,
summary = " Delete a comment " ,
params = ( path ( " comment_uid " , " Uid of the comment. " ) , ) ,
) ,
Action (
name = " list_projects " ,
method = " GET " ,
path = " /projects " ,
summary = " List projects " ,
params = (
query ( " tab " , " Projects tab. " ) ,
query ( " search " , " Search query. " ) ,
query ( " user_uid " , " Filter by owner uid. " ) ,
query ( " project_type " , " Filter by project type. " ) ,
query ( " before " , " Pagination cursor. " ) ,
) ,
) ,
Action (
name = " view_project " ,
method = " GET " ,
path = " /projects/ {project_slug} " ,
summary = " View a project by slug " ,
params = ( path ( " project_slug " , " Exact project slug copied from a /projects/... link in a listing response; do not build it from the title. " ) , ) ,
) ,
Action (
name = " create_project " ,
method = " POST " ,
path = " /projects/create " ,
summary = " Create a new project " ,
params = (
body ( " title " , " Project title. " , required = True ) ,
body ( " description " , " Project description. " , required = True ) ,
body ( " release_date " , " Release date. " ) ,
body ( " demo_date " , " Demo date. " ) ,
body ( " project_type " , " Project type. " ) ,
body ( " platforms " , " Supported platforms. " ) ,
body ( " status " , " Project status. " ) ,
body ( " attachment_uids " , ATTACHMENTS ) ,
) ,
) ,
Action (
name = " delete_project " ,
method = " POST " ,
path = " /projects/delete/ {project_slug} " ,
summary = " Delete a project " ,
params = ( path ( " project_slug " , " Exact project slug copied from a /projects/... link in a listing response; do not build it from the title. " ) , ) ,
) ,
2026-06-09 06:41:27 +02:00
Action (
name = " project_set_private " ,
method = " POST " ,
path = " /projects/ {project_slug} /private " ,
summary = " Mark a project private (owner-only) or public " ,
description = (
" Set value=true to hide the project from everyone except its owner (and administrators), "
2026-06-09 16:06:02 +02:00
" or value=false to make it public again. This is a significant change: you MUST ask the "
" user for explicit confirmation BEFORE calling it, and only pass confirm=true once they "
" have agreed. Only the project owner may change this. "
2026-06-09 06:41:27 +02:00
) ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " value " , " true to make the project private, false to make it public. " , required = True ) ,
2026-06-09 16:06:02 +02:00
body ( " confirm " , " Must be true, set only after the user has explicitly confirmed. " , required = True ) ,
2026-06-09 06:41:27 +02:00
) ,
) ,
Action (
name = " project_set_readonly " ,
method = " POST " ,
path = " /projects/ {project_slug} /readonly " ,
summary = " Mark a project read-only (immutable files) or writable again " ,
description = (
" Set value=true to make every file in the project immutable - no writes, edits, line "
" edits, moves, deletes, or uploads succeed afterwards, from anyone including you - or "
" value=false to allow changes again. This is a significant change: you MUST ask the user "
" for explicit confirmation BEFORE calling it, and only pass confirm=true once they have "
" agreed. Only the project owner may change this. "
) ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " value " , " true to make the project read-only, false to make it writable. " , required = True ) ,
body ( " confirm " , " Must be true, set only after the user has explicitly confirmed. " , required = True ) ,
) ,
) ,
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
Action (
name = " project_list_files " ,
method = " GET " ,
path = " /projects/ {project_slug} /files " ,
summary = " List every file and directory in a project filesystem " ,
description = " Returns the flat list of nodes (path, type, size, mime). Use it to inspect the project tree before reading or writing files. " ,
params = ( path ( " project_slug " , " Project slug or uid. " ) , ) ,
requires_auth = False ,
) ,
Action (
name = " project_read_file " ,
method = " GET " ,
path = " /projects/ {project_slug} /files/raw " ,
summary = " Read one file from a project filesystem " ,
description = " Returns the file metadata plus the text content. Binary files return a url instead of content. " ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
query ( " path " , " Relative file path inside the project, e.g. src/main.py. " , required = True ) ,
) ,
requires_auth = False ,
) ,
Action (
name = " project_write_file " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/write " ,
2026-06-09 06:41:27 +02:00
summary = " Create or overwrite a whole text file in a project (parent directories are created automatically) " ,
description = (
" Replaces the ENTIRE file with the content you send. Creating a new file needs no prior "
" read, but overwriting an existing one requires reading it first (project_read_file). "
" For an existing file prefer the surgical line tools (project_replace_lines, "
" project_insert_lines, project_delete_lines, project_append_file) instead of rewriting it, "
" and never batch several writes in one turn. Use this only to create a new file or fully "
" rewrite a small one. Missing parent directories are created recursively. "
) ,
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " path " , " Relative file path, e.g. src/app/main.py. " , required = True ) ,
body ( " content " , " Full file content. " , required = True ) ,
) ,
) ,
2026-06-09 06:41:27 +02:00
Action (
name = " project_read_lines " ,
method = " GET " ,
path = " /projects/ {project_slug} /files/lines " ,
summary = " Read a 1-indexed line range of a text file in a project " ,
description = (
" Returns { path, start, end, total_lines, lines, content} for the requested range. Use it "
" to inspect part of a large file before editing, and to learn total_lines so you can target "
" the right range with the line-edit tools. "
) ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
query ( " path " , " Relative file path inside the project. " , required = True ) ,
query ( " start " , " First line to read (1-indexed, default 1). " ) ,
query ( " end " , " Last line to read (inclusive); omit for end of file. " ) ,
) ,
requires_auth = False ,
) ,
Action (
name = " project_replace_lines " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/replace-lines " ,
summary = " Replace an inclusive 1-indexed line range of a text file with new content " ,
description = (
" Surgically rewrites lines start..end (inclusive) with content (which may be any number of "
" lines, or empty to delete the range). The preferred way to edit an existing file: it leaves "
" the rest of the file untouched and never overflows the model output limit. "
) ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " path " , " Relative file path. " , required = True ) ,
body ( " start " , " First line to replace (1-indexed). " , required = True ) ,
body ( " end " , " Last line to replace (inclusive). " , required = True ) ,
body ( " content " , " Replacement text for those lines (empty deletes them). " ) ,
) ,
) ,
Action (
name = " project_insert_lines " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/insert-lines " ,
summary = " Insert content before a 1-indexed line of a text file " ,
description = (
" Inserts content before line ' at ' without touching existing lines. Use at=1 to prepend and "
" at=total_lines+1 to insert at the end. "
) ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " path " , " Relative file path. " , required = True ) ,
body ( " at " , " Insert before this 1-indexed line (1 prepends, total+1 appends). " , required = True ) ,
body ( " content " , " Text to insert. " , required = True ) ,
) ,
) ,
Action (
name = " project_delete_lines " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/delete-lines " ,
summary = " Delete an inclusive 1-indexed line range from a text file " ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " path " , " Relative file path. " , required = True ) ,
body ( " start " , " First line to delete (1-indexed). " , required = True ) ,
body ( " end " , " Last line to delete (inclusive). " , required = True ) ,
) ,
) ,
Action (
name = " project_append_file " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/append " ,
summary = " Append content to the end of a text file in a project " ,
description = " Adds content as new lines at the end of the file. Use it to grow a large file across turns without resending the whole thing. " ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " path " , " Relative file path. " , required = True ) ,
body ( " content " , " Text to append. " , required = True ) ,
) ,
) ,
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
Action (
name = " project_upload_file " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/upload " ,
summary = " Upload a local file into a project directory (parents created automatically) " ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
upload ( " file " , " Local filesystem path of the file to upload. " ) ,
body ( " path " , " Target directory inside the project, empty for the root. " ) ,
) ,
) ,
Action (
name = " project_make_dir " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/mkdir " ,
summary = " Create a directory (and parents) in a project filesystem " ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " path " , " Relative directory path, e.g. src/components. " , required = True ) ,
) ,
) ,
Action (
name = " project_move_file " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/move " ,
summary = " Move or rename a file or directory within a project " ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " from_path " , " Existing path. " , required = True ) ,
body ( " to_path " , " New path. " , required = True ) ,
) ,
) ,
Action (
name = " project_delete_file " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/delete " ,
summary = " Delete a file or directory (recursive) from a project filesystem " ,
params = (
path ( " project_slug " , " Project slug or uid. " ) ,
body ( " path " , " Relative path to delete. " , required = True ) ,
) ,
) ,
2026-06-09 01:32:57 +02:00
Action (
name = " zip_project " ,
method = " POST " ,
path = " /projects/ {project_slug} /zip " ,
summary = " Build a downloadable zip archive of a whole project " ,
description = (
" Queues a background zip job and returns { uid, status_url}. Poll the status_url with "
" zip_status until status is ' done ' , then give the user the download_url. "
) ,
params = ( path ( " project_slug " , " Project slug or uid. " ) , ) ,
requires_auth = False ,
) ,
Action (
name = " zip_status " ,
method = " GET " ,
path = " /zips/ {uid} " ,
summary = " Check a zip job and obtain its download link once finished " ,
description = (
" Returns the job status and stats. When status is ' done ' , download_url points at the "
" ready archive; while ' pending ' or ' running ' , poll again shortly. "
) ,
params = ( path ( " uid " , " Zip job uid returned by zip_project. " ) , ) ,
requires_auth = False ,
) ,
2026-06-09 16:06:02 +02:00
Action (
name = " fork_project " ,
method = " POST " ,
path = " /projects/ {project_slug} /fork " ,
summary = " Fork a project into a new project owned by the current user " ,
description = (
" Queues a background fork job and returns { uid, status_url}. Poll the status_url with "
" fork_status until status is ' done ' , then give the user the project_url of the new fork. "
) ,
params = (
path ( " project_slug " , " Slug or uid of the project to fork. " ) ,
body ( " title " , " Title for the new forked project. " , required = True ) ,
) ,
requires_auth = True ,
) ,
Action (
name = " fork_status " ,
method = " GET " ,
path = " /forks/ {uid} " ,
summary = " Check a fork job and obtain the new project once finished " ,
description = (
" Returns the job status. When status is ' done ' , project_url points at the new forked "
" project; while ' pending ' or ' running ' , poll again shortly. "
) ,
params = ( path ( " uid " , " Fork job uid returned by fork_project. " ) , ) ,
requires_auth = False ,
) ,
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
Action (
name = " search_users " ,
method = " GET " ,
path = " /profile/search " ,
summary = " Search for users by name " ,
params = ( query ( " q " , " Search query. " ) , ) ,
) ,
Action (
name = " view_profile " ,
method = " GET " ,
path = " /profile/ {username} " ,
summary = " View a user profile " ,
params = (
path ( " username " , " Username to view. " ) ,
query ( " tab " , " Profile tab. " ) ,
) ,
) ,
Action (
name = " update_profile " ,
method = " POST " ,
path = " /profile/update " ,
summary = " Update the current user ' s profile " ,
params = (
body ( " bio " , " Profile biography. " ) ,
body ( " location " , " Location. " ) ,
body ( " git_link " , " Git profile link. " ) ,
body ( " website " , " Personal website. " ) ,
) ,
) ,
Action (
name = " regenerate_api_key " ,
method = " POST " ,
path = " /profile/regenerate-api-key " ,
summary = " Issue a new API key and invalidate the current one " ,
description = (
" Returns the new api_key. Warning: this immediately invalidates any key currently "
" used for authentication, so confirm with the user before calling it. "
) ,
) ,
Action (
name = " list_messages " ,
method = " GET " ,
path = " /messages " ,
summary = " View direct message conversations " ,
params = (
query ( " with_uid " , " Open a conversation with a user uid. " ) ,
query ( " search " , " Search conversations. " ) ,
) ,
) ,
Action (
name = " search_message_users " ,
method = " GET " ,
path = " /messages/search " ,
summary = " Search users to message " ,
params = ( query ( " q " , " Search query. " ) , ) ,
) ,
Action (
name = " send_message " ,
method = " POST " ,
path = " /messages/send " ,
summary = " Send a direct message " ,
params = (
body ( " content " , " Message body. " , required = True ) ,
body ( " receiver_uid " , " Recipient user uid. " , required = True ) ,
body ( " attachment_uids " , ATTACHMENTS ) ,
) ,
) ,
Action (
name = " list_notifications " ,
method = " GET " ,
path = " /notifications " ,
summary = " List notifications " ,
params = ( query ( " before " , " Pagination cursor. " ) , ) ,
) ,
Action (
name = " notification_counts " ,
method = " GET " ,
path = " /notifications/counts " ,
summary = " Get unread notification and message counts " ,
) ,
Action (
name = " open_notification " ,
method = " GET " ,
path = " /notifications/open/ {notification_uid} " ,
summary = " Open a notification and follow it " ,
params = ( path ( " notification_uid " , " Notification uid. " ) , ) ,
) ,
Action (
name = " mark_notification_read " ,
method = " POST " ,
path = " /notifications/mark-read/ {notification_uid} " ,
summary = " Mark a single notification read " ,
params = ( path ( " notification_uid " , " Notification uid. " ) , ) ,
) ,
Action (
name = " mark_all_notifications_read " ,
method = " POST " ,
path = " /notifications/mark-all-read " ,
summary = " Mark all notifications read " ,
) ,
Action (
name = " vote " ,
method = " POST " ,
path = " /votes/ {target_type} / {target_uid} " ,
summary = " Cast or toggle a vote on a target " ,
description = " Re-sending the same value removes the vote. Returns the net/up/down tally. " ,
ajax = True ,
params = (
path ( " target_type " , TARGET_TYPE ) ,
path ( " target_uid " , " Uid of the target, copied from a listing response; do not invent it. " ) ,
body ( " value " , " Vote value: 1 to upvote, -1 to downvote (re-send to remove). " , required = True ) ,
) ,
) ,
Action (
name = " react " ,
method = " POST " ,
path = " /reactions/ {target_type} / {target_uid} " ,
summary = " Toggle an emoji reaction on a target " ,
description = " Re-sending the same emoji removes it. Returns reaction counts. " ,
ajax = True ,
params = (
path ( " target_type " , TARGET_TYPE ) ,
path ( " target_uid " , " Uid of the target, copied from a listing response; do not invent it. " ) ,
body ( " emoji " , " One of the allowed reaction emoji. " , required = True ) ,
) ,
) ,
Action (
name = " list_bookmarks " ,
method = " GET " ,
path = " /bookmarks/saved " ,
summary = " List saved bookmarks " ,
params = ( query ( " before " , " Pagination cursor. " ) , ) ,
) ,
Action (
name = " toggle_bookmark " ,
method = " POST " ,
path = " /bookmarks/ {target_type} / {target_uid} " ,
summary = " Toggle a bookmark on a target " ,
description = " Re-sending removes the bookmark. Returns { saved: true|false}. " ,
ajax = True ,
params = (
path ( " target_type " , TARGET_TYPE ) ,
path ( " target_uid " , " Uid of the target, copied from a listing response; do not invent it. " ) ,
) ,
) ,
Action (
name = " vote_poll " ,
method = " POST " ,
path = " /polls/ {poll_uid} /vote " ,
summary = " Vote in a poll " ,
description = " Casts or changes your vote on a poll option. Returns the option tallies. " ,
ajax = True ,
params = (
path ( " poll_uid " , " Poll uid. " ) ,
body ( " option_uid " , " Chosen option uid. " , required = True ) ,
) ,
) ,
Action (
name = " follow_user " ,
method = " POST " ,
path = " /follow/ {username} " ,
summary = " Follow a user " ,
params = ( path ( " username " , " Username to follow. " ) , ) ,
) ,
Action (
name = " unfollow_user " ,
method = " POST " ,
path = " /follow/unfollow/ {username} " ,
summary = " Unfollow a user " ,
params = ( path ( " username " , " Username to unfollow. " ) , ) ,
) ,
Action (
name = " list_followers " ,
method = " GET " ,
path = " /profile/ {username} /followers " ,
summary = " List the users who follow a profile " ,
requires_auth = False ,
params = (
path ( " username " , " Username whose followers to list. " ) ,
query ( " page " , " Page number, 25 per page. " ) ,
) ,
) ,
Action (
name = " list_following " ,
method = " GET " ,
path = " /profile/ {username} /following " ,
summary = " List the users a profile follows " ,
requires_auth = False ,
params = (
path ( " username " , " Username whose following to list. " ) ,
query ( " page " , " Page number, 25 per page. " ) ,
) ,
) ,
Action (
name = " view_leaderboard " ,
method = " GET " ,
path = " /leaderboard " ,
summary = " View the leaderboard " ,
) ,
Action (
name = " list_bugs " ,
method = " GET " ,
path = " /bugs " ,
summary = " List reported bugs " ,
) ,
Action (
name = " create_bug " ,
method = " POST " ,
path = " /bugs/create " ,
summary = " Report a bug " ,
params = (
body ( " title " , " Bug title. " , required = True ) ,
body ( " description " , " Bug description. " , required = True ) ,
body ( " attachment_uids " , ATTACHMENTS ) ,
) ,
) ,
Action (
name = " list_gists " ,
method = " GET " ,
path = " /gists " ,
summary = " List gists " ,
params = (
query ( " language " , " Filter by language. " ) ,
query ( " user_uid " , " Filter by owner uid. " ) ,
query ( " before " , " Pagination cursor. " ) ,
) ,
) ,
Action (
name = " view_gist " ,
method = " GET " ,
path = " /gists/ {gist_slug} " ,
summary = " View a gist by slug " ,
params = ( path ( " gist_slug " , " Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title. " ) , ) ,
) ,
Action (
name = " create_gist " ,
method = " POST " ,
path = " /gists/create " ,
summary = " Create a gist " ,
params = (
body ( " title " , " Gist title. " , required = True ) ,
body ( " source_code " , " Gist source code. " , required = True ) ,
body ( " description " , " Gist description. " ) ,
body ( " language " , " Programming language. " ) ,
body ( " attachment_uids " , ATTACHMENTS ) ,
) ,
) ,
Action (
name = " edit_gist " ,
method = " POST " ,
path = " /gists/edit/ {gist_slug} " ,
summary = " Edit a gist " ,
params = (
path ( " gist_slug " , " Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title. " ) ,
body ( " title " , " Gist title. " , required = True ) ,
body ( " source_code " , " Gist source code. " , required = True ) ,
body ( " description " , " Gist description. " ) ,
body ( " language " , " Programming language. " ) ,
) ,
) ,
Action (
name = " delete_gist " ,
method = " POST " ,
path = " /gists/delete/ {gist_slug} " ,
summary = " Delete a gist " ,
params = ( path ( " gist_slug " , " Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title. " ) , ) ,
) ,
Action (
name = " list_news " ,
method = " GET " ,
path = " /news " ,
summary = " List news articles " ,
params = ( query ( " before " , " Pagination cursor. " ) , ) ,
) ,
Action (
name = " view_news " ,
method = " GET " ,
path = " /news/ {news_slug} " ,
summary = " View a news article by slug " ,
params = ( path ( " news_slug " , " Exact news slug copied from a /news/... link in a listing response; do not build it from the title. " ) , ) ,
) ,
Action (
name = " upload_file " ,
method = " POST " ,
path = " /uploads/upload " ,
summary = " Upload a local file and obtain its attachment uid " ,
params = ( upload ( " file " , " Local filesystem path of the file to upload. " ) , ) ,
) ,
Action (
name = " delete_attachment " ,
method = " DELETE " ,
path = " /uploads/delete/ {attachment_uid} " ,
summary = " Delete an uploaded attachment " ,
params = ( path ( " attachment_uid " , " Uid of the attachment. " ) , ) ,
) ,
Action (
name = " admin_overview " ,
method = " GET " ,
path = " /admin " ,
summary = " View the admin overview " ,
) ,
Action (
name = " site_analytics " ,
method = " GET " ,
path = " /admin/analytics " ,
summary = " Site-wide aggregate analytics in one call (admin only) " ,
description = (
" Returns JSON: total members, active users in the last 24h/7d/30d, users signed in "
" now, new signups (24h/7d/30d), content totals (posts, comments, gists, projects, "
" news), and top authors. Use this for any ' how many ' / ' how active ' /count question "
" instead of paging through admin_list_users. "
) ,
params = ( query ( " top_n " , " How many top authors to include (1-50). " ) , ) ,
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
requires_admin = True ,
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
) ,
Action (
name = " ai_usage " ,
method = " GET " ,
path = " /admin/ai-usage/data " ,
summary = " AI gateway usage, cost, latency, and reliability metrics (admin only) " ,
description = (
" Returns JSON for a bounded window: request volume and throughput, token usage with "
" averages and percentiles, latency (upstream, gateway overhead, queue wait, connect), "
" error rates by category, cost in USD (per model, per caller, input vs output, projected "
" monthly burn, caching savings), caller behavior, and an hourly breakdown. Use this for "
" any question about AI spend, token consumption, gateway performance, or errors. "
) ,
params = (
query ( " hours " , " Lookback window in hours (1-168, default 48). " ) ,
query ( " top_n " , " How many rows in each top-N breakdown (default 10). " ) ,
) ,
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
requires_admin = True ,
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
) ,
Action (
name = " admin_list_users " ,
method = " GET " ,
path = " /admin/users " ,
summary = " List users for administration " ,
params = ( query ( " page " , " Page number. " ) , ) ,
) ,
Action (
name = " admin_set_user_role " ,
method = " POST " ,
path = " /admin/users/ {uid} /role " ,
summary = " Set a user ' s role " ,
params = (
path ( " uid " , " User uid. " ) ,
body ( " role " , " New role. " , required = True ) ,
) ,
) ,
Action (
name = " admin_set_user_password " ,
method = " POST " ,
path = " /admin/users/ {uid} /password " ,
summary = " Set a user ' s password " ,
params = (
path ( " uid " , " User uid. " ) ,
body ( " password " , " New password. " , required = True ) ,
) ,
) ,
Action (
name = " admin_toggle_user " ,
method = " POST " ,
path = " /admin/users/ {uid} /toggle " ,
summary = " Toggle a user ' s active state " ,
params = ( path ( " uid " , " User uid. " ) , ) ,
) ,
Action (
name = " admin_get_settings " ,
method = " GET " ,
path = " /admin/settings " ,
summary = " View site settings " ,
) ,
Action (
name = " admin_save_settings " ,
method = " POST " ,
path = " /admin/settings " ,
summary = " Save site settings " ,
params = (
body ( " site_name " , " Site name. " ) ,
body ( " site_description " , " Site description. " ) ,
body ( " site_tagline " , " Site tagline. " ) ,
body ( " max_upload_size_mb " , " Maximum upload size in megabytes. " ) ,
body ( " allowed_file_types " , " Allowed file types. " ) ,
body ( " max_attachments_per_resource " , " Maximum attachments per resource. " ) ,
body ( " rate_limit_per_minute " , " Rate limit per minute. " ) ,
body ( " rate_limit_window_seconds " , " Rate limit window in seconds. " ) ,
body ( " session_max_age_days " , " Session maximum age in days. " ) ,
body ( " session_remember_days " , " Remember-me duration in days. " ) ,
body ( " registration_open " , " Whether registration is open. " ) ,
body ( " maintenance_mode " , " Whether maintenance mode is on. " ) ,
body ( " maintenance_message " , " Maintenance message. " ) ,
) ,
) ,
Action (
name = " admin_list_news " ,
method = " GET " ,
path = " /admin/news " ,
summary = " List news for administration " ,
params = ( query ( " page " , " Page number. " ) , ) ,
) ,
Action (
name = " admin_toggle_news " ,
method = " POST " ,
path = " /admin/news/ {uid} /toggle " ,
summary = " Toggle a news article " ,
params = ( path ( " uid " , " News uid. " ) , ) ,
) ,
Action (
name = " admin_publish_news " ,
method = " POST " ,
path = " /admin/news/ {uid} /publish " ,
summary = " Publish a news article " ,
params = ( path ( " uid " , " News uid. " ) , ) ,
) ,
Action (
name = " admin_landing_news " ,
method = " POST " ,
path = " /admin/news/ {uid} /landing " ,
summary = " Set a news article as landing content " ,
params = ( path ( " uid " , " News uid. " ) , ) ,
) ,
Action (
name = " admin_delete_news " ,
method = " POST " ,
path = " /admin/news/ {uid} /delete " ,
summary = " Delete a news article " ,
params = ( path ( " uid " , " News uid. " ) , ) ,
) ,
Action (
name = " admin_list_services " ,
method = " GET " ,
path = " /admin/services " ,
summary = " View managed services " ,
) ,
Action (
name = " admin_services_data " ,
method = " GET " ,
path = " /admin/services/data " ,
summary = " Get live status, metrics, and log tail for every background service " ,
) ,
Action (
name = " admin_service_status " ,
method = " GET " ,
path = " /admin/services/ {name} /data " ,
summary = " Get live status, metrics, and log tail for one background service " ,
params = ( path ( " name " , " Service name (e.g. news, bots, openai). " ) , ) ,
) ,
Action (
name = " admin_start_service " ,
method = " POST " ,
path = " /admin/services/ {name} /start " ,
summary = " Start a managed service " ,
params = ( path ( " name " , " Service name. " ) , ) ,
) ,
Action (
name = " admin_stop_service " ,
method = " POST " ,
path = " /admin/services/ {name} /stop " ,
summary = " Stop a managed service " ,
params = ( path ( " name " , " Service name. " ) , ) ,
) ,
Action (
name = " admin_run_service " ,
method = " POST " ,
path = " /admin/services/ {name} /run " ,
summary = " Run a managed service once " ,
params = ( path ( " name " , " Service name. " ) , ) ,
) ,
Action (
name = " admin_clear_service_logs " ,
method = " POST " ,
path = " /admin/services/ {name} /clear-logs " ,
summary = " Clear a managed service ' s logs " ,
params = ( path ( " name " , " Service name. " ) , ) ,
) ,
Action (
name = " admin_config_service " ,
method = " POST " ,
path = " /admin/services/ {name} /config " ,
summary = " Update a managed service ' s configuration " ,
params = ( path ( " name " , " Service name. " ) , ) ,
freeform_body = True ,
) ,
)
PLATFORM_CATALOG = Catalog ( actions = ACTIONS )