kbagent auth login signs you in to a Keboola stack through a real browser
instead of a pasted Storage API token. The result is a programmatic
session: a short-lived access token (kbc_at_*) plus a rotating refresh
token (kbc_rt_*) that kbagent renews for you (since v0.80.0).
Read this first:
auth loginneeds a human at a browser to approve.A person has to open the browser (or device-code page) and click approve — that part is inherent to
auth loginand cannot be scripted away. But driving the command is not the human's part: in an attended session (a human present in the chat), an AI agent with a background shell should runauth login --device-code --stack URL --register-projectsthere, relay the printed verification URL + code into the chat for the human to approve, and confirm success withauth status. What an agent must never do is runauth loginin a foreground tool shell (a ~120s timeout kills the flow mid-flight and tells you nothing) or from an unattended task (nobody is there to approve). With no background shell available, hand the plain command to the user's own terminal instead.For CI, containers, cron, or any other unattended context, you have two options: a static Storage token (
kbagent project add --token ..., or the token-onlyKBAGENT_PROJECT_FROM_ENV=1+KBC_TOKEN+KBC_STORAGE_API_URLpath -- unaffected by anything on this page), orkbagent auth login-password(since v0.84.0) if you specifically need a full USER-scoped session rather than a single project's token -- see section 2b below. It is the one deliberate exception to "no unattended path": it needs an account's password (and TOTP seed, if MFA is on) as CI secrets, not a browser.
# 0. No Keboola account at all? Create a project from nothing, then hand the
# printed confirmation link to a human to take ownership of it.
kbagent project create --url https://connection.keboola.com
# 1. Sign in to a stack (a browser opens; finish the login there)
kbagent auth login --stack https://connection.keboola.com
# 2. Pick which projects the session should register locally
kbagent auth register-projects # interactive picker
kbagent auth register-projects --all # or non-interactively
# 3. Use them like any other project
kbagent project list # the Auth column says "session"
kbagent storage buckets --project my-project-9840
# 4. Check on / tear down the session
kbagent auth status
kbagent auth logout --remove-projectsEverything else on this page is about the parts that are not symmetric with a static token: which commands work, what the errors mean, and what you are accepting when you serve a session project over HTTP.
The credential type is a property of each project entry, not of the stack
and not a global mode. It lives in that entry's token field in config.json:
| Static token | Browser-login session | |
|---|---|---|
config.json token field |
the real Storage token | the sentinel kbc-session://{project_id} |
| Where the live credential lives | that same field | auth.json, keyed by stack URL |
| Sent to the API as | X-StorageApi-Token: <token> |
Authorization: Bearer <access token> |
| Renewal | you rotate it (project refresh) |
automatic refresh-token rotation |
| Identity | the token's own scope | the user who logged in |
| Registered by | project add, org setup |
auth login --register-projects, auth register-projects |
project list Auth column |
static |
session |
One config.json can freely mix both. make_client_factory
(services/base.py) is the single branch point and decides per project on
every command, so a static-token project never touches any of the session
machinery: no auth.json read, no refresh, no bearer header.
A session-registered project has no static token stored anywhere; the sentinel
occupies that field. So a code path that only understands static Storage
tokens cannot quietly fall back — it fails with
AUTH_NOT_SUPPORTED_ON_STACK (see section 4).
This is deliberate: the alternative is sending the literal string
kbc-session://9840 to an API as if it were a credential, which yields an
opaque 401 at best and a persisted garbage credential at worst.
The reverse direction is possible, because it is an explicit request — see section 6.
(since 0.95.0)
kbagent project create --url URL [--project ALIAS] [--name NAME] \
[--backend snowflake|bigquery] [--sync-backend-init]Everything else on this page assumes you already have a Keboola account.
kbagent project create is the one command that does not: it provisions a
brand-new project on a stack where you have no identity at all, and hands
back a working session for it. It lives in the project group but is an auth
flow, which is why it is documented here.
What one call does:
POST /manage/programmatic-projects-- unauthenticated by design; the request itself is the credential. Gated by theagent-provisioningstack feature.- Stores the returned session (a project-pinned, Manage-less access +
refresh token pair) in
auth.json, exactly likeauth loginwould. - Registers the new project in
config.jsonunder thekbc-session://sentinel, becoming the default project if nothing else was registered.
The project it creates is owned by nobody. The result's confirm_url is a
single-use link, valid for a few days, that a human opens and signs in at to
take ownership. Until then the project exists, is billable, and has only a
synthetic admin as a member. The link is the only path to ownership, so:
- Copy it out of the output, or re-read it later from
kbagent auth status(agent_confirm_urlunder--json), which keeps printing it while the claim is outstanding. - Confirming revokes the session
project creategave you. That is the design: the synthetic agent identity is retired when a real one takes over. Afterwards, runkbagent auth login --stack URLto sign in as yourself. The registered alias survives the switch untouched -- the sentinel keys on project id + stack, never on a session id.
Other behaviour worth knowing:
- One session per stack. The command refuses (exit 5,
CONFIG_ERROR) whenauth.jsonalready holds a session for that stack: provisioning would replace a real login, and anyone who has one has an account and can create a project in the Keboola UI. --urlis required and never inferred. On a fresh machine there is no default project to infer it from, and provisioning a billable project on a guessed stack is not a mistake worth being able to make.- The command is always available; the capability is not. It is gated by
the
agent-provisioningstack feature (STACK_FEATURES__AGENT_PROVISIONING), off on most stacks, sokbagent project create --helpsays nothing about whether this stack will accept it. Without the feature the stack answers 404, surfaced asAUTH_NOT_SUPPORTED_ON_STACK(exit 1) with a message naming the missing feature, the flag an operator flips, and how to connect an existing project instead -- not a routing bug and not a credential problem. - The call is never retried automatically, on any status. It is not idempotent: each success creates an organization, a project and a credit grant. A 503 is stack-wide provisioning contention and means nothing was created; retry it yourself if you want to.
--backendomitted keeps the stack maintainer's own default.--sync-backend-initwaits for backend initialization; without it the stack finishes in the background and the result warns that the first Storage command may fail until it lands.
kbagent auth login [--stack URL|ALIAS] [--device-code] [--register-projects]- The stack is resolved, never discovered.
--stackaccepts a stack URL (https://connection.keboola.com) or an existing project alias, whose stack URL is then used; an alias is matched first, since aliases are an exact, closed set. Without--stack, the default project's stack is used. With neither, the command fails and names the fix. - PKCE authorization-code flow by default. A loopback listener on localhost receives the callback after you approve in the browser.
- The device flow is the fallback, and it is chosen automatically when a
same-machine browser cannot work: a remote SSH session (
SSH_CONNECTION/SSH_TTY), a container (/.dockerenvor$container), WSL without a workingwslview, or no browser opener at all. The reason is printed. It also takes over if PKCE fails before the code exchange (bind failure, browser could not open, callback timed out).--device-codeforces it. - On the device flow the verification URL and short code are printed — to
stderr under
--json, so piping stdout still leaves the human something to read. Session tokens themselves are never printed. - The new session is persisted before the one it replaces is revoked, so an interruption can never leave you with no usable credential.
- Right after a successful login, kbagent offers the project picker described
below (TTY, non-
--json), or prints a one-line hint pointing atauth register-projects.
kbagent auth login-password --email EMAIL --password PASSWORD [--totp-secret SECRET] \
[--stack URL|alias] [--register-projects]The one command in this whole page that IS safe for a CI job or an agent to run non-interactively -- because it needs credentials handed to it, not a browser. A password grant, straight to the auth service, no loopback listener, no user interaction of any kind.
--email/--password/--totp-secretalso read fromKBC_LOGIN_EMAIL/KBC_LOGIN_PASSWORD/KBC_LOGIN_TOTP_SECRETenv vars (the exact conventionKBC_TOKENalready uses), so a workflow sets them once in a step'senv:block instead of passing flags:- name: Sign in env: KBC_LOGIN_EMAIL: ${{ secrets.KBC_LOGIN_EMAIL }} KBC_LOGIN_PASSWORD: ${{ secrets.KBC_LOGIN_PASSWORD }} KBC_LOGIN_TOTP_SECRET: ${{ secrets.KBC_LOGIN_TOTP_SECRET }} run: kbagent auth login-password --register-projects
--totp-secretis the account's base32 TOTP seed -- the same string an authenticator app scans from the enrollment QR code, not a live 6-digit code. kbagent computes the current code itself (auth/totp.py, plain stdlib RFC 6238 -- no dependency added) at the moment it calls the login endpoint. Nobody types a live code; the seed is the only secret involved.- Only TOTP-based MFA can be resolved this way. If the account's MFA
factor is WebAuthn/passkey instead, there is no shared secret to compute
a response from -- a WebAuthn ceremony is a live cryptographic exchange
that can only run in a real browser holding the actual passkey/security
key, a hard constraint of the protocol, not a missing feature here -- this
command fails fast with
AUTH_MFA_INVALIDnamingauth loginas the fallback for that account. - The resulting session is stored in
auth.jsonand, from here on, shares the same mechanics as a browser-login session: same bearer dispatch, same refresh rotation, same--register-projectscontract, sameproject listAuthcolumn (session), same section 4 restrictions. Its privilege is not always the same -- see the next point. - For an MFA-enabled account, this session carries a live 3-hour "sudo"
window that a browser-login session usually does not. The password
flow completes MFA and creates the session in one server-side step
(
createSessionAfterMfa), which stamps the sudo timestamp unconditionally; PKCE/device instead inherit whatever sudo state the browser session already had, which is typically stale or absent. Sudo gates exactly the account-takeover-shaped operations on the Connection UI/API (PAT create/revoke, TOTP delete, WebAuthn delete/register, recovery-code regeneration, revoke-all-sessions) -- none of which kbagent itself calls, but any script holding this session's tokens effectively can for the next 3 hours. Treat the CI secrets backinglogin-passwordaccordingly. - Two CI jobs must not share one MFA-enabled account within the same
30-second window. The server accepts each TOTP code exactly once; a
second
login-passwordcall submitting a code for the same time slice fails outright, and a 429/5xx retry never resubmits a stale code either (see the code-level note inauth/auth_client.py). Give concurrent matrix-build legs their own service account, or serialize the login step. - Security posture matters here more than for a single project's token. A password (+ TOTP seed) is the account's full ambient identity, not a scoped credential -- whoever holds these CI secrets can do anything that account can do, everywhere it has access, not just one project's Storage routes. Use a dedicated, least-privileged service account created specifically for this pipeline; never a real person's own login. Revoking access means changing that account's password (and re-enrolling MFA), not a lightweight per-secret revoke.
kbagent auth register-projects [--stack URL|ALIAS] [--all] [--project-id ID ...] \
[--alias ID=ALIAS ...] [--yes]Registers projects the session can see as local aliases. Run it any time after
a login — you do not have to remember --register-projects at login time.
| Invocation | Selection |
|---|---|
--all |
Every accessible project. Mutually exclusive with --project-id. |
--project-id ID (repeatable) |
Only those ids. An id the session cannot access is an error, not a silent skip. |
| Neither | Interactive checkbox picker (arrow keys, space to toggle, a for all, enter to accept). Requires a TTY without --json. |
--all and --project-id are the non-interactive forms, so this command
is safe for an agent to run once a human has logged in. Suggested aliases are
the project name slugified and suffixed with the project id, so two projects
sharing a name still get distinct, usable aliases; --alias ID=ALIAS
overrides. An already-registered project is reported as exists and left
alone — nothing overwrites an alias you already have.
kbagent auth status [--stack URL|ALIAS]| Status | Meaning | Exit code |
|---|---|---|
live |
Access token still valid | 0 |
refreshed |
Access token was renewed during the check | 0 |
degraded |
The auth service was unreachable; on-disk expiry data is being reported instead | 0 |
expired |
The refresh token expired or was revoked | 3 |
missing |
No session is stored for this stack | 3 |
degraded exits 0 on purpose: a network blip must not be reported as a dead
session. Scripts can branch on the exit code without parsing --json.
The output also lists the projects the session can access, and any orphaned
server sessions — sessions whose remote revoke did not confirm; auth logout
retries them.
kbagent auth logout [--stack URL|ALIAS] [--remove-projects] [--yes]Local credentials are always cleared, even when the remote revoke fails or
is uncertain; that outcome is reported distinctly rather than dressed up as a
clean success. --remove-projects additionally drops the local aliases this
session registered — projects you later converted to a static token are left
alone, since they no longer belong to the session
(section 6).
Because --remove-projects deletes project entries, it needs the admin
permission class, while the bare auth logout needs only write. A policy that
denies cli:admin to keep an agent out of the project registry still lets it end
its own session.
Logout clears the whole per-stack record, so an orphan it could not revoke is
also forgotten locally — the last place it is reported is that logout's own
output (orphans_remaining in --json). A session listed there is still live on
the server until its refresh token expires; end it from the Keboola UI if that
matters. Repeated logins do not have this problem: each one carries the
outstanding orphan list forward, so auth logout still retries every one of
them.
Both files sit in the same config directory — ~/.config/keboola-agent-cli/ by
default, or whatever --config-dir, KBAGENT_CONFIG_DIR, or a project-local
.kbagent/ resolves to (see User Guide):
| File | Contents | Mode |
|---|---|---|
config.json |
Projects, aliases, defaults. Session projects hold the sentinel here. | 0600 |
auth.json |
The live sessions, keyed by stack URL. | 0600 |
auth.json.lock |
Sidecar lock guarding each read/write of auth.json. Concurrent refreshes are serialised by a lease recorded inside auth.json itself, so this lock is never held while a request is in flight. |
— |
The sentinel is an ordinary string in an existing config.json field, so the
file's schema and CURRENT_CONFIG_VERSION accommodate it as they are and any
kbagent build can still load it. Such a build simply sees a project whose token
looks odd, and every path that would spend it as a credential refuses to.
auth.json is re-tightened to 0600 if another process widens it.
| Area | On a session project |
|---|---|
Storage API — storage, config, job, flow (read/list), branch, workspace, search, sync, transformation |
Works, over bearer auth, including refresh rotation and a single 401 retry |
Manage API — project (members, invitations), org, feature, sharing (project-token path) |
Works |
kbagent serve (REST API + Web UI) |
Works — the REST API inherits the CLI's session behaviour, bearer support and fail-fast guards alike, with the accepted risks in section 5 |
kai |
AUTH_NOT_SUPPORTED_ON_STACK |
semantic-layer (Metastore Service) |
Works, over bearer auth — except semantic-layer token --encrypt, which still needs a static token (AUTH_NOT_SUPPORTED_ON_STACK) |
data-app (Data Science Service) |
Works, over bearer auth |
stream (Data Streams Service) |
Works, over bearer auth |
AI Service paths — docs query, config examples, config new, component detail, component list --query, flow new / update / validate --project |
Works, over bearer auth |
Scheduler Service paths — flow schedule, flow schedule-remove |
Works, over bearer auth |
Storage bucket sharing — sharing share, sharing unshare |
Works, over bearer auth — the Storage API enforces the master privilege |
The importable SDK (keboola_agent_cli.Client) |
AUTH_NOT_SUPPORTED_ON_STACK — construct it with a static token (Python SDK) |
The rows above that still report AUTH_NOT_SUPPORTED_ON_STACK are
SESSION_UNSUPPORTED_FEATURES in services/_auth_registration.py. auth login
and auth register-projects print that list once they have registered
something, and carry it as
session_unsupported_features in --json, so you learn the restrictions up
front rather than at first use. auth status does not carry that field.
Notes worth knowing before you hit them:
dev-portalis unaffected. It authenticates with its own Developer Portal identity (dev-portal identity add), not with a project token, so a session project changes nothing there.flowworks on a session.flow list/flow detailare plain Storage calls.flow new/flow update/flow validate --projectfetch the live schema from the AI Service, andflow schedule/flow schedule-removereach the Scheduler. Both backends now accept a session's bearer token.configworks, includingconfig new.config list,detail,search,update, the row and metadata subcommands andvariables-*are pure Storage calls.config newbuilds the new configuration from the component schema fetched from the AI Service, which now accepts a session's bearer token, so it runs normally on a session project.- The failure is immediate and typed, not an opaque 401 from the service: the guard fires before the client is even constructed and names the feature it refused.
serve supports session projects because it never turns a project into
credentials itself — every service in its registry resolves its own client
factory, so the REST surface inherits exactly the CLI's behaviour, bearer
support and fail-fast guards alike.
Two properties come with that, and both are consciously accepted in
exchange for being able to drive a session-backed project from the web UI at
all: the serve token borrows a user identity (whoever holds
KBAGENT_SERVE_TOKEN acts as the signed-in Keboola user for as long as the
session lives), and refresh-token rotation was designed for short CLI
invocations rather than a daemon up for weeks. Both are spelled out, with what
to do about them, in
kbagent serve > Session-registered projects
— read that before exposing a session project over HTTP. For a project you
would rather not expose this way, register it with a static Storage token; that
path has neither property.
How a credential failure reaches a REST caller:
| Condition | HTTP | error_code |
|---|---|---|
| A static-token-only path was handed a session project | 400 | AUTH_NOT_SUPPORTED_ON_STACK |
| The session expired or was revoked while the server ran | 401 | SESSION_EXPIRED |
| No session is stored for the project's stack | 401 | SESSION_NOT_FOUND |
The 400 lines up with exit code 5 on the CLI — same code, same meaning, over
either surface. The two 401s are the caller's authentication problem rather
than an upstream fault, and the server cannot clear them on its own: a browser
login only completes where a human sits, so the remedy names
kbagent auth login on the host running serve.
Sometimes you want a project registered by browser login to keep working somewhere sessions do not reach — a scheduled agent, a CI step, the importable SDK. Give that project a static token explicitly:
kbagent project edit --project my-project-9840 --token YOUR_STATIC_TOKENThis is allowed and it warns while doing it: the project becomes a
static-token project, so kbagent auth logout --remove-projects will no longer
clean it up — remove it with kbagent project remove when you are done.
What will not convert it behind your back:
kbagent project refreshandkbagent org setup --refreshskip session projects and say why ("nothing to refresh; session access tokens rotate automatically"), including under--force. A bulk maintenance command never changes a project's credential type.- Any other path that would overwrite the sentinel with a token is rejected
with
AUTH_NOT_SUPPORTED_ON_STACK.
To see which is which at a glance: kbagent project list and
kbagent project status carry an Auth column, kbagent project info an
Auth row, and --json output an auth_mode field (session / static) on
every project entry. In the human tables a session project's Token cell is a
dash, so a masked sentinel never reads as a plausible truncated credential.
--json output carries a stable error_code; branch on that, never on the
message text. Full catalogue:
Error Code Reference.
| Code | What happened | What to do |
|---|---|---|
AUTH_NOT_SUPPORTED_ON_STACK |
Either the stack has no browser login, or a static-token-only path was handed a session project | Use a static Storage token for that project or that command (section 4) |
AUTH_BROWSER_UNAVAILABLE |
No usable browser for the loopback flow | Nothing to do: the no-browser case degrades to the device flow rather than failing, so this code rarely surfaces. --device-code skips the loopback attempt entirely |
AUTH_FLOW_TIMEOUT |
The callback or device poll deadline elapsed | Re-run auth login and complete the browser step |
AUTH_FLOW_DENIED |
You (or the authorization server) declined the request | Re-run and approve, or check your stack permissions |
AUTH_FLOW_EXPIRED |
The device code / authorization code expired unused | Re-run auth login |
AUTH_STATE_MISMATCH |
The PKCE callback's state did not match the one issued |
Re-run auth login; if it repeats, something is intercepting the callback |
SESSION_EXPIRED |
The refresh token expired or was revoked | kbagent auth login again — on the host, if this came from serve |
SESSION_NOT_FOUND |
No session is stored for this stack | kbagent auth login --stack <url-or-alias> |
AUTH_MFA_INVALID |
auth login-password hit an MFA factor it cannot resolve (e.g. WebAuthn-only) |
Use kbagent auth login for that account instead |
In a multi-project command, a per-project failure appears in the errors array
of the result envelope with its own error_code, so one session project cannot
mask the outcome for the static ones alongside it.
The same codes reach a REST caller of kbagent serve with the HTTP statuses in
section 5.
| Document | What it adds |
|---|---|
| README | The four ways to register projects, side by side |
| User Guide | Config directories, per-directory isolation, the permission firewall |
| Tutorial | End-to-end walkthrough of registering projects and installing the plugin |
| Error Code Reference | Every error_code kbagent emits |
kbagent serve |
The REST/Web UI side of session projects |
| Python SDK | The importable Client, which takes a static token |
| Design record | Why the flow is shaped this way, in detail |