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.
What the service must do
Section titled “What the service must do”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.
Seal it as a named route
Section titled “Seal it as a named route”cd my-agenthexaeight-activate add-api --name weather --port 38490hexaeight-activate restart agentadd-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:
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-ONLYThe 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.
| setting | value | what it means |
|---|---|---|
external.api.enabled | true | the 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 |
authTiers | not present | this 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 APIthen api.tiers does not contain dde-auth. That is the whole meaning of the message.
One agent is one trust boundary
Section titled “One agent is one trust boundary”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.
Letting another agent call it
Section titled “Letting another agent call it”Two rules, one on each side. They are not the same rule, and putting both on one agent is the usual mistake:
# 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-policywrites the baseline set;--add-ruleadds 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.
What you have now
Section titled “What you have now”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.