A thin, stateless HTTP front-end that exposes one or more Anda engines over a
signed CBOR/JSON RPC endpoint plus .well-known discovery routes. Agent logic,
tools, and any session/persistence state live in the anda_engine
runtime; this crate only authenticates callers and forwards typed requests to
the target engine.
- Multi-Engine Support: Serve multiple Anda engine instances behind one
server, addressed by engine principal or the
defaultalias. - Signed RPC:
POST /{id}dispatchesagent_run/tool_call/information, with CBOR or JSON negotiated from the requestContent-Type. - Discovery:
.well-knownroutes advertise each engine's publicEngineCard(only exported agents/tools) to anonymous callers. - Authentication: ICP
SignedEnvelopeverification of the request body and optional bearer CWT tokens; unauthenticated callers are treated as anonymous and gated by each engine's visibility policy. - Pluggable middleware: Compression, an optional
x-api-keyguard, and arbitrary axum/tower layers.
See the anda_cli for example usage with this server.
| Endpoint | Response | Authentication and caching |
|---|---|---|
GET /, GET /.well-known/information, GET /.well-known/agents |
AppInformation, including the resolved caller |
Anonymous access is allowed; supplied credentials must verify. Responses use Cache-Control: no-store. |
GET /.well-known/agents/{id} |
Public EngineCard with exported functions |
Does not authenticate or personalize the card; private functions are excluded. |
POST /{id} |
RPC result for agent_run, tool_call, or information |
Supplied credentials must verify; the engine enforces execution permissions. |
{id} is an engine principal or default. Every registered map key must equal
the associated Engine::id(); build_router() rejects mismatches. An explicit
default must exist in that map; otherwise the first key in map order is used.
Discovery responses negotiate JSON/CBOR using Accept quality weights and
wildcards. A more specific range overrides a wildcard, including an explicit
q=0 exclusion. JSON wins equal weights and is the default when Accept is
absent. If neither representation is acceptable, the server returns 406.
Both representations are emitted without media parameters; ranges requiring
additional media parameters do not match. Discovery adds Vary: Accept, which
is retained alongside the compression layer's Vary: Accept-Encoding.
RPC uses the request Content-Type for both the outer envelope and the encoded
params/success payload: application/cbor, application/json, or a type with
a +cbor/+json suffix. Other types return 415, and an undecodable envelope
returns 400. Accept does not change the RPC codec. Execution and
parameter errors are returned inside the existing RPCResponse::Err envelope
with HTTP 200; invalid credentials return 401. The default axum request
body limit is 2 MiB and can be customized with a router middleware.
Middlewares are applied in registration order, with each router.layer(...)
wrapping the previous router: registering A then B runs requests as
B → A → handler, and responses as handler → A → B.
with_app_name and with_app_version configure serve() startup logs; use
with_extra_info to add discovery metadata.
Bearer and ICP scheme names are case-insensitive and accept one or more
spaces before the token. Token bytes are preserved. With trusted Ed25519 keys
configured, a bearer attempt must verify as a CWT with a valid principal in
sub and an exp claim. The CWT verifier also checks nbf when present and
allows five minutes of clock skew. Trusted keys are server-wide: bearer tokens
are not bound to the request body or a particular engine, and this server does
not enforce aud or scope claims. Engine access still depends on the resolved
principal's permissions.
Signed envelopes are read from Authorization: ICP ... or legacy ic-auth-*
headers. On RPC calls the client must provide an envelope digest equal to the
SHA3-256 hash of the received body. A malformed credential is rejected rather
than becoming anonymous. The public engine-card GET endpoint does not use
credentials, even when supplied; it returns no caller-specific information.
Direct-key signatures authenticate the body hash but do not provide freshness
or automatic engine binding. expect_target restricts only delegations that
carry target lists; now_ms is used for delegation expiry and canister-signature
certificates. Clients should set RequestMeta::engine, which both agent_run
and tool_call validate, and use TLS. A captured direct-key request can otherwise
be replayed, including against another engine where that principal has access.
Requiring an explicit digest does not add cryptographic domain separation: the signature is still over the same body hash. Preventing replay or signatures obtained from another use of the same key requires a coordinated protocol change with freshness and domain binding across the signing library and clients.
The server is a stateless forwarder in front of the Anda engine, handling:
- HTTP request routing and CBOR/JSON codec negotiation
- Authentication (ICP signature verification and bearer CWT)
- Agent and tool dispatch into the target engine
Session management, tool integrations (shell, filesystem, fetch, etc.), and
per-caller access control are provided by anda_engine, not
by this crate.
RPC handlers release encoded parameter buffers after decoding, before awaiting the engine. Server discovery borrows static metadata during serialization; engine cards are rebuilt so refreshed dynamic tool definitions remain visible. Skipping SHA3 for anonymous/CWT calls remains a profiling candidate; request extraction and signature verification continue to use the existing body hash.
Copyright © 2026 LDC Labs.
ldclabs/anda is licensed under the MIT License. See the MIT license for the full license text.