Labs / Lab 09

Build a host-like CLI agent

Put a host in front of a tool so the user can see what exists, approve a call, and get a structured result back.

What you'll build

A host CLI that owns tool dispatch and approval.

By the end of this lab you will have a tiny host program that knows which tools exist, checks whether a tool needs approval, and then dispatches the call itself. The result comes back as JSON, which makes the whole flow easy for both humans and other software to inspect.

This matters because tools do not make a system usable on their own. Something has to sit in front of them and decide what can run, when it can run, and whether a human needs to say yes first. That something is the host.

Run it

cd ai_ecosystem_labs
python3 09-host-cli/host_cli.py tools
python3 09-host-cli/host_cli.py run term_count '{"term":"agent","files":["sample_docs/agents.txt"]}' --approve
Starting here? Quick setup
git clone https://github.com/BanditF/ai_ecosystem_labs
cd ai_ecosystem_labs
python3 09-host-cli/host_cli.py tools
python3 09-host-cli/host_cli.py run term_count '{"term":"agent","files":["sample_docs/agents.txt"]}' --approve

Requires Python 3.8+. No extra packages needed for this lab.

Time guide. Setup: ~2 min. Working through it: 25–40 min because this is where several earlier ideas start touching each other.

Why this piece exists

The host is the layer that turns tools into a product surface.

A model does not decide what tools are available. A tool does not decide whether it is safe to run. The host does both. It owns the registry, owns the user interaction, and owns the decision to dispatch a call or block it. Without that layer, every tool has to invent its own safety rules and interface, which gets messy fast.

In this lab the approval gate is deliberately simple, but the pattern is the same one real hosts use. Cursor, Claude Code, and similar systems do not let the model directly write files or run shell commands. The host sits in the middle and asks for approval first. Roughly speaking, this is like a package receptionist who knows which contractors are allowed in the building and which deliveries need a signature before they move.

The code

host_cli.py

Walk through it

Four things worth noticing.

The host owns tool dispatch

The model does not get to invent tools on the fly. The host decides what exists through the TOOLS dictionary, and if args.tool not in TOOLS is the enforcement point. That registry is the contract: if a tool is not listed there, it is not callable.

requires_approval: True marks risky tools

The term_count entry includes "requires_approval": True. In this toy host that means the call is blocked unless approval is present. In real hosts the same pattern gets used for file writes, shell commands, network actions, or anything else you do not want firing automatically.

Approval is synchronous

The decision happens before dispatch. The host checks TOOLS[args.tool]["requires_approval"] and only continues if approval is explicit. Here that approval is represented by the --approve flag rather than an interactive yes/no prompt, but the control flow is the same: no approval, no tool call.

The host is the trust boundary

Notice that term_count() itself does not know anything about approval. It just counts terms. The safety rule lives outside the tool, in main(), where the host can enforce policy consistently. Tools stay dumb; hosts stay safe.

Expected output

What blocked and approved runs look like.

$ cd ai_ecosystem_labs
$ python3 09-host-cli/host_cli.py run term_count '{"term":"agent","files":["sample_docs/agents.txt"]}'
{
  "ok": false,
  "approval_required": true,
  "tool": "term_count"
}

$ python3 09-host-cli/host_cli.py run term_count '{"term":"agent","files":["sample_docs/agents.txt"]}' --approve
{
  "ok": true,
  "items": [
    {
      "file": "sample_docs/agents.txt",
      "count": 2
    }
  ],
  "summary": {
    "total": 2
  }
}

Try this

Three things to try before moving on.

  1. Run it and deny the approval. Execute term_count without --approve and notice the tool never fires. The host returns a blocked JSON response instead of dispatching the call.
  2. Add a second tool with no approval requirement. Put something simple like echo in the TOOLS dict with requires_approval: False, wire it into the host, and run it. You should see it execute without any approval step.
  3. Turn off approval for term_count. Change requires_approval to False and re-run. The host skips the gate and dispatches immediately. This is the same basic mechanism you would use for a trusted internal tool.

Concepts behind this

Read Agents for the bigger picture on where a host sits in an agent stack.

Read Extensions — hooks and approval gates for the broader pattern this lab is demonstrating.