Sandboxed coding assistant
You’ll build a coding assistant that can read and write files only inside one workspace directory, asks you before every write, and records each file access in a signed audit log. Use this pattern whenever an agent edits files on a machine you care about.
import asyncio
from pathlib import Path
import anyio
from loomflow import Agent, Mode, StandardPermissions, tool
from loomflow.core.types import ToolCall
from loomflow.security import FileAuditLog, FilesystemSandbox
from loomflow.tools import InProcessToolHost
WORKSPACE = Path("./workspace").resolve()
@tool
def read_file(path: str) -> str:
"""Read a file from the workspace."""
return (WORKSPACE / path).read_text() # relative paths open in the workspace
@tool(destructive=True)
def write_file(path: str, content: str) -> str:
"""Write content to a file (destructive, so it needs approval)."""
(WORKSPACE / path).write_text(content)
return f"wrote {len(content)} bytes to {path}"
async def confirm_write(call: ToolCall, user_id: str | None) -> bool:
question = f"Allow {call.tool} to {call.args.get('path')}? [y/N] "
answer = await anyio.to_thread.run_sync(input, question)
return answer.strip().lower() == "y"
async def main():
inner = InProcessToolHost([read_file, write_file])
sandbox = FilesystemSandbox(inner, roots=[WORKSPACE])
agent = Agent(
"You are a coding assistant. Only touch files inside the workspace.",
model="claude-opus-4-7",
tools=sandbox,
permissions=StandardPermissions(mode=Mode.DEFAULT),
approval_handler=confirm_write,
audit_log=FileAuditLog("./audit.jsonl", secret="prod-secret"),
)
await agent.run("Refactor utils.py to use type hints.")
asyncio.run(main())How the layers split the work:
FilesystemSandboxrejects any path that resolves outside the workspace, including through a symlink, before the tool runs. It detects path arguments by name (path,file,directory, etc.) and by value (strings containing/or\). It checks a relative path against its first root (base=), so the tools open relative paths there too (WORKSPACE / path) rather than in the process’s working directory. See Sandboxes.Mode.DEFAULTletsread_filerun freely and sends everywrite_filecall to the approval handler, which asks in the terminal. Answering anything butydenies the write, and the model sees the refusal on its next turn. The prompt comes before the sandbox check, so approving a path outside the workspace still gets refused.FileAuditLogrecords each run and tool call with an HMAC signature. See Audit log attribution.
To let the agent write without asking (for example in CI, against a
throwaway checkout), switch to Mode.ACCEPT_EDITS and name the tool
as a file editor; the sandbox still applies. ACCEPT_EDITS only
auto-approves the tools in edit_tools (the built-in write and
edit by default), so a custom write_file must be listed:
permissions=StandardPermissions(mode=Mode.ACCEPT_EDITS, edit_tools=["write_file"]),Any other destructive tool, a shell tool for example, still goes to the approval handler, and is denied when none is wired.
Last updated on