ML Intern Tool Approval Workflow: How HF Jobs and File Uploads Stay Secure

The ML Intern tool approval workflow is a four-stage gatekeeping system that intercepts sensitive operations—such as launching HF Jobs, uploading files to private repositories, and modifying repository metadata—requiring explicit user consent before execution proceeds.

The huggingface/ml-intern repository implements this mandatory human-in-the-loop validation to prevent accidental compute costs and destructive data changes. Every tool call proposed by the LLM is evaluated against safety rules that distinguish between harmless read-only queries and high-risk actions that alter remote state or incur billing.

Which Operations Require Approval

The workflow specifically targets tools with side effects or financial impact. According to the source code in agent/core/agent_loop.py, the following operations trigger mandatory approval:

  • hf_jobs – Any run using non-CPU hardware (GPU, TPU, etc.). CPU-only jobs may bypass approval if config.confirm_cpu_jobs is disabled.
  • hf_repo_files – Upload and delete operations that modify file contents in a repository.
  • hf_repo_git – Permanent structural changes including delete_branch, delete_tag, merge_pr, create_repo, and update_repo.
  • hf_private_repos – Upload operations when config.auto_file_upload is not enabled.
  • sandbox_create – Always requires explicit approval to prevent accidental resource allocation.

Stage 1: Detection Logic in _needs_approval

The approval process begins inside agent/core/agent_loop.py where the _needs_approval function inspects every tool call. This function evaluates the tool name, arguments, and current configuration to determine if execution can proceed automatically.


# agent/core/agent_loop.py

def _needs_approval(tool_name: str, tool_args: dict, config: Config | None = None) -> bool:
    # Bypass all checks if yolo_mode is enabled

    if config and config.yolo_mode:
        return False
    
    # Always require approval for sandbox creation

    if tool_name == "sandbox_create":
        return True
    
    # HF Jobs: CPU-only can be whitelisted, others need approval

    if tool_name == "hf_jobs":
        hardware_flavor = tool_args.get("hardware_flavor") or tool_args.get("flavor") or "cpu-basic"
        is_cpu_job = hardware_flavor in CPU_FLAVORS
        if is_cpu_job and not (config and config.confirm_cpu_jobs):
            return False
        return True
    
    # File operations in repos

    if tool_name == "hf_repo_files" and tool_args.get("operation") in ["upload", "delete"]:
        return True
    
    # Git structural operations

    if tool_name == "hf_repo_git" and tool_args.get("operation") in [
        "delete_branch", "delete_tag", "merge_pr", "create_repo", "update_repo"]:
        return True
    
    return False

If this function returns True, the tool is held in a pending state rather than executed immediately.

Stage 2: Broadcasting the Approval Request

When one or more tools require approval, the agent loop pauses execution and emits a Server-Sent Event (SSE) to the frontend. This happens in agent/core/agent_loop.py where the system constructs an approval_required event containing serialized tool data.


# agent/core/agent_loop.py

if approval_required_tools:
    tools_data = [
        {
            "tool": tool_name,
            "arguments": tool_args,
            "tool_call_id": tc.id,
        }
        for tc, tool_name, tool_args in approval_required_tools
    ]
    await session.send_event(Event(
        event_type="approval_required",
        data={"tools": tools_data, "count": len(tools_data)},
    ))
    # Store pending state to survive page refreshes

    session.pending_approval = {
        "tool_calls": [tc for tc, _, _ in approval_required_tools],
    }
    # Halt the turn until user responds

    return None

The session.pending_approval dictionary persists the tool call IDs server-side, ensuring that reloading the browser does not lose the approval request.

Stage 3: User Submission via the /api/approve Endpoint

The frontend renders a modal displaying each pending tool, its arguments, and an optional script editor. When the user submits their decision, the UI sends a POST request to /api/approve defined in backend/routes/agent.py.

The request payload conforms to the ToolApproval model in backend/models.py:


# backend/models.py

class ToolApproval(BaseModel):
    tool_call_id: str
    approved: bool
    feedback: str | None = None
    edited_script: str | None = None

The backend route validates the user owns the session, then forwards the approvals to the session manager:


# backend/routes/agent.py

@router.post("/approve")
async def submit_approval(
    request: ApprovalRequest, user: dict = Depends(get_current_user)
) -> dict:
    _check_session_access(request.session_id, user)
    approvals = [
        {
            "tool_call_id": a.tool_call_id,
            "approved": a.approved,
            "feedback": a.feedback,
            "edited_script": a.edited_script,
        }
        for a in request.approvals
    ]
    success = await session_manager.submit_approval(request.session_id, approvals)
    if not success:
        raise HTTPException(status_code=404, detail="Session not found or inactive")
    return {"status": "success"}

Stage 4: Execution and Conversation Recovery

Once approvals are submitted, Handlers.exec_approval in agent/core/agent_loop.py processes the decisions. This method maps each tool_call_id to the user's response, handles script modifications, and manages the tool lifecycle.

For approved tools, the system:

  1. Emits a tool_state_change event with state approved
  2. Replaces the script with edited_script if provided
  3. Executes the tool via session.tool_router.call_tool
  4. Streams the output via a tool_output event
  5. Inserts results into the LLM conversation context

For rejected tools, the system emits a tool_state_change event with state rejected and records a cancellation message (including any user feedback) in the conversation history.

After processing all decisions, session.pending_approval is cleared to prevent duplicate execution, and the agent loop restarts with await Handlers.run_agent(session, "") to allow the LLM to continue the conversation using the new tool results.


# agent/core/agent_loop.py (simplified excerpt from exec_approval)

async def exec_approval(session: Session, approvals: list[dict]) -> None:
    tool_calls = session.pending_approval.get("tool_calls", [])
    approval_map = {a["tool_call_id"]: a for a in approvals}
    
    for tc in tool_calls:
        tool_name = tc.function.name
        tool_args = json.loads(tc.function.arguments)
        decision = approval_map.get(tc.id, {"approved": False})
        
        if decision.get("approved"):
            # Apply script edits if present

            if decision.get("edited_script") and "script" in tool_args:
                tool_args["script"] = decision["edited_script"]
            # Execute and stream results...

        else:
            # Handle rejection with feedback...

    
    session.pending_approval = None
    await Handlers.run_agent(session, "")  # Resume conversation

Configuration Overrides

The ML Intern tool approval workflow respects several configuration flags that alter default behavior:

  • config.yolo_mode – When set to True, _needs_approval returns False for all tools, effectively disabling the approval layer for rapid development.
  • config.confirm_cpu_jobs – Forces approval even for CPU-only HF Jobs when True.
  • config.auto_file_upload – Allows hf_private_repos upload operations to proceed without approval when enabled.

Summary

  • The ML Intern tool approval workflow gates destructive and paid operations through a four-stage pipeline: detection, broadcast, user submission, and execution.
  • Detection occurs in agent/core/agent_loop.py via _needs_approval, which inspects tool names and arguments against safety rules.
  • Broadcast sends an approval_required SSE event containing pending tool metadata, while storing state in session.pending_approval.
  • Submission travels via POST /api/approve using the ToolApproval Pydantic model, supporting optional script editing and rejection feedback.
  • Execution is handled by Handlers.exec_approval, which manages state transitions, applies edits, and resumes the agent loop with updated context.
  • Configuration options like yolo_mode allow power users to bypass approvals entirely when appropriate.

Frequently Asked Questions

What happens if I reject a tool approval?

When a tool is rejected, the system records a "Job execution cancelled by user" message in the conversation context, including any feedback text provided. The LLM receives this cancellation notice and can adjust its strategy or ask clarifying questions before proposing alternative actions.

Can I edit a script before approving it?

Yes. The ToolApproval model includes an optional edited_script field. If you modify the code in the approval modal, the frontend sends the edited version to the backend, and exec_approval substitutes the original script with your version before execution.

What is YOLO mode in ML Intern?

YOLO mode is a configuration setting (config.yolo_mode) that disables the entire approval workflow. When enabled, _needs_approval immediately returns False for all tools, allowing the agent to execute HF Jobs, file uploads, and repository modifications without user intervention.

Does refreshing the page lose pending approvals?

No. Pending approvals are stored in session.pending_approval on the server side, not in the browser. If you refresh the page while an approval modal is open, the frontend reconnects to the SSE stream and retrieves the current pending state from the session manager.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →