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

> Secure your sensitive ML operations with the ML Intern tool approval workflow. Learn how HF Jobs and file uploads get explicit user consent before execution.

- Repository: [Hugging Face/ml-intern](https://github.com/huggingface/ml-intern)
- Tags: how-to-guide
- Published: 2026-04-24

---

**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`](https://github.com/huggingface/ml-intern/blob/main/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`](https://github.com/huggingface/ml-intern/blob/main/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.

```python

# 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`](https://github.com/huggingface/ml-intern/blob/main/agent/core/agent_loop.py) where the system constructs an `approval_required` event containing serialized tool data.

```python

# 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`](https://github.com/huggingface/ml-intern/blob/main/backend/routes/agent.py).

The request payload conforms to the `ToolApproval` model in [`backend/models.py`](https://github.com/huggingface/ml-intern/blob/main/backend/models.py):

```python

# 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:

```python

# 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`](https://github.com/huggingface/ml-intern/blob/main/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.

```python

# 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`](https://github.com/huggingface/ml-intern/blob/main/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.