tools
table is this module's data model; the agent-selection link
(agent_tools)
lives in Agent Engine. Below is a concept, not an API contract.
A tool is a callable function the platform exposes to the model as a tool schema. The catalog of tools is owned by Admin.
Distinct things; don't conflate them.
A tool schema comes from a structured source, not from admin prose — the call contract is reliable and won't break on a text edit.
The catalog is open; the mechanism is a type registry, mirroring the Harvester connector registry: a type declares itself as a class (name · tool schema · credential kind · body) and self-registers.
type — a class in code, self-registering ──▶
instance — a row in
tools
(flags + key)
preset v1web_search,
fetch_url.
custom v1custom/ folder (custom tool).
mcp · openapi v2
Provider behind an interface. web_search is
one tool to the model (web_search(query)), while the provider
(Tavily · Brave · Serper · Google CSE) is chosen by the
admin on a screen, not in a config file: the choice
lands in the non-secret config column, the key in
credential_enc. The model sees one stable schema; how it's
executed is the adapter's business. The same technique used for
ai_models providers and the built-in embedder.
A tool's secret (API key) is not in config (JSONB) but in a
separate credential_enc column: AES-256-GCM ciphertext from
Auth's crypto core,
exposed outward only as a "set" flag. Like provider and SMTP keys. The
human-readable name goes through t(); the DB holds only the
key.
The organization adds its own tool as one class following the contract:
the body is a call method, the tool schema is in the
manifest, registration via a decorator.
✦ canonical pip package → entry point achilles.tools core untouched · platform upgrade stays clean ○ quick dev in-tree → achilles/ai_foundation/tools/custom/ own folder · doesn't disturb the built-ins
call# 3 · discovery picks it up itself @register_tool class WebSearchTool(BaseTool): # contract: schema + call manifest = ToolManifest( # defines what the model sees name = "web_search", # key + name in the tool schema params = {"query": str}, # arguments for the model access = ReadOnly, # read_only · write config = [Provider], # Tavily · Brave · Serper · CSE credential = [ApiKey], # → credential_enc ) def call(self, args): # the tool body return provider.search(args.query) def probe(self): # "Test" button → status return provider.ping() # active · error # either way below — in-tree or as a pip package — the class is visible # only after rebuilding the image and redeploying the container: # discovery reads classes at process start, not from Admin
├── base.py BaseTool — contract: manifest + call ├── registry.py discovery: package + entry points ├── web_search.py ┐ built-in ├── fetch_url.py ┘ presets (read-only) └── custom/ ← platform doesn't touch this folder └── jira.py ← your class goes here
# core untouched — discovery finds it via the entry point ├── tool.py ← the same class following the contract └── pyproject.toml [project.entry-points."achilles.tools"]
The contract is neutral: read-only is the policy of the
platform presets, while a custom class may write — at the organization's
own responsibility, like a custom connector (shared process, no sandbox;
the operator decides what to install). The contract < 1.0
isn't frozen — the package is rebuilt against the platform version.
The on-screen list is a live type registry, not a table:
the tools
table stores only state (flags + key) and is linked to the type by
name. So a new class enters the catalog on its own — no
migration and no UI changes.
tools = both
marks false, enablement).
tools record is created lazily — on
first enablement or when a key is entered.
name and gets out of the
way.
One catalog feeds both surfaces, but their loops differ.
chat_enabled)search · graph · sql, always, locked)An agent's KS core can't be removed. Removing an added tool is graceful degradation: the agent continues on the core and does not enter the readiness gate (unlike removing the model, which stops the agent).
"Locked" is about configuration, not unconditionality.
"Always" and "locked" on the core mean "can't be turned off manually by
either admin or owner," not "present in any state of the base." There is
a second, derived axis: with an empty base
(is_empty) the KS tools aren't supplied at all — there's
nothing to search. This is one principle across both surfaces: both
search_knowledge in chat and the agent's core fall away
while the base holds no data; this is decided by the shared
harness, not by an admin mark. Not a "removal" and not config-driven
degradation — simply the absence of data; once the base fills, the core
returns on its own.
The catalog is only a type registry; a tool's admission to work is set by two independent marks, one per surface. Both are off by default — a tool enters service only through a deliberate admin action; it stays active as long as at least one mark supplies it.
chat_enabled — the tool is visible to chat.agents_allowed — selectable by the owner in the agent editor.
Enabling a tool and changing its settings (provider, key) is an admin
action: it lands in the audit log
alongside other access changes (the model calls themselves aren't logged
there — the log holds significant events, not every request). Default OFF
and auditing of enablement form a perimeter of caution: an external exit
is opened explicitly and under observation. A rate limit / call budget for
web_search is v2.
The key is set, the provider chosen — but does it work? The admin
shouldn't learn of a stale key from user complaints. Every tool with an
external exit has a "Test" button: the platform calls the
type's probe() (a light provider ping, not a real model
request) and stores the result. This mirrors the connection check for
AI model providers.
tools.status
and last_check_at — state persists between visits; on
screen it's a pill "checked · N ago" or "error."
web_search
(provider + key), fetch_url (egress reachability). The KS
core isn't tested — it's internal and always available.
probe() is part of the type contract, beside call;
an organization's custom tool declares it itself and joins the check
alongside the presets.
Connecting external MCP servers and write tools is the next iteration. The locked design: per-user OAuth (each user authorizes for themselves), the server holds its own ACL, access is read-only via admin curation. In the wireframes — grey inactive stubs.