Skip to content

Profiles and auth

This guide explains how kneo-client resolves connection details (URL, API key, auth scheme, timeout) into a profile, what the two API-key header schemes mean, and how to set up multi-profile workflows for dev / staging / prod.

What "profile" means in kneo-client

A profile is a frozen dataclass bundling (name, url, api_key, auth_scheme, timeout):

@dataclass(frozen=True)
class Profile:
    name: str
    url: str
    api_key: str
    auth_scheme: AuthScheme = AuthScheme.BEARER
    timeout: float = 30.0

Everything Transport needs to talk to one Kneo Agent Platform instance fits in those five fields. A KneoClient is built around exactly one profile; you construct one per platform instance you talk to.

Profiles are typically named — default, staging, prod, ci, etc. The name is informational (it appears in log lines) but doubles as the section name in the TOML config file.

Resolution order

load_profile() and KneoClient.from_profile() merge values from three sources, with later sources overriding earlier ones:

  1. TOML config file, by default ~/.config/kneo/client.toml (XDG-style via platformdirs). Pass config_file=Path(...) to point at a different file. If the file doesn't exist, it's skipped silently — env vars and explicit kwargs are still consulted.
  2. Environment variables: KNEO_URL, KNEO_API_KEY, KNEO_AUTH_SCHEME, KNEO_TIMEOUT. (KNEO_PROFILE selects which TOML section to load — it does not override field values.)
  3. Explicit keyword arguments to the function call.

If url or api_key cannot be resolved from any source, a ProfileError is raised with details on which sources were checked. Bad TOML, an unknown auth_scheme, or a non-numeric timeout also surface as ProfileError.

from kneo_client.core.profiles import load_profile

p = load_profile()                           # 'default' from TOML + env
p = load_profile("staging")                  # explicit profile name
p = load_profile(url="https://ad-hoc", api_key=token)  # explicit kwargs win

TOML format

Each top-level table is one profile:

# ~/.config/kneo/client.toml
[default]
url = "https://kneo.example.com"
api_key = "prod-key"
auth_scheme = "bearer"        # or "kneo_api_key"
timeout = 30.0                # seconds

[staging]
url = "https://staging-kneo.example.com"
api_key = "staging-key"

[local]
url = "http://127.0.0.1:8000"
api_key = "dev-token"
auth_scheme = "kneo_api_key"

auth_scheme and timeout are optional; their defaults are "bearer" and 30.0.

Picking a profile at call time:

client = KneoClient.from_profile()           # 'default' (or $KNEO_PROFILE)
client = KneoClient.from_profile("staging")  # explicit
client = KneoClient.from_profile("local")    # explicit

Environment variables

Variable Purpose
KNEO_PROFILE Profile name to load. Falls back to "default" if unset.
KNEO_URL Override the profile's URL.
KNEO_API_KEY Override the profile's API key.
KNEO_AUTH_SCHEME Override the scheme. Accepts "bearer" or "kneo_api_key".
KNEO_TIMEOUT Override the per-request timeout (float seconds).

CI environments typically set just KNEO_URL and KNEO_API_KEY and skip the TOML file entirely. The lack of a config file is not an error — env vars + kwargs can satisfy resolution on their own.

A bad value for KNEO_TIMEOUT (non-numeric) raises ProfileError with the variable name in the message — easier to debug than a silent fallback.

Auth schemes — what the platform accepts

The platform accepts the API key in either of two header schemes. They are semantically equivalent — both end up at the same platform code path — but operationally they have different trade-offs:

Scheme Header sent When to choose
bearer (default) Authorization: Bearer <key> Works with most reverse proxies. Easy to revoke at the gateway layer. Standard HTTP semantics.
kneo_api_key X-Kneo-Api-Key: <key> Useful when your edge stack already uses the Authorization header for something else (mutual TLS auth, an upstream OAuth flow, etc.). Avoids the collision.

When in doubt, start with bearer. You can switch schemes per-profile without code changes — just update the TOML or the env var.

The two schemes are implemented by kneo_client.core.auth.ApiKeyAuth, an httpx.Auth subclass that injects whichever header the active profile selects. Internally the auth flow runs inside httpx's request flow (after Transport has added its other headers), so the API key reaches every redirect / retry attempt at the right layer.

Multi-profile workflows

A common pattern in CI / local dev:

import os
from kneo_client import KneoClient

profile_name = "ci" if os.getenv("CI") else "default"
async with KneoClient.from_profile(profile_name) as client:
    ...

Or override explicitly when the situation calls for an ad-hoc connection:

async with KneoClient.from_profile(url="https://ad-hoc.example.com", api_key=tok) as client:
    ...

Explicit kwargs always win, so you can keep the TOML file as a baseline and override per-call.

Programmatic profile construction (when secrets come from a vault / secrets manager) skips load_profile() entirely:

from kneo_client import KneoClient
from kneo_client.core.auth import AuthScheme
from kneo_client.core.profiles import Profile

def profile_from_vault() -> Profile:
    secret = vault.get("kneo/prod")
    return Profile(
        name="prod",
        url=secret["url"],
        api_key=secret["api_key"],
        auth_scheme=AuthScheme.BEARER,
        timeout=30.0,
    )

async with KneoClient(profile_from_vault()) as client:
    ...

Profile is a frozen dataclass — pass it directly to KneoClient(profile).

Inspecting a resolved profile

KneoClient.profile returns the Profile actually in use:

async with KneoClient.from_profile() as client:
    print(f"connected to {client.profile.url} as profile {client.profile.name!r}")

The api_key field is on the dataclass — handle it like any other secret. The redaction-aware logger in kneo_client.core.logging masks the key whenever it logs request / response headers, but the dataclass itself is not redacted when you print it directly.

If you do want to log a profile safely:

print(f"profile name={p.name!r} url={p.url!r} scheme={p.auth_scheme.value!r} timeout={p.timeout}")

…just exclude p.api_key from anything that goes to a log sink.

Profile errors

ProfileError covers four failure modes:

Trigger Message pattern
Missing url after all sources "profile 'X': 'url' is not set …"
Missing api_key after all sources "profile 'X': 'api_key' is not set …"
Malformed TOML "failed to parse <path>: …"
Unknown auth scheme "unknown auth_scheme '…'; expected one of: bearer, kneo_api_key"
Non-numeric KNEO_TIMEOUT "$KNEO_TIMEOUT must be a float, got '…'"

All five are explicit and name the offending source. Catch ProfileError (or Exception if you don't care which) at process startup to fail fast with a clear message rather than blowing up on the first call.

Default config path

kneo_client.core.profiles.default_config_path() returns the XDG-style default — ~/.config/kneo/client.toml on Linux, ~/Library/Application Support/kneo/client.toml on macOS, the appropriate %APPDATA%\kneo\client.toml on Windows. Resolved via platformdirs.user_config_dir("kneo").

If you want a project-local TOML (committed to a repo, picked up by CI without needing a user-config), pass it explicitly:

from pathlib import Path
client = KneoClient.from_profile(config_file=Path(".kneo.toml"))