apodex/frontier-agent-demo
14
1"""File writing tool — writes content to files in E2B sandbox or local fallback.2 3References:4- DeerFlow: sandbox/tools.py write_file_tool()5"""6 7from __future__ import annotations8 9import logging10import os11 12from frontier_agent.core.tool import tool13from plugins.tools._deliverable_policy import output_write_error14from plugins.tools._path_auth import _authorized_local_path15from plugins.tools._sandbox import (16 aget_sandbox,17 arun_sandbox_cmd,18 asandbox_write_file,19 resolve_runtime_path,20 resolve_sandbox_mode,21 sandbox_available,22)23 24logger = logging.getLogger(__name__)25 26_MAX_CONTENT_BYTES = 1_048_576 # 1MB27_LOCAL_OUTPUT_DIR = "/tmp/agent-outputs"28 29 30@tool31async def write_file(path: str, content: str, append: bool = False) -> str:32 """Write plain-text content or source code to a file.33 34 In sandbox mode, writes to the E2B sandbox filesystem.35 Without sandbox, writes to /tmp/agent-outputs/ only.36 This tool only writes bytes; it does not execute generated scripts or load37 runtime packages. Use `bash` to run a generated JavaScript file. Use38 `create_file`, not this tool, for .docx/.xlsx/.pptx deliverables.39 40 Args:41 path: Absolute file path (e.g., /tmp/output.html, /root/chart.png).42 content: The text content to write.43 append: If True, append to existing file instead of overwriting.44 45 Returns:46 Confirmation message with the file path.47 """48 if not path or not path.strip():49 return "Error: file path is required."50 51 deliverable_error = output_write_error(path)52 if deliverable_error:53 return f"Error: {deliverable_error}"54 path = resolve_runtime_path(path)55 56 if len(content) > _MAX_CONTENT_BYTES:57 return f"Error: content exceeds maximum size of {_MAX_CONTENT_BYTES // 1024}KB."58 59 local_path, _reason = _authorized_local_path(path, write_access=True)60 61 # Prefer local writes for allowed repo paths62 if local_path is not None:63 try:64 local_path.parent.mkdir(parents=True, exist_ok=True)65 mode = "a" if append else "w"66 with open(local_path, mode, encoding="utf-8") as f:67 f.write(content)68 return f"File written: {local_path} ({len(content)} bytes)"69 except Exception as e:70 return f"Error writing file: {e}"71 72 if sandbox_available():73 try:74 sandbox = await aget_sandbox()75 parent = os.path.dirname(path)76 if parent:77 await arun_sandbox_cmd(78 sandbox, f"mkdir -p {parent}", timeout=10,79 )80 81 mode = "a" if append else "w"82 ok, err = await asandbox_write_file(83 sandbox, path, content, mode=mode,84 )85 if not ok:86 raise RuntimeError(err)87 88 return f"File written: {path} ({len(content)} bytes)"89 except Exception as e:90 # Container mode: /outputs & /workspace are REAL mounts inside the91 # isolated task container, so a write failure is a genuine error —92 # NOT a cue to silently redirect the deliverable to /tmp (which93 # would make it vanish from /outputs and emit no file_delta). Fail94 # loudly so the misconfiguration surfaces.95 if resolve_sandbox_mode() == "container":96 return (97 f"Error writing file {path!r}: {e}. "98 "(container mode: writes must target the mounted "99 "/outputs or /workspace; no /tmp fallback)"100 )101 logger.warning("Sandbox write_file failed, trying local: %s", e)102 103 # Local fallback — restricted to /tmp/agent-outputs/104 if not path.startswith(_LOCAL_OUTPUT_DIR):105 path = os.path.join(_LOCAL_OUTPUT_DIR, os.path.basename(path))106 107 try:108 os.makedirs(os.path.dirname(path), exist_ok=True)109 mode = "a" if append else "w"110 with open(path, mode, encoding="utf-8") as f:111 f.write(content)112 return f"File written: {path} ({len(content)} bytes)"113 except Exception as e:114 return f"Error writing file: {e}"115 