Skip to content

feat: accept browser-login (session) tokens for the commands whose backend supports them (CLI-13) - #759

Merged
soustruh merged 6 commits into
mainfrom
cli-13-session-tokens
Sep 15, 2026
Merged

soustruh merged 6 commits into
mainfrom
cli-13-session-tokens

Conversation

@soustruh

@soustruh soustruh commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Why

kbagent registers a browser-login project with a kbc-session:// sentinel token, but only the Storage and Manage clients accepted it. Every other client required a static Storage token and refused a session, even though those backends already accept an OAuth bearer token (rollout under DMD-1593, Done). The block was only on the kbagent side and was tracked nowhere.

What changed

services/base.py adds make_session_aware_client_factory(config_store, client_cls): it builds any BaseHttpClient subclass with BearerAuth for a kbc-session:// project and with a static token otherwise. make_client_factory delegates to it. Five service clients (SchedulerClient, StreamClient, DataScienceClient, MetastoreClient, AiServiceClient) accept http_auth and drop the X-StorageApi-Token header when it is set, the same as ManageClient.

A browser-login project now reaches these interfaces:

  • Scheduler – flow schedule, flow schedule-remove
  • Data Streams – the stream command group
  • Data Science – the data-app command group
  • Metastore (semantic layer) – the semantic-layer command group, except semantic-layer token --encrypt (see Out of scope)
  • AI Service – docs query, config new, config examples, component detail, flow new / update / validate
  • Storage bucket sharing – sharing share, sharing unshare: resolve_master_token no longer refuses a session, and the Storage API enforces the master privilege

config oauth-url and data-app git-credentials already worked for a master session: both check the token's real isMasterToken privilege, not whether it is static or a session. No change.

The five clients no longer declare SESSION_AUTH_FEATURE, and they are recorded in the BEARER_CAPABLE_CLIENTS allowlist that check-sentinel-guards enforces. SESSION_UNSUPPORTED_FEATURES shrinks to the three paths that still need a static token, and auth login / auth register-projects disclose that shorter list.

Still to do

  • kbagent kai: it goes through the external kai_client library, which takes a plain Storage token and has no bearer path, so kai_service keeps its static-token guard until the library accepts one.

Out of scope

  • semantic-layer token --encrypt stays static-token-only by design: it encrypts the project's own token so a metastore component can store and reuse it later, and a kbc-session:// sentinel is not a reusable credential.
  • The importable SDK (keboola_agent_cli.Client) stays static-token-only by design: it is a stateless entry point with no config directory and no session store to read a bearer from.
  • Manage API commands (org setup, feature *, project refresh / invite / member-*). A session has no Manage API access by design (DMD-1711); these need a manage token, the same as a static-token project.

Testing

make check passes. TestSessionAwareServiceClients builds each of the five clients through the session-aware factory and checks that a static token keeps the X-StorageApi-Token header, while a session token drops it and gets a bearer. Live-verified on a real stack: the scheduler (read → 200; the activate write path reaches its permission check → 404, not 403), and stream / data-app / semantic-layer / docs query read commands return ok with a session token where the released build refuses with "does not support browser-login".

Refs: CLI-13; backend scheduler#145, mcp-server#406, sandboxes-service#625, keboola-as-code#2621.

The scheduler, data streams, data science, metastore and AI clients
refused a kbc-session:// project and required a static Storage token,
even though their backends already accept an OAuth bearer token. Route
them through the session, the same as the Storage and Manage clients.

services/base.py gains make_session_aware_client_factory(config_store,
client_cls): it builds any BaseHttpClient subclass with BearerAuth for a
kbc-session:// project and with a static token otherwise.
make_client_factory now delegates to it. Each client below accepts
http_auth and drops the X-StorageApi-Token header when it is set, the
same as ManageClient, and each service builds it through the
session-aware factory. A browser-login project now reaches:

- flow schedule / flow schedule-remove (Scheduler)
- stream (Data Streams)
- data-app (Data Science)
- semantic-layer (Metastore)
- kai, docs query, config new / config examples, component detail,
  flow new / update / validate (AI service)

The static-token guard stays as the fallback for the no-bearer path: a
client built with a session token and no bearer still fails fast.
resolve_master_token fell back to require_static_token, which rejected a
kbc-session:// project outright. It now returns the project's own token
instead. The session-aware client factory sends that token as a bearer,
and the Storage API enforces the master privilege. A master session
passes. A non-master one gets its own 403.

config oauth-url and data-app git-credentials already work for a session
project. Both build a session-aware client and check the token's real
isMasterToken privilege, not whether it is static or a session. Neither
needs a change.
@linear-code

linear-code Bot commented Sep 14, 2026

Copy link
Copy Markdown

CLI-13

The scheduler, data streams, data science, metastore and AI clients now
reach their backend over a session bearer (c98eddf), but each still
declared SESSION_AUTH_FEATURE. Constructing one with a session token and
no bearer therefore failed fast, and the disclosed scope claimed those
services reject a session. Both are now wrong: the session-aware factory
always supplies the bearer.

Remove SESSION_AUTH_FEATURE from the five clients and record them in the
BEARER_CAPABLE_CLIENTS allowlist that check-sentinel-guards enforces.
SESSION_UNSUPPORTED_FEATURES shrinks to the three paths that really need
a static token: kbagent kai, semantic-layer token --encrypt, and the
importable SDK Client. auth login / auth register-projects disclose this
shorter list, shipped as session_unsupported_features in --json.

Drop the default_* client factories the services no longer call and the
tests that constructed them. The SESSION_AUTH_FEATURE mechanism stays on
BaseHttpClient as the way to mark a future client static-token-only, now
tested against a synthetic subclass instead of a real one.
The auth docs and the kbagent skill/agent references still said a session
covers "Storage + Manage only" and that everything else fails fast. CLI-13
made the Scheduler, Data Streams, Data Science, the semantic-layer group,
the AI Service and Storage bucket sharing accept a session, so that framing
misleads both a human reader and the kbagent agent.

Rewrite the scope in docs/auth.md (the capability table plus the flow and
config notes), docs/web-server.md, and the skill references (gotchas.md,
auth-workflow.md, commands-reference.md), plus the keboola-expert agent and
CLAUDE.md. Each now says a session works with almost every command and names
the three features that still need a static token: kbagent kai,
semantic-layer token --encrypt, and the importable SDK. The multi-project
errors[] examples no longer imply a session fails those readers.
@soustruh
soustruh marked this pull request as ready for review September 14, 2026 12:30

@keboola-pr-reviewer-bot keboola-pr-reviewer-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: needs_human (risk 3/5) · profile keboola-mcp-server

Broad auth-surface change expanding browser-login session support across six service clients — well-built and tested, but the auth boundary warrants a human.

Concerns:

  • src/keboola_agent_cli/services/sharing_service.py: resolve_master_token drops static-token guard; session now forwarded to server for master check
  • src/keboola_agent_cli/services/base.py: New generic session-aware factory extends bearer auth to five service clients at once

Suggested reviewers: @keboola/ai-swimlane-kai-assistant

CLI-13 widened session support to the Scheduler, Data Streams, Data
Science, semantic-layer, AI Service and bucket-sharing clients, and
shrank SESSION_UNSUPPORTED_FEATURES to three entries. Several docs and
docstrings still described the old "Storage + Manage only" scope.

- context.py AGENT_CONTEXT still listed the now-working services as
  fail-fast. `kbagent context` loads it as the agent's primary
  reference, so the stale copy makes an agent refuse commands that now
  work. Rewrite it to the three-entry list.
- Shrink the same stale list in the SessionAuthUnsupportedError
  docstring (errors.py) and the _render_session_restrictions docstring
  (auth.py).
- Tag the rewritten gotchas.md bullet (since vNEXT). An agent on an
  older kbagent must not read the widened support as always-present.
- Drop six now-dead FEATURE_ALIASES entries in
  check_sentinel_guards.py. CLI-13 removed their SESSION_AUTH_FEATURE
  guards.
- Add request-level tests. Each session-aware service client sends
  Authorization: Bearer plus X-KBC-ProjectId and drops
  X-StorageApi-Token on a real request. The static path is the mirror
  image.

@zajca zajca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable findings from the automated review.

"DeveloperPortalClient", # own username/password identity, never a project token
# CLI-13: these reach their backend over a bearer via
# make_session_aware_client_factory; the backends accept OAuth tokens.
"SchedulerClient",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed by Opus.

The five clients are added to BEARER_CAPABLE_CLIENTS (so Check 2 skips them) and their SESSION_AUTH_FEATURE declarations are deleted (so the http_base.py:131 runtime guard is inert for them), but they are not added to PROJECT_CREDENTIAL_CLIENTS (line 116), which is the only seed Check 4 walks (_descendants_of(root, set(PROJECT_CREDENTIAL_CLIENTS)), line 392). These clients are constructed from project.token, which is exactly the value that can be a kbc-session:// sentinel — the situation Check 4's own docstring describes for KeboolaClient.

Verified on this head commit:

$ uv run python -c "from keboola_agent_cli.ai_client import AiServiceClient; \
  c=AiServiceClient(stack_url='https://connection.keboola.com', token='kbc-session://10105'); \
  print(repr(c._client.headers.get('x-storageapi-token')))"
'kbc-session://10105'

Before this PR the same call raised SessionAuthUnsupportedError. Today every call site goes through make_session_aware_client_factory, so nothing is broken now — but the PR removes the runtime net without extending the static one, so the next direct construction or hand-rolled default factory lands silently and sends the literal sentinel string as a credential (an opaque 401), which is precisely the drift check_sentinel_guards.py exists to stop.

Suggested fix: add SchedulerClient, StreamClient, DataScienceClient, MetastoreClient and AiServiceClient to PROJECT_CREDENTIAL_CLIENTS. Note that client/stream.py:34 (StreamClient(stack_url=..., token=self._token, http_auth=self._http_auth)) will then be flagged and needs an explicit, reasoned entry in the allowlist — it inherits the parent client's bearer hook and is safe.

headers: dict[str, str] = {}
if http_auth is None:
headers["X-StorageApi-Token"] = token
super().__init__(base_url=base_url, token=token, headers=headers, http_auth=http_auth)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed by Opus.

MetastoreClient._do_request (lines 104-137) exists specifically because the metastore answers a valid non-master token with HTTP 401 "Failed to create project scope" — a permission gate, not a credential failure, remapped to MISSING_MASTER_TOKEN. Routing the client through http_auth puts BearerAuth.auth_flow (auth/token_provider.py:468-475) underneath it, and that hook retries any 401 after calling provider.force_refresh(token), which performs a real refresh-token exchange and rotates the refresh token in auth.json.

Failure scenario: a browser-login user whose session is not project-admin runs kbagent semantic-layer list. The metastore returns the project-scope 401; BearerAuth burns a network refresh + rotation and resends; the second 401 arrives and only then is the diagnosis produced. Every such call costs one pointless rotation and a doubled round trip. Worse, if that forced refresh itself fails (concurrent rotation by another kbagent process, revoked session), the user gets a "session expired — run kbagent auth login" error instead of the accurate MISSING_MASTER_TOKEN remedy, sending them to re-login for a problem re-login cannot fix.

The same 401-retry semantics already applied to Storage/Manage, but the metastore is the one client with a documented 401 that is not an auth failure, so it is worth either short-circuiting the retry for this exception string or noting the interaction where the http_auth wiring is added. Related: the remapped message still interpolates self._masked_token, which is "***" for a session client (mask_token("")), and advises kbagent project edit --token ..., which is not the right remedy for a session project.

@soustruh soustruh changed the title feat: accept browser-login (session) tokens for the commands whose backend supports them feat: accept browser-login (session) tokens for the commands whose backend supports them (CLI-13) Sep 15, 2026
O001: The drift guard (check_sentinel_guards.py) stopped covering the five
clients CLI-13 made bearer-capable, so it no longer catches a session
sentinel sent as a static token. Nothing leaked in practice, but the safety
net was gone. Re-add the clients to the guard, with a test.

O002: The metastore answers a valid non-master token with a 401. BearerAuth
mistook that for an expired token and force-refreshed, wasting a token
rotation and hiding the real "needs a master token" error. Skip the refresh
for the metastore.

@keboola-pr-reviewer-bot keboola-pr-reviewer-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: needs_human (risk 4/5) · profile keboola-mcp-server

Auth-surface change: extends session bearer tokens to 5 service clients and drops fail-fast static-token guards, so a human should confirm the accept/reject shift.

Impact flags: possible rollback re-introduction — see Check Run summary.

Suggested reviewers: @keboola/ai-swimlane-kai-assistant

@zajca zajca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No actionable findings were found by the automated review.

@soustruh
soustruh merged commit 10a8ac2 into main Sep 15, 2026
4 checks passed
@soustruh
soustruh deleted the cli-13-session-tokens branch September 15, 2026 11:10
@soustruh soustruh mentioned this pull request Sep 15, 2026
Jiri-Soukup pushed a commit to Jiri-Soukup/cli that referenced this pull request Sep 16, 2026
…re helper

SyncService imported `_default_ds_client_factory`, which keboola#759 (CLI-13)
removed in favor of `make_session_aware_client_factory`. The import came
from keboola#752 (CLI-8), valid on its own base. Both PRs passed CI alone; the
combination broke `import sync_service`, so the whole CLI failed to load
on main.

Build the factory like DataAppService does. Static-token projects keep
the same behavior; session projects now reach the DS API too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants