AgentKit Cloud
The production provider. The app names an agent profile; the cloud resolves the provider, model, system prompt, and tool schemas server-side. No provider key and no model name ship in the binary — go to production covers why.
Name a profile
let agent = try runtime.makeAgent(
provider: .backendRouterCloud(
endpoint: URL(string: "https://api.agkit.cloud/v1/agent/stream")!,
agentId: "video-editor",
tier: "pro",
maxOutputTokens: 2048,
publishableKey: "ak_pk_live_…",
userToken: endUserJWT
),
role: AgentRole(staticPersona: "You are a precise video-editing assistant.")
)
Each request carries the profile name (agentId, tier), the active tool
names, the conversation, and a token ceiling — no system prompt, no tool
schemas, no model name. maxOutputTokens is a ceiling you propose; the
cloud clamps it to the profile's own. Tools still execute in the app.
The persona you pass in role stays on-device (the profile owns the system
prompt), while live context and per-turn directives
ride the first user message, the same routing as
your own backend.
Two credentials, one secret
| Credential | Travels as | Role |
|---|---|---|
publishableKey |
X-AgentKit-Publishable-Key |
public app identifier (ak_pk_live_…) — safe to embed |
userToken |
Authorization: Bearer … |
short-lived end-user JWT — the only credential that authorizes |
The JWT comes from an issuer the cloud trusts for your project — Sign in with Apple, Firebase Auth, or your own backend. Every request also carries a fresh request id, the idempotency key behind duplicate-request rejection.
The credentials live in BackendRouterCloudAuth; the profile half lives in
BackendRouterCloudProfile. The factory above assembles both — construct
them directly when you need to hold the provider:
let provider = BackendRouterProvider(
endpoint: URL(string: "https://api.agkit.cloud/v1/agent/stream")!,
mode: .cloudProfile(BackendRouterCloudProfile(
agentId: "video-editor",
tier: "pro",
maxOutputTokens: 2048,
auth: BackendRouterCloudAuth(publishableKey: "ak_pk_live_…", userToken: endUserJWT)
))
)
Sign every request
Static credentials cover most apps. When auth must be computed per request —
a JWT that needs refreshing, device attestation — pass a signer instead. A
CloudRequestSigner is called once per request, after the body is
serialized, and returns the headers to attach:
struct RefreshingSigner: CloudRequestSigner {
let publishableKey: String
let tokens: TokenStore // your refresh logic
func headers(for context: CloudRequestContext) async throws -> [HTTPHeader] {
let jwt = await tokens.freshToken()
return try await BackendRouterCloudAuth(
publishableKey: publishableKey,
userToken: jwt
).headers(for: context)
}
}
let provider: AgentProviderSpec = .backendRouterCloud(
endpoint: URL(string: "https://api.agkit.cloud/v1/agent/stream")!,
agentId: "video-editor",
tier: "pro",
maxOutputTokens: 2048,
signer: RefreshingSigner(publishableKey: "ak_pk_live_…", tokens: tokens)
)
context gives the signer the endpoint, the exact body bytes, and the
request id. Signing is bounded (10 seconds by default;
BackendRouterCloudProfile(signingTimeoutSeconds:) adjusts it) — a signer
that fails or times out fails the request with
BackendRouterError.signingFailed, and nothing leaves the device. Signers
add headers; they can never replace provider-owned ones like the request id.
Attest the device
Tiers can require App Attest: proof that requests come from your unmodified app on real Apple hardware. Onboard a key once per device and user, then sign every request with it.
let challengeURL = URL(string: "https://api.agkit.cloud/v1/attest/challenge")!
let auth = BackendRouterCloudAuth(publishableKey: "ak_pk_live_…", userToken: endUserJWT)
// Once, at first launch or sign-in:
let registrar = AppAttestRegistrar(
registrationEndpoint: URL(string: "https://api.agkit.cloud/v1/attest/register")!,
challengeEndpoint: challengeURL,
auth: auth
)
let keyID = try await registrar.register()
// Persist keyID — key storage is yours; the Keychain is the usual home.
// Every session afterwards:
let signer = CompositeCloudSigner(signers: [
auth,
AppAttestSigner(keyID: keyID, challengeEndpoint: challengeURL),
])
let provider: AgentProviderSpec = .backendRouterCloud(
endpoint: URL(string: "https://api.agkit.cloud/v1/agent/stream")!,
agentId: "video-editor",
tier: "pro",
maxOutputTokens: 2048,
signer: signer
)
CompositeCloudSigner concatenates signers in order, so the static
credentials ride alongside the per-request attestation headers.
What to know in production:
- If onboarding fails mid-flight, retry with
register(reusingKeyID:). Re-attesting a key that never finished registering is permitted, and an identical re-registration is idempotent server-side. - Reuse one
AppAttestSignerper key. Assertion generation is serialized per signer instance; two instances wrapping the same key race each other into retryable rejections. - App Attest needs real Apple hardware. On simulators it fails closed with a typed unsupported-device error rather than silently skipping attestation.
What it doesn't do
- Structured output.
generate()throwsStructuredOutputError.unsupportedbefore any request is sent. Run structured turns against a provider that supports it — see get data, not prose. - Images everywhere. What the cloud accepts depends on the agent profile's tier — see send images.
When it fails
Every cloud failure surfaces as a typed BackendRouterError carrying retry
guidance — refresh the token, back off, or stop. See
when it fails.
App Attest onboarding throws its own typed errors — AppAttestRegistrarError
and AppAttestSignerError — catalogued in the
error reference.
Redirects fail closed — your own backend owns the rule. The attestation registration POST carries the same guard.