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
19 changes: 15 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,10 +401,13 @@ kbagent auth register-projects [--stack URL|alias] [--all] [--project-id ID ...]
# is also the FIRST router to enforce the `permissions` policy: every route declares
# `Depends(require_permission(...))`, so a denial answers HTTP 403 `PERMISSION_DENIED` over REST
# exactly as on the CLI. The other ~30 routers do not check the engine yet.
# The policy comes from the config dir `serve` RESOLVES (its own `--config-dir`, then
# KBAGENT_CONFIG_DIR, then the local/global chain) -- not from the root callback's `--config-dir`
# -- plus the session flags of the invocation. Reachable ways to enforce: a persisted narrow
# policy (`kbagent permissions set --mode allow --deny auth.register-projects`, needs a real
# The policy comes from the config dir `serve` RESOLVES (its own `--config-dir`, then -- since
# vNEXT, #679 -- an explicit root `kbagent --config-dir`, then KBAGENT_CONFIG_DIR, then the
# local/global chain) plus the session flags of the invocation. On 0.90.1 the root-level
# `--config-dir` was IGNORED here, so a policy stored beside the projects the caller named was
# silently not the one enforced; on that version pass --config-dir to `serve` itself.
# Reachable ways to enforce: a persisted narrow policy
# (`kbagent permissions set --mode allow --deny auth.register-projects`, needs a real
# terminal for the confirmation code), or `--mode deny` with `serve` (and the reads you want) in
# the allow list. `kbagent --deny-writes serve` does NOT work: `serve` is admin-class and
# `--deny-writes` appends `cli:write`, which spans write+destructive+admin, so the CLI callback
Expand Down Expand Up @@ -1000,6 +1003,14 @@ kbagent update [--beta]
kbagent changelog [--limit N] [--full]
# Default shows a one-line summary (first sentence) per version; --full / -v expands every note.
kbagent serve [--host HOST] [--port PORT] [--ui] [--ui-dist PATH] [--reload] [--log-level LVL] [--cors-origin ORIGIN] [--config-dir DIR] [--no-banner]
# `--config-dir` on serve (since vNEXT, #679): `serve` is the only subcommand with a --config-dir of
# its own, and most specific wins -- `serve --config-dir X` beats a root `kbagent --config-dir Y`,
# which in turn beats KBAGENT_CONFIG_DIR / the .kbagent walk-up / global. Passing both is NOT an
# error. Up to 0.90.1 the ROOT flag was ignored by serve entirely, silently: `kbagent --config-dir A
# serve` exposed a different project set than the caller named, and (since 0.90.1's /auth/*
# enforcement) applied a different directory's `permissions` policy. On <=0.90.1 always pass
# --config-dir to `serve` itself. Only an explicit root flag propagates -- an env-var/walk-up/global
# resolution is left to the server, which reaches the identical directory on its own.
# `--no-banner` (since 0.90.0): suppress the web UI's "What's new" popup. The UI shows a curated
# per-version highlights modal once per version (localStorage `kbagent.whatsnew.seen`); this flag
# turns the UNSOLICITED popup off fleet-wide. Surfaced to the SPA via `GET /ui-config`
Expand Down
30 changes: 26 additions & 4 deletions docs/web-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,11 @@ carried. Two consequences worth knowing before you reach for a flag:
policy works too, but its allow list must then include `serve` (and the reads
you want to keep), or the server will not start for the same reason as above.

Pass `--config-dir` **to `serve`**: the server resolves its own config dir
(`--config-dir` on the `serve` command, then `KBAGENT_CONFIG_DIR`, then the
local/global chain), so a root-level `kbagent --config-dir ... serve` sets the
directory for the CLI invocation, not for the served process.
Either spelling of `--config-dir` selects the served directory *(since
vNEXT)* — see [Which config directory `serve`
uses](#which-config-directory-serve-uses) below. On **0.90.1 and older**,
only `serve --config-dir` did: a root-level `kbagent --config-dir ... serve`
was ignored, so the policy above was silently not the one enforced.

A missing or expired session reaches `GET /auth/projects` and `POST
/auth/register-projects` as a **thrown error**, both funnelled through
Expand Down Expand Up @@ -600,6 +601,27 @@ the same view of Keboola the operator does, it can call any endpoint in
the reference, and its full response (including any tools it called)
is captured into the run history.

### Which config directory `serve` uses

`serve` is the only subcommand with a `--config-dir` of its own, so there are
two places the flag can appear. Most specific wins *(since vNEXT)*:

1. `kbagent serve --config-dir X` → serves `X`.
2. `kbagent --config-dir Y serve` → serves `Y`.
3. Neither → `KBAGENT_CONFIG_DIR`, then the `.kbagent` walk-up from the CWD,
then the global directory (`config_store.resolve_config_dir`).

Giving both is not an error — the `serve`-level flag simply wins, as in rule 1.
Only an explicit root flag is forwarded; an env-var/walk-up/global resolution is
left to the server, which reaches the identical directory on its own.

> **On 0.90.1 and older, rule 2 did not exist** (issue #679): the root-level
> `--config-dir` was ignored by `serve` entirely, with no warning. The server
> then exposed a different set of projects than the caller named, and — once
> `/auth/*` began enforcing `permissions` in 0.90.1 — enforced a different
> directory's policy. On those versions always pass `--config-dir` to `serve`
> itself.

### State on disk

Everything the server persists lives under one config directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ CLI parity for the `/agents` REST surface. Reads/writes `<config_dir>/agents.jso
- `doctor` -- health checks (no `--fix` since v0.85.0 -- it only installed the MCP server). Includes a `sync_secrets` check (since 0.55.0): when run inside a sync working tree (`.keboola/manifest.json`), warns if any in-sync config holds plaintext `#`-secrets (#378); `skip` outside a sync tree. Also an `mcp_tool_tasks` check: FAILs when `agents.json` holds tasks using the removed `mcp_tool` action, listing each task in `details.tasks[]` -- map the tool to its native command via `docs/mcp-migration.md` and recreate the task as `--type cli_command`
- `version` -- show the kbagent version and check for kbagent updates (kbagent only since v0.85.0); on a standalone binary it advertises the native channel's upgrade command instead of `kbagent update` (v0.79.0+)
- `context` -- full usage instructions for AI agents
- `serve [--host HOST] [--port PORT] [--ui] [--ui-dist PATH] [--reload] [--log-level LVL] [--cors-origin ORIGIN] [--config-dir DIR] [--no-banner]` -- launch the FastAPI HTTP server backing the web UI. `--ui` mounts the built SPA at `/` and authenticates the browser with an HttpOnly `kbagent_session` cookie (nothing is injected into `index.html`). `--no-banner` *(since 0.90.0)* suppresses the UI's "What's new" popup -- a curated per-version highlights modal shown once per version, dismissal persisted in localStorage `kbagent.whatsnew.seen`. The SPA reads the switch from `GET /ui-config` -> `{"banner": bool}`; it gates only the UNSOLICITED popup, so the command palette's "What's new" action still opens it on request. Requires the optional `server` extra
- `serve [--host HOST] [--port PORT] [--ui] [--ui-dist PATH] [--reload] [--log-level LVL] [--cors-origin ORIGIN] [--config-dir DIR] [--no-banner]` -- launch the FastAPI HTTP server backing the web UI. `--ui` mounts the built SPA at `/` and authenticates the browser with an HttpOnly `kbagent_session` cookie (nothing is injected into `index.html`). `--no-banner` *(since 0.90.0)* suppresses the UI's "What's new" popup -- a curated per-version highlights modal shown once per version, dismissal persisted in localStorage `kbagent.whatsnew.seen`. The SPA reads the switch from `GET /ui-config` -> `{"banner": bool}`; it gates only the UNSOLICITED popup, so the command palette's "What's new" action still opens it on request. `--config-dir` picks the directory the server exposes (projects AND the `permissions` policy the `/auth/*` routes enforce); *(since vNEXT)* an explicit root-level `kbagent --config-dir Y serve` is honored too, with the serve-level flag winning when both are given -- on 0.90.1 and older the root-level one was silently ignored, so pass it to `serve` itself there. Requires the optional `server` extra

## Global Flags
| Flag | Description |
Expand Down
30 changes: 27 additions & 3 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -4475,7 +4475,8 @@ though single-project `job list` looked correctly time-ordered.
## `kbagent serve` permission enforcement is `/auth/*`-only so far (since v0.90.1)

`create_app` builds a `PermissionEngine` from the persisted `permissions`
policy of **the config dir `serve` resolves** (its own `--config-dir`, then
policy of **the config dir `serve` resolves** (its own `--config-dir`, then --
since vNEXT -- an explicit root-level `kbagent --config-dir`, then
`KBAGENT_CONFIG_DIR`, then the local/global chain), and `kbagent serve`
forwards only the session FLAGS of the invocation on top. But of the ~30
routers, only the three `/auth/*` routes (`server/routers/auth.py`) declare
Expand Down Expand Up @@ -4504,8 +4505,6 @@ shapes.

A `--mode deny` policy works too, but then `serve` (and the reads you want to
keep) must be in its allow list, or the server will not start either.
Pass `--config-dir` to `serve` itself: the root-level `kbagent --config-dir
... serve` sets the dir for the CLI invocation, not for the server process.
- **A deny policy does NOT firewall the whole REST surface.**
`permissions set --mode deny --deny cli:write` blocks `POST
/auth/register-projects` (HTTP 403, `error_code: PERMISSION_DENIED`) but
Expand All @@ -4520,3 +4519,28 @@ shapes.
- Treat this as a gap being closed incrementally, not the design end state:
today a deny policy gates only `/auth/*`, not the ~30 other routers a
session token can otherwise reach.

## `serve` honors the root-level `--config-dir` (since vNEXT)

`serve` is the only subcommand carrying a `--config-dir` of its own, so the
flag has two possible positions. The precedence is **most specific wins**,
matching what `kbagent repl` does with the root flags:

1. `kbagent serve --config-dir X` -> serves `X`.
2. `kbagent --config-dir Y serve` -> serves `Y`.
3. neither -> `KBAGENT_CONFIG_DIR`, then the `.kbagent` walk-up, then global.

Passing both is not an error and produces no warning -- rule 1 simply wins.
Only an *explicit* root flag is forwarded; an env-var / walk-up / global
resolution is left to the server, which lands on the same directory anyway.

- **On 0.90.1 and older, rule 2 did not exist** (issue #679) and nothing was
printed about it: `kbagent --config-dir A serve` served whatever rule 3
resolved instead, so `GET /projects` listed a
different set of aliases than the caller named, and the `permissions` policy
enforced on `/auth/*` came from that other directory too. If you are on
<= 0.90.1, always pass `--config-dir` to `serve` itself.
- Quick check on any version: start the server and
`curl -s -H "Authorization: Bearer $KBAGENT_SERVE_TOKEN" localhost:PORT/projects`
-- if the aliases are not the ones in the directory you named, the flag was
in the position your version ignores.
42 changes: 35 additions & 7 deletions scripts/check_version_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,25 @@

# The silent-drift surfaces from CONTRIBUTING.md's "Plugin synchronization map".
# Anything an AI agent reads to decide whether a command exists belongs here.
#
# ``src/**/*.py`` is in scope because a version gate written in a Python
# comment is agent-facing documentation exactly like a markdown one -- and its
# absence is not hypothetical: ``(since vNEXT)`` in ``permissions.py`` survived
# the 0.90.1 release precisely because nothing under ``src/`` was scanned
# (``commands/context.py`` used to be listed alone; this glob subsumes it).
# Its first run also surfaced a stale ``(since v0.26.1)`` in ``commands/
# project.py`` -- a version that never shipped, left behind by a renumber.
#
# ``scripts/*.py`` is deliberately NOT scanned. This module has to NAME the
# placeholder it is looking for (its ``--release`` usage line and the
# ``VNEXT_TOKEN`` constant), so it would flag itself forever -- a self-
# referencing failure no regex can tell apart from a real gate. Do not retry it.
SCANNED_GLOBS: tuple[str, ...] = (
"CLAUDE.md",
"docs/*.md",
"plugins/kbagent/**/*.md",
"plugins/kbagent/.claude-plugin/CLAUDE.md",
"src/keboola_agent_cli/commands/context.py",
"src/**/*.py",
)

# ``(since v0.84.0)`` / ``(since 0.84.0)`` and ``0.73.0+`` / ``v0.73.0+``.
Expand All @@ -85,10 +98,15 @@
# The placeholder a feature PR writes when it cannot know its release version.
VNEXT_TOKEN = "vNEXT"

# An inline-code span. Stripping these before looking for VNEXT_TOKEN is what
# separates a live gate from prose quoting the token (see the module docstring
# for why this must not be applied to GATE_RE).
INLINE_CODE_RE = re.compile(r"`[^`]*`")
# An inline-code span, single- OR double-backtick. Stripping these before
# looking for VNEXT_TOKEN is what separates a live gate from prose quoting the
# token (see the module docstring for why this must not be applied to GATE_RE).
# The double-backtick alternative must come FIRST -- regex alternation is
# left-biased, so a single-backtick-first pattern would match the empty span
# between the two opening backticks of ``x`` and leave the token exposed.
# It matters because Python docstrings under ``src/`` use the RST convention:
# a ``(since vNEXT)`` written there is prose, not a gate, exactly as in markdown.
INLINE_CODE_RE = re.compile(r"``[^`]*``|`[^`]*`")

# Fenced blocks are deliberately NOT stripped, even though the same "code means
# quotation" argument seems to apply. Measured: CLAUDE.md's `## All CLI
Expand Down Expand Up @@ -121,7 +139,15 @@ def find_vnext_residue(paths: list[Path]) -> list[VnextResidue]:
rel = path.relative_to(REPO_ROOT).as_posix()
except ValueError:
rel = path.as_posix()
for lineno, line in enumerate(path.read_text(errors="replace").splitlines(), start=1):
# UTF-8 is pinned, never left to the platform default: on Windows that
# default is cp1252, and 73 of the files this now scans carry non-ASCII
# (em dashes, box-drawing rules in section comments). Decoded as cp1252
# their bytes turn to mojibake, which can move or destroy the backticks
# INLINE_CODE_RE keys on -- so a real placeholder could read as quoted
# prose on one OS and as residue on another.
for lineno, line in enumerate(
path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1
):
if VNEXT_TOKEN not in line:
continue
if VNEXT_TOKEN in INLINE_CODE_RE.sub("", line):
Expand All @@ -143,7 +169,9 @@ def collect_gates(paths: list[Path]) -> dict[str, list[tuple[str, int]]]:
rel = path.relative_to(REPO_ROOT).as_posix()
except ValueError:
rel = path.as_posix()
for lineno, line in enumerate(path.read_text(errors="replace").splitlines(), start=1):
for lineno, line in enumerate(
path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1
):
for match in GATE_RE.finditer(line):
version = match.group(1) or match.group(2)
gates[version].append((rel, lineno))
Expand Down
11 changes: 11 additions & 0 deletions src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1866,6 +1866,15 @@
subprocesses). Requires the optional 'server' extra:
`uv pip install -e ".[server]"`.

Config directory served -- most specific wins: `serve --config-dir X`,
then an explicit root-level `kbagent --config-dir Y serve`, then the
normal chain (KBAGENT_CONFIG_DIR, .kbagent walk-up, global). Passing both
is not an error; the serve-level flag wins. That directory decides which
projects the REST surface exposes AND which persisted `permissions`
policy the /auth/* routes enforce. NOTE for older installs: up to 0.90.1
`serve` ignored the root-level flag entirely, silently serving a
different directory -- there, always pass --config-dir to `serve` itself.

--no-banner (since 0.90.0) suppresses the web UI's "What's new" popup --
a curated per-version highlights modal the UI shows once per version
(dismissal persisted in localStorage `kbagent.whatsnew.seen`). The SPA
Expand Down Expand Up @@ -1993,6 +2002,8 @@

8. Config resolution order:
--config-dir flag > KBAGENT_CONFIG_DIR env > .kbagent/ in CWD/parents > ~/.config/keboola-agent-cli/
`serve` is the only subcommand with a --config-dir of its own: its flag wins over the
root-level one, which wins over the rest of the chain above (see `kbagent serve`).

9. Historical MCP tool names: the `tool` group was removed in v0.85.0. If a
user or an old script names a tool (get_configs, query_data, ...), map it to
Expand Down
2 changes: 1 addition & 1 deletion src/keboola_agent_cli/commands/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ def project_info(
formatter.output(result, _format_info_table)


# ── Project members & invitations (since v0.26.1) ─────────────────────
# ── Project members & invitations (since v0.29.0) ─────────────────────


def _format_invite_result(console: Console, data: dict[str, Any]) -> None:
Expand Down
Loading