Skip to main content
A tool of your own is a Python file in your harness’s tools/ directory. There is no registration, no manifest and no deploy step: the file is the tool.
Give an agent tools=["tools/telegram.py"] and this is what it is offered:
Name, first docstring line, and a schema derived from the type hints. Nothing else about the file reaches the model. Tools has the full rules — classes and 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 with os.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:
Then check the model’s view of it — the description and the schema are what actually decide whether it gets called:
If a description reads as ambiguous to you, it reads as ambiguous to the model. For the round trip — does the agent reach for it, and does the run come out right — write an eval case. Evals covers 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 to programs/<name>/main.py:
The platform asks 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.