Skip to content

An API you already run

You have a service. It might be internal, it might wrap a vendor API you pay for, it might hold a key you would rather not distribute.

Putting it behind your agent gives it an identity. Callers reach it by name, over an encrypted channel, and never learn the host, the port, or the key.

The service itself does not change.

Three HTTP endpoints, bound to loopback only — the agent is the only thing that should reach it directly.

GET /health is it alive?
GET /<name>/describe what is this, and what does it answer?
POST /<name> {"query": "..."}

The reply:

{
"contract": "heia-service/1",
"status": "ok",
"results": [
{ "score": 1.0, "document": "id", "source": "where it came from", "text": "the answer" }
]
}

Bind it to loopback:

ThreadingHTTPServer(("127.0.0.1", 38490), Handler)

The description is read by a model, not a person

Section titled “The description is read by a model, not a person”

/<name>/describe is not documentation. It is the text a calling model reads to decide whether to use your service at all.

  • it is read live on every request, so you can improve it without restarting anything
  • it is capped at 400 characters

Spend them on what the service knows and when to reach for it. Not on how it was built.

Terminal window
cd my-agent
hexaeight-activate add-api --name weather --port 38490
hexaeight-activate restart agent

add-api probes /health and /describe before sealing anything. A service that cannot answer is refused, and the required contract is printed — so you find out now rather than when a caller does.

To attach a credential the caller never sees:

Terminal window
hexaeight-activate add-api --name weather --port 38490 --header 'X-Api-Key: ...'

The header is sealed with the route. Callers get answers; the key stays on your machine.

After the restart, confirm the route:

[api] 1 route(s): weather->127.0.0.1:38490[GET/POST] | tiers=dde-auth | API-ONLY

The three settings that decide whether callers get in

Section titled “The three settings that decide whether callers get in”

An API agent is called by other agents, not by people signing in. Three settings say so. Get one wrong and the agent runs normally, the route is sealed, and every call is refused.

All three live in hexaeight-agent.json, in the agent’s folder — see the settings reference.

settingvaluewhat it means
external.api.enabledtruethe API layer is on. A sealed route with it off is invisible. add-api sets this
api.tiers["dde-auth"]the tier name for agent-to-agent calls. This exact string
authTiersnot presentthis key is for people signing in. Setting it closes the door agents use

If a caller is refused with:

tier 'dde-auth' may not call the API

then api.tiers does not contain dde-auth. That is the whole meaning of the message.

Everything sealed on one agent shares that agent’s permissions. A caller admitted to reach one route can reach them all.

Per-route gating exists but is currently advisory — it records the decision and allows the call.

Where two services must not share callers, give them separate agents with separate identities. Sharing one identity also means a caller cannot tell which service answered.

Two rules, one on each side. They are not the same rule, and putting both on one agent is the usual mistake:

Terminal window
# on the SERVING agent - "this caller may reach me"
./hexaeight-agent-linux-x64 hexaeight-agent.json \
--add-rule '<calling-agent-name>,<this-agent-name>,inbound,allow'
# on the CALLING agent - "I may call out to that peer"
./hexaeight-agent-linux-x64 hexaeight-agent.json \
--add-rule '*,<serving-agent-name>,outbound,allow'

The calling side uses * as the principal because the sender is engine:<name>, not the agent’s own name — see Trust and permission.

Why the agent binary here, when everything else is hexaeight-activate? Policy is sealed under the agent’s own key, so only the agent can write it. hexaeight-activate add-policy writes the baseline set; --add-rule adds one specific row. Same store, two front ends — see the settings reference.

On the calling agent, add a pointer so its model knows the service exists — see Documents it can search, which uses the same pointer mechanism.

What the call looks like from the other side

Section titled “What the call looks like from the other side”

The caller never sees your host or port. It asks by name, and its agent does the rest:

memory_search("weather", "what is it doing in London")

That reaches your agent over an encrypted channel, arrives at your sealed route, and is handed to your service as the plain HTTP request it already understands:

POST /weather { "query": "what is it doing in London" }

Your service answers in the contract shape, and the caller gets the results array.

A service that was a port and a key is now a name, reachable only by principals you admitted, over a channel nothing in the middle can read.

Rotating the upstream key is a local edit. Revoking a caller is one policy line. Nothing was rewritten to get there.