Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
38f378d
feat(CLI-17): extend MetastoreClient for org/targeted RLS policy scope
Matovidlo Sep 17, 2026
29df256
feat(CLI-17): add RlsService and condition primitive compiler
Matovidlo Sep 17, 2026
0a0aa21
feat(CLI-17): add kbagent rls command group
Matovidlo Sep 17, 2026
0cd3ac0
feat(CLI-17): expose rls over kbagent serve REST API
Matovidlo Sep 17, 2026
2db5b09
test(CLI-17): add RLS service/CLI/server/E2E test coverage
Matovidlo Sep 17, 2026
1896c74
docs(CLI-17): document the rls command group across all doc surfaces
Matovidlo Sep 17, 2026
1cbc2a2
feat(CLI-17): add kbagent cls command group for column-level security…
Matovidlo Oct 2, 2026
f1d31de
fix(CLI-17): resolve the default schema version for rls/cls schema an…
Matovidlo Oct 2, 2026
4b58c4d
fix(CLI-17): address Copilot review on rls/cls
Matovidlo Oct 5, 2026
ea0e4ce
fix(CLI-17): send target project ids the way the metastore expects, a…
Matovidlo Oct 5, 2026
24e0568
fix(CLI-17): keep a policy's scope on partial updates; validate targe…
Matovidlo Oct 5, 2026
c0831eb
fix(CLI-17): reject conditions with several composition keys or stray…
Matovidlo Oct 5, 2026
3a0491c
fix(CLI-17): validate principal fields by value and reject an unresol…
Matovidlo Oct 5, 2026
2485c9a
fix(CLI-17): setup honours rls.create denial and exit-code classes; u…
Matovidlo Oct 5, 2026
9004cfb
fix(CLI-17): reject bool/non-integral project ids; keep the failures'…
Matovidlo Oct 5, 2026
86124ca
fix(CLI-17): make the project-id types covariant so `ty` passes
Matovidlo Oct 5, 2026
b7a9308
fix(CLI-17): adopt main's metastore scope client, accept integer proj…
Matovidlo Oct 7, 2026
d482a15
feat(CLI-17): add MetastoreClient.patch_item for partial updates
Matovidlo Oct 7, 2026
b3d63d5
fix(CLI-17): refuse policies that block every query; preview like the…
Matovidlo Oct 7, 2026
b41590c
fix(CLI-17): targeted scope by default, backend dialect, PATCH update…
Matovidlo Oct 7, 2026
c9e2358
docs(CLI-17): correct rls/cls scope, prerequisites and command shape
Matovidlo Oct 7, 2026
2f683d9
test(CLI-17): E2E authors a targeted policy and changes its rules
Matovidlo Oct 7, 2026
c5e379d
fix(CLI-17): reject organization scope with target projects up front;…
Matovidlo Oct 7, 2026
dd081e4
fix(CLI-17): ASCII-only numeric target project IDs; patch_item tests;…
Matovidlo Oct 7, 2026
91bffeb
refactor(CLI-17): share rls/cls command bodies; one 409 path; reuse e…
Matovidlo Oct 7, 2026
302e9a6
refactor(CLI-17): one exit-code mapping, single-source dialect/scope,…
Matovidlo Oct 7, 2026
8d0edf5
feat(CLI-17): author rls-policy / cls-policy schema 1.1.0 (groups, OR…
Matovidlo Oct 8, 2026
4dafed3
docs(CLI-17): rls/cls 1.1.0 -- policies combine with AND, engine requ…
Matovidlo Oct 8, 2026
a22832a
docs(CLI-17): regenerate the serve endpoint reference after rebasing …
Matovidlo Oct 13, 2026
662a07d
fix(CLI-17): fit the SKILL.md description and the agent prompt in the…
Matovidlo Oct 13, 2026
92a2fe4
feat(CLI-17): accept the "*" wildcard selector in rls/cls policies (s…
Matovidlo Oct 13, 2026
532ca8e
fix(CLI-17): keep the keboola-expert prompt under its byte budget aft…
Matovidlo Oct 15, 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
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,60 @@ kbagent semantic-layer reference-data delete --project P --id ID [--yes]
# 0.97.0)` cannot be written on these `# ` comment lines (check_version_gates.py parses them as
# ATX headings).

kbagent rls list --project P
kbagent rls detail --project P --policy-id ID
kbagent rls schema --project P
kbagent rls create --project P --table-id TABLE_ID --rules JSON|@file|- [--default JSON|@file|-] [--dialect snowflake|bigquery] [--scope targeted|organization] [--target-project ALIAS|ID ...] [--dry-run] [--yes]
kbagent rls update --project P --policy-id ID [--table-id TABLE_ID] [--dialect snowflake|bigquery] [--rules JSON|@file|-] [--default JSON|@file|-] [--target-project ALIAS|ID ... | --clear-target-projects] [--dry-run] [--yes]
kbagent rls delete --project P --policy-id ID [--dry-run] [--yes]
kbagent rls setup --project P [--dialect snowflake|bigquery] [--rules JSON|@file|-] [--scope targeted|organization] [--target-project ALIAS|ID ...] [--yes]
# rls: row-level security policy authoring, metastore-backed (`rls-policy` object type,
# one object per protected table, named by it). SCOPE: `targeted` by default (the schema's own
# default: the owning project plus --target-project grants); `--scope organization` governs the
# table in EVERY project of the org (the enforcement applies every listed policy) and is
# destructive-class (FLAG_ESCALATIONS); there is no project scope. WHO: a project admin may
# write `targeted` policies of its own project without grants; grants and organization scope
# need the organization-admin role (403 otherwise). Enforced only where the project has the
# `row-level-security` feature. Policy schema 1.1.0 (the metastore default): a rule selects
# identities by principal, principals or IdP `groups`; within a policy every rule matching one
# identity applies and the conditions combine with OR; several policies on one table combine
# with AND (an added policy only narrows); a reader with no identity is always refused;
# the wildcard `"*"` (sole entry of `principal`/`principals`/`groups`) matches every identified reader and is how a table is opened to everyone under default deny; in a governed workspace
# (the platform denies by default, admins never exempt) a table with no rls-policy is refused;
# `--default` is the condition for an identified reader no rule matches (absent = refused;
# {"false": true} = no rows; to remove a default, recreate the policy). Needs an MCP engine
# that reads 1.1.0 (keboola/mcp-server#709). Writes refuse a --dialect other than the project backend (it defaults to the
# backend; the enforcement would refuse every read of that table). --rules is a JSON array of
# {principal|principals|groups, condition}; condition is a declarative primitive tree
# (column/op/value comparisons, in/not_in, is_null/is_not_null, and/or nesting, {"true": true}
# or {"false": true}) -- never a free-text predicate; a null comparison is refused (use
# is_null); `value: {"$identity": "email"}` / `values: {"$identity": "groups"}` are resolved
# per reader by the enforcement. `update` reads the policy, validates the MERGED result, then PATCHes
# only the changed keys (grants first); the result is re-read. `delete` is destructive-class.
# `--dry-run` previews with columns quoted like the enforcement (keboola-mcp-server
# `query_data`), but it is not the enforcement. `rls setup` is interactive-terminal-only
# (under --json / no TTY: INVALID_ARGUMENT, exit 2) and has no REST route. REST: POST
# /rls/{project}, PATCH /rls/{project}/{policy_id}, DELETE ...?dry_run=; body field
# `target_projects` (alias or ID). A stack whose metastore predates the schemas answers
# NOT_FOUND. New error code: INVALID_RLS_POLICY. Version gate for this whole entry lives in
# gotchas.md -- a `(since vNEXT)` tag cannot be written on these `# ` comment lines
# (check_version_gates.py parses them as ATX markdown headings).

kbagent cls list --project P
kbagent cls detail --project P --policy-id ID
kbagent cls schema --project P
kbagent cls create --project P --table-id TABLE_ID --rules JSON|@file|- [--dialect snowflake|bigquery] [--scope targeted|organization] [--target-project ALIAS|ID ...] [--dry-run] [--yes]
kbagent cls update --project P --policy-id ID [--table-id TABLE_ID] [--dialect snowflake|bigquery] [--rules JSON|@file|-] [--target-project ALIAS|ID ... | --clear-target-projects] [--dry-run] [--yes]
kbagent cls delete --project P --policy-id ID [--dry-run] [--yes]
# cls: column-level security policy authoring (`cls-policy` object type, one object per
# protected table), sibling of `rls`: same scope default, write permissions, dialect check,
# partial update and permission classes. --rules is a JSON array of
# {principal|principals|groups, visible_columns: [col, ...]} objects -- an allowlist projection
# (masking is not supported); within a policy an identity several rules match sees the union
# of their columns, several policies intersect; no `--default`. `--dry-run` prints each selector's projection. No `setup`
# wizard; the REST routes under /cls/{project} mirror /rls.
# New error code: INVALID_CLS_POLICY. Version gate lives in gotchas.md.

kbagent http get PATH [--timeout SECONDS]
kbagent http post PATH [--body JSON|@file|-] [--timeout SECONDS]
kbagent http patch PATH [--body JSON|@file|-] [--timeout SECONDS]
Expand Down
7 changes: 7 additions & 0 deletions docs/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,10 @@ of `ErrorCode` in `src/keboola_agent_cli/errors.py`.
|---|---|
| `MR_NOT_READY_TO_MERGE` | Merge answered 409 with `storage.mergeRequests.notReadyToMerge`: a project merge lock is held, the MR is in a state that cannot merge, or another MR in the project is already processing. Transient -- retryable |
| `MR_MERGE_CONFLICT` | Merge answered 409 with `storage.mergeRequests.validation` (or, on older stacks, no code): configurations changed on both branches; `details.api_error_params.errors` lists them. Not retryable -- resolve the conflicts and merge again |

### Row-Level and Column-Level Security

| Code | Description |
|---|---|
| `INVALID_CLS_POLICY` | A `cls create`/`update` candidate body (`table`/`dialect`/`rules`/`visible_columns`) failed validation -- either the schema-independent local checks (`principal`/`principals` not exactly one, empty or non-identifier `visible_columns`) or the live JSON-Schema check against the metastore's `cls-policy` schema when reachable. The message lists every violation found; nothing was written. Not retryable |
| `INVALID_RLS_POLICY` | An `rls create`/`update` candidate body (`table`/`dialect`/`rules`/`condition`) failed validation -- either the schema-independent local checks (unknown op, `principal`/`principals` not exactly one, a `"*"` wildcard that is not the sole entry of its list, or one used against a metastore whose `rls-policy` schema is older than 1.1.0) or the live JSON-Schema check against the metastore's `rls-policy` schema when reachable. The message lists every violation found; nothing was written. Not retryable |
28 changes: 27 additions & 1 deletion docs/web-server-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ auth, and the concepts behind these routes live in
[`web-server.md`](web-server.md); a running server serves the same spec
interactively at `/docs` (Swagger) and `/openapi.json`.

**257 operations** across **222 paths** and **32 routers**.
**269 operations** across **228 paths** and **34 routers**.

Paths are shown as the server registers them. Reaching them through the
Node BFF (or single-process `--ui` mode) prefixes every path with `/api`.
Expand Down Expand Up @@ -424,6 +424,32 @@ Model, validate, import/export, diff, promote, and build semantic layer artifact
| `PUT` | `/semantic-layer/scope/{context_id}/elevation-request` | Request scope elevation |
| `DELETE` | `/semantic-layer/scope/{context_id}/elevation-request` | Withdraw scope elevation request |

### `rls` (6 operations)

Row-level security policies (metastore-backed `rls-policy` objects), authored at `targeted` scope unless `organization` is asked for -- never `project` scope. Mirrors `kbagent rls list|detail|schema|create|update|delete` (`rls setup`'s interactive picker has no endpoint of its own, same carve-out as `auth register-projects`).

| Method | Path | Summary |
|---|---|---|
| `GET` | `/rls/{project}` | List RLS policies |
| `POST` | `/rls/{project}` | Create a RLS policy |
| `GET` | `/rls/{project}/schema` | Fetch the live rls-policy JSON Schema |
| `GET` | `/rls/{project}/{policy_id}` | Get one RLS policy |
| `PATCH` | `/rls/{project}/{policy_id}` | Update a RLS policy |
| `DELETE` | `/rls/{project}/{policy_id}` | Delete a RLS policy |

### `cls` (6 operations)

Column-level security policies (metastore-backed `cls-policy` objects: a per-principal `visible_columns` allowlist), authored at `targeted` scope unless `organization` is asked for -- never `project` scope. Mirrors `kbagent cls list|detail|schema|create|update|delete`.

| Method | Path | Summary |
|---|---|---|
| `GET` | `/cls/{project}` | List CLS policies |
| `POST` | `/cls/{project}` | Create a CLS policy |
| `GET` | `/cls/{project}/schema` | Fetch the live cls-policy JSON Schema |
| `GET` | `/cls/{project}/{policy_id}` | Get one CLS policy |
| `PATCH` | `/cls/{project}/{policy_id}` | Update a CLS policy |
| `DELETE` | `/cls/{project}/{policy_id}` | Delete a CLS policy |

## AI & Tools

### `kai` (6 operations)
Expand Down
1 change: 1 addition & 0 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ been retired, so its absence is NOT a promise (see §1 Rule 6).
| Read a semantic-layer model (models, metrics, datasets, constraints) | `kbagent --json semantic-layer show --project P [--model M] [--type metric\|dataset\|relationship\|constraint\|glossary]`; `model list`; `search-context` / `get-context` for glob/id lookup; `validate [--deep]` before trusting one. Any valid, non-disabled, non-expired Storage token works for reads (0.97.0, PSGO-282 -- the metastore no longer requires a master token for GET/List); a stale `MISSING_MASTER_TOKEN` on a plain read means an outdated kbagent still carrying the old reclassification, not a real gate | -- | hand-rolled `httpx` loops against `metastore.*.keboola.com` (bypasses retry/backoff and the kbagent error envelope) |
| ANY semantic-layer write (add / edit / remove / import / promote / build / scope add|remove|set|request-create) | `kbagent semantic-layer export` FIRST (the metastore has no soft-delete and no version history -- the snapshot is the only restore path), then the write, `--dry-run` where offered. Needs a token whose Storage Admin role is `admin` on the project (a master token qualifies, but so does any project-admin user token as of PSGO-282) -- a non-admin token still gets `MISSING_MASTER_TOKEN`/403 on writes, just not on reads. `Read` [semantic-layer-workflow.md](../skills/kbagent/references/semantic-layer-workflow.md) before starting: it carries the per-verb recipes, the rename cascade and the promote classification | `semantic-layer diff` (`--project-a/-b` or `--file-a/-b`) to confirm what a write would change | raw metastore REST (no rollback, no orphan scan, no modelUUID rewrite); a write with no export taken |
| Share a semantic-layer object across projects, or make it org-wide | **ASK THE USER which project(s) before EVER passing `--scope organization\|targeted` (on `model create`/`add <kind>`) or running `scope set --scope organization`/`scope request-create` -- never guess or default to widening visibility.** Once the user names the target(s): `--scope targeted --target-project ALIAS\|ID` grants specific projects; `scope add\|remove --project P --type T --context-id ID --target-project ALIAS\|ID` adjusts an existing item's grants from the owning project (a merge), `scope set --target-project ...` / `--clear` replaces the list; org-wide needs `scope request-create` then an org-admin's `scope set --scope organization --yes` (preview with `--dry-run`) -- **irreversible, no downgrade endpoint, destructive-class**. `add <kind>` without `--scope` inherits its model's scope. `scope get --type T --context-id ID` shows the current state first. See [metastore-scope-workflow.md](../skills/kbagent/references/metastore-scope-workflow.md) | `scope request-list --type T` to find items an org-admin should act on | passing `--scope organization\|targeted` or running `scope set --scope organization` without the user having named the target project(s) first; assuming `--scope organization` succeeds on a non-admin token (it 403s -- creating directly at organization scope needs the SAME organization-admin role as elevating); forgetting that `import`/`promote`/`build --model` into a shared model also share every new item (they take the model's scope unless `import`/`promote` get `--scope`); trying to elevate an item created before 0.97.0 (schema version 1.0.0: recreate it via export, delete, create, import) |
| Author or change a row-level (RLS) or column-level (CLS) security policy | `kbagent {rls,cls} create/update/delete --project P --table-id T ...` -- `targeted` scope by default (`--target-project` grants); `--scope organization` applies in EVERY org project (org-admin): only if asked; never `project` scope. Schema 1.1.0: rules select by `principal`/`principals`/`groups` (`"*"` = every identified reader), matching rules OR, several policies AND, no identity = refused; needs an MCP engine reading 1.1.0. Needs the `row-level-security` feature. Details: [rls-workflow.md](../skills/kbagent/references/rls-workflow.md) | `rls schema` / `cls schema --project P`; `--dry-run` first | a raw predicate as `condition`; headless `rls setup` (use `rls create`) |
| User asks to "log in" / authenticate via browser / register a session's projects | ATTENDED session + a background shell: run `kbagent auth login --device-code --stack URL --register-projects` in a **BACKGROUND** shell (capture stdout+stderr, human mode -- `--json` puts the panel on stderr), relay the verification URL + user code to the user, then poll `kbagent --json auth status` (exit 0 = signed in, exit 3 = not yet). The human's part is approving in the browser, not typing the command. No background shell -> hand the plain `auth login --stack URL --register-projects` to the user's terminal. To register projects from an EXISTING session, `kbagent auth register-projects --all` or `--project-id ID` is non-interactive and agent-safe | -- | running `auth login` in a FOREGROUND tool shell (~120 s timeout kills it mid-flight); running it unattended (nobody can approve); re-running it blind without checking `auth status` first (orphans a session); the flagless `register-projects` picker unattended; reading the token out of `auth.json`; using a numeric project id as an alias |
| User has NO Keboola account / project at all and asks to get started | `kbagent project create --url URL [--project ALIAS] [--name NAME] [--backend snowflake\|bigquery]` (0.95.0+) -- provisions a real project, stores its session and registers the alias in one call, agent-runnable. **Then relay the result's `confirm_url` to the human verbatim and say the project is owned by nobody until they open it**; after they confirm, the agent session is revoked by design -> `kbagent auth login --stack URL` | the user creating the project in the Keboola UI, then `auth login` / `project add --token-stdin` | calling it on a stack where a session already exists (exit 5 -- one session per stack); reporting success without the confirm link (an unclaimed project is a billable orphan); retrying it after a 5xx/429/503 without being asked (each success creates a new organization + project + credit grant) |
| CI task has account credentials | `kbagent auth login-password --email E (--password-stdin \| --password P) [--totp-secret SEED]` (0.84.0+), agent-runnable | a static Storage token | `auth login` unattended |
Expand Down
20 changes: 17 additions & 3 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ description: >
Covers: searching configurations, job history, data
lineage, dev branches, merge requests, workspace SQL debugging, GitOps sync,
bucket sharing/linking, encrypting secrets,
Storage tables/files/snapshots, large (200 GB) file uploads, data apps,
Storage tables/files/snapshots, 200 GB file uploads, data apps,
flows/schedules, invitations,
feature flags, OTLP data streams, scoped Storage tokens, semantic
layer, Developer Portal,
first-time setup and logout.
layer, RLS/CLS, Developer Portal,
first-time setup, logout.
Triggers: kbagent, Keboola, keboola
config, keboola job, keboola lineage, keboola sync, gitops, dev branch,
merge request,
Expand Down Expand Up @@ -370,6 +370,19 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Flag a project-scoped item as awaiting an org-admin's step-up decision | `kbagent sl scope request-create --project PROJECT --type TYPE- --context-id CONTEXT-ID` |
| Withdraw a pending scope-elevation request. | `kbagent sl scope request-delete --project PROJECT --type TYPE- --context-id CONTEXT-ID` |
| List items of --type awaiting an org-admin's elevation decision, across the org | `kbagent sl scope request-list --project PROJECT --type TYPE-` |
| List row-level security policies visible to a project | `kbagent rls list --project PROJECT` |
| Show one RLS policy's full rule set | `kbagent rls detail --project PROJECT --policy-id POLICY-ID` |
| Print the live ``rls-policy`` JSON Schema fetched from the metastore | `kbagent rls schema --project PROJECT` |
| Create one RLS policy for one table (``targeted`` scope unless ``--scope organization``) | `kbagent rls create --project PROJECT --table-id TABLE-ID --rules RULES` |
| Update an existing RLS policy | `kbagent rls update --project PROJECT --policy-id POLICY-ID` |
| Delete an RLS policy -- its table's rows are no longer filtered | `kbagent rls delete --project PROJECT --policy-id POLICY-ID` |
| Guided RLS setup: pick tables, build a condition, preview, then write | `kbagent rls setup --project PROJECT` |
| List column-level security policies visible to a project | `kbagent cls list --project PROJECT` |
| Show one CLS policy's full rule set | `kbagent cls detail --project PROJECT --policy-id POLICY-ID` |
| Print the live ``cls-policy`` JSON Schema fetched from the metastore | `kbagent cls schema --project PROJECT` |
| Create one CLS policy for one table (``targeted`` scope unless ``--scope organization``) | `kbagent cls create --project PROJECT --table-id TABLE-ID --rules RULES` |
| Update an existing CLS policy | `kbagent cls update --project PROJECT --policy-id POLICY-ID` |
| Delete a CLS policy -- its table's columns become unrestricted | `kbagent cls delete --project PROJECT --policy-id POLICY-ID` |
| GET an endpoint on the running kbagent serve | `kbagent http get <PATH>` |
| POST to an endpoint on the running kbagent serve | `kbagent http post <PATH>` |
| PATCH an endpoint on the running kbagent serve | `kbagent http patch <PATH>` |
Expand Down Expand Up @@ -466,6 +479,7 @@ For detailed response parsing rules and common pitfalls, see [gotchas](reference
| SQL migration (input mapping removal) | [sql-migration-workflow](references/sql-migration-workflow.md) |
| **Semantic layer (metastore)** -- models, metrics, datasets, constraints, glossary; validate / export / diff / promote / build / token | [semantic-layer-workflow](references/semantic-layer-workflow.md) |
| **Share a semantic-layer item across projects / make it org-wide** -- `--scope`, `--target-project`, `scope get\|add\|remove\|set\|request-*` | [metastore-scope-workflow](references/metastore-scope-workflow.md) |
| **Row-level / column-level security (RLS, CLS)** -- author `rls-policy` / `cls-policy` objects, organization/targeted scope, guided RLS setup, condition primitives, column allowlists | [rls-workflow](references/rls-workflow.md) |
| **Developer Portal** (identity CRUD, list/get apps, create/patch/upload-icon/publish/deprecate; TTY-confirm on writes) | [dev-portal-workflow](references/dev-portal-workflow.md) |
| **Config metadata** (list/get/set/delete arbitrary key-value metadata on a configuration) | [config-metadata-workflow](references/config-metadata-workflow.md) |
| **Storage descriptions** (describe bucket / table / column, batch from YAML) | [storage-describe-workflow](references/storage-describe-workflow.md) |
Expand Down
Loading
Loading