Creating Tools
Tools are Python plugins that Centaur discovers at API startup and exposes as
REST endpoints at /tools/{name}/{method}. Put organization-specific tools in
an overlay repo under tools/ so the base Centaur repo stays generic. See
Using an overlay for packaging, mount paths, and chart
configuration.
Tools are loaded from TOOL_DIRS. In an overlay deployment, the tool must exist
under /app/overlay/org/tools in the API container. Later tool directories can
shadow earlier tools with the same name, so an overlay can replace a base tool
intentionally.
See the Tool Directory for the integrations that ship with Centaur.
Define metadata
Each tool needs pyproject.toml with a [tool.centaur] block:
[project]
name = "warehouse"
description = "Internal warehouse queries"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27.0"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.centaur]
module = "client.py"
secrets = [
{type = "http", name = "WAREHOUSE_API_KEY", match_headers = ["Authorization"], hosts = ["warehouse.internal.example.com"]},
]Each entry in secrets declares one credential the tool can request with
secret(...). The fields tell iron-proxy what to swap and where:
type = "http"is the common case: an HTTP credential injected into outbound requests. Replace-mode HTTP secrets give the tool a placeholder fromsecret("..."); iron-proxy swaps that placeholder for the real value at the network boundary.type = "oauth_token"is for OAuth2 APIs. iron-proxy resolves the declaredfields, runs arefresh_token,client_credentials,password, orjwt_bearerexchange, caches and refreshes the access token, then injectsAuthorization: Bearer ...for the configuredhosts. Settoken_endpoint_headersto send extra headers on the token POST itself (for endpoints that require an API key alongside the standard form-body client auth). Forjwt_bearer(RFC 7523), supplyissuer,subject, andprivate_key(an RSA PEM) infields, plus a top-levelaudience; an optionalprivate_key_idfield is emitted as the JWTkidheader.type = "brokered_token"routes OAuth2 refresh-token rotation through iron-token-broker instead of iron-proxy. Use this when the upstream IdP rotates refresh tokens with strict reuse detection (OpenAI Codex, Anthropic Claude Code OAuth, modern Okta or Auth0 with rotation enabled) and more than one proxy shares the credential. Requiredfields:client_id,refresh_token. Optional:client_secret. Therefresh_tokenfield names the writable credential blob the broker rewrites on every rotation; the other fields are read-only. Read-side fields andtoken_endpoint_headersentries acceptjson_keyto pluck a value out of a JSON-encoded secret; therefresh_tokenfield does not (the broker rewrites the whole document).type = "gcp_auth"is for Google service-account JSON. iron-proxy resolves the keyfile, mints Google OAuth tokens forscopes, and injects them for the configured Google APIhosts. If omitted, hosts default to*.googleapis.comand scopes default tocloud-platform.type = "pg_dsn"is for Postgres. iron-proxy resolves the real upstream DSN, while the sandbox gets a local proxy DSN in an environment variable named byname;databasemust match the upstream database name.nameis the placeholder string the sandbox sees and whatsecret("...")looks up for replace-mode HTTP secrets.match_headers,match_query, ormatch_pathtell iron-proxy where in the request the placeholder is allowed to appear. At least one is required.hostsis the upstream allowlist for this secret. iron-proxy will only inject the real value on requests to these hosts.
Use optional_secrets for credentials the tool can run without.
Write the client
client.py exports a _client() factory. Public methods on the returned object
become tool methods.
import httpx
from centaur_sdk.tool_sdk import secret
class WarehouseClient:
def query(self, sql: str) -> dict:
token = secret("WAREHOUSE_API_KEY", "")
response = httpx.post(
"https://warehouse.internal.example.com/query",
headers={"authorization": f"Bearer {token}"},
json={"sql": sql},
timeout=30,
)
response.raise_for_status()
return response.json()
def _client() -> WarehouseClient:
return WarehouseClient()Do not call load_dotenv() in client.py. Server-side tools should use
secret("KEY"); standalone CLIs may load local .env files in their CLI
wrapper.
Verify
After deploy:
kubectl exec -n centaur-system deploy/centaur-centaur-api -- \
curl -fsS http://localhost:8000/health/tools | jqCheck that the tool appears and that missing-secret warnings match what you
expect. If a tool is missing, inspect the overlay image contents, TOOL_DIRS,
the tool directory name, and [tool.centaur] module = "client.py".