fix: extend content delete authorization to allow administrators alongside owners across all endpoints

This commit is contained in:
2026-06-11 22:40:27 +00:00
parent 9a93debfc9
commit 1e2f304935
14 changed files with 164 additions and 61 deletions
+36 -16
View File
@@ -23,6 +23,20 @@ def upload(name: str, description: str) -> Param:
return Param(name=name, location="file", description=description, required=True)
def confirm() -> Param:
return Param(
name="confirm",
location="body",
description=(
"Set to true ONLY after the user has explicitly confirmed this irreversible action. "
"Leave it unset on the first call: the tool refuses and tells you to confirm with the "
"user, and only a repeat call with confirm=true actually performs it."
),
required=False,
type="boolean",
)
ATTACHMENTS = "Comma separated attachment uids returned by upload_file."
TARGET_TYPE = "Target type, e.g. post, comment, project, gist."
@@ -159,12 +173,13 @@ ACTIONS: tuple[Action, ...] = (
name="delete_post",
method="POST",
path="/posts/delete/{post_slug}",
summary="Delete a post",
summary="Delete a post (your own, or any post when you are an administrator). Soft delete, confirmation required",
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.",
),
confirm(),
),
),
Action(
@@ -185,8 +200,8 @@ ACTIONS: tuple[Action, ...] = (
name="delete_comment",
method="POST",
path="/comments/delete/{comment_uid}",
summary="Delete a comment",
params=(path("comment_uid", "Uid of the comment."),),
summary="Delete a comment (your own, or any comment when you are an administrator). Soft delete, confirmation required",
params=(path("comment_uid", "Uid of the comment."), confirm()),
),
Action(
name="list_projects",
@@ -254,12 +269,13 @@ ACTIONS: tuple[Action, ...] = (
name="delete_project",
method="POST",
path="/projects/delete/{project_slug}",
summary="Delete a project",
summary="Delete a project and all its files (your own, or any project when you are an administrator). Soft delete, confirmation required",
params=(
path(
"project_slug",
"Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.",
),
confirm(),
),
),
Action(
@@ -475,10 +491,11 @@ ACTIONS: tuple[Action, ...] = (
name="project_delete_file",
method="POST",
path="/projects/{project_slug}/files/delete",
summary="Delete a file or directory (recursive) from a project filesystem",
summary="Delete a file or directory (recursive) from a project filesystem (project owner, or any project when you are an administrator). Soft delete, confirmation required",
params=(
path("project_slug", "Project slug or uid."),
body("path", "Relative path to delete.", required=True),
confirm(),
),
),
Action(
@@ -779,14 +796,14 @@ ACTIONS: tuple[Action, ...] = (
name="delete_media",
method="POST",
path="/media/{uid}/delete",
summary="Delete one of your uploaded media attachments",
summary="Delete an uploaded media attachment (your own, or anyone's when you are an administrator)",
description=(
"Removes the attachment from the user's Media tab and from any post, project, or "
"gist where it was attached. Show the user the exact media, get explicit "
"confirmation, then call again with confirm=true."
"gist where it was attached. Soft delete (restorable from admin trash). Show the user "
"the exact media, get explicit confirmation, then call again with confirm=true."
),
requires_auth=True,
params=(path("uid", "Attachment uid to delete."),),
params=(path("uid", "Attachment uid to delete."), confirm()),
),
Action(
name="view_leaderboard",
@@ -871,12 +888,13 @@ ACTIONS: tuple[Action, ...] = (
name="delete_gist",
method="POST",
path="/gists/delete/{gist_slug}",
summary="Delete a gist",
summary="Delete a gist (your own, or any gist when you are an administrator). Soft delete, confirmation required",
params=(
path(
"gist_slug",
"Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.",
),
confirm(),
),
),
Action(
@@ -932,8 +950,8 @@ ACTIONS: tuple[Action, ...] = (
name="delete_attachment",
method="DELETE",
path="/uploads/delete/{attachment_uid}",
summary="Delete an uploaded attachment",
params=(path("attachment_uid", "Uid of the attachment."),),
summary="Delete an uploaded attachment (your own, or anyone's when you are an administrator). Soft delete, confirmation required",
params=(path("attachment_uid", "Uid of the attachment."), confirm()),
),
Action(
name="admin_overview",
@@ -1080,8 +1098,8 @@ ACTIONS: tuple[Action, ...] = (
name="admin_delete_news",
method="POST",
path="/admin/news/{uid}/delete",
summary="Delete a news article",
params=(path("uid", "News uid."),),
summary="Delete a news article (admin only). Soft delete, confirmation required",
params=(path("uid", "News uid."), confirm()),
requires_admin=True,
),
Action(
@@ -1165,7 +1183,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/users/{uid}/reset-ai-quota",
summary="Reset a user's AI quota (admin only)",
description="Clears the AI quota ledger for a specific user, allowing them to use AI features again.",
params=(path("uid", "User uid."),),
params=(path("uid", "User uid."), confirm()),
requires_admin=True,
),
Action(
@@ -1174,7 +1192,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/media/{uid}/purge",
summary="Permanently remove soft-deleted media (admin only)",
description="Permanently deletes a soft-deleted attachment from the database and filesystem.",
params=(path("uid", "Attachment uid to purge."),),
params=(path("uid", "Attachment uid to purge."), confirm()),
requires_admin=True,
),
Action(
@@ -1183,6 +1201,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/ai-quota/reset-guests",
summary="Reset all guest AI quotas (admin only)",
description="Clears the AI quota ledger for all guest sessions.",
params=(confirm(),),
requires_admin=True,
),
Action(
@@ -1191,6 +1210,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/ai-quota/reset-all",
summary="Reset ALL AI quotas including member quotas (admin only)",
description="Clears the AI quota ledger for every user and guest. Use with caution.",
params=(confirm(),),
requires_admin=True,
),
Action(
@@ -81,6 +81,13 @@ CONTAINER_ACTIONS: tuple[Action, ...] = (
"start, stop, restart, pause, resume, delete, or sync.",
required=True,
),
arg(
"confirm",
"Required only for action=delete: set true ONLY after the user has explicitly "
"confirmed destroying the instance. Leave unset otherwise; the delete is refused "
"until you pass confirm=true.",
kind="boolean",
),
),
),
Action(
@@ -112,6 +119,13 @@ CONTAINER_ACTIONS: tuple[Action, ...] = (
"Command to run, e.g. 'git clone ... && ls'. Runs in /app already; do not prepend 'cd /app'.",
required=True,
),
arg(
"confirm",
"Required only when the command is destructive (rm, dd, truncate, drop, etc.): set "
"true ONLY after the user has explicitly confirmed. Leave unset otherwise; a "
"destructive command is refused until you pass confirm=true.",
kind="boolean",
),
),
),
Action(
@@ -50,6 +50,9 @@ CONFIRM_REQUIRED = {
"admin_reset_all_ai_quota",
"admin_reset_guest_ai_quota",
"admin_reset_user_ai_quota",
}
CONDITIONAL_CONFIRM = {
"container_instance_action",
"container_exec",
}
@@ -200,6 +203,12 @@ def confirmation_error(name: str, arguments: dict[str, Any]) -> ToolInputError |
f"such as rm, dd, truncate, or drop): {command!r}. Show the user the exact command, get "
"explicit confirmation, then call again with confirm=true."
)
if name in CONFIRM_REQUIRED:
return ToolInputError(
"This removes the item as a soft delete: it disappears from every surface and is only "
"restorable from the admin trash. Show the user exactly what will be deleted, get "
"explicit confirmation, then call again with confirm=true."
)
return None
+13 -8
View File
@@ -98,14 +98,19 @@ SYSTEM_PROMPT = (
"tool result has error 'tool_input_truncated', your output was cut off - resend that single write or "
"line edit on its own.\n\n"
"DELETING IS ALWAYS CONFIRMED\n"
"Deleting is irreversible, so you MUST get explicit user confirmation before ANY deletion and only "
"then pass confirm=true. This covers deleting a file (project_delete_file), deleting a whole project "
"(delete_project), deleting a container instance (container_instance_action with action 'delete'), "
"and running a destructive shell command via container_exec (anything that removes or wipes data: "
"rm, rmdir, unlink, shred, truncate, dd, mkfs, 'find -delete', redirecting over a file, or SQL "
"'drop'/'delete from'). Never delete a file, database, container, or any data on your own initiative, "
"even while testing or fixing something - show the user exactly what will be removed, wait for a clear "
"yes, and only then retry with confirm=true. If in doubt, ask first.\n\n"
"Deleting is irreversible from the user's point of view, so you MUST get explicit user confirmation "
"before ANY deletion and only then pass confirm=true. Content deletions are soft deletes: the item "
"disappears from every surface but stays restorable from the admin trash. This covers deleting a "
"post (delete_post), a comment (delete_comment), a gist (delete_gist), a project (delete_project), a "
"project file or directory (project_delete_file), an uploaded media item (delete_media) or attachment "
"(delete_attachment), a news article (admin_delete_news), a container instance "
"(container_instance_action with action 'delete'), and running a destructive shell command via "
"container_exec (anything that removes or wipes data: rm, rmdir, unlink, shred, truncate, dd, mkfs, "
"'find -delete', redirecting over a file, or SQL 'drop'/'delete from'). When you are an administrator "
"you may delete ANY member's content this way, not only your own; members may delete only their own. "
"Never delete anything on your own initiative, even while testing or fixing something - show the user "
"exactly what will be removed, wait for a clear yes, and only then retry with confirm=true. If in "
"doubt, ask first.\n\n"
"PROJECT PRIVACY AND READ-ONLY\n"
"A project owner can mark a project private (project_set_private) so only the owner and "
"administrators can see it, and read-only (project_set_readonly) so every file becomes immutable. "
+22
View File
@@ -34,3 +34,25 @@ CATALOG = Catalog(
+ BEHAVIOR_ACTIONS
+ VIRTUAL_TOOL_ACTIONS
)
def _assert_confirm_params(catalog: Catalog) -> None:
from .actions.dispatcher import CONFIRM_REQUIRED, CONDITIONAL_CONFIRM
by_name = catalog.by_name()
gated = set(CONFIRM_REQUIRED) | set(CONDITIONAL_CONFIRM)
missing = sorted(
name
for name in gated
if name in by_name
and not any(param.name == "confirm" for param in by_name[name].params)
)
if missing:
raise RuntimeError(
"Confirmation-gated Devii tools lack a declared 'confirm' parameter, so the model can "
"never satisfy the gate and the agent loops forever: " + ", ".join(missing) + ". Add "
"confirm() (catalog.py) or arg('confirm', ..., kind='boolean') to each."
)
_assert_confirm_params(CATALOG)