chore: add agents/reports to gitignore and update agent infrastructure with timestamp streaming, write budget, and codename generation

- Add `agents/reports/` to `.gitignore` to prevent generated agent report files from being tracked
- Implement `_TimestampStream` class and `install_timestamps()` function in `agents/agent.py` for prefixing stdout/stderr with timestamps and elapsed time
- Export `install_timestamps`, `set_write_budget`, `clear_write_budget`, and `set_shell_restricted` from `agents/base.py`; add `_RUN_LOCK`, `AGENT_ICONS` dictionary, and `WRITE_BUDGET = 20` constant
- Add `report_codename()` function and `CODENAME_ADJECTIVES`/`CODENAME_ANIMALS` tuples to `agents/core/__init__.py` for generating random agent report codenames
- Expand `WRITE_TOOLS` tuple in `agents/core/__init__.py` to include `replace_lines`, `insert_lines`, and `delete_lines`; remove `SWARM_TOOLS` from `payloads_for` exclusion list
- Update `CLAUDE.md` and `AGENTS.md` documentation with dataset column initialization rules and new agent infrastructure details
- Reorder route table in `README.md` to list `/uploads` before `/messages` and document Swagger/ReDoc/OpenAPI schema endpoints
This commit is contained in:
2026-06-12 03:37:12 +00:00
parent 7fc9b0f715
commit 31841fece4
80 changed files with 2095 additions and 323 deletions
@@ -94,6 +94,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/feed",
summary="View the activity feed",
requires_auth=False,
params=(
query("tab", "Feed tab to view."),
query("topic", "Filter by topic."),
@@ -123,6 +124,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/posts/{post_slug}",
summary="View a single post by slug",
requires_auth=False,
params=(
path(
"post_slug",
@@ -191,6 +193,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/projects",
summary="List projects",
requires_auth=False,
params=(
query("tab", "Projects tab."),
query("search", "Search query."),
@@ -204,6 +207,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/projects/{project_slug}",
summary="View a project by slug",
requires_auth=False,
params=(
path(
"project_slug",
@@ -489,6 +493,22 @@ ACTIONS: tuple[Action, ...] = (
params=(path("project_slug", "Project slug or uid."),),
requires_auth=False,
),
Action(
name="project_files_zip",
method="POST",
path="/projects/{project_slug}/files/zip",
summary="Build a downloadable zip archive of a project subdirectory",
description=(
"Queues a background zip job for a subpath inside the project and returns "
"{uid, status_url}. Poll the status_url with zip_status until status is 'done', "
"then give the user the download_url. When path is empty the whole project is zipped."
),
params=(
path("project_slug", "Project slug or uid."),
query("path", "Relative subpath inside the project to zip; empty for the whole project."),
),
requires_auth=False,
),
Action(
name="zip_status",
method="GET",
@@ -540,6 +560,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/profile/{username}",
summary="View a user profile",
requires_auth=False,
params=(
path("username", "Username to view."),
query("tab", "Profile tab."),
@@ -566,6 +587,13 @@ ACTIONS: tuple[Action, ...] = (
"Returns the new api_key. Warning: this immediately invalidates any key currently "
"used for authentication, so confirm with the user before calling it."
),
params=(
body(
"confirm",
"Must be true, set only after the user has explicitly confirmed.",
required=True,
),
),
),
Action(
name="list_messages",
@@ -607,6 +635,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/notifications/counts",
summary="Get unread notification and message counts",
requires_auth=False,
),
Action(
name="open_notification",
@@ -763,12 +792,14 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/leaderboard",
summary="View the leaderboard",
requires_auth=False,
),
Action(
name="list_bugs",
method="GET",
path="/bugs",
summary="List reported bugs",
requires_auth=False,
),
Action(
name="create_bug",
@@ -786,6 +817,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/gists",
summary="List gists",
requires_auth=False,
params=(
query("language", "Filter by language."),
query("user_uid", "Filter by owner uid."),
@@ -797,6 +829,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/gists/{gist_slug}",
summary="View a gist by slug",
requires_auth=False,
params=(
path(
"gist_slug",
@@ -850,6 +883,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/news",
summary="List news articles",
requires_auth=False,
params=(query("before", "Pagination cursor."),),
),
Action(
@@ -857,6 +891,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/news/{news_slug}",
summary="View a news article by slug",
requires_auth=False,
params=(
path(
"news_slug",
@@ -904,6 +939,7 @@ ACTIONS: tuple[Action, ...] = (
method="GET",
path="/admin",
summary="View the admin overview",
requires_admin=True,
),
Action(
name="site_analytics",
@@ -946,6 +982,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/users",
summary="List users for administration",
params=(query("page", "Page number."),),
requires_admin=True,
),
Action(
name="admin_set_user_role",
@@ -956,6 +993,7 @@ ACTIONS: tuple[Action, ...] = (
path("uid", "User uid."),
body("role", "New role.", required=True),
),
requires_admin=True,
),
Action(
name="admin_set_user_password",
@@ -966,6 +1004,7 @@ ACTIONS: tuple[Action, ...] = (
path("uid", "User uid."),
body("password", "New password.", required=True),
),
requires_admin=True,
),
Action(
name="admin_toggle_user",
@@ -973,12 +1012,14 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/users/{uid}/toggle",
summary="Toggle a user's active state",
params=(path("uid", "User uid."),),
requires_admin=True,
),
Action(
name="admin_get_settings",
method="GET",
path="/admin/settings",
summary="View site settings",
requires_admin=True,
),
Action(
name="admin_save_settings",
@@ -1000,6 +1041,7 @@ ACTIONS: tuple[Action, ...] = (
body("maintenance_mode", "Whether maintenance mode is on."),
body("maintenance_message", "Maintenance message."),
),
requires_admin=True,
),
Action(
name="admin_list_news",
@@ -1007,6 +1049,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/news",
summary="List news for administration",
params=(query("page", "Page number."),),
requires_admin=True,
),
Action(
name="admin_toggle_news",
@@ -1014,6 +1057,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/news/{uid}/toggle",
summary="Toggle a news article",
params=(path("uid", "News uid."),),
requires_admin=True,
),
Action(
name="admin_publish_news",
@@ -1021,6 +1065,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/news/{uid}/publish",
summary="Publish a news article",
params=(path("uid", "News uid."),),
requires_admin=True,
),
Action(
name="admin_landing_news",
@@ -1028,6 +1073,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/news/{uid}/landing",
summary="Set a news article as landing content",
params=(path("uid", "News uid."),),
requires_admin=True,
),
Action(
name="admin_delete_news",
@@ -1035,18 +1081,21 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/news/{uid}/delete",
summary="Delete a news article",
params=(path("uid", "News uid."),),
requires_admin=True,
),
Action(
name="admin_list_services",
method="GET",
path="/admin/services",
summary="View managed services",
requires_admin=True,
),
Action(
name="admin_services_data",
method="GET",
path="/admin/services/data",
summary="Get live status, metrics, and log tail for every background service",
requires_admin=True,
),
Action(
name="admin_service_status",
@@ -1054,6 +1103,7 @@ ACTIONS: tuple[Action, ...] = (
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)."),),
requires_admin=True,
),
Action(
name="admin_start_service",
@@ -1061,6 +1111,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/services/{name}/start",
summary="Start a managed service",
params=(path("name", "Service name."),),
requires_admin=True,
),
Action(
name="admin_stop_service",
@@ -1068,6 +1119,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/services/{name}/stop",
summary="Stop a managed service",
params=(path("name", "Service name."),),
requires_admin=True,
),
Action(
name="admin_run_service",
@@ -1075,6 +1127,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/services/{name}/run",
summary="Run a managed service once",
params=(path("name", "Service name."),),
requires_admin=True,
),
Action(
name="admin_clear_service_logs",
@@ -1082,6 +1135,7 @@ ACTIONS: tuple[Action, ...] = (
path="/admin/services/{name}/clear-logs",
summary="Clear a managed service's logs",
params=(path("name", "Service name."),),
requires_admin=True,
),
Action(
name="admin_config_service",
@@ -1090,6 +1144,62 @@ ACTIONS: tuple[Action, ...] = (
summary="Update a managed service's configuration",
params=(path("name", "Service name."),),
freeform_body=True,
requires_admin=True,
),
Action(
name="admin_user_ai_usage",
method="GET",
path="/admin/users/{uid}/ai-usage",
summary="Get a user's AI gateway usage (admin only)",
description="Returns per-user AI usage data including token counts, cost, and request volume for the given lookback window.",
params=(
path("uid", "User uid."),
query("hours", "Lookback window in hours (default 24)."),
),
requires_admin=True,
),
Action(
name="admin_reset_user_ai_quota",
method="POST",
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."),),
requires_admin=True,
),
Action(
name="admin_media_purge",
method="POST",
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."),),
requires_admin=True,
),
Action(
name="admin_reset_guest_ai_quota",
method="POST",
path="/admin/ai-quota/reset-guests",
summary="Reset all guest AI quotas (admin only)",
description="Clears the AI quota ledger for all guest sessions.",
requires_admin=True,
),
Action(
name="admin_reset_all_ai_quota",
method="POST",
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.",
requires_admin=True,
),
Action(
name="restore_media",
method="POST",
path="/media/{uid}/restore",
summary="Restore a soft-deleted media attachment (admin only)",
description="Restores a previously soft-deleted attachment, making it visible again.",
params=(path("uid", "Attachment uid to restore."),),
requires_admin=True,
),
)
@@ -32,6 +32,7 @@ from .spec import Action, Catalog
MUTATING_METHODS = ("POST", "DELETE", "PUT", "PATCH")
CONFIRM_REQUIRED = {
"project_set_private",
"project_set_readonly",
"customize_set_css",
"customize_set_js",
@@ -39,6 +40,11 @@ CONFIRM_REQUIRED = {
"project_delete_file",
"delete_project",
"delete_media",
"regenerate_api_key",
"delete_post",
"delete_comment",
"delete_gist",
"delete_attachment",
}
DESTRUCTIVE_COMMAND = re.compile(
@@ -112,6 +118,13 @@ def confirmation_error(name: str, arguments: dict[str, Any]) -> ToolInputError |
"Deleting customizations cannot be undone. Ask the user to confirm, then call again with "
"confirm=true."
)
if name == "project_set_private":
value = str(arguments.get("value", "")).strip()
return ToolInputError(
f"Setting a project {'private' if value == 'true' else 'public'} "
"hides it from (or exposes it to) other members. Ask the user to confirm this "
"change explicitly, then call again with confirm=true."
)
if name == "project_set_readonly":
return ToolInputError(
"Setting a project read-only makes every file immutable and blocks all further "
@@ -135,6 +148,36 @@ def confirmation_error(name: str, arguments: dict[str, Any]) -> ToolInputError |
"project. Show the user the exact media, get explicit confirmation, then call again "
"with confirm=true."
)
if name == "regenerate_api_key":
return ToolInputError(
"Regenerating your API key immediately invalidates the current key, which may break "
"scripts or services that use it. Ask the user to confirm explicitly, then call again "
"with confirm=true."
)
if name == "delete_post":
slug = str(arguments.get("slug", arguments.get("post_slug", ""))).strip()
return ToolInputError(
f"Deleting the post '{slug}' is permanent and removes it from the site. "
"Ask the user to confirm explicitly, then call again with confirm=true."
)
if name == "delete_comment":
uid = str(arguments.get("uid", "")).strip() or "(unspecified)"
return ToolInputError(
f"Deleting comment '{uid}' is permanent and cannot be undone. "
"Ask the user to confirm explicitly, then call again with confirm=true."
)
if name == "delete_gist":
uid = str(arguments.get("uid", "")).strip() or "(unspecified)"
return ToolInputError(
f"Deleting gist '{uid}' is permanent. Show the user the exact gist, "
"get explicit confirmation, then call again with confirm=true."
)
if name == "delete_attachment":
uid = str(arguments.get("uid", "")).strip() or "(unspecified)"
return ToolInputError(
f"Deleting attachment '{uid}' is permanent and removes it from its parent. "
"Ask the user to confirm explicitly, then call again with confirm=true."
)
if (
name == "container_instance_action"
and str(arguments.get("action", "")).strip().lower() == "delete"
+3 -3
View File
@@ -261,14 +261,14 @@ class DeviiSession:
def _pick_target(self) -> Any:
best = None
best_rank = None
highest_rank = None
for ws in self._conns:
meta = self._conn_meta.get(ws)
if meta is None:
continue
rank = (meta["focused"], meta["visible"], meta["seq"])
if best_rank is None or rank > best_rank:
best_rank = rank
if highest_rank is None or rank > highest_rank:
highest_rank = rank
best = ws
return best