Private agents
Keep an agent, MCP server, or skill out of public discovery and share it with only the agents you choose. Privacy here has two independent layers, and you almost always want both: who can find it, and who can call it.
aroha-sig to stop it being called.Layer 1 — visibility
Every registered agent, MCP server, and skill has one of three visibility levels:
Listed in the Hub and returned by capability search. The default.
Never appears in listings or search, but anyone who knows the DID can resolve and call it. Share-by-link.
Invisible unless you own it or hold a live grant. Strangers get a 404 — not a 403 — so the endpoint can't be used to prove it exists.
Set it in Studio under Sharing & visibility on your agent's page, or directly against the registry:
curl -X PATCH "$REGISTRY/v1/agents/$DID_HASH/visibility" \
-H "Authorization: Bearer $AROHA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"visibility":"private"}'You can also register privately from the start by passing visibility in the registration body. Reviving a deactivated private agent keeps it private — the level is never silently reset.
Layer 2 — access grants
A grant says “this specific party may see this private resource”. Grants carry an optional capability subset, a label, and an expiry, and can be revoked at any time (revocation is a soft delete, so the audit trail survives).
# Share with a partner org so their key can discover it
curl -X POST "$REGISTRY/v1/grants" \
-H "Authorization: Bearer $AROHA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"resourceDidHash": "'$DID_HASH'",
"resourceType": "agent",
"granteeApiKey": "sk-aroha-partner-key",
"note": "Partner integration",
"ttlMs": 2592000000
}'
# …or with a specific agent DID, for call-time allow-lists
# "granteeDid": "did:aroha:their-agent"
curl "$REGISTRY/v1/grants?resource=$DID_HASH" -H "Authorization: Bearer $AROHA_API_KEY"
curl -X DELETE "$REGISTRY/v1/grants/42" -H "Authorization: Bearer $AROHA_API_KEY"Two grantee forms exist because the layers authenticate differently. Registry callers identify with an API key, so discovery grants are key-scoped. Agents identify with a DID, so call-time grants are DID-scoped. Set both to cover both.
Layer 3 — who may call it
A bearer token is a shared secret: everyone holding it looks identical, and you cannot revoke one caller without rotating for all. For a private agent you want caller identity. Set auth: "aroha-sig" and list the DIDs allowed to call:
import { serve } from "@aroha-sdk/run";
serve("private-agent", async ({ message }) => `handled: ${message}`, {
auth: "aroha-sig",
allowedCallers: ["did:aroha:alice", "did:aroha:ci-pipeline"],
// resolveCallerKey defaults to the Aroha registry
}).start(8000);Callers sign each request with their agent key:
import { signRequest } from "@aroha-sdk/run";
const body = JSON.stringify({ message: "hello" });
const headers = await signRequest({
did: "did:aroha:alice",
privateKey: alicePrivateKey,
method: "POST",
path: "/v1/run",
body,
});
await fetch("https://private-agent.internal/v1/run", {
method: "POST",
headers: { "Content-Type": "application/json", ...headers },
body,
});What the signature actually protects
The signature covers METHOD | PATH | TIMESTAMP | NONCE | SHA-256(body), so a captured request cannot be reused elsewhere or altered:
- Tampering — changing the body after signing invalidates the signature
- Cross-endpoint replay — a signature for /v1/run won't verify against another path or method
- Impersonation — signing with your own key while claiming another DID fails verification
- Replay — a nonce guard rejects byte-identical repeats; a forged request never burns a genuine caller's nonce
- Stale requests — timestamps outside a 5-minute window (configurable) are rejected
- Unknown callers — a DID whose key can't be resolved is rejected — as is any caller off the allow-list, before key resolution even runs
examples/private-agent/demo.mjs.MCP servers and skills
Visibility and grants work identically for MCP servers and skills — use /v1/mcps/:didHash/visibility, /v1/skills/:didHash/visibility, and resourceType of mcp or skill on grants.
One nuance worth planning for: an MCP server is usually called by an agent rather than directly, so the cleanest private-MCP pattern is to expose it through an agent that holds the caller check — put mcp-guard in front of the tools, and aroha-sig in front of the agent.
Choosing a setup
Internal fleet, nothing public
private + aroha-sig with an allowedCallers list
Nobody can find it, and only your agents can call it.
Sharing with one partner
private + a DID grant and a key grant for them
They can discover and call it; nobody else can do either.
Beta agent, not ready to announce
unlisted
Reachable by link for testers, invisible in the Hub.
Public agent, restricted actions
public + mandates or mcp-guard
Anyone can find and call it; authority is what's bounded.