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 AppAttestSigner per 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() throws StructuredOutputError.unsupported before 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.