tools/ directory. There
is no registration, no manifest and no deploy step: the file is the tool.
tools=["tools/telegram.py"] and this is what it is offered:
scope, @tool, the hint-to-schema table, what refuses a publish.
This page is the practical loop.
The docstring is the tool’s interface
Whatever else you write, spend the effort on the first line. It is the whole of what the model knows before deciding whether this is the tool for the job, so say what the tool refuses to do as well as what it does — “plain text only, no formatting, no attachments” is worth more than another paragraph of prose the model never sees. The same goes for failure. An exception is not a crash: it comes back as<Type>: <message> and the run continues, so the message is the cheapest place
to teach.
Secrets
Never put a key in the file. A harness is a git repository, and the repository is what gets published. Put it in Connections → Secrets instead, where it is encrypted and injected into the machine as an environment variable, and read it withos.environ as the
example above does. The key names reach the agent through
~/tools/catalog.json; the values only ever exist in the environment.
Connections covers the screen.
A tool that needs a per-user credential — one that belongs to the person talking
to the harness rather than to you — is not this. That is
an MCP connection, where the platform holds the secret
outside the machine entirely.
Dependencies
Three files, at the root of the tree and nowhere else:requirements.txt,
pyproject.toml and setup.sh, applied in that order. They run in the sandbox as
part of the same step that checks the tree out, and a turn that changed none of
them runs no pip at all.
When an import fails for something the tree clearly declares, the whole install
transcript is at /tmp/splox-harness-deps.log.
Testing it
Your tool is ordinary Python on a machine you have a shell on, so test it as ordinary Python first:harness_eval.
Getting it in front of the agent
Two separate things, and they land at different speeds. Editing the body of a tool takes effect on the very next call. The tool server compares each file’s mtime on every list and every call, and re-imports what moved — no restart and no publish. Changing a signature or a description reaches the model on the next run, because the catalog is asked once per run. Naming the file in an agent is an edit toprograms/<name>/main.py:
main.py who an agent is at the moment it needs to know, so
that edit lands on the next turn too. git push origin main is what publishes a
version of the harness — and it is where a broken import or a bad tool name is
caught, with the interpreter’s own words. See
Publishing a version.
Naming one file gives the agent that file’s tools and only those. A call to a
tool from another file is refused rather than run, even though one server holds
the whole tree.
When to write one at all
Usually not for new capability — for a smaller surface.system:compute can do
anything a shell can do; a tools/deploy.py with one function called release
that runs the four commands in the right order, refuses the wrong branch and says
so, is a different thing entirely. The platform’s packages are what an agent
can do. Your tools/ directory is what this harness does.
