Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.84.1",
"version": "0.84.2",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,16 @@ kbagent sharing edges [--project NAME]
kbagent org setup --org-id ID --url URL [--dry-run] [--yes] [--token-description PREFIX] [--refresh]
kbagent org setup --project-ids 1,2,3 --url URL [--dry-run] [--yes] [--token-description PREFIX] [--refresh]

kbagent billing credits [--project ALIAS ...]
# billing credits (0.84.2+, issue #594 secondary ask): read-only PAYG credit balance, fanned out
# across all registered projects in parallel by default (--project repeatable narrows). A project
# without the `pay-as-you-go` owner.features flag never touches the billing host (NXDOMAIN on some
# non-PAYG stacks) -- it gets a per-project error_code PAYG_NOT_AVAILABLE instead; per-project
# failures degrade individually, the run never aborts. Rows report the API's native unit (credits)
# AND derived minutes (1 credit = 60 min, matching the Keboola UI). Purchase history / Stripe
# invoice IDs are NOT available here -- that data lives on connection.{stack}
# /pay-as-you-go/billing/*, which does not accept a Storage token (issue #594 primary ask, open).
Comment thread
padak marked this conversation as resolved.

# feature: requires a super-admin Manage API token (inline hidden prompt; never persisted; --allow-env-manage-token for CI). --project resolves the stack URL (+ project_id for project ops) from config.
kbagent feature list --project ALIAS
kbagent feature project-show --project ALIAS
Expand Down
6 changes: 6 additions & 0 deletions docs/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,9 @@ of `ErrorCode` in `src/keboola_agent_cli/errors.py`.
| `SESSION_EXPIRED` | The programmatic-auth session's refresh token expired or was revoked; run `kbagent auth login` again |
| `SESSION_NOT_FOUND` | No programmatic-auth session is persisted for this stack; run `kbagent auth login` |
| `AUTH_MFA_INVALID` | `auth login-password` hit an MFA factor it cannot resolve without a browser (e.g. WebAuthn-only) -- use `kbagent auth login` for that account instead |

### Billing (Pay-As-You-Go)

| Code | Description |
|---|---|
| `PAYG_NOT_AVAILABLE` | The project does not have the `pay-as-you-go` feature, so it has no credit balance; the billing host may not even resolve on this stack |
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.84.1",
"version": "0.84.2",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
4 changes: 3 additions & 1 deletion plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ description: >
data stream, OTLP, scoped token, bucket sharing, encrypt secrets,
feature flag, flow schedule, invite member, SQL transformation edit,
sync action, keboola docs, table snapshot, auth, login, sign in,
browser login.
browser login, PAYG credits.
---

# kbagent -- Keboola Agent CLI
Expand Down Expand Up @@ -101,6 +101,7 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Mint a scoped Storage API token (secret shown once) | `kbagent token create --project PROJECT --description DESCRIPTION` |
| Revoke a Storage API token immediately (destructive; only non-master tokens) | `kbagent token delete --project PROJECT --token-id TOKEN-ID` |
| Rotate a token: generate a new value and invalidate the old one (secret shown once) | `kbagent token refresh --project PROJECT --token-id TOKEN-ID` |
| Show the current PAYG credit balance for one or more projects | `kbagent billing credits` |
| List available components from connected projects | `kbagent component list` |
| Show detailed information about a specific component | `kbagent component detail --component-id COMPONENT-ID` |
| Run a synchronous component action such as testConnection | `kbagent component sync-action <ACTION-NAME> --component-id COMPONENT-ID --project PROJECT` |
Expand Down Expand Up @@ -395,6 +396,7 @@ For detailed response parsing rules and common pitfalls, see [gotchas](reference
| **Typify a typeless table** (profile -> CTAS -> swap-tables -> validate -> handoff) | [typify-table-workflow](references/typify-table-workflow.md) |
| Bucket sharing & linking | [sharing-workflow](references/sharing-workflow.md) |
| **Project members & invitations** (single + bulk via CSV, role change, remove) | [member-workflow](references/member-workflow.md) |
| **Billing / PAYG credits** (balance only; the shape of the invoice-history gap; PAYG_NOT_AVAILABLE; units) | [billing-workflow](references/billing-workflow.md) |
| Dev branches | [branch-workflow](references/branch-workflow.md) |
| Encrypting secrets for MCP tools | [encrypt-workflow](references/encrypt-workflow.md) |
| Sync & Git-branching (GitOps) | [sync-workflow](references/sync-workflow.md) |
Expand Down
157 changes: 157 additions & 0 deletions plugins/kbagent/skills/kbagent/references/billing-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Billing (PAYG Credits) workflow

> Audience: a developer or a kbagent agent asked about Keboola PAYG (pay-as-
> you-go) usage, balance, or invoices. Goal: know exactly what `kbagent
> billing` can and cannot answer *before* burning a loop hunting for a
> command that does not exist. (since v0.84.2; issue
> https://github.com/keboola/cli/issues/594)

## The shape of the gap (read this first)

`kbagent billing` is **one command**: `billing credits`. It reports the
*current PAYG balance*, nothing more. There is **no** `billing history`,
`billing invoices`, or anything that returns `idStripeInvoice` — and there is
no way to build one from the CLI today. If a user asks to "reconcile our
Stripe invoices against Keboola projects" or "when did we last top up," the
correct answer is **that data is not reachable from kbagent** (see
[What is NOT reachable](#what-is-not-reachable-and-why) below) — not a
workaround, not a guess, and not a hand-rolled HTTP call.

## What IS reachable today

```bash
kbagent --json billing credits --project prod --project staging
```

```json
{
"credits": [
{
"project_alias": "prod",
"project_id": 9621,
"consumed": 12.5,
"remaining": 25.5,
"total": 38.0,
"consumed_minutes": 750.0,
"remaining_minutes": 1530.0,
"component_jobs_consumed": 11.75,
"workspace_jobs": [
{"workspace_type": "sandbox-sql", "warehouse_size": "small", "consumed": 0.5},
{"workspace_type": "writer", "warehouse_size": "small", "consumed": 0.25}
]
},
{
"project_alias": "staging",
"project_id": null,
"consumed": 0.0,
"remaining": 0.0,
"total": 0.0,
"consumed_minutes": 0.0,
"remaining_minutes": 0.0,
"component_jobs_consumed": 0.0,
"workspace_jobs": []
}
],
"errors": []
}
```

- `--project ALIAS` is repeatable; omit it to fan out across every registered
project in parallel.
- `consumed` / `remaining` come straight off `GET /credits` on the
`billing.{stack}` host, which -- unlike the invoice endpoints below --
**does** accept the CLI's normal per-project `X-StorageApi-Token`. No
manage token, no extra login step.
- `total` is a client-side convenience: `consumed + remaining`. It is NOT
named `purchased`, and must not be reported as an amount purchased. It
equals the purchased amount only if every credit ever added is either
still available or already consumed, so it silently mis-reports against
expired, revoked, or promo credits -- and credit purchases are precisely
what this command cannot see (see the gap section below). If a user asks
"how much have we bought", the honest answer is that the CLI cannot tell
them, not this number.
- Per-project failures land in `errors`, never abort the run -- always check
both arrays, not just `credits`.

## The units trap

The API speaks **credits**. The Keboola UI speaks **minutes**. The
conversion is fixed: **1 credit = 60 minutes**. Every row already carries
both — never hand-convert, and never convert in the other direction (minutes
-> credits) on a value that already came from the CLI.

```
consumed_minutes = consumed * 60
remaining_minutes = remaining * 60
```

Money is a separate axis again, and this is where it is easy to mislead a
user. The credits-to-minutes factor above is invariant; **the price per credit
is not**. It is contract- and stack-specific, the API never returns it, and the
CLI has no price field. Issue #594 records **$8.40 ex. VAT per credit** on one
contract — that is a single historical observation, NOT a platform constant.
Never quote it to a user as their rate, and never derive a dollar figure from
it. It appears here only to make the UI's arithmetic legible: on that contract,
8 credits rendered as "480 minutes ($67.20)". If a user needs the money number,
it comes from their contract or invoice, not from `billing credits`.

## The PAYG gate

Not every project is PAYG. Before `billing credits` ever calls the billing
host, it checks the `pay-as-you-go` flag in the project token's
`owner.features` (`GET /v2/storage/tokens/verify`). A project without that
flag gets a per-project entry:

```json
{"project_alias": "legacy-project", "error_code": "PAYG_NOT_AVAILABLE",
"message": "Project 'legacy-project' does not have the pay-as-you-go feature enabled; PAYG balance is only available on PAYG projects."}
```

**This is a feature-flag verdict, not a network failure.** On a non-PAYG
stack (e.g. plain `eu-central-1`) the service index still *advertises* a
`billing.eu-central-1.keboola.com` host, but it does not resolve (NXDOMAIN)
— the feature check exists specifically so a non-PAYG project never dials
that host at all. Do not treat `PAYG_NOT_AVAILABLE` as something to retry or
debug as connectivity; it means "this project has no PAYG balance to show."

## What is NOT reachable, and why

Purchase history and `idStripeInvoice` live on `connection.{stack}` under
`/pay-as-you-go/billing/*` — a **completely different host and API surface**
from `billing.{stack}/credits`. That endpoint does not accept a Storage API
token: a request with `X-StorageApi-Token` gets the byte-identical
302-to-login response as an unauthenticated request, and presenting the
token as a bearer credential gets a plain 401. There is no
project-token-based path to it, so there is nothing for `kbagent billing` to
wrap.

This is the **still-open primary ask of issue #594**:
https://github.com/keboola/cli/issues/594 — link it verbatim when a user
asks about invoice access so they can track the maintainer's answer.

**Do not improvise a substitute.** In particular:

- Do not attempt to reach `/pay-as-you-go/billing/*` with `kbagent http`, a
raw `httpx`/`curl` call, or by asking the user to paste a manage/session
token for it — none of those change what the endpoint accepts.
- Do not fall back to matching invoices to projects by `(date, amount)`
heuristics. It silently breaks the moment two projects top up the same
credit amount on the same day, and produces a wrong-but-confident answer
instead of an honest "not available."
- If asked to reconcile invoices, tell the user directly: kbagent can report
the current balance (`billing credits`) but not purchase history; that
reconciliation needs the Stripe/billing portal directly until #594 lands.

## The money guardrail

`POST /credits` on the billing service triggers a **real automatic top-up**
— actual money is charged. It is deliberately not wrapped by any `kbagent`
command, CLI or REST. Never reach for it, and never construct a raw HTTP
call to it (via `kbagent http`, a manual `httpx` request, or otherwise) even
if a user asks "just top up my credits" — that action requires going through
Keboola's own billing UI, not an agent-driven CLI.

## Permission class

`billing.credits` = read. Safe to run under `--deny-writes` /
`--deny-destructive`; it makes no mutating calls.
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ The `permissions` subcommands persist a write/destructive policy to config.json
- `org setup --org-id ID --url URL [--dry-run] [--yes]` -- bulk-onboard all projects from an org (org admin; manage token via interactive prompt by default, or `--allow-env-manage-token` + `KBC_MANAGE_API_TOKEN` for CI on 0.29.0+)
- `org setup --project-ids 1,2,3 --url URL [--dry-run] [--yes]` -- onboard specific projects by ID (any project member; manage token / Personal Access Token via interactive prompt by default, or `--allow-env-manage-token` + `KBC_MANAGE_API_TOKEN` for CI on 0.29.0+)

## Billing (PAYG Credits) (since v0.84.2)
- `billing credits [--project ALIAS ...]` -- read-only PAYG credit balance (`GET /credits` on `billing.{stack}`, plain Storage token). Fans out across all registered projects in parallel by default; `--project` (repeatable) narrows. Per-project failures degrade individually and are collected in `errors`, never abort the run. A project without the `pay-as-you-go` `owner.features` flag never calls the billing host (NXDOMAIN on some non-PAYG stacks) -- it gets an `error_code: PAYG_NOT_AVAILABLE` entry instead. `--json` emits `{"credits": [...], "errors": [...]}`. Rows carry the API's native unit (`consumed`/`remaining` credits) plus derived `*_minutes` fields (1 credit = 60 minutes, matching the Keboola UI). Gives the current balance only -- purchase history / Stripe invoice IDs are not reachable with a project token (issue #594 primary ask, still open; that data lives on `connection.{stack}` `/pay-as-you-go/billing/*`). See [billing-workflow.md](billing-workflow.md) for the full shape of the invoice-history gap and why it must not be worked around.

## Feature Flags (since v0.48.0)
Requires a **super-admin** Manage API token (same kind as `org setup`). Same default-deny token policy: interactive hidden prompt by default, or `--allow-env-manage-token` + `KBC_MANAGE_API_TOKEN` for CI. `--project ALIAS` resolves the stack URL (and, for project ops, the numeric `project_id`) from config -- the alias is the only handle you pass.
- `feature list --project ALIAS` -- the stack-wide feature catalogue (`GET /manage/features`). Returns `{alias, stack_url, features: [{name, title, description, type, ...}]}`. Only `name` is a stable identifier; extra fields pass through unmodified.
Expand Down
36 changes: 36 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -3564,3 +3564,39 @@ machine's locale, and tolerates files that are not valid UTF-8.
--value-file`, and `semantic-layer reference-data set --members-file` read
UTF-8 without a fallback, so a genuinely mis-encoded input file fails loudly
and identically everywhere rather than being silently mis-parsed.

## `billing credits`: credits vs. minutes, array not object, feature-gated before it ever hits the network (since v0.84.2)

`billing credits [--project ALIAS ...]` is a read-only PAYG (pay-as-you-go)
balance check (`GET /credits` on `billing.{stack}`, plain Storage token).
Four things a coding agent will otherwise get wrong:

- **The API speaks credits; the Keboola UI speaks minutes.** `consumed` and
`remaining` in the raw response are in PAYG credits. The CLI derives
`consumed_minutes` / `remaining_minutes` as `credits * 60`
(`MINUTES_PER_CREDIT` in `constants.py`) because that is the unit the UI
actually displays. Always report the pre-computed `*_minutes` fields when a
user asks "how many minutes do I have left" -- do not hand-multiply the
credit fields yourself, and never assume the reverse conversion.
- **`stats.workspaceJobs` is an ARRAY, not the object shape the public
Keboola docs show.** The real payload is
`"workspaceJobs": [{"workspaceType": "sandbox-sql", "warehouseSize":
"small", "consumed": 5.0}, ...]` -- one entry per workspace type/size
combination, not a single object keyed by type. The CLI's `row.workspace_jobs`
is already this list, projected to snake_case; do not try to read it as a
dict.
- **`PAYG_NOT_AVAILABLE` is a feature-flag verdict, not a network failure.**
Before calling the billing host at all, the service checks the
`pay-as-you-go` flag in `owner.features` (`client.has_feature(PAYG_FEATURE)`).
A project without that flag never makes a billing-host request -- on some
stacks (e.g. a plain `eu-central-1` project) the `billing.{stack}` hostname
does not even resolve (NXDOMAIN). Seeing `error_code: PAYG_NOT_AVAILABLE`
in a project's `errors` entry means "this project isn't on PAYG," not "the
billing service is down." A genuine connection/DNS failure past the
feature gate is reported separately and mentions the host is unreachable.
- **Balance only -- no purchase history, no invoice IDs.** `billing credits`
cannot answer "when did we last top up" or "what's our Stripe invoice ID."
That data lives on `connection.{stack}` `/pay-as-you-go/billing/*`, which
does not accept a Storage API token -- it is the still-open primary ask of
issue #594. Do not imply this command covers billing/invoice history; tell
the user it is out of reach from the CLI today.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-cli"
version = "0.84.1"
version = "0.84.2"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
28 changes: 28 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.84.2": [
"New (#594): `kbagent billing credits [--project ALIAS ...]` reads the Pay-As-You-Go "
"credit balance, fanned out across every registered project in parallel. Wraps `GET "
"/credits` on the `billing.{stack}` host, which accepts a plain per-project Storage "
"token -- the endpoint was reachable all along and simply had no command over it. Rows "
"carry the API's native unit (credits: `consumed` / `remaining` / derived `total`) "
"AND derived minutes, because the Keboola UI displays minutes = credits x 60; the "
"conversion only ever runs in that direction, so a unit bug cannot invert into a wrong "
"credit figure. The per-workspace breakdown is parsed tolerantly: the live API returns "
"`stats.workspaceJobs` as an ARRAY, not the object its public docs show. A project "
"without the `pay-as-you-go` flag in `owner.features` is gated out BEFORE any billing "
"request and reported as the new `PAYG_NOT_AVAILABLE` error code -- deliberately not a "
"mapped 4xx, because on non-PAYG stacks the service index advertises a `billing.` host "
"that does not resolve at all, so an ungated call would surface a DNS failure instead "
"of the real reason. Per-project failures degrade individually and never abort the "
"run. Mirrored on `kbagent serve` as `GET /billing/credits`. Read-only by design: the "
"billing service's `POST /credits` triggers a real automatic top-up (real money) and "
"is wrapped by nothing in kbagent.",
"Note (#594): credit PURCHASE history and the Stripe invoice IDs Keboola already "
"stores per project remain unreachable from the CLI. That data lives on "
"`connection.{stack}` under `/pay-as-you-go/billing/*`, which ignores "
"`X-StorageApi-Token` entirely (a Storage-token request returns the byte-identical "
"302-to-login as an unauthenticated one) and rejects it as a bearer with 401 -- so no "
"CLI, script, or scheduled agent can reach it, and anyone funding several PAYG "
'projects on one billing identity still cannot answer "which project does this '
'Stripe invoice belong to" without matching on (date, amount). Issue #594 tracks the '
"ask; `billing credits` is the half that was already implementable.",
],
"0.84.1": [
"Fix: `kbagent config new --push` schema validation now validates the body's "
"`parameters` section instead of the whole configuration object (closes #587). A "
Expand Down
Loading