Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fb7267c
docs: implementation plan for PKCE + device authorization login (prog…
zajca Jul 24, 2026
dd6eaa1
docs: store session tokens plaintext in auth.json (drop keyring/encry…
zajca Jul 27, 2026
57a9bcb
docs: address PR #535 review (B-1..B-4, NB-1..NB-5)
zajca Jul 27, 2026
31ac7cb
feat(auth): browser login via PKCE + device authorization (#535)
zajca Jul 27, 2026
f9ed373
test(auth): type the cross-process race fake as an AuthClient subclass
zajca Jul 27, 2026
24d558e
feat(auth): interactive project picker after login (#535)
zajca Jul 28, 2026
981a519
fix(auth): classify a server-rejected refresh without relying on its …
zajca Jul 28, 2026
95e3fe5
feat(auth): arrow-key + spacebar project picker
zajca Jul 28, 2026
20c29f4
docs(auth): sync command docs for the arrow-key project picker
zajca Jul 28, 2026
4b9b4b7
refactor(auth): drop a duplicate TYPE_CHECKING block left by a rebase
zajca Jul 28, 2026
b523c59
fix(auth): honour a server-sent refresh-token expiry at login, not on…
zajca Jul 28, 2026
875065f
fix(auth): protect and surface a project's credential type
zajca Jul 29, 2026
452d0e8
fix: keep a per-project error code intact in multi-project commands
zajca Jul 29, 2026
05a9109
fix(auth): bound the refresh call, canonicalize stack URLs, tighten p…
zajca Jul 29, 2026
b629c7c
refactor(auth): disclose the v1 scope up front and move selection out…
zajca Jul 29, 2026
9c29438
fix(server): answer 401 for an expired session, keep a subclass error…
zajca Jul 29, 2026
6b348c0
test(auth): wire the bearer E2E suite into a target that actually runs
zajca Jul 29, 2026
eec88ab
docs(auth): add a user-facing guide and correct the v1 scope everywhere
zajca Jul 29, 2026
bdef160
docs(auth): correct which commands disclose the session restriction list
zajca Jul 29, 2026
ca1da89
fix(permissions): gate `auth logout --remove-projects` behind the adm…
zajca Jul 29, 2026
b5f71f2
refactor(auth): make the static-token guard a property of the client
zajca Jul 29, 2026
6dc5818
ci: reject an unguarded session-sentinel path mechanically
zajca Jul 29, 2026
905e742
fix(auth): keep orphans recorded across re-login, refuse a non-https …
zajca Jul 29, 2026
b734faa
fix(auth): enforce the refresh ceiling, narrow the grant-rejection ma…
zajca Jul 29, 2026
7114e88
ci: catch every credential-write shape and unguarded client construction
zajca Jul 29, 2026
a70a8e2
test(auth): drive auth status through the CLI, name a migration conse…
zajca Jul 29, 2026
35750fd
chore: retarget release at 0.78.0
zajca Jul 29, 2026
906d8d0
docs(auth): state what abandoning a stalled refresh actually does
zajca Jul 29, 2026
ce4a8f4
fix(auth): serialise refresh with a lease instead of the file lock
zajca Jul 29, 2026
f65ef78
fix(auth): classify a 400 by subject plus verdict, not by fixed phrases
zajca Jul 29, 2026
2134c36
ci: resolve names and base chains in the sentinel gate
zajca Jul 29, 2026
c518a61
fix(auth): stop a no-op logout from creating an auth.json
zajca Jul 30, 2026
8fd80b7
fix(auth): reject a lease from a wrong clock, veto a request-shape 400
zajca Jul 30, 2026
881ca79
chore: retarget release at 0.81.0
zajca Aug 3, 2026
e0bc128
ci: seed the client-construction check with the base that writes the …
padak Aug 1, 2026
f20b12b
fix(auth): retry an abandoned refresh inside the server's grace window
zajca Aug 3, 2026
bb0c742
fix(auth): replay a refresh the server rolled back on a deadlock
zajca Aug 3, 2026
be68a4c
fix(auth): survive a refresh body the decoder cannot parse
zajca Aug 3, 2026
caf257c
fix(auth): make the contention replay traceable and its give-up actio…
zajca Aug 3, 2026
07dec86
docs(auth): state the window the abandon recovery actually reaches
zajca Aug 3, 2026
1a8e247
chore: untrack 19.6 MB of build cache and the per-PR review write-ups
padak Aug 3, 2026
2c50666
chore: release this at 0.80.0 instead of opening a new version
zajca Aug 3, 2026
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,20 @@ htmlcov/
# ruff
.ruff_cache/

# Generic tool cache directory, at any depth (uv, ruff and others use it)
.cache/

# Explorer (deprecated -- use 'llm export' instead)
kbc-explorer/

# Docs (local-only subdirectories)
docs/done/
docs/AGENT-REPORTS/

# Per-PR review write-ups: working notes for one PR, not repo documentation.
# Root-anchored so a genuine document under docs/ can never match the glob.
/review-pr-*.md

# OS
.DS_Store
Thumbs.db
Expand Down
53 changes: 53 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ Full author checklist: see `CONTRIBUTING.md` > "Releasing a beta (pre-release) v

Forgetting any of these does not fail tests or lint -- it ships an AI agent that quietly recommends commands that do not exist on the user's installed kbagent version, or refuses commands that do. Treat the change as **not done** until every applicable file has been updated.

18. **Session-sentinel guards are CI-enforced.** `make check-sentinel-guards`
(in `make check`) rejects three kinds of drift: a `config.json` credential
write that is not sentinel-aware, a `BaseHttpClient` subclass that neither
declares `SESSION_AUTH_FEATURE` nor is recorded as bearer-capable, and a
`require_static_token` guard missing from `SESSION_UNSUPPORTED_FEATURES`
(`services/_auth_registration.py`) -- the tuple `auth login` /
`auth register-projects` disclose and every doc surface defers to. Run
`python scripts/check_sentinel_guards.py --list` to see the inventory.

## Claude Code Plugin (Marketplace)

This repo doubles as a Claude Code plugin marketplace. The plugin lives in `plugins/kbagent/` and exposes four AI surfaces: a CLI (`kbagent`), a skill (`kbagent`), a slash command (`/keboola`), and a specialist subagent (`keboola-expert`). All are namespaced under `kbagent:`.
Expand Down Expand Up @@ -280,6 +289,50 @@ plugins/kbagent/
# Global options: --json, --verbose, --no-color, --config-dir, --deny-writes, --deny-destructive, --allow-env-manage-token
# Headless / token-only (0.50.0+): export KBAGENT_PROJECT_FROM_ENV=1 + KBC_TOKEN + KBC_STORAGE_API_URL to synthesize an in-memory `__env__` project (no `project add`, no config.json on disk; token never persisted). Use `--project __env__`. Same env setup also powers `kbagent serve`.

kbagent auth login [--stack URL|alias] [--device-code] [--register-projects]
kbagent auth status [--stack URL|alias]
kbagent auth logout [--stack URL|alias] [--remove-projects] [--yes]
kbagent auth register-projects [--stack URL|alias] [--all] [--project-id ID ...] [--alias ID=ALIAS ...] [--yes]
# auth (since 0.80.0): browser-based login -- PKCE authorization-code by default (falls back to the
# RFC 8628 device flow ONLY on a pre-exchange failure: no loopback browser, callback timeout, or an
# SSH/container/WSL heuristic; --device-code forces it). REQUIRES A HUMAN AT A BROWSER -- never attempt
# from an unattended AI agent task; use a static Storage token for CI/headless instead. Issues a
# USER-scoped "programmatic session" (kbc_at_* access token + kbc_rt_* refresh token) stored in
# auth.json (0600), a sibling of config.json -- config.json's schema and CURRENT_CONFIG_VERSION are
# unchanged. --register-projects writes each accessible project into config.json with the sentinel
# token `kbc-session://{project_id}`. v1 scope is the Storage + Manage paths: the CLI commands and
# `serve` both reach them, because `serve` delegates to the same already-guarded services (see
# server/dependencies.py). Everything outside those paths fails fast on a sentinel-token project
# (AUTH_NOT_SUPPORTED_ON_STACK) naming the static-token fallback; the authoritative list is
# SESSION_UNSUPPORTED_FEATURES in services/_auth_registration.py, shipped to callers as
# `session_unsupported_features` in --json -- do not re-derive it by hand. `dev-portal` is NOT on it
# (it authenticates with its own identity, never a project token). Over `serve`, a session that
# expires at runtime answers HTTP 401 with
# error_code SESSION_EXPIRED -- a browser login only completes on the host. Serving session projects
# means whoever holds KBAGENT_SERVE_TOKEN acts as the signed-in USER; see docs/web-server.md.
# New error codes: AUTH_NOT_SUPPORTED_ON_STACK, AUTH_FLOW_TIMEOUT, AUTH_FLOW_DENIED, AUTH_FLOW_EXPIRED,
# AUTH_BROWSER_UNAVAILABLE, AUTH_STATE_MISMATCH, SESSION_EXPIRED, SESSION_NOT_FOUND.
# `auth register-projects` (0.80.0+): fixes the usability gap where nothing was registered unless
# --register-projects was passed at login, and where the alias offered was a slug of the project
# NAME (never the numeric id, so `--project 9840` never resolves). Lists every project the session
# can access with a collision-free suggested alias, then lets the caller pick which to register.
# --all selects every candidate; --project-id ID (repeatable) selects specific ones (unknown id ->
# ConfigError); omitting both runs an interactive arrow-key + spacebar checkbox picker (every
# not-yet-registered project preselected, [a] toggles all, [enter] accepts) followed by a single
# "Edit aliases?" confirm (default No) that opens the old per-project alias prompt only if opted
# into -- each row already shows its suggested alias. On a piped stdin or a terminal without real
# interactive capabilities, it falls back to the original numbers/ranges/'all'/'none' typed prompt.
# In a non-TTY or --json context with neither --all nor --project-id, it fails fast telling the
# caller to pass --all or --project-id. --alias ID=ALIAS (repeatable) overrides the
# suggested alias in every mode. --yes skips only the picker's final confirmation. Never overwrites
# an existing alias: same project+stack already registered -> status "exists" (no-op); alias taken by
# something else -> status "skipped" with a rename hint. `auth login` (no --register-projects) also
# offers this same picker interactively right after a successful login when stdout is a TTY and
# --json was not used; otherwise it just prints the `auth register-projects` hint. Same picker fix
# applies retroactively to `auth login --register-projects` (now suffixes on an alias collision
# instead of silently skipping the second project). See docs/programmatic-auth-login-plan.md
# section 4.5 for the full design.

kbagent project add --project NAME --url URL --token TOKEN
kbagent project list
kbagent project remove --project NAME
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ before the PR is mergeable.
- [ ] **Run `make typecheck`** -- `ty` must pass clean (0 diagnostics; the backlog was cleared in 0.45.0, so the gate is blocking, not warning-only)
- [ ] **No new `tuple[...]` returns** -- multi-value returns use a `@dataclass` ([Code Quality Patterns](#code-quality-patterns))
- [ ] **No raw error-code strings** -- `make check-error-codes` enforces `ErrorCode` enum usage
- [ ] **Session sentinels stay guarded** -- `make check-sentinel-guards` enforces it; if you added an HTTP client, declare `SESSION_AUTH_FEATURE` (or record why it is bearer-capable), and if you added a guard, add the matching entry to `SESSION_UNSUPPORTED_FEATURES`
- [ ] **File-size budgets respected** -- see the table in [Code Quality Patterns](#code-quality-patterns); split before crossing the hard ceiling

### UX considerations
Expand Down Expand Up @@ -677,13 +678,14 @@ foot-gun.
## Running CI Locally

```bash
make check # CI parity: lint + format + typecheck + skill + version + command-sync + changelog + error-codes + test
make check # CI parity: lint + format + typecheck + skill + version + command-sync + changelog + error-codes + sentinel-guards + test
make lint # Just the ruff linter
make format # Auto-format code
make typecheck # Static type check (Astral `ty`)
make test # Just the test suite (no coverage)
make test-cov # Test suite + informational coverage report (term-missing)
make command-sync-check # Verify every CLI command is registered + documented
make check-sentinel-guards # Verify no kbc-session:// sentinel path is unguarded
make skill-gen # Regenerate SKILL.md from CLI command metadata
```

Expand Down
14 changes: 10 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.DEFAULT_GOAL := help

.PHONY: help install install-mcp install-server sync test test-unit test-integration test-e2e test-e2e-local test-e2e-invite test-e2e-feature test-e2e-stream test-file test-cov lint lint-fix format format-check typecheck typecheck-warn skill-check skill-gen version-sync version-check changelog changelog-check check-error-codes loc-check loc-report loc-baseline parity-check command-sync-check gen-command-reference check clean hooks web-install web-dev-backend web-dev-frontend web-build web-clean
.PHONY: help install install-mcp install-server sync test test-unit test-integration test-e2e test-e2e-local test-e2e-invite test-e2e-feature test-e2e-stream test-e2e-auth test-file test-cov lint lint-fix format format-check typecheck typecheck-warn skill-check skill-gen version-sync version-check changelog changelog-check check-error-codes check-sentinel-guards loc-check loc-report loc-baseline parity-check command-sync-check gen-command-reference check clean hooks web-install web-dev-backend web-dev-frontend web-build web-clean

help: ## Show this help message
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
Expand All @@ -26,8 +26,8 @@ test-unit: ## Run unit tests only (exclude integration and e2e)
test-integration: ## Run integration tests only
uv run pytest tests/ -v -m integration

test-e2e: ## Run E2E tests (E2E_API_TOKEN and E2E_URL required)
uv run pytest tests/test_e2e.py tests/test_server_semantic_layer_routes_e2e.py -v -s --tb=long
test-e2e: ## Run E2E tests (E2E_API_TOKEN and E2E_URL required; auth tests skip without session env vars)
uv run pytest tests/test_e2e.py tests/test_server_semantic_layer_routes_e2e.py tests/test_e2e_auth.py -v -s --tb=long

test-e2e-local: ## Run E2E against a project in a local config.json (CONFIG_DIR=/path/.kbagent ALIAS=my-proj)
KBAGENT_E2E_CONFIG_DIR=$(CONFIG_DIR) KBAGENT_E2E_ALIAS=$(ALIAS) \
Expand All @@ -42,6 +42,9 @@ test-e2e-feature: ## Run feature-flag E2E (E2E_MANAGE_TOKEN super-admin + E2E_AP
test-e2e-stream: ## Run Data Streams OTLP E2E (E2E_API_TOKEN + E2E_URL required; creates + deletes a temp source)
uv run pytest tests/test_e2e.py -v -s --tb=long -k test_stream_otlp_e2e

test-e2e-auth: ## Run programmatic-auth E2E (E2E_URL + E2E_SESSION_REFRESH_TOKEN + E2E_SESSION_PROJECT_ID required)
uv run pytest tests/test_e2e_auth.py -v -s --tb=long

test-file: ## Run a specific test file (FILE=tests/test_cli.py)
uv run pytest $(FILE) -v

Expand Down Expand Up @@ -116,6 +119,9 @@ parity-check: ## Diff live keboola-mcp-server tool catalog vs the parity map (ne
command-sync-check: ## Verify every CLI command is registered + documented (silent-drift gate)
uv run python scripts/check_command_sync.py

check-sentinel-guards: ## Reject an unguarded kbc-session:// sentinel path (silent-drift gate)
uv run python scripts/check_sentinel_guards.py

gen-command-reference: ## Generate command-reference.md from the live Typer app (release asset)
uv run python scripts/gen_command_reference.py --output command-reference.md

Expand All @@ -124,7 +130,7 @@ hooks: ## Install git pre-commit hook (lint + format on staged files)
chmod +x .git/hooks/pre-commit
@echo "Pre-commit hook installed."

check: lint format-check typecheck skill-check version-check command-sync-check changelog-check check-error-codes loc-check test ## Run all checks (lint + format + typecheck + skill + version + command-sync + changelog + error-codes + file-size + test)
check: lint format-check typecheck skill-check version-check command-sync-check changelog-check check-error-codes check-sentinel-guards loc-check test ## Run all checks (lint + format + typecheck + skill + version + command-sync + changelog + error-codes + sentinel-guards + file-size + test)

clean: ## Remove build artifacts and caches
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ kbagent workspace query --project prod --workspace-id WS_ID \

## Setup options

Three ways to register projects, depending on what you have.
Four ways to register projects, depending on what you have. If you are a human
at a terminal with a browser, start with **browser login** (last one below);
the token-based options are the ones to use for CI and anything unattended.

**Single project** — you have a Storage API token from the UI:
```bash
Expand All @@ -193,6 +195,22 @@ KBC_MANAGE_API_TOKEN=your-org-admin-manage-token \
kbagent --allow-env-manage-token org setup --org-id 123 --url https://connection.keboola.com --yes
```

**Browser login** — you have no token and a browser on this machine (since 0.80.0):
```bash
# Opens a browser (PKCE), or prints a code to type in on another device.
kbagent auth login --stack https://connection.keboola.com

# Then pick which of the accessible projects to register locally.
kbagent auth register-projects # interactive picker
kbagent auth register-projects --all # non-interactive
```
> **Needs a human at a browser.** There is no headless path, so never run
> `auth login` from an unattended AI-agent task or a CI step — use a static
> Storage token there. Session-registered projects also do not work with every
> command (MCP tools, Kai, data apps, semantic layer, streams, the Python SDK
> need a static token). Details, capability matrix and error codes:
> [docs/auth.md](docs/auth.md).

Run `kbagent doctor` to verify setup (token validity, CLI version, MCP server, Claude Code plugin install).

> **Step-by-step guide with dry-runs, token descriptions, expiry, and
Expand All @@ -204,6 +222,7 @@ Full command reference with flags: [SKILL.md](plugins/kbagent/skills/kbagent/SKI

```
kbagent search QUERY [--type table|bucket|config|flow|data-app|transformation] # cross-project search (0.30.0)
kbagent auth login | status | register-projects | logout # browser login, needs a human (0.80.0)
kbagent project add | list | remove | edit | status | refresh | info | use | current
description-get | description-set
invite | member-list | member-remove | member-set-role
Expand Down Expand Up @@ -260,6 +279,7 @@ kbagent init | context | doctor | version | update | changelog
| Guide | What it covers |
|-------|---------------|
| [Tutorial](docs/TUTORIAL.md) | End-to-end walkthrough: register projects (1, N, whole org), global vs local config, plugin install, using the specialist subagent and `/keboola` slash command. |
| [Browser login](docs/auth.md) | `kbagent auth login` / `status` / `register-projects` / `logout`: the two credential models, what works on a session-registered project, error codes, and the accepted risks of serving one over HTTP. |
| [User Guide](docs/guide.md) | Configuration, permissions, per-directory isolation, workflows |
| [Python SDK](docs/sdk.md) | The in-process importable `Client`: method reference, typed result models, `py.typed`, idempotent jobs, gotchas, and how to extend the SDK. Demo: [`examples/storage_tui/`](examples/storage_tui/). |
| [Build a REST client](docs/build-your-own-client.md) | The `kbagent serve` HTTP API spec for non-Python callers (JS, Go, Slack bots, Web UIs). |
Expand Down
Loading
Loading