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 ifconfig.confirm_cpu_jobsis disabled.hf_repo_files– Upload and delete operations that modify file contents in a repository.hf_repo_git– Permanent structural changes includingdelete_branch,delete_tag,merge_pr,create_repo, andupdate_repo.hf_private_repos– Upload operations whenconfig.auto_file_uploadis 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:
- Emits a
tool_state_changeevent with stateapproved - Replaces the script with
edited_scriptif provided - Executes the tool via
session.tool_router.call_tool - Streams the output via a
tool_outputevent - 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 toTrue,_needs_approvalreturnsFalsefor all tools, effectively disabling the approval layer for rapid development.config.confirm_cpu_jobs– Forces approval even for CPU-only HF Jobs whenTrue.config.auto_file_upload– Allowshf_private_reposupload 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.pyvia_needs_approval, which inspects tool names and arguments against safety rules. - Broadcast sends an
approval_requiredSSE event containing pending tool metadata, while storing state insession.pending_approval. - Submission travels via POST
/api/approveusing theToolApprovalPydantic 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_modeallow 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →