mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-23 07:12:21 +02:00
fix(tasks): apply the admin-only action policy in manage_tasks (#6358)
POST /api/tasks and the scheduler both refuse action tasks whose action is in ADMIN_ONLY_TASK_ACTIONS (run_local, run_script, ssh_command, cookbook_serve) for owners without admin task privileges: those actions reach subprocess.run(shell=True) or SSH with no sandbox. The model-facing manage_tasks tool applied neither check, so an agent could create or edit such a task, resume it, or trigger it, and only the scheduler's execution-time guard stood in the way (pausing the task after the fact). Route the tool through the same src.task_action_policy helpers the HTTP route uses: create checks the requested type/action, edit checks the task as it would be after the edit (so an llm task cannot be flipped into a shell action), and resume/run check the stored task. Admins, non- privileged actions and llm/research tasks are unaffected; pause still works for everyone since it only narrows. Fixes #6021 Claude-Session: https://claude.ai/code/session_01UAB5sz5srMKR7rbeDFtdMH Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Alexandre Teixeira
parent
9712a54001
commit
88772b3278
@@ -325,6 +325,26 @@ def _task_structured_schedule(args, fallback_time=None):
|
||||
'cron_expression': f'{parsed.minute} {parsed.hour} * * {cron_days}'}
|
||||
|
||||
|
||||
def _admin_task_action_denied(owner: Optional[str], task_type: Optional[str], action_name: Optional[str]) -> Optional[Dict]:
|
||||
"""Apply the shared admin-only task-action policy to a tool call.
|
||||
|
||||
``POST /api/tasks`` and the scheduler both refuse ``run_local``,
|
||||
``run_script``, ``ssh_command`` and ``cookbook_serve`` action tasks for
|
||||
owners without admin task privileges (``src.task_action_policy``).
|
||||
Those actions reach ``subprocess.run(shell=True)`` / SSH with no sandbox,
|
||||
so the model-facing tool must apply the same policy at create/edit/
|
||||
resume/run time instead of storing a task the scheduler will only pause
|
||||
later. Returns the tool error dict to hand back, or None when allowed.
|
||||
"""
|
||||
from src.task_action_policy import (
|
||||
is_admin_only_task_action,
|
||||
owner_has_admin_task_privileges,
|
||||
)
|
||||
if is_admin_only_task_action(task_type, action_name) and not owner_has_admin_task_privileges(owner):
|
||||
return {"error": f"Action '{action_name}' requires admin privileges", "exit_code": 1}
|
||||
return None
|
||||
|
||||
|
||||
async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
"""Handle manage_tasks tool calls: CRUD on scheduled tasks."""
|
||||
import uuid as _uuid
|
||||
@@ -462,6 +482,9 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
return {"error": "Prompt is required for llm/research tasks", "exit_code": 1}
|
||||
if task_type == "action" and not args.get("action_name"):
|
||||
return {"error": "action_name is required for action tasks", "exit_code": 1}
|
||||
denied = _admin_task_action_denied(owner, task_type, args.get("action_name"))
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
# Compute next_run for schedule triggers
|
||||
next_run = None
|
||||
@@ -525,6 +548,14 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
if owner and task.owner != owner:
|
||||
return {"error": "Access denied", "exit_code": 1}
|
||||
|
||||
# Check the policy against the task as it would be after the
|
||||
# edit, so an llm task can't be flipped into a shell action.
|
||||
next_task_type = args["task_type"] if args.get("task_type") is not None else task.task_type
|
||||
next_action = args["action_name"] if args.get("action_name") is not None else task.action
|
||||
denied = _admin_task_action_denied(owner, next_task_type, next_action)
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
if 'weekdays' in args or 'day_of_month' in args:
|
||||
clock = task.scheduled_time
|
||||
if task.schedule == 'cron':
|
||||
@@ -624,6 +655,9 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
if action == "pause":
|
||||
task.status = "paused"
|
||||
else:
|
||||
denied = _admin_task_action_denied(owner, task.task_type, task.action)
|
||||
if denied:
|
||||
return denied
|
||||
task.status = "active"
|
||||
if (task.trigger_type or "schedule") == "schedule":
|
||||
task.next_run = compute_next_run(
|
||||
@@ -644,6 +678,9 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
return error
|
||||
if owner and task.owner != owner:
|
||||
return {"error": "Access denied", "exit_code": 1}
|
||||
denied = _admin_task_action_denied(owner, task.task_type, task.action)
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
from src.event_bus import get_task_scheduler
|
||||
scheduler = get_task_scheduler()
|
||||
|
||||
Reference in New Issue
Block a user