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.
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.
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.
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.