Skip to main content

Documentation Index

Fetch the complete documentation index at: https://alfred.black/docs/llms.txt

Use this file to discover all available pages before exploring further.

Most Alfred tenants are sealed from each other. Alfred Prime is the exception: a single tenant granted permission to reach across the tailnet and consult its peers’ Alfreds, by name, with the peer’s consent baked into provisioning.

What Prime is

A Prime tenant is one that’s been configured, at provisioning time, to talk to a fixed list of other Alfred tenants. When Sir asks his Alfred “what does Miguel think we should do about the Erste deck?”, Sir’s Alfred doesn’t guess. He routes the question over the tailnet to Miguel’s Alfred, lets Miguel’s Alfred reason over Miguel’s vault using Miguel’s tokens, and returns the answer. A non-Prime tenant has none of this. The tools simply don’t exist in that agent’s tool list — there’s no tenant, no ask_alfred. From the outside, every tenant is a sealed unit; only Prime sees more than one. Today exactly one tenant is Prime: David’s. The peer list contains four federated tenants — Miguel Ortiz (Upstring), Zsolt Rapali, Dave Szabó, and RJ. Every other tenant runs the same MCP server but with ALFRED_PRIME unset.

The trust model

Federation is provisioned, not negotiated. There is no peer discovery, no mutual TLS handshake, no runtime certificate exchange. An operator writes a JSON list of peer descriptors into Prime’s environment, and that list is the entire trust scope. Peers see incoming HTTPS requests on their tailnet endpoint authenticated with a key they issued at install time. Two layers of trust stack on top of each other:
  1. Tailscale — the WireGuard mesh. Every tenant is a node on the tailnet with a stable MagicDNS hostname like alfred-alfred-miguel-mnd9thwe.tail5ec603.ts.net. Only authorised nodes can route packets at all. Public internet sees nothing.
  2. API key per peer — issued by the peer at install time, stored in Prime’s CROSS_TENANT_PEERS JSON. Each request carries it as a Bearer token. The peer’s ctrl-api validates it the same way it validates any other call into /api/v1/*.
Both layers must succeed. Without Tailscale, the request never reaches the peer; without the api-key, the peer’s ctrl-api returns 401 even on a tailnet-local call. The peer can revoke the key by rotating its own AAS_API_KEY — Prime then sees a 401 until the new value is rolled in.

Configuration

Prime is gated by two environment variables, both passed explicitly by packages/openclaw/init/entrypoint.sh to the MCP server child process:
ALFRED_PRIME=true
CROSS_TENANT_PEERS='[
  {
    "id": "miguel",
    "tailscaleHost": "alfred-alfred-miguel-mnd9thwe.tail5ec603.ts.net",
    "tailscaleIp":   "100.72.147.32",
    "apiKey":        "<peer tenant AAS_API_KEY>",
    "label":         "Miguel Ortiz (Upstring)"
  },
  { "id": "rapali",   ... },
  { "id": "daveszab", ... },
  { "id": "raj313",   ... }
]'
The init container (packages/openclaw/init/entrypoint.sh, step 7c) only forwards these two variables to the MCP server’s env block when they’re set on the host. On a non-Prime tenant the keys are simply absent — the MCP server reads process.env.ALFRED_PRIME, sees undefined, and registers a single tool. There is no flag to flip at runtime. When a new tenant is provisioned, packages/ctrl/src/infra/peer-registration.ts automatically appends the new peer to Prime’s CROSS_TENANT_PEERS list and dedupes by id — so adding a tenant to the fleet quietly grants Prime visibility into it, by design.

The API surface

The receiving endpoints live in packages/ctrl/src/api/routes/crossTenant.ts. Two routes are registered on every tenant — Prime and non-Prime alike. The asymmetry is in who calls them, not who hosts them.

POST /api/v1/cross-tenant/ask

The receiving side. Any tenant exposes this. Request body:
{
  "prompt": "What's Sir's top priority this week?",
  "callerId": "alfred-prime",
  "timeoutSeconds": 240
}
The handler spawns a fresh OpenClaw session (sessions_spawn against the local gateway), prepends an instruction block telling the agent to read USER.md, SOUL.md, and MEMORY.md from the workspace, then polls sessions_history until the agent emits a <final>...</final> block — or until the deadline expires. The response shape is:
{
  "answer": "...",
  "sessionKey": "sess_...",
  "durationMs": 18432
}
prompt is hard-capped at 8000 characters; timeoutSeconds is clamped to 480.

GET /api/v1/cross-tenant/peers

Lists the peers configured on this tenant. On Prime it returns four entries; on a non-Prime tenant it returns an empty list because CROSS_TENANT_PEERS is unset. Response:
{ "peers": [ { "id": "miguel", "label": "...", "tailscaleIp": "..." } ] }
The api-key field is deliberately not in the response — it’s a write-only secret as far as the API surface is concerned.

Inside the OpenClaw gateway

packages/openclaw/mcp/ctrl-server.mjs is the MCP server that bridges the agent’s tool calls to ctrl-api. It registers either one tool or three, based on ALFRED_PRIME:
ToolAlwaysWhose tokensWhat it does
selfyesthis tenantGeneric HTTP proxy to this tenant’s own ctrl-api.
tenantPrime onlyPrime’sDirect CRUD on a named peer’s ctrl-api over Tailscale. Same endpoint catalogue as self. No peer LLM invoked.
ask_alfredPrime onlypeer’sHands a prompt to the peer’s Alfred and returns the reasoned reply.
assembleTools() in ctrl-server.mjs is the gate: if (IS_PRIME) tools.push(tenantTool, askAlfredTool). On a non-Prime tenant the agent’s tool list contains exactly one entry — self — so no amount of clever prompting can coax it into a cross-tenant call. The surface isn’t there. The companion skill at packages/openclaw/workspace-template/skills/alfred-prime-federation/SKILL.md teaches the agent when each tool is the right choice; that skill is itself only copied into the workspace when ALFRED_PRIME=true (entrypoint.sh line 72).

Scope and limits

A peer’s Alfred decides what to share. The receiving endpoint spawns the peer’s normal main agent — same persona, same tools, same vault, same auth filters that apply to Sir’s own conversational reads. Prime’s prompt is just text; nothing about it bypasses the peer’s permissions. In practice, the tenant tool gives Prime the full read/write surface of a peer’s ctrl-api (it’s authenticated as a normal admin call), while ask_alfred only ever returns whatever the peer’s agent chooses to write. Neither tool exfiltrates raw secrets — ctrl-api doesn’t expose .env files, LUKS keys, or device tokens through any /api/v1/* route. Cross-tenant writes are possible through tenant and have caused real damage before — the alfred-prime-federation skill carries an explicit rule to never write to a peer’s vault without Sir’s per-call approval. That’s a discipline encoded in the skill prompt, not a database constraint, and it exists because of one specific past incident.

Why on the tailnet

Tailscale gives every peer a stable hostname (MagicDNS) and an authenticated WireGuard route. Prime resolves alfred-alfred-miguel-mnd9thwe.tail5ec603.ts.net, gets a tailnet IP, and the connection is encrypted before any application traffic flows. The peer’s ctrl-api binds to localhost; only Tailscale Serve exposes it on :3100 to other tailnet nodes. There is no public route to test against, no DNS record to enumerate, no port to portscan from the open internet. The api-key is the application-layer secondary check. Even a compromised tailnet node would still need a valid bearer token to reach /api/v1/cross-tenant/ask. The two layers are independently revokable: pull a peer off the tailnet and Prime sees connection refused; rotate the peer’s AAS_API_KEY and Prime sees 401.

Failure modes

Cross-tenant calls fail in predictable, non-blocking ways. The relevant cases the receiving code handles explicitly (spawnAndPoll in crossTenant.ts):
  • Peer offline — Tailscale returns no route; Prime’s fetch raises a network error and ask_alfred returns error: true.
  • Peer api-key revokedctrl-api returns 401; surfaces as a tool error with status code.
  • Peer Alfred busy / timeoutspawnAndPoll returns a structured timeout reply with whatever partial content the peer agent produced; the loop has a tagless fallback after 4 substantive turns so a peer that forgets to wrap in <final> tags still answers.
  • Peer LLM out of credits — detected by pattern-matching the assistant text for “billing error” / “402”; returns a plain-language explanation rather than a hung session.
Errors are surfaced to Sir verbatim. The skill prompt instructs the agent never to retry silently; cross-tenant failures are visible.

The deciding logic

When can Sir’s Alfred answer locally, and when does he reach across? There’s no separate routing workflow. The decision lives in the main agent’s tool catalogue and the alfred-prime-federation skill prompt. If Sir asks “what’s on this afternoon?”, the question is about Sir’s calendar — answered with self. If Sir asks “what’s on Miguel’s afternoon?”, the question names a peer — answered with tenant or ask_alfred depending on whether structured data or reasoned synthesis is wanted. The skill teaches the heuristic; the agent decides. There is no router service, no separate Temporal workflow, no classifier — just the agent reading the room and picking the right tool from the three (or one) it has.

Agent

The main agent, the four specialists, and the broader tool surface that Prime extends.

Infrastructure

The Hetzner + Tailscale + Cloudflare substrate that makes federation safe.