lewtun HF Staff Claude Fable 5 commited on
Commit
7943555
·
unverified ·
1 Parent(s): 4ecc3bd

Remove ~900 lines of dead code across agent, backend, and frontend (#318)

Browse files

* Remove dead code across agent, backend, and frontend

A vulture/knip audit with manual repo-wide verification found ~900 lines
of dead code:

- Delete agent/tools/private_hf_repo_tools.py (650 lines): disabled when
hf_repo_files/hf_repo_git replaced it, never imported since.
- Drop orphaned functions, constants, and type aliases across agent/ and
backend/ (record_jobs_access_blocked, last_fetch_error,
update_local_save_status, verify_session_access, OperationType aliases,
OpType.INTERRUPT in both enums, etc.).
- Trim unused Sandbox client surface (pause/restart/to_dict/
tool_definitions and unused constants); keep Sandbox.connect, which the
live auth integration test uses.
- Remove production code only its own tests kept alive: _needs_approval
(tests retargeted to _base_needs_approval / production path),
fetch_hf_user_plan, _cleanup_user_orphan_sandboxes (superseded by
scripts/sweep_orphan_sandboxes.py), DEFAULT_MODEL_ID alias.
- Drop unused boto3 dependency (still available transitively via the
eval extra for inspect-ai).
- Frontend: delete unused ThinkingIndicator component, 12 orphaned event
payload interfaces, ToolApproval interface, and the duplicate theme
default export.

No behavior change. ruff, 485 unit tests, tsc build, and eslint all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop stale INTERRUPT reference from agent README

Addresses PR #318 review: OpType.INTERRUPT no longer exists, so remove
it from the handled-operations list in the architecture notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

agent/README.md CHANGED
@@ -7,7 +7,7 @@ Async agent loop with LiteLLM.
7
  **Queue-based async system:**
8
  - Submissions in (user input) → Agent Loop → Events output for possible UI updates
9
  - Session maintains state (context + tools) for possible future Context Engineering
10
- - Handlers operations like (USER_INPUT, INTERRUPT, COMPACT, UNDO, SHUTDOWN) for possible UI control
11
 
12
  ## Components
13
 
 
7
  **Queue-based async system:**
8
  - Submissions in (user input) → Agent Loop → Events output for possible UI updates
9
  - Session maintains state (context + tools) for possible future Context Engineering
10
+ - Handlers operations like (USER_INPUT, COMPACT, UNDO, SHUTDOWN) for possible UI control
11
 
12
  ## Components
13
 
agent/core/agent_loop.py CHANGED
@@ -324,17 +324,6 @@ def _base_needs_approval(
324
  return False
325
 
326
 
327
- def _needs_approval(
328
- tool_name: str, tool_args: dict, config: Config | None = None
329
- ) -> bool:
330
- """Legacy sync approval predicate used by tests and CLI display helpers."""
331
- if _is_scheduled_hf_job_run(tool_name, tool_args):
332
- return True
333
- if config and config.yolo_mode:
334
- return False
335
- return _base_needs_approval(tool_name, tool_args, config)
336
-
337
-
338
  def _session_auto_approval_enabled(session: Session | None) -> bool:
339
  return bool(session and getattr(session, "auto_approval_enabled", False))
340
 
 
324
  return False
325
 
326
 
 
 
 
 
 
 
 
 
 
 
 
327
  def _session_auto_approval_enabled(session: Session | None) -> bool:
328
  return bool(session and getattr(session, "auto_approval_enabled", False))
329
 
agent/core/hf_access.py CHANGED
@@ -21,7 +21,6 @@ HF_BILLING_URL = "https://huggingface.co/settings/billing"
21
  HF_PRO_SUBSCRIBE_URL = "https://huggingface.co/subscribe/pro"
22
 
23
  HfUserPlan = Literal["free", "pro"]
24
- HfUserPlanStatus = Literal["free", "pro", "unknown"]
25
 
26
 
27
  @dataclass(frozen=True)
@@ -32,7 +31,6 @@ class JobsAccess:
32
  org_names: list[str]
33
  eligible_namespaces: list[str]
34
  default_namespace: str | None
35
- access_known: bool = True
36
 
37
 
38
  class JobsAccessError(Exception):
@@ -126,17 +124,6 @@ async def fetch_whoami_v2(token: str, timeout: float = 5.0) -> dict[str, Any] |
126
  return None
127
 
128
 
129
- async def fetch_hf_user_plan(
130
- token: str | None,
131
- timeout: float = 5.0,
132
- ) -> HfUserPlanStatus:
133
- """Return the token owner's HF plan, or ``unknown`` if lookup fails."""
134
- if not token:
135
- return "unknown"
136
- whoami = await fetch_whoami_v2(token, timeout=timeout)
137
- return normalize_hf_user_plan(whoami) or "unknown"
138
-
139
-
140
  async def get_jobs_access(token: str) -> JobsAccess | None:
141
  whoami = await fetch_whoami_v2(token)
142
  if whoami is None:
 
21
  HF_PRO_SUBSCRIBE_URL = "https://huggingface.co/subscribe/pro"
22
 
23
  HfUserPlan = Literal["free", "pro"]
 
24
 
25
 
26
  @dataclass(frozen=True)
 
31
  org_names: list[str]
32
  eligible_namespaces: list[str]
33
  default_namespace: str | None
 
34
 
35
 
36
  class JobsAccessError(Exception):
 
124
  return None
125
 
126
 
 
 
 
 
 
 
 
 
 
 
 
127
  async def get_jobs_access(token: str) -> JobsAccess | None:
128
  whoami = await fetch_whoami_v2(token)
129
  if whoami is None:
agent/core/hf_router_catalog.py CHANGED
@@ -7,7 +7,6 @@ pricing, context length, and tool-use support. We use it to:
7
  • Validate ``/model`` switches with live data instead of a hard-coded allowlist.
8
  • Show the user which providers serve a model, at what price, and whether they
9
  support tool calls.
10
- • Derive a reasonable context-window limit for any routed model.
11
 
12
  The listing is cached in-memory for a few minutes so repeated lookups during a
13
  session are free. On fetch failure we return stale data if we have it, or an
@@ -42,7 +41,6 @@ class ProviderInfo:
42
  input_price: Optional[float]
43
  output_price: Optional[float]
44
  supports_tools: bool
45
- supports_structured_output: bool
46
 
47
 
48
  @dataclass
@@ -54,11 +52,6 @@ class ModelInfo:
54
  def live_providers(self) -> list[ProviderInfo]:
55
  return [p for p in self.providers if p.status == "live"]
56
 
57
- @property
58
- def max_context_length(self) -> Optional[int]:
59
- lengths = [p.context_length for p in self.live_providers if p.context_length]
60
- return max(lengths) if lengths else None
61
-
62
  @property
63
  def any_supports_tools(self) -> bool:
64
  return any(p.supports_tools for p in self.live_providers)
@@ -97,9 +90,6 @@ def _parse_entry(entry: dict) -> ModelInfo:
97
  input_price=pricing.get("input"),
98
  output_price=pricing.get("output"),
99
  supports_tools=bool(p.get("supports_tools", False)),
100
- supports_structured_output=bool(
101
- p.get("supports_structured_output", False)
102
- ),
103
  )
104
  )
105
  return ModelInfo(id=entry.get("id", ""), providers=providers)
@@ -119,10 +109,6 @@ def lookup(model_id: str) -> Optional[ModelInfo]:
119
  return None
120
 
121
 
122
- def last_fetch_error() -> Optional[str]:
123
- return _last_fetch_error
124
-
125
-
126
  def fuzzy_suggest(model_id: str, limit: int = 3) -> list[str]:
127
  """Return the closest model ids from the catalog."""
128
  bare = model_id.split(":", 1)[0]
 
7
  • Validate ``/model`` switches with live data instead of a hard-coded allowlist.
8
  • Show the user which providers serve a model, at what price, and whether they
9
  support tool calls.
 
10
 
11
  The listing is cached in-memory for a few minutes so repeated lookups during a
12
  session are free. On fetch failure we return stale data if we have it, or an
 
41
  input_price: Optional[float]
42
  output_price: Optional[float]
43
  supports_tools: bool
 
44
 
45
 
46
  @dataclass
 
52
  def live_providers(self) -> list[ProviderInfo]:
53
  return [p for p in self.providers if p.status == "live"]
54
 
 
 
 
 
 
55
  @property
56
  def any_supports_tools(self) -> bool:
57
  return any(p.supports_tools for p in self.live_providers)
 
90
  input_price=pricing.get("input"),
91
  output_price=pricing.get("output"),
92
  supports_tools=bool(p.get("supports_tools", False)),
 
 
 
93
  )
94
  )
95
  return ModelInfo(id=entry.get("id", ""), providers=providers)
 
109
  return None
110
 
111
 
 
 
 
 
112
  def fuzzy_suggest(model_id: str, limit: int = 3) -> list[str]:
113
  """Return the closest model ids from the catalog."""
114
  bare = model_id.split(":", 1)[0]
agent/core/model_ids.py CHANGED
@@ -10,8 +10,6 @@ MINIMAX_M27_MODEL_ID = "MiniMaxAI/MiniMax-M2.7:novita"
10
  GLM_51_MODEL_ID = "zai-org/GLM-5.1:novita"
11
  DEEPSEEK_V4_PRO_MODEL_ID = "deepseek-ai/DeepSeek-V4-Pro:novita"
12
 
13
- DEFAULT_MODEL_ID = CLAUDE_OPUS_48_MODEL_ID
14
-
15
  HOSTED_MODEL_IDS = {
16
  CLAUDE_OPUS_48_MODEL_ID,
17
  GPT_55_MODEL_ID,
 
10
  GLM_51_MODEL_ID = "zai-org/GLM-5.1:novita"
11
  DEEPSEEK_V4_PRO_MODEL_ID = "deepseek-ai/DeepSeek-V4-Pro:novita"
12
 
 
 
13
  HOSTED_MODEL_IDS = {
14
  CLAUDE_OPUS_48_MODEL_ID,
15
  GPT_55_MODEL_ID,
agent/core/session.py CHANGED
@@ -80,7 +80,6 @@ def _get_max_tokens_safe(model_name: str) -> int:
80
  class OpType(Enum):
81
  USER_INPUT = "user_input"
82
  EXEC_APPROVAL = "exec_approval"
83
- INTERRUPT = "interrupt"
84
  UNDO = "undo"
85
  COMPACT = "compact"
86
  NEW = "new"
@@ -631,26 +630,6 @@ class Session:
631
  logger.error(f"Failed to save session locally: {e}")
632
  return None
633
 
634
- def update_local_save_status(
635
- self, filepath: str, upload_status: str, dataset_url: Optional[str] = None
636
- ) -> bool:
637
- """Update the upload status of an existing local save file"""
638
- try:
639
- with open(filepath, "r") as f:
640
- data = json.load(f)
641
-
642
- data["upload_status"] = upload_status
643
- data["upload_url"] = dataset_url
644
- data["last_save_time"] = datetime.now().isoformat()
645
-
646
- with open(filepath, "w") as f:
647
- json.dump(data, f, indent=2)
648
-
649
- return True
650
- except Exception as e:
651
- logger.error(f"Failed to update local save status: {e}")
652
- return False
653
-
654
  def _personal_trace_repo_id(self) -> Optional[str]:
655
  """Resolve the per-user trace repo id from config + HF username.
656
 
 
80
  class OpType(Enum):
81
  USER_INPUT = "user_input"
82
  EXEC_APPROVAL = "exec_approval"
 
83
  UNDO = "undo"
84
  COMPACT = "compact"
85
  NEW = "new"
 
630
  logger.error(f"Failed to save session locally: {e}")
631
  return None
632
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  def _personal_trace_repo_id(self) -> Optional[str]:
634
  """Resolve the per-user trace repo id from config + HF username.
635
 
agent/core/session_persistence.py CHANGED
@@ -518,10 +518,3 @@ def get_session_store() -> NoopSessionStore | MongoSessionStore:
518
  db_name = os.environ.get("MONGODB_DB", "ml-intern")
519
  _store = MongoSessionStore(uri, db_name) if uri else NoopSessionStore()
520
  return _store
521
-
522
-
523
- def _reset_store_for_tests(
524
- store: NoopSessionStore | MongoSessionStore | None = None,
525
- ) -> None:
526
- global _store
527
- _store = store
 
518
  db_name = os.environ.get("MONGODB_DB", "ml-intern")
519
  _store = MongoSessionStore(uri, db_name) if uri else NoopSessionStore()
520
  return _store
 
 
 
 
 
 
 
agent/core/telemetry.py CHANGED
@@ -326,30 +326,6 @@ async def record_feedback(
326
  logger.debug("record_feedback failed (non-fatal): %s", e)
327
 
328
 
329
- async def record_jobs_access_blocked(
330
- session: Any,
331
- *,
332
- tool_call_ids: list[str],
333
- plan: str,
334
- eligible_namespaces: list[str],
335
- ) -> None:
336
- from agent.core.session import Event
337
-
338
- try:
339
- await session.send_event(
340
- Event(
341
- event_type="jobs_access_blocked",
342
- data={
343
- "tool_call_ids": tool_call_ids,
344
- "plan": plan,
345
- "eligible_namespaces": eligible_namespaces,
346
- },
347
- )
348
- )
349
- except Exception as e:
350
- logger.debug("record_jobs_access_blocked failed (non-fatal): %s", e)
351
-
352
-
353
  async def record_pro_cta_click(
354
  session: Any,
355
  *,
 
326
  logger.debug("record_feedback failed (non-fatal): %s", e)
327
 
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  async def record_pro_cta_click(
330
  session: Any,
331
  *,
agent/core/tools.py CHANGED
@@ -51,12 +51,6 @@ from agent.tools.research_tool import RESEARCH_TOOL_SPEC, research_handler
51
  from agent.tools.sandbox_tool import get_sandbox_tools
52
  from agent.tools.web_search_tool import WEB_SEARCH_TOOL_SPEC, web_search_handler
53
 
54
- # NOTE: Private HF repo tool disabled - replaced by hf_repo_files and hf_repo_git
55
- # from agent.tools.private_hf_repo_tools import (
56
- # PRIVATE_HF_REPO_TOOL_SPEC,
57
- # private_hf_repo_handler,
58
- # )
59
-
60
  # Suppress aiohttp deprecation warning
61
  warnings.filterwarnings(
62
  "ignore", category=DeprecationWarning, module="aiohttp.connector"
 
51
  from agent.tools.sandbox_tool import get_sandbox_tools
52
  from agent.tools.web_search_tool import WEB_SEARCH_TOOL_SPEC, web_search_handler
53
 
 
 
 
 
 
 
54
  # Suppress aiohttp deprecation warning
55
  warnings.filterwarnings(
56
  "ignore", category=DeprecationWarning, module="aiohttp.connector"
agent/core/usage_thresholds.py CHANGED
@@ -1,13 +1,11 @@
1
  """Helpers for session usage-threshold approval warnings."""
2
 
3
- from typing import Any, Literal
4
 
5
  USAGE_THRESHOLD_TOOL_NAME = "usage_threshold"
6
  USAGE_WARNING_FIRST_THRESHOLD_USD = 5.0
7
  USAGE_WARNING_MULTIPLIER = 2.0
8
 
9
- UsageApprovalContinuation = Literal["continue_agent", "complete_turn"]
10
-
11
 
12
  def normalize_usage_threshold(value: Any) -> float:
13
  """Return a usable positive threshold, defaulting to the first warning."""
 
1
  """Helpers for session usage-threshold approval warnings."""
2
 
3
+ from typing import Any
4
 
5
  USAGE_THRESHOLD_TOOL_NAME = "usage_threshold"
6
  USAGE_WARNING_FIRST_THRESHOLD_USD = 5.0
7
  USAGE_WARNING_MULTIPLIER = 2.0
8
 
 
 
9
 
10
  def normalize_usage_threshold(value: Any) -> float:
11
  """Return a usable positive threshold, defaulting to the first warning."""
agent/tools/hf_repo_files_tool.py CHANGED
@@ -5,7 +5,7 @@ Operations: list, read, upload, delete
5
  """
6
 
7
  import asyncio
8
- from typing import Any, Dict, Literal, Optional
9
 
10
  from huggingface_hub import HfApi, hf_hub_download
11
  from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
@@ -13,8 +13,6 @@ from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
13
  from agent.core.hub_artifacts import is_known_hub_artifact, register_hub_artifact
14
  from agent.tools.types import ToolResult
15
 
16
- OperationType = Literal["list", "read", "upload", "delete"]
17
-
18
 
19
  async def _async_call(func, *args, **kwargs):
20
  """Wrap synchronous HfApi calls for async context."""
 
5
  """
6
 
7
  import asyncio
8
+ from typing import Any, Dict, Optional
9
 
10
  from huggingface_hub import HfApi, hf_hub_download
11
  from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
 
13
  from agent.core.hub_artifacts import is_known_hub_artifact, register_hub_artifact
14
  from agent.tools.types import ToolResult
15
 
 
 
16
 
17
  async def _async_call(func, *args, **kwargs):
18
  """Wrap synchronous HfApi calls for async context."""
agent/tools/hf_repo_git_tool.py CHANGED
@@ -5,7 +5,7 @@ Operations: branches, tags, PRs, repo management
5
  """
6
 
7
  import asyncio
8
- from typing import Any, Dict, Literal, Optional
9
 
10
  from huggingface_hub import HfApi
11
  from huggingface_hub.utils import RepositoryNotFoundError
@@ -13,23 +13,6 @@ from huggingface_hub.utils import RepositoryNotFoundError
13
  from agent.core.hub_artifacts import register_hub_artifact
14
  from agent.tools.types import ToolResult
15
 
16
- OperationType = Literal[
17
- "create_branch",
18
- "delete_branch",
19
- "create_tag",
20
- "delete_tag",
21
- "list_refs",
22
- "create_pr",
23
- "list_prs",
24
- "get_pr",
25
- "merge_pr",
26
- "close_pr",
27
- "comment_pr",
28
- "change_pr_status",
29
- "create_repo",
30
- "update_repo",
31
- ]
32
-
33
 
34
  async def _async_call(func, *args, **kwargs):
35
  """Wrap synchronous HfApi calls for async context."""
 
5
  """
6
 
7
  import asyncio
8
+ from typing import Any, Dict, Optional
9
 
10
  from huggingface_hub import HfApi
11
  from huggingface_hub.utils import RepositoryNotFoundError
 
13
  from agent.core.hub_artifacts import register_hub_artifact
14
  from agent.tools.types import ToolResult
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  async def _async_call(func, *args, **kwargs):
18
  """Wrap synchronous HfApi calls for async context."""
agent/tools/jobs_tool.py CHANGED
@@ -10,7 +10,7 @@ import http.client
10
  import logging
11
  import re
12
  import shlex
13
- from typing import Any, Awaitable, Callable, Dict, Literal, Optional
14
 
15
  import httpx
16
  from huggingface_hub import HfApi
@@ -63,24 +63,6 @@ GPU_FLAVORS_DESC = (
63
  "l4x1(8vCPU/30GB/GPU 24GB), l4x4(48vCPU/186GB/GPU 96GB), "
64
  "l40sx1(8vCPU/62GB/GPU 48GB), l40sx4(48vCPU/382GB/GPU 192GB), l40sx8(192vCPU/1534GB/GPU 384GB)"
65
  )
66
- SPECIALIZED_FLAVORS = ["inf2x6"]
67
- ALL_FLAVORS = CPU_FLAVORS + GPU_FLAVORS + SPECIALIZED_FLAVORS
68
-
69
- # Operation names
70
- OperationType = Literal[
71
- "run",
72
- "ps",
73
- "logs",
74
- "inspect",
75
- "cancel",
76
- "scheduled run",
77
- "scheduled ps",
78
- "scheduled inspect",
79
- "scheduled delete",
80
- "scheduled suspend",
81
- "scheduled resume",
82
- ]
83
-
84
  # Constants
85
  UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
86
 
 
10
  import logging
11
  import re
12
  import shlex
13
+ from typing import Any, Awaitable, Callable, Dict, Optional
14
 
15
  import httpx
16
  from huggingface_hub import HfApi
 
63
  "l4x1(8vCPU/30GB/GPU 24GB), l4x4(48vCPU/186GB/GPU 96GB), "
64
  "l40sx1(8vCPU/62GB/GPU 48GB), l40sx4(48vCPU/382GB/GPU 192GB), l40sx8(192vCPU/1534GB/GPU 384GB)"
65
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  # Constants
67
  UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
68
 
agent/tools/papers_tool.py CHANGED
@@ -120,19 +120,6 @@ async def _s2_get_json(
120
  return None
121
 
122
 
123
- async def _s2_get_paper(
124
- client: httpx.AsyncClient,
125
- arxiv_id: str,
126
- fields: str,
127
- ) -> dict | None:
128
- """Fetch a single paper from S2 by arxiv ID. Returns None on failure."""
129
- return await _s2_get_json(
130
- client,
131
- f"/graph/v1/paper/{_s2_paper_id(arxiv_id)}",
132
- {"fields": fields},
133
- )
134
-
135
-
136
  # ---------------------------------------------------------------------------
137
  # HTML paper parsing
138
  # ---------------------------------------------------------------------------
 
120
  return None
121
 
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  # ---------------------------------------------------------------------------
124
  # HTML paper parsing
125
  # ---------------------------------------------------------------------------
agent/tools/private_hf_repo_tools.py DELETED
@@ -1,650 +0,0 @@
1
- """
2
- Private HF Repos Tool - Manage private Hugging Face repositories
3
-
4
- PRIMARY USE: Store job outputs, training scripts, and logs from HF Jobs.
5
- Since job results are ephemeral, this tool provides persistent storage in private repos.
6
-
7
- SECONDARY USE: Read back stored files and list repo contents.
8
- """
9
-
10
- import asyncio
11
- from typing import Any, Dict, Literal, Optional
12
-
13
- from huggingface_hub import HfApi, hf_hub_download
14
- from huggingface_hub.utils import HfHubHTTPError
15
-
16
- from agent.tools.types import ToolResult
17
-
18
- # Operation names
19
- OperationType = Literal[
20
- "upload_file", "create_repo", "check_repo", "list_files", "read_file"
21
- ]
22
-
23
-
24
- async def _async_call(func, *args, **kwargs):
25
- """Wrap synchronous HfApi calls for async context."""
26
- return await asyncio.to_thread(func, *args, **kwargs)
27
-
28
-
29
- def _build_repo_url(repo_id: str, repo_type: str = "dataset") -> str:
30
- """Build the Hub URL for a repository."""
31
- type_path = "" if repo_type == "model" else f"{repo_type}s"
32
- return f"https://huggingface.co/{type_path}/{repo_id}".replace("//", "/")
33
-
34
-
35
- def _content_to_bytes(content: str | bytes) -> bytes:
36
- """Convert string or bytes content to bytes."""
37
- if isinstance(content, str):
38
- return content.encode("utf-8")
39
- return content
40
-
41
-
42
- class PrivateHfRepoTool:
43
- """Tool for managing private Hugging Face repositories."""
44
-
45
- def __init__(self, hf_token: Optional[str] = None):
46
- self.api = HfApi(token=hf_token)
47
-
48
- async def execute(self, params: Dict[str, Any]) -> ToolResult:
49
- """Execute the specified upload operation."""
50
- operation = params.get("operation")
51
- args = params.get("args", {})
52
-
53
- # If no operation provided, return usage instructions
54
- if not operation:
55
- return self._show_help()
56
-
57
- # Normalize operation name
58
- operation = operation.lower()
59
-
60
- # Check if help is requested
61
- if args.get("help"):
62
- return self._show_operation_help(operation)
63
-
64
- try:
65
- # Route to appropriate handler
66
- if operation == "upload_file":
67
- return await self._upload_file(args)
68
- elif operation == "create_repo":
69
- return await self._create_repo(args)
70
- elif operation == "check_repo":
71
- return await self._check_repo(args)
72
- elif operation == "list_files":
73
- return await self._list_files(args)
74
- elif operation == "read_file":
75
- return await self._read_file(args)
76
- else:
77
- return {
78
- "formatted": f'Unknown operation: "{operation}"\n\n'
79
- "Available operations: upload_file, create_repo, check_repo, list_files, read_file\n\n"
80
- "Call this tool with no operation for full usage instructions.",
81
- "totalResults": 0,
82
- "resultsShared": 0,
83
- "isError": True,
84
- }
85
-
86
- except HfHubHTTPError as e:
87
- return {
88
- "formatted": f"API Error: {str(e)}",
89
- "totalResults": 0,
90
- "resultsShared": 0,
91
- "isError": True,
92
- }
93
- except Exception as e:
94
- return {
95
- "formatted": f"Error executing {operation}: {str(e)}",
96
- "totalResults": 0,
97
- "resultsShared": 0,
98
- "isError": True,
99
- }
100
-
101
- def _show_help(self) -> ToolResult:
102
- """Show usage instructions when tool is called with no arguments."""
103
- usage_text = """# Private HF Repos Tool
104
-
105
- **PRIMARY USE:** Store job outputs, scripts, and logs from HF Jobs to private repos.
106
- Since job results are ephemeral, use this tool for persistent storage.
107
-
108
- **SECONDARY USE:** Read back stored files and list repo contents.
109
-
110
- ## Available Commands
111
-
112
- ### Write Operations
113
- - **upload_file** - Upload file content to a repository
114
- - **create_repo** - Create a new private repository
115
-
116
- ### Read Operations
117
- - **list_files** - List all files in a repository
118
- - **read_file** - Read content of a specific file from a repository
119
- - **check_repo** - Check if a repository exists
120
-
121
- ## Examples
122
-
123
- ### Upload a script to a dataset repo
124
- Call this tool with:
125
- ```json
126
- {
127
- "operation": "upload_file",
128
- "args": {
129
- "file_content": "import pandas as pd\\nprint('Hello from HF!')",
130
- "path_in_repo": "scripts/hello.py",
131
- "repo_id": "my-dataset",
132
- "repo_type": "dataset",
133
- "create_if_missing": true,
134
- "commit_message": "Add hello script"
135
- }
136
- }
137
- ```
138
-
139
- ### Upload logs from a job
140
- Call this tool with:
141
- ```json
142
- {
143
- "operation": "upload_file",
144
- "args": {
145
- "file_content": "Job started...\\nJob completed successfully!",
146
- "path_in_repo": "jobs/job-abc123/logs.txt",
147
- "repo_id": "job-results",
148
- "create_if_missing": true
149
- }
150
- }
151
- ```
152
-
153
- ### Create a repository
154
- Call this tool with:
155
- ```json
156
- {
157
- "operation": "create_repo",
158
- "args": {
159
- "repo_id": "my-results",
160
- "repo_type": "dataset"
161
- }
162
- }
163
- ```
164
-
165
- ### Create a Space
166
- Call this tool with:
167
- ```json
168
- {
169
- "operation": "create_repo",
170
- "args": {
171
- "repo_id": "my-gradio-app",
172
- "repo_type": "space",
173
- "space_sdk": "gradio"
174
- }
175
- }
176
- ```
177
- Note: Repositories are always created as private. For spaces, `space_sdk` is required (gradio, streamlit, static, or docker).
178
-
179
- ### Check if a repository exists
180
- Call this tool with:
181
- ```json
182
- {
183
- "operation": "check_repo",
184
- "args": {
185
- "repo_id": "my-dataset",
186
- "repo_type": "dataset"
187
- }
188
- }
189
- ```
190
-
191
- ### List files in a repository
192
- Call this tool with:
193
- ```json
194
- {
195
- "operation": "list_files",
196
- "args": {
197
- "repo_id": "job-results",
198
- "repo_type": "dataset"
199
- }
200
- }
201
- ```
202
-
203
- ### Read a file from a repository
204
- Call this tool with:
205
- ```json
206
- {
207
- "operation": "read_file",
208
- "args": {
209
- "repo_id": "job-results",
210
- "path_in_repo": "jobs/job-abc123/script.py",
211
- "repo_type": "dataset"
212
- }
213
- }
214
- ```
215
-
216
- ## Repository Types
217
-
218
- - **dataset** (default) - For storing data, results, logs, scripts
219
- - **model** - For ML models and related artifacts
220
- - **space** - For Spaces and applications
221
-
222
- ## Tips
223
-
224
- - **Content-based**: Pass file content directly as strings or bytes, not file paths
225
- - **Repo ID format**: Use just the repo name (e.g., "my-dataset"). Username is automatically inferred from HF_TOKEN
226
- - **Automatic repo creation**: Set `create_if_missing: true` to auto-create repos (requires user approval)
227
- - **Organization**: Use path_in_repo to organize files (e.g., "jobs/job-123/script.py")
228
- - **After jobs**: Upload job scripts and logs after compute jobs complete for reproducibility
229
- """
230
- return {"formatted": usage_text, "totalResults": 1, "resultsShared": 1}
231
-
232
- def _show_operation_help(self, operation: str) -> ToolResult:
233
- """Show help for a specific operation."""
234
- help_text = f"Help for operation: {operation}\n\nCall with appropriate arguments. Use the main help for examples."
235
- return {"formatted": help_text, "totalResults": 1, "resultsShared": 1}
236
-
237
- async def _upload_file(self, args: Dict[str, Any]) -> ToolResult:
238
- """Upload file content to a Hub repository."""
239
- # Validate required arguments
240
- file_content = args.get("file_content")
241
- path_in_repo = args.get("path_in_repo")
242
- repo_id = args.get("repo_id")
243
-
244
- if not file_content:
245
- return {
246
- "formatted": "file_content is required",
247
- "totalResults": 0,
248
- "resultsShared": 0,
249
- "isError": True,
250
- }
251
-
252
- if not path_in_repo:
253
- return {
254
- "formatted": "path_in_repo is required",
255
- "totalResults": 0,
256
- "resultsShared": 0,
257
- "isError": True,
258
- }
259
-
260
- if not repo_id:
261
- return {
262
- "formatted": "repo_id is required",
263
- "totalResults": 0,
264
- "resultsShared": 0,
265
- "isError": True,
266
- }
267
-
268
- repo_type = args.get("repo_type", "dataset")
269
- create_if_missing = args.get("create_if_missing", False)
270
-
271
- # Check if repo exists
272
- try:
273
- repo_exists = await _async_call(
274
- self.api.repo_exists, repo_id=repo_id, repo_type=repo_type
275
- )
276
-
277
- # Create repo if needed
278
- if not repo_exists and create_if_missing:
279
- create_args = {
280
- "repo_id": repo_id,
281
- "repo_type": repo_type,
282
- "private": True,
283
- }
284
- # Pass through space_sdk if provided (required for spaces)
285
- if "space_sdk" in args:
286
- create_args["space_sdk"] = args["space_sdk"]
287
- await self._create_repo(create_args)
288
- elif not repo_exists:
289
- return {
290
- "formatted": f"Repository {repo_id} does not exist. Set create_if_missing: true to create it.",
291
- "totalResults": 0,
292
- "resultsShared": 0,
293
- "isError": True,
294
- }
295
-
296
- except Exception as e:
297
- return {
298
- "formatted": f"Failed to check repository: {str(e)}",
299
- "totalResults": 0,
300
- "resultsShared": 0,
301
- "isError": True,
302
- }
303
-
304
- # Convert content to bytes
305
- file_bytes = _content_to_bytes(file_content)
306
-
307
- # Upload file
308
- try:
309
- await _async_call(
310
- self.api.upload_file,
311
- path_or_fileobj=file_bytes,
312
- path_in_repo=path_in_repo,
313
- repo_id=repo_id,
314
- repo_type=repo_type,
315
- commit_message=args.get("commit_message", f"Upload {path_in_repo}"),
316
- )
317
-
318
- repo_url = _build_repo_url(repo_id, repo_type)
319
- file_url = f"{repo_url}/blob/main/{path_in_repo}"
320
-
321
- response = f"""✓ File uploaded successfully!
322
-
323
- **Repository:** {repo_id}
324
- **File:** {path_in_repo}
325
- **View at:** {file_url}
326
- **Browse repo:** {repo_url}"""
327
-
328
- return {"formatted": response, "totalResults": 1, "resultsShared": 1}
329
-
330
- except Exception as e:
331
- return {
332
- "formatted": f"Failed to upload file: {str(e)}",
333
- "totalResults": 0,
334
- "resultsShared": 0,
335
- "isError": True,
336
- }
337
-
338
- async def _create_repo(self, args: Dict[str, Any]) -> ToolResult:
339
- """Create a new Hub repository."""
340
- repo_id = args.get("repo_id")
341
-
342
- if not repo_id:
343
- return {
344
- "formatted": "repo_id is required",
345
- "totalResults": 0,
346
- "resultsShared": 0,
347
- "isError": True,
348
- }
349
-
350
- repo_type = args.get("repo_type", "dataset")
351
- private = True # Always create private repos
352
- space_sdk = args.get("space_sdk") # Required if repo_type is "space"
353
-
354
- try:
355
- # Check if repo already exists
356
- repo_exists = await _async_call(
357
- self.api.repo_exists, repo_id=repo_id, repo_type=repo_type
358
- )
359
-
360
- if repo_exists:
361
- repo_url = _build_repo_url(repo_id, repo_type)
362
- return {
363
- "formatted": f"Repository {repo_id} already exists.\n**View at:** {repo_url}",
364
- "totalResults": 1,
365
- "resultsShared": 1,
366
- }
367
-
368
- # Validate space_sdk for spaces
369
- if repo_type == "space" and not space_sdk:
370
- return {
371
- "formatted": "space_sdk is required when creating a space. Valid values: gradio, streamlit, static, docker",
372
- "totalResults": 0,
373
- "resultsShared": 0,
374
- "isError": True,
375
- }
376
-
377
- # Create repository
378
- create_kwargs = {
379
- "repo_id": repo_id,
380
- "repo_type": repo_type,
381
- "private": private,
382
- "exist_ok": True,
383
- }
384
- # Add space_sdk only for spaces
385
- if repo_type == "space" and space_sdk:
386
- create_kwargs["space_sdk"] = space_sdk
387
-
388
- repo_url = await _async_call(self.api.create_repo, **create_kwargs)
389
-
390
- response = f"""✓ Repository created successfully!
391
-
392
- **Repository:** {repo_id}
393
- **Type:** {repo_type}
394
- **Private:** Yes
395
- **View at:** {repo_url}"""
396
-
397
- return {"formatted": response, "totalResults": 1, "resultsShared": 1}
398
-
399
- except Exception as e:
400
- return {
401
- "formatted": f"Failed to create repository: {str(e)}",
402
- "totalResults": 0,
403
- "resultsShared": 0,
404
- "isError": True,
405
- }
406
-
407
- async def _check_repo(self, args: Dict[str, Any]) -> ToolResult:
408
- """Check if a Hub repository exists."""
409
- repo_id = args.get("repo_id")
410
-
411
- if not repo_id:
412
- return {
413
- "formatted": "repo_id is required",
414
- "totalResults": 0,
415
- "resultsShared": 0,
416
- "isError": True,
417
- }
418
-
419
- repo_type = args.get("repo_type", "dataset")
420
-
421
- try:
422
- repo_exists = await _async_call(
423
- self.api.repo_exists, repo_id=repo_id, repo_type=repo_type
424
- )
425
-
426
- if repo_exists:
427
- repo_url = _build_repo_url(repo_id, repo_type)
428
- response = f"""✓ Repository exists!
429
-
430
- **Repository:** {repo_id}
431
- **Type:** {repo_type}
432
- **View at:** {repo_url}"""
433
- else:
434
- response = f"""Repository does not exist: {repo_id}
435
-
436
- To create it, call this tool with:
437
- ```json
438
- {{
439
- "operation": "create_repo",
440
- "args": {{
441
- "repo_id": "{repo_id}",
442
- "repo_type": "{repo_type}"
443
- }}
444
- }}
445
- ```"""
446
-
447
- return {
448
- "formatted": response,
449
- "totalResults": 1 if repo_exists else 0,
450
- "resultsShared": 1 if repo_exists else 0,
451
- }
452
-
453
- except Exception as e:
454
- return {
455
- "formatted": f"Failed to check repository: {str(e)}",
456
- "totalResults": 0,
457
- "resultsShared": 0,
458
- "isError": True,
459
- }
460
-
461
- async def _list_files(self, args: Dict[str, Any]) -> ToolResult:
462
- """List all files in a Hub repository."""
463
- repo_id = args.get("repo_id")
464
-
465
- if not repo_id:
466
- return {
467
- "formatted": "repo_id is required",
468
- "totalResults": 0,
469
- "resultsShared": 0,
470
- "isError": True,
471
- }
472
-
473
- repo_type = args.get("repo_type", "dataset")
474
-
475
- try:
476
- # List all files in the repository
477
- files = await _async_call(
478
- self.api.list_repo_files, repo_id=repo_id, repo_type=repo_type
479
- )
480
-
481
- if not files:
482
- return {
483
- "formatted": f"No files found in repository: {repo_id}",
484
- "totalResults": 0,
485
- "resultsShared": 0,
486
- }
487
-
488
- # Format file list
489
- file_list = "\n".join(f"- {f}" for f in sorted(files))
490
- repo_url = _build_repo_url(repo_id, repo_type)
491
-
492
- response = f"""✓ Files in repository: {repo_id}
493
-
494
- **Total files:** {len(files)}
495
- **Repository URL:** {repo_url}
496
-
497
- **Files:**
498
- {file_list}"""
499
-
500
- return {
501
- "formatted": response,
502
- "totalResults": len(files),
503
- "resultsShared": len(files),
504
- }
505
-
506
- except Exception as e:
507
- return {
508
- "formatted": f"Failed to list files: {str(e)}",
509
- "totalResults": 0,
510
- "resultsShared": 0,
511
- "isError": True,
512
- }
513
-
514
- async def _read_file(self, args: Dict[str, Any]) -> ToolResult:
515
- """Read content of a specific file from a Hub repository."""
516
- repo_id = args.get("repo_id")
517
- path_in_repo = args.get("path_in_repo")
518
-
519
- if not repo_id:
520
- return {
521
- "formatted": "repo_id is required",
522
- "totalResults": 0,
523
- "resultsShared": 0,
524
- "isError": True,
525
- }
526
-
527
- if not path_in_repo:
528
- return {
529
- "formatted": "path_in_repo is required",
530
- "totalResults": 0,
531
- "resultsShared": 0,
532
- "isError": True,
533
- }
534
-
535
- repo_type = args.get("repo_type", "dataset")
536
-
537
- try:
538
- # Download file to cache and read it
539
- file_path = await _async_call(
540
- hf_hub_download,
541
- repo_id=repo_id,
542
- filename=path_in_repo,
543
- repo_type=repo_type,
544
- token=self.api.token,
545
- )
546
-
547
- # Read file content
548
- with open(file_path, "r", encoding="utf-8") as f:
549
- content = f.read()
550
-
551
- repo_url = _build_repo_url(repo_id, repo_type)
552
- file_url = f"{repo_url}/blob/main/{path_in_repo}"
553
-
554
- response = f"""✓ File read successfully!
555
-
556
- **Repository:** {repo_id}
557
- **File:** {path_in_repo}
558
- **Size:** {len(content)} characters
559
- **View at:** {file_url}
560
-
561
- **Content:**
562
- ```
563
- {content}
564
- ```"""
565
-
566
- return {"formatted": response, "totalResults": 1, "resultsShared": 1}
567
-
568
- except UnicodeDecodeError:
569
- # If file is binary, return size info instead
570
- try:
571
- with open(file_path, "rb") as f:
572
- binary_content = f.read()
573
-
574
- return {
575
- "formatted": f"File is binary ({len(binary_content)} bytes). Cannot display as text.",
576
- "totalResults": 1,
577
- "resultsShared": 1,
578
- }
579
- except Exception as e:
580
- return {
581
- "formatted": f"Failed to read binary file: {str(e)}",
582
- "totalResults": 0,
583
- "resultsShared": 0,
584
- "isError": True,
585
- }
586
- except Exception as e:
587
- return {
588
- "formatted": f"Failed to read file: {str(e)}",
589
- "totalResults": 0,
590
- "resultsShared": 0,
591
- "isError": True,
592
- }
593
-
594
-
595
- # Tool specification for agent registration
596
- PRIVATE_HF_REPO_TOOL_SPEC = {
597
- "name": "hf_private_repos",
598
- "description": (
599
- "Manage private HF repositories - create, upload, read, list files in models/datasets/spaces. "
600
- "⚠️ PRIMARY USE: Store job outputs persistently (job storage is EPHEMERAL - everything deleted after completion). "
601
- "**Use when:** (1) Job completes and need to store logs/scripts/results, (2) Creating repos for training outputs, "
602
- "(3) Reading back stored files, (4) Managing Space files, (5) Organizing job artifacts by path. "
603
- "**Pattern:** hf_jobs (ephemeral) → hf_private_repos upload_file (persistent) → can read_file later. "
604
- "ALWAYS pass file_content as string/bytes (✓), never file paths (✗) - this is content-based, no filesystem access. "
605
- "**Operations:** create_repo (new private repo), upload_file (store content), read_file (retrieve content), list_files (browse), check_repo (verify exists). "
606
- "**Critical for reliability:** Jobs lose all files after completion - use this tool to preserve important outputs. "
607
- "Repositories created are ALWAYS private by default (good for sensitive training data/models). "
608
- "For Spaces: must provide space_sdk ('gradio', 'streamlit', 'static', 'docker') when creating. "
609
- "**Then:** After uploading, provide user with repository URL for viewing/sharing."
610
- ),
611
- "parameters": {
612
- "type": "object",
613
- "properties": {
614
- "operation": {
615
- "type": "string",
616
- "enum": [
617
- "upload_file",
618
- "create_repo",
619
- "check_repo",
620
- "list_files",
621
- "read_file",
622
- ],
623
- "description": (
624
- "Operation to execute. Valid values: [upload_file, create_repo, check_repo, list_files, read_file]"
625
- ),
626
- },
627
- "args": {
628
- "type": "object",
629
- "description": (
630
- "Operation-specific arguments as a JSON object. "
631
- "Write ops: file_content (string/bytes), path_in_repo (string), repo_id (string), "
632
- "repo_type (dataset/model/space), create_if_missing (boolean), commit_message (string), "
633
- "space_sdk (gradio/streamlit/static/docker - required when repo_type=space). "
634
- "Read ops: repo_id (string), path_in_repo (for read_file), repo_type (optional)."
635
- ),
636
- "additionalProperties": True,
637
- },
638
- },
639
- },
640
- }
641
-
642
-
643
- async def private_hf_repo_handler(arguments: Dict[str, Any]) -> tuple[str, bool]:
644
- """Handler for agent tool router."""
645
- try:
646
- tool = PrivateHfRepoTool()
647
- result = await tool.execute(arguments)
648
- return result["formatted"], not result.get("isError", False)
649
- except Exception as e:
650
- return f"Error executing Private HF Repo tool: {str(e)}", False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agent/tools/sandbox_client.py CHANGED
@@ -48,17 +48,6 @@ import httpx
48
  from huggingface_hub import CommitOperationAdd, HfApi
49
 
50
  TEMPLATE_SPACE = "burtenshaw/sandbox"
51
- HARDWARE_OPTIONS = [
52
- "cpu-basic",
53
- "cpu-upgrade",
54
- "t4-small",
55
- "t4-medium",
56
- "a10g-small",
57
- "a10g-large",
58
- "a100-large",
59
- ]
60
- OUTPUT_LIMIT = 25000
61
- LINE_LIMIT = 4000
62
  DEFAULT_READ_LIMIT = 2000
63
  DEFAULT_TIMEOUT = 240
64
  MAX_TIMEOUT = 1200
@@ -497,9 +486,6 @@ class ToolResult:
497
  return self.output or "(no output)"
498
  return f"ERROR: {self.error}"
499
 
500
- def to_dict(self) -> dict:
501
- return {"success": self.success, "output": self.output, "error": self.error}
502
-
503
 
504
  @dataclass
505
  class Sandbox:
@@ -794,15 +780,6 @@ class Sandbox:
794
  if log:
795
  log("Deleted.")
796
 
797
- def pause(self):
798
- """Pause the Space (stops billing, preserves state)."""
799
- self._hf_api.pause_space(self.space_id)
800
-
801
- def restart(self):
802
- """Restart the Space."""
803
- self._hf_api.restart_space(self.space_id)
804
- self._wait_for_api()
805
-
806
  @property
807
  def url(self) -> str:
808
  """Public URL of the Space."""
@@ -1131,10 +1108,6 @@ class Sandbox:
1131
  },
1132
  }
1133
 
1134
- @classmethod
1135
- def tool_definitions(cls) -> list[dict]:
1136
- return [{"name": name, **spec} for name, spec in cls.TOOLS.items()]
1137
-
1138
  def call_tool(self, name: str, arguments: dict[str, Any]) -> ToolResult:
1139
  dispatch = {
1140
  "bash": lambda a: self.bash(
 
48
  from huggingface_hub import CommitOperationAdd, HfApi
49
 
50
  TEMPLATE_SPACE = "burtenshaw/sandbox"
 
 
 
 
 
 
 
 
 
 
 
51
  DEFAULT_READ_LIMIT = 2000
52
  DEFAULT_TIMEOUT = 240
53
  MAX_TIMEOUT = 1200
 
486
  return self.output or "(no output)"
487
  return f"ERROR: {self.error}"
488
 
 
 
 
489
 
490
  @dataclass
491
  class Sandbox:
 
780
  if log:
781
  log("Deleted.")
782
 
 
 
 
 
 
 
 
 
 
783
  @property
784
  def url(self) -> str:
785
  """Public URL of the Space."""
 
1108
  },
1109
  }
1110
 
 
 
 
 
1111
  def call_tool(self, name: str, arguments: dict[str, Any]) -> ToolResult:
1112
  dispatch = {
1113
  "bash": lambda a: self.bash(
agent/tools/sandbox_tool.py CHANGED
@@ -18,7 +18,7 @@ import threading
18
  import uuid
19
  import weakref
20
  from collections.abc import Callable
21
- from datetime import datetime, timedelta, timezone
22
  from typing import Any
23
 
24
  from huggingface_hub import HfApi, SpaceHardware
@@ -42,11 +42,6 @@ DEFAULT_CPU_SANDBOX_HARDWARE = "cpu-basic"
42
  # user-renamed lookalikes).
43
  SANDBOX_SPACE_NAME_RE = re.compile(r"^sandbox-[a-f0-9]{8}$")
44
 
45
- # How stale a sandbox must be before we treat it as definitely orphan.
46
- # Anything more recent could be tied to a still-live session in another tab,
47
- # so we leave it alone.
48
- _ORPHAN_STALE_AFTER = timedelta(hours=1)
49
-
50
  # HF Space duplication/build APIs can behave poorly when multiple private
51
  # sandboxes are created concurrently for the same namespace. Keep session
52
  # creation non-blocking, but serialize the actual Hub create path per owner.
@@ -344,64 +339,6 @@ async def _clear_persisted_sandbox(session: Any) -> None:
344
  # ── Tool name mapping (short agent names → Sandbox client names) ──────
345
 
346
 
347
- def _cleanup_user_orphan_sandboxes(
348
- api: HfApi,
349
- owner: str,
350
- log: Any,
351
- ) -> int:
352
- """Delete stale ``sandbox-<8hex>`` Spaces in ``owner``'s account.
353
-
354
- "Stale" = not modified in the last hour. The naming pattern + staleness
355
- filter together make this safe:
356
-
357
- * Naming: only matches ``sandbox-<exactly 8 lowercase hex>``, the
358
- pattern Sandbox.create produces. Won't touch user-renamed Spaces.
359
- * Staleness: anything modified in the last hour might still be tied
360
- to a live session in another tab/replica, so we leave it alone.
361
-
362
- Runs blocking — call via ``asyncio.to_thread``. Best-effort: failures
363
- are logged but never raised, so a flaky HF API never blocks creation.
364
- """
365
- cutoff = datetime.now(timezone.utc) - _ORPHAN_STALE_AFTER
366
- deleted = 0
367
- try:
368
- spaces = list(api.list_spaces(author=owner, limit=200, full=True))
369
- except Exception as e:
370
- log(f"orphan sweep: list_spaces failed: {e}")
371
- return 0
372
-
373
- for space in spaces:
374
- space_name = space.id.rsplit("/", 1)[-1]
375
- if not SANDBOX_SPACE_NAME_RE.match(space_name):
376
- continue
377
-
378
- last_mod = getattr(space, "lastModified", None) or getattr(
379
- space, "last_modified", None
380
- )
381
- if isinstance(last_mod, str):
382
- try:
383
- last_mod = datetime.fromisoformat(last_mod.replace("Z", "+00:00"))
384
- except ValueError:
385
- last_mod = None
386
- if last_mod is None:
387
- log(f"orphan sweep: skipping {space.id}; missing lastModified")
388
- continue
389
- if last_mod and last_mod > cutoff:
390
- # Recent — could be a concurrent live session. Skip.
391
- continue
392
-
393
- try:
394
- api.delete_repo(repo_id=space.id, repo_type="space")
395
- deleted += 1
396
- log(f"orphan sweep: deleted {space.id}")
397
- except Exception as e:
398
- log(f"orphan sweep: failed to delete {space.id}: {e}")
399
-
400
- if deleted:
401
- log(f"orphan sweep: cleaned up {deleted} stale sandbox(es) before create")
402
- return deleted
403
-
404
-
405
  async def _ensure_sandbox(
406
  session: Any,
407
  hardware: str = DEFAULT_CPU_SANDBOX_HARDWARE,
 
18
  import uuid
19
  import weakref
20
  from collections.abc import Callable
21
+ from datetime import datetime, timezone
22
  from typing import Any
23
 
24
  from huggingface_hub import HfApi, SpaceHardware
 
42
  # user-renamed lookalikes).
43
  SANDBOX_SPACE_NAME_RE = re.compile(r"^sandbox-[a-f0-9]{8}$")
44
 
 
 
 
 
 
45
  # HF Space duplication/build APIs can behave poorly when multiple private
46
  # sandboxes are created concurrently for the same namespace. Keep session
47
  # creation non-blocking, but serialize the actual Hub create path per owner.
 
339
  # ── Tool name mapping (short agent names → Sandbox client names) ──────
340
 
341
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  async def _ensure_sandbox(
343
  session: Any,
344
  hardware: str = DEFAULT_CPU_SANDBOX_HARDWARE,
agent/utils/particle_logo.py CHANGED
@@ -59,10 +59,6 @@ class Particle:
59
  self.x += self.vx
60
  self.y += self.vy
61
 
62
- @property
63
- def at_target(self) -> bool:
64
- return abs(self.x - self.target_x) < 1.5 and abs(self.y - self.target_y) < 1.5
65
-
66
 
67
  def run_particle_logo(console: Console, hold_seconds: float = 1.5) -> None:
68
  """Run the particle coalesce effect."""
 
59
  self.x += self.vx
60
  self.y += self.vy
61
 
 
 
 
 
62
 
63
  def run_particle_logo(console: Console, hold_seconds: float = 1.5) -> None:
64
  """Run the particle coalesce effect."""
backend/dependencies.py CHANGED
@@ -22,15 +22,11 @@ logger = logging.getLogger(__name__)
22
 
23
  OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
24
  AUTH_ENABLED = bool(os.environ.get("OAUTH_CLIENT_ID", ""))
25
- HF_EMPLOYEE_ORG = os.environ.get("HF_EMPLOYEE_ORG", "huggingface")
26
 
27
  # Simple in-memory token cache: token -> (user_info, expiry_time)
28
  _token_cache: dict[str, tuple[dict[str, Any], float]] = {}
29
  TOKEN_CACHE_TTL = 300 # 5 minutes
30
 
31
- # Org membership cache: key -> expiry_time (only caches positive results)
32
- _org_member_cache: dict[str, float] = {}
33
-
34
  DEV_USER: dict[str, Any] = {
35
  "user_id": "dev",
36
  "username": "dev",
@@ -211,31 +207,6 @@ async def _dev_user_from_env() -> dict[str, Any]:
211
  }
212
 
213
 
214
- async def check_org_membership(token: str, org_name: str) -> bool:
215
- """Check if the token owner belongs to an HF org. Only caches positive results."""
216
- now = time.time()
217
- key = token + org_name
218
- cached = _org_member_cache.get(key)
219
- if cached and cached > now:
220
- return True
221
-
222
- async with httpx.AsyncClient(timeout=10.0) as client:
223
- try:
224
- resp = await client.get(
225
- f"{OPENID_PROVIDER_URL}/api/whoami-v2",
226
- headers={"Authorization": f"Bearer {token}"},
227
- )
228
- if resp.status_code != 200:
229
- return False
230
- orgs = {o.get("name") for o in resp.json().get("orgs", [])}
231
- if org_name in orgs:
232
- _org_member_cache[key] = now + TOKEN_CACHE_TTL
233
- return True
234
- return False
235
- except httpx.HTTPError:
236
- return False
237
-
238
-
239
  async def get_current_user(request: Request) -> dict[str, Any]:
240
  """FastAPI dependency: extract and validate the current user.
241
 
@@ -277,27 +248,3 @@ async def get_current_user(request: Request) -> dict[str, Any]:
277
  detail="Not authenticated. Please log in via /auth/login.",
278
  headers={"WWW-Authenticate": "Bearer"},
279
  )
280
-
281
-
282
- def _extract_token(request: Request) -> str | None:
283
- """Pull the HF access token from the Authorization header or cookie.
284
-
285
- Mirrors the lookup order used by ``get_current_user``.
286
- """
287
- token = bearer_token_from_header(request.headers.get("Authorization", ""))
288
- if token:
289
- return token
290
- return request.cookies.get("hf_access_token")
291
-
292
-
293
- async def require_huggingface_org_member(request: Request) -> bool:
294
- """Return True if the caller is a member of the ``huggingface`` org.
295
-
296
- Returns True unconditionally in dev mode so local testing isn't blocked.
297
- """
298
- if not AUTH_ENABLED:
299
- return True
300
- token = _extract_token(request)
301
- if not token:
302
- return False
303
- return await check_org_membership(token, HF_EMPLOYEE_ORG)
 
22
 
23
  OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
24
  AUTH_ENABLED = bool(os.environ.get("OAUTH_CLIENT_ID", ""))
 
25
 
26
  # Simple in-memory token cache: token -> (user_info, expiry_time)
27
  _token_cache: dict[str, tuple[dict[str, Any], float]] = {}
28
  TOKEN_CACHE_TTL = 300 # 5 minutes
29
 
 
 
 
30
  DEV_USER: dict[str, Any] = {
31
  "user_id": "dev",
32
  "username": "dev",
 
207
  }
208
 
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  async def get_current_user(request: Request) -> dict[str, Any]:
211
  """FastAPI dependency: extract and validate the current user.
212
 
 
248
  detail="Not authenticated. Please log in via /auth/login.",
249
  headers={"WWW-Authenticate": "Bearer"},
250
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/models.py CHANGED
@@ -11,7 +11,6 @@ class OpType(str, Enum):
11
 
12
  USER_INPUT = "user_input"
13
  EXEC_APPROVAL = "exec_approval"
14
- INTERRUPT = "interrupt"
15
  UNDO = "undo"
16
  COMPACT = "compact"
17
  SHUTDOWN = "shutdown"
 
11
 
12
  USER_INPUT = "user_input"
13
  EXEC_APPROVAL = "exec_approval"
 
14
  UNDO = "undo"
15
  COMPACT = "compact"
16
  SHUTDOWN = "shutdown"
backend/session_manager.py CHANGED
@@ -1957,27 +1957,6 @@ class SessionManager:
1957
  self._touch(agent_session)
1958
  return self._auto_approval_summary(session)
1959
 
1960
- def get_session_owner(self, session_id: str) -> str | None:
1961
- """Get the user_id that owns a session, or None if session doesn't exist."""
1962
- agent_session = self.sessions.get(session_id)
1963
- if not agent_session:
1964
- return None
1965
- return agent_session.user_id
1966
-
1967
- def verify_session_access(self, session_id: str, user_id: str) -> bool:
1968
- """Check if a user has access to a session.
1969
-
1970
- Returns True if:
1971
- - The session exists AND the user owns it
1972
- - The user_id is "dev" (dev mode bypass)
1973
- """
1974
- owner = self.get_session_owner(session_id)
1975
- if owner is None:
1976
- return False
1977
- if user_id == "dev" or owner == "dev":
1978
- return True
1979
- return owner == user_id
1980
-
1981
  def get_session_info(self, session_id: str) -> dict[str, Any] | None:
1982
  """Get information about a session."""
1983
  agent_session = self.sessions.get(session_id)
 
1957
  self._touch(agent_session)
1958
  return self._auto_approval_summary(session)
1959
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1960
  def get_session_info(self, session_id: str) -> dict[str, Any] | None:
1961
  """Get information about a session."""
1962
  agent_session = self.sessions.get(session_id)
frontend/src/components/Chat/ThinkingIndicator.tsx DELETED
@@ -1,48 +0,0 @@
1
- import { Box, Typography } from '@mui/material';
2
-
3
- /** Pulsing dots shown while the agent is processing. */
4
- export default function ThinkingIndicator() {
5
- return (
6
- <Box sx={{ pt: 0.75 }}>
7
- <Typography
8
- variant="caption"
9
- sx={{
10
- fontWeight: 700,
11
- fontSize: '0.72rem',
12
- color: 'var(--muted-text)',
13
- textTransform: 'uppercase',
14
- letterSpacing: '0.04em',
15
- display: 'flex',
16
- alignItems: 'center',
17
- gap: 0.75,
18
- }}
19
- >
20
- Thinking
21
- <Box
22
- component="span"
23
- sx={{
24
- display: 'inline-flex',
25
- gap: '3px',
26
- '& span': {
27
- width: 4,
28
- height: 4,
29
- borderRadius: '50%',
30
- bgcolor: 'primary.main',
31
- animation: 'dotPulse 1.4s ease-in-out infinite',
32
- },
33
- '& span:nth-of-type(2)': { animationDelay: '0.2s' },
34
- '& span:nth-of-type(3)': { animationDelay: '0.4s' },
35
- '@keyframes dotPulse': {
36
- '0%, 80%, 100%': { opacity: 0.25, transform: 'scale(0.8)' },
37
- '40%': { opacity: 1, transform: 'scale(1)' },
38
- },
39
- }}
40
- >
41
- <span />
42
- <span />
43
- <span />
44
- </Box>
45
- </Typography>
46
- </Box>
47
- );
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/theme.ts CHANGED
@@ -218,6 +218,3 @@ export const lightTheme = createTheme({
218
  },
219
  shape: sharedShape,
220
  });
221
-
222
- // Keep default export for backwards compat
223
- export default darkTheme;
 
218
  },
219
  shape: sharedShape,
220
  });
 
 
 
frontend/src/types/agent.ts CHANGED
@@ -36,13 +36,6 @@ export interface SessionMeta {
36
  autoApprovalRemainingUsd?: number | null;
37
  }
38
 
39
- export interface ToolApproval {
40
- tool_call_id: string;
41
- approved: boolean;
42
- feedback?: string | null;
43
- namespace?: string | null;
44
- }
45
-
46
  export interface User {
47
  authenticated: boolean;
48
  username?: string;
 
36
  autoApprovalRemainingUsd?: number | null;
37
  }
38
 
 
 
 
 
 
 
 
39
  export interface User {
40
  authenticated: boolean;
41
  username?: string;
frontend/src/types/events.ts CHANGED
@@ -31,68 +31,3 @@ export interface AgentEvent {
31
  data?: Record<string, unknown>;
32
  seq?: number;
33
  }
34
-
35
- export interface ReadyEventData {
36
- message: string;
37
- }
38
-
39
- export interface ProcessingEventData {
40
- message: string;
41
- }
42
-
43
- export interface AssistantMessageEventData {
44
- content: string;
45
- }
46
-
47
- export interface ToolCallEventData {
48
- tool: string;
49
- arguments: Record<string, unknown>;
50
- }
51
-
52
- export interface ToolOutputEventData {
53
- tool: string;
54
- output: string;
55
- success: boolean;
56
- }
57
-
58
- export interface ToolLogEventData {
59
- tool: string;
60
- log: string;
61
- }
62
-
63
- export interface PlanUpdateEventData {
64
- plan: Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed' }>;
65
- }
66
-
67
- export interface ApprovalRequiredEventData {
68
- tools: ApprovalToolItem[];
69
- count: number;
70
- yolo_budget?: boolean;
71
- auto_approval_blocked?: boolean;
72
- block_reason?: string | null;
73
- estimated_cost_usd?: number | null;
74
- remaining_cap_usd?: number | null;
75
- }
76
-
77
- export interface ApprovalToolItem {
78
- tool: string;
79
- arguments: Record<string, unknown>;
80
- tool_call_id: string;
81
- auto_approval_blocked?: boolean;
82
- block_reason?: string | null;
83
- estimated_cost_usd?: number | null;
84
- remaining_cap_usd?: number | null;
85
- }
86
-
87
- export interface TurnCompleteEventData {
88
- history_size: number;
89
- }
90
-
91
- export interface CompactedEventData {
92
- old_tokens: number;
93
- new_tokens: number;
94
- }
95
-
96
- export interface ErrorEventData {
97
- error: string;
98
- }
 
31
  data?: Record<string, unknown>;
32
  seq?: number;
33
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml CHANGED
@@ -12,7 +12,6 @@ dependencies = [
12
  # Agent runtime dependencies
13
  "requests>=2.33.0",
14
  "litellm>=1.83.0",
15
- "boto3>=1.35.0",
16
  "huggingface-hub>=1.12.0",
17
  "fastmcp>=3.2.0",
18
  "prompt-toolkit>=3.0.0",
 
12
  # Agent runtime dependencies
13
  "requests>=2.33.0",
14
  "litellm>=1.83.0",
 
15
  "huggingface-hub>=1.12.0",
16
  "fastmcp>=3.2.0",
17
  "prompt-toolkit>=3.0.0",
scripts/sweep_orphan_sandboxes.py CHANGED
@@ -69,7 +69,6 @@ from huggingface_hub import HfApi
69
  from huggingface_hub.utils import HfHubHTTPError
70
 
71
  SANDBOX_NAME_RE = re.compile(r"^[^/]+/sandbox-[a-f0-9]{8}$")
72
- TEMPLATE_REPO = "burtenshaw/sandbox"
73
 
74
 
75
  def log(record: dict) -> None:
 
69
  from huggingface_hub.utils import HfHubHTTPError
70
 
71
  SANDBOX_NAME_RE = re.compile(r"^[^/]+/sandbox-[a-f0-9]{8}$")
 
72
 
73
 
74
  def log(record: dict) -> None:
tests/integration/test_live_thinking_models.py CHANGED
@@ -20,7 +20,7 @@ from agent.core.agent_loop import (
20
  _call_llm_streaming,
21
  )
22
  from agent.core.llm_params import _resolve_llm_params
23
- from agent.core.model_ids import DEFAULT_MODEL_ID
24
 
25
 
26
  if env_file := os.environ.get("ML_INTERN_LIVE_ENV_FILE"):
@@ -77,9 +77,9 @@ async def test_live_default_router_model_does_not_replay_reasoning_metadata():
77
  _skip_without_live_flag()
78
  _skip_without_hf_token()
79
 
80
- session = _session(DEFAULT_MODEL_ID)
81
  llm_params = _resolve_llm_params(
82
- DEFAULT_MODEL_ID,
83
  os.environ["HF_TOKEN"],
84
  reasoning_effort="low",
85
  )
 
20
  _call_llm_streaming,
21
  )
22
  from agent.core.llm_params import _resolve_llm_params
23
+ from agent.core.model_ids import CLAUDE_OPUS_48_MODEL_ID
24
 
25
 
26
  if env_file := os.environ.get("ML_INTERN_LIVE_ENV_FILE"):
 
77
  _skip_without_live_flag()
78
  _skip_without_hf_token()
79
 
80
+ session = _session(CLAUDE_OPUS_48_MODEL_ID)
81
  llm_params = _resolve_llm_params(
82
+ CLAUDE_OPUS_48_MODEL_ID,
83
  os.environ["HF_TOKEN"],
84
  reasoning_effort="low",
85
  )
tests/unit/test_auto_approval_policy.py CHANGED
@@ -74,9 +74,6 @@ async def test_scheduled_hf_jobs_always_require_manual_approval(operation):
74
  assert decision.requires_approval is True
75
  assert decision.auto_approval_blocked is True
76
  assert "Scheduled HF jobs" in decision.block_reason
77
- assert agent_loop._needs_approval(
78
- "hf_jobs", {"operation": operation}, session.config
79
- )
80
 
81
 
82
  @pytest.mark.asyncio
 
74
  assert decision.requires_approval is True
75
  assert decision.auto_approval_blocked is True
76
  assert "Scheduled HF jobs" in decision.block_reason
 
 
 
77
 
78
 
79
  @pytest.mark.asyncio
tests/unit/test_hf_access.py CHANGED
@@ -1,7 +1,4 @@
1
- import pytest
2
-
3
  from agent.core.hf_access import (
4
- fetch_hf_user_plan,
5
  is_billing_error,
6
  is_inference_billing_error,
7
  jobs_access_from_whoami,
@@ -97,28 +94,3 @@ def test_normalize_hf_user_plan_uses_ispro_only():
97
  assert normalize_hf_user_plan({"isPro": False}) == "free"
98
  assert normalize_hf_user_plan({"plan": "HF Pro"}) == "free"
99
  assert normalize_hf_user_plan(None) is None
100
-
101
-
102
- @pytest.mark.asyncio
103
- async def test_fetch_hf_user_plan_returns_unknown_without_token():
104
- assert await fetch_hf_user_plan(None) == "unknown"
105
-
106
-
107
- @pytest.mark.asyncio
108
- async def test_fetch_hf_user_plan_returns_unknown_when_whoami_unavailable(monkeypatch):
109
- async def fake_fetch_whoami_v2(_token, timeout=5.0):
110
- return None
111
-
112
- monkeypatch.setattr("agent.core.hf_access.fetch_whoami_v2", fake_fetch_whoami_v2)
113
-
114
- assert await fetch_hf_user_plan("hf-token") == "unknown"
115
-
116
-
117
- @pytest.mark.asyncio
118
- async def test_fetch_hf_user_plan_normalizes_whoami(monkeypatch):
119
- async def fake_fetch_whoami_v2(_token, timeout=5.0):
120
- return {"isPro": True}
121
-
122
- monkeypatch.setattr("agent.core.hf_access.fetch_whoami_v2", fake_fetch_whoami_v2)
123
-
124
- assert await fetch_hf_user_plan("hf-token") == "pro"
 
 
 
1
  from agent.core.hf_access import (
 
2
  is_billing_error,
3
  is_inference_billing_error,
4
  jobs_access_from_whoami,
 
94
  assert normalize_hf_user_plan({"isPro": False}) == "free"
95
  assert normalize_hf_user_plan({"plan": "HF Pro"}) == "free"
96
  assert normalize_hf_user_plan(None) is None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/test_sandbox_auto_start.py CHANGED
@@ -6,7 +6,7 @@ import pytest
6
 
7
  from agent.config import Config
8
  from agent.core import agent_loop
9
- from agent.core.agent_loop import _needs_approval
10
  from agent.core.session import OpType
11
  from agent.core.tools import create_builtin_tools
12
  from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC
@@ -16,17 +16,23 @@ from agent.tools.sandbox_tool import get_sandbox_tools
16
  def test_default_cpu_sandbox_create_does_not_require_approval():
17
  config = SimpleNamespace(yolo_mode=False)
18
 
19
- assert _needs_approval("sandbox_create", {}, config) is False
20
- assert _needs_approval("sandbox_create", {"hardware": "cpu-basic"}, config) is False
 
 
 
21
 
22
 
23
  def test_non_default_sandbox_create_still_requires_approval():
24
  config = SimpleNamespace(yolo_mode=False)
25
 
26
  assert (
27
- _needs_approval("sandbox_create", {"hardware": "cpu-upgrade"}, config) is True
 
 
 
 
28
  )
29
- assert _needs_approval("sandbox_create", {"hardware": "t4-small"}, config) is True
30
 
31
 
32
  def test_prompt_and_tool_specs_do_not_require_cpu_sandbox_create():
 
6
 
7
  from agent.config import Config
8
  from agent.core import agent_loop
9
+ from agent.core.agent_loop import _base_needs_approval
10
  from agent.core.session import OpType
11
  from agent.core.tools import create_builtin_tools
12
  from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC
 
16
  def test_default_cpu_sandbox_create_does_not_require_approval():
17
  config = SimpleNamespace(yolo_mode=False)
18
 
19
+ assert _base_needs_approval("sandbox_create", {}, config) is False
20
+ assert (
21
+ _base_needs_approval("sandbox_create", {"hardware": "cpu-basic"}, config)
22
+ is False
23
+ )
24
 
25
 
26
  def test_non_default_sandbox_create_still_requires_approval():
27
  config = SimpleNamespace(yolo_mode=False)
28
 
29
  assert (
30
+ _base_needs_approval("sandbox_create", {"hardware": "cpu-upgrade"}, config)
31
+ is True
32
+ )
33
+ assert (
34
+ _base_needs_approval("sandbox_create", {"hardware": "t4-small"}, config) is True
35
  )
 
36
 
37
 
38
  def test_prompt_and_tool_specs_do_not_require_cpu_sandbox_create():
tests/unit/test_sandbox_private_spaces.py CHANGED
@@ -332,31 +332,6 @@ def test_sandbox_tool_forces_private_spaces(monkeypatch):
332
  assert "Visibility: private" in out
333
 
334
 
335
- def test_orphan_sweep_preserves_spaces_without_last_modified():
336
- deleted: list[str] = []
337
- logs: list[str] = []
338
-
339
- class FakeApi:
340
- def list_spaces(self, **kwargs):
341
- assert kwargs["full"] is True
342
- return [SimpleNamespace(id="alice/sandbox-12345678")]
343
-
344
- def delete_repo(self, repo_id, repo_type):
345
- deleted.append(repo_id)
346
-
347
- count = sandbox_tool._cleanup_user_orphan_sandboxes(
348
- FakeApi(),
349
- "alice",
350
- logs.append,
351
- )
352
-
353
- assert count == 0
354
- assert deleted == []
355
- assert logs == [
356
- "orphan sweep: skipping alice/sandbox-12345678; missing lastModified"
357
- ]
358
-
359
-
360
  def test_ensure_sandbox_overrides_private_argument(monkeypatch):
361
  captured_kwargs = {}
362
  persisted: list[dict] = []
@@ -398,7 +373,6 @@ def test_ensure_sandbox_overrides_private_argument(monkeypatch):
398
  pass
399
 
400
  monkeypatch.setattr(sandbox_tool, "HfApi", FakeApi)
401
- monkeypatch.setattr(sandbox_tool, "_cleanup_user_orphan_sandboxes", lambda *args: 0)
402
  monkeypatch.setattr(Sandbox, "create", staticmethod(fake_create))
403
  monkeypatch.setattr(telemetry, "record_sandbox_create", fake_record_sandbox_create)
404
  monkeypatch.setattr("huggingface_hub.metadata_update", _fail_metadata_update)
@@ -524,7 +498,6 @@ def test_sandbox_creation_is_serialized_per_owner(monkeypatch):
524
  pass
525
 
526
  monkeypatch.setattr(sandbox_tool, "HfApi", FakeApi)
527
- monkeypatch.setattr(sandbox_tool, "_cleanup_user_orphan_sandboxes", lambda *args: 0)
528
  monkeypatch.setattr(Sandbox, "create", staticmethod(fake_create))
529
  monkeypatch.setattr(telemetry, "record_sandbox_create", fake_record_sandbox_create)
530
  monkeypatch.setattr("huggingface_hub.metadata_update", _fail_metadata_update)
 
332
  assert "Visibility: private" in out
333
 
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  def test_ensure_sandbox_overrides_private_argument(monkeypatch):
336
  captured_kwargs = {}
337
  persisted: list[dict] = []
 
373
  pass
374
 
375
  monkeypatch.setattr(sandbox_tool, "HfApi", FakeApi)
 
376
  monkeypatch.setattr(Sandbox, "create", staticmethod(fake_create))
377
  monkeypatch.setattr(telemetry, "record_sandbox_create", fake_record_sandbox_create)
378
  monkeypatch.setattr("huggingface_hub.metadata_update", _fail_metadata_update)
 
498
  pass
499
 
500
  monkeypatch.setattr(sandbox_tool, "HfApi", FakeApi)
 
501
  monkeypatch.setattr(Sandbox, "create", staticmethod(fake_create))
502
  monkeypatch.setattr(telemetry, "record_sandbox_create", fake_record_sandbox_create)
503
  monkeypatch.setattr("huggingface_hub.metadata_update", _fail_metadata_update)
uv.lock CHANGED
@@ -1775,7 +1775,6 @@ version = "0.1.0"
1775
  source = { editable = "." }
1776
  dependencies = [
1777
  { name = "apscheduler" },
1778
- { name = "boto3" },
1779
  { name = "datasets" },
1780
  { name = "fastapi" },
1781
  { name = "fastmcp" },
@@ -1822,7 +1821,6 @@ eval = [
1822
  [package.metadata]
1823
  requires-dist = [
1824
  { name = "apscheduler", specifier = ">=3.10,<4" },
1825
- { name = "boto3", specifier = ">=1.35.0" },
1826
  { name = "datasets", specifier = ">=4.4.1" },
1827
  { name = "datasets", marker = "extra == 'eval'", specifier = ">=4.3.0" },
1828
  { name = "fastapi", specifier = ">=0.115.0" },
 
1775
  source = { editable = "." }
1776
  dependencies = [
1777
  { name = "apscheduler" },
 
1778
  { name = "datasets" },
1779
  { name = "fastapi" },
1780
  { name = "fastmcp" },
 
1821
  [package.metadata]
1822
  requires-dist = [
1823
  { name = "apscheduler", specifier = ">=3.10,<4" },
 
1824
  { name = "datasets", specifier = ">=4.4.1" },
1825
  { name = "datasets", marker = "extra == 'eval'", specifier = ">=4.3.0" },
1826
  { name = "fastapi", specifier = ">=0.115.0" },