Lesson 02 · Basics

Same loop, more tools

“The loop didn’t change at all — I just added things to the TOOLS array.” — s02_tool_use.py, verbatim.

⏱ ~10 min · 📝 3 interactive widgets · 🧑‍💻 Based on shareAI-lab · s02_tool_use.py

What do you actually touch when adding a tool?

The s01 agent only knows bash. To also support read_file / write_file / edit_file, what do you change?

Most people’s first instinct: modify the loop. Wrong. The loop doesn’t need a single change. You only need three things:

  1. Write a Python handler (run_read(path, limit)).
  2. Register it in the TOOL_HANDLERS dispatch map ("read_file": lambda **kw: run_read(...)).
  3. Add a JSON schema entry to the TOOLS array (tells the model the tool’s name and parameters).

When the loop sees a tool_use block it looks up block.name in the dispatch map, executes the handler, and stuffs the output into a tool_result. Exactly the same path as bash.

# Dispatch map: name → handler lambda
TOOL_HANDLERS = {
    "bash":       lambda **kw: run_bash(kw["command"]),
    "read_file":  lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file":  lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}

See how dispatch routes a request

The widget below lets you click a tool_use request the model might send, and watch how it gets routed to the right Python handler. Key insight: block.name determines the path.

safe_path: the fence you must not skip

Giving an agent file access creates an obvious security risk: path escape. The model is supposed to work inside /home/user/project/, but it sends read_file("../../etc/passwd").

s02 has a small function that handles this:

def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()  # normalize, resolve .. and symlinks
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

The critical pair is .resolve() + .is_relative_to(): expand to an absolute path first, then verify it’s still inside the sandbox. Without the first step, foo/../../etc slips through. Without the second, there’s no check at all.

Identifying safe vs unsafe paths

Five paths a model might pass as a read_file argument. Which ones does safe_path allow, and which does it block? Assume WORKDIR = /home/user/project.

Don’t add dangerous tools to your agent

Adding tools is easy, but remember: every tool you add extends the model’s capability boundary. s02 already has a bash blacklist (rm -rf /, sudo, shutdown) and write_file is sandboxed by safe_path. Before adding a tool to production, ask yourself three questions:

  • Can this tool make irreversible changes? (rm, send email, git push)
  • What can it leak? (env vars, .ssh, cookies)
  • Could malicious output be re-injected as instructions? (prompt injection via tool output)

Lesson s07 will add a “permissions layer” that lifts these decisions out of code and makes them declarative.

Interactive

Widget 1 · Tool Dispatch · how a tool_use block reaches its handler

Click a tool_use request to watch the loop look up block.name in the dispatch map, execute the handler, and wrap the output in a tool_result block.

tool_use requests from the model
TOOL_HANDLERS dispatch map

        
Execution result · tool_result block
(click any tool_use above to trigger routing)
Interactive

Widget 2 · Safe Path · 5-path escape detection quiz

Each path is a possible read_file argument. Assume WORKDIR = /home/user/project. Click Allow or Deny — test your path-escape instincts.

Correct: 0 / 5
Interactive

Widget 3 · Add a Tool · what exactly changes when you add glob

Say you want a new glob(pattern) tool so the agent can find files by pattern. Fill in the blanks: three places must change, all of them required.

Step 1 · Write a handler function
def run_glob(pattern: str) -> str:
    import glob
    return "\n".join(glob.glob(pattern))
Step 2 · Register in the dispatch map (pick the correct line)
Step 3 · Add a schema to the TOOLS array (pick the correct entry)