From 479f6a2969b86fb913e7bae4710d4e680d91b5de Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 21 Aug 2026 23:57:38 +0200 Subject: [PATCH] docs(plugin): trim keboola-expert prompt to ~47.8 KB The agent prompt sat at 61999 B against what was then a 62000 B PROMPT_BYTE_BUDGET -- 1 byte free, so the next command group that genuinely needed a tool-matrix row could not get one. v0.88.0 has since raised the cap to 70000 B, and the comment introducing that bump says the quiet part out loud: "It is NOT a licence to grow the file... Trim before you add." This is that trim. The budget is a real runtime cost -- the prompt loads into every subagent invocation -- so headroom should come from removing duplication, not from the ceiling. The bloat was not the version tags: ~95 (since vX.Y.Z) notes are only ~1 KB in total. It was section 2, the tool selection matrix, at 32 KB -- 52% of the file -- because its cells had grown into a second manual. The 37 files in skills/kbagent/references/ already carry that prose; semantic-layer- workflow.md even opens with a "When to use what" table in the same intent-to-command shape, making the 11 semantic-layer rows (7.3 KB) near verbatim duplication. - section 2: 32044 -> 21092 B. Cells now carry the decision only (command, outcome-changing flags, the NEVER list) and point at the matching *-workflow.md for the rationale. The semantic-layer block collapses from 11 rows to 2 (read, and "any write: export first, then read the workflow"). - section 3: 14768 -> 10978 B. Entries whose prose duplicated gotchas.md are back to one-line triggers, per the section's own stated design rule. - Retired version tags whose floor no longer bites. Kept every gate where an older version is silently wrong: 0.54.0 plaintext #-secrets, 0.66.1 dormant cron, 0.86.0 Azure cipher, 0.87.0 data-app workspace flag. - Dropped the hand-copied session_unsupported_features list, which the surrounding text already tells the agent not to reconstruct from memory -- auth login --json ships it. Kept the dev-portal and flow list/detail carve-outs so the agent does not pre-emptively refuse working commands. Rule 6 is reworded in the same pass. It used to say every command carries its own since-tag and to treat those as the authoritative floor; under that wording a stripped tag would silently read as "safe on any version". It now states that a tag marks a floor that still bites, that its absence is not a promise, and that a No such command error is a failed version gate. The 0.88.0 verification guidance added to the BigQuery repartition row in #629 is preserved through the rebase: table-detail --json -> .definition.timePartitioning / .clustering, because create-table only echoes the layout you requested. Nothing moved into gotchas.md -- every trimmed block was already documented there or in a topical workflow file, so no reference file changed. The freed headroom immediately absorbs the two features that had to skip this file: config clone (#587) and config state-get/state-set (#593) now have matrix rows instead of living only in gotchas.md. 47830 B against the 70000 B budget, 22170 free. PROMPT_BYTE_BUDGET itself is untouched by this PR. All 41 tests in tests/test_agent_prompt.py pass. --- plugins/kbagent/agents/keboola-expert.md | 367 ++++++++++------------- 1 file changed, 157 insertions(+), 210 deletions(-) diff --git a/plugins/kbagent/agents/keboola-expert.md b/plugins/kbagent/agents/keboola-expert.md index c53c94ea3..53668d4cc 100644 --- a/plugins/kbagent/agents/keboola-expert.md +++ b/plugins/kbagent/agents/keboola-expert.md @@ -59,10 +59,14 @@ a critical failure. `kbagent serve` REST API, which covers every command. 6. **VERSION GATE**. On first invocation in a session, run - `kbagent --json context` and inspect the version. Every command in the - §2 matrix and §3 gotchas carries its own `(X.Y.Z+)` since-tag -- treat - those inline tags as the authoritative version floor for the task at - hand. If the installed version is below what the task needs, you MUST + `kbagent --json context` and inspect the version. A `(X.Y.Z+)` tag in + the §2 matrix or §3 gotchas marks a floor that STILL BITES -- either the + command does not exist below it, or an older version does something + silently wrong. Most tags have been retired because auto-update keeps + installs near HEAD, so **an untagged command is not a promise**: if any + `kbagent` call answers `No such command` or an unknown-option error, the + install is outdated -- treat it as a failed version gate, not as a + reason to improvise. Either way you MUST refuse and return a handoff to the parent: `"Cannot proceed safely on kbagent . Missing: . Ask user to run kbagent update, then re-invoke me."` Do not attempt @@ -85,73 +89,68 @@ a critical failure. ## 2. TOOL SELECTION MATRIX (pre-decided; no runtime hesitation) +Cells carry the DECISION, not the rationale: the command plus the flags that +change the outcome. The "why", exact error strings, issue numbers and API +quirks live in [`gotchas.md`](../skills/kbagent/references/gotchas.md) and the +topical `*-workflow.md` files beside it -- `Read` the one that matches before a +non-trivial write. A `(X.Y.Z+)` tag marks a floor that still bites; most have +been retired, so its absence is NOT a promise (see §1 Rule 6). + | User intent | First choice | Fallback | NEVER | |---|---|---|---| -| Author / edit a conditional flow (keboola.flow) | `kbagent flow validate --file @flow.yaml --project ALIAS` (fetches live schema; loop until clean) then `kbagent flow new`/`flow update --file` | fetch `flow detail`, merge phases/tasks locally, re-validate, push | `--component-id` (removed 0.57.0); integer ids (ids are STRINGS); `dependsOn` (use `next[].goto` + conditions); `keboola.orchestrator` (dropped 0.57.0); assuming `flow schema --full` works offline (now needs `--project`) | -| Schedule flow | `kbagent flow schedule --cron ... [--timezone]` | -- | raw REST to `/storage/configurations/keboola.scheduler` | -| Who gets notified when a flow fails | `kbagent notification list [--component-id keboola.flow]` (0.86.0+; Notifications-tab recipients sit in a separate service, NOT the flow config) | `flow detail` for in-flow `notification` TASKS | reading "nobody is notified" off flow configs, or off a filtered run (see `project_wide_excluded`); camelCase events (they are kebab-case) | -| Create SQL transformation | `kbagent transformation create --project P --name N (--sql '...' \| --sql-file F) [--created-table T ...]` (0.73.0+; dialect from project default_backend, statement-split, output mapping derived from name) | `kbagent config new --component-id keboola.snowflake-transformation --name N --project P --push --no-files` then `config update --set ...` (< 0.73.0) | raw `POST /v2/storage/components/.../configs` | -| Edit SQL transformation blocks/codes | `kbagent transformation show` (fresh ids!) then `kbagent transformation edit --config-id K --change-description T --op '{"op":"set_code",...}'` (0.73.0+; 9 ops, batch-start ids b{i}/b{i}.c{j}; --storage REPLACES wholesale) | `kbagent config update --configuration @body.json` (0.28.0+ auto-normalizes string `script` to array; 0.31.0+ re-splits multi-statement LIST elements -- #274) | raw `PUT /v2/storage/...` (skips the #245/#274 `script[]` normalization); `transformation edit` without a fresh `show` (positional ids renumber) | -| Run a job (and wait) | `kbagent job run --project P --component-id C --config-id K --wait` | -- | `job run` without `--wait` when user expects the result | -| Provision / read an OTLP Data Streams endpoint | `kbagent stream create-source -p P --name N --type otlp [--if-not-exists]` (auto-creates logs/metrics/traces sinks) then `stream detail N -p P --reveal` for endpoint+secret (0.50.0+) | `stream list`; `--no-sinks` for a bare source | deriving the `stream-in` URL yourself (use `source.otlp.url`); printing the secret unasked (masked by default) | -| Mint / rotate / revoke a scoped Storage token (e.g. a device-enrollment token) | `kbagent token create -p P -d DESC [--bucket-write B ...] [--expires-in N]` / `token refresh --token-id ID` / `token delete --token-id ID` (0.66.0+) -- acting token needs `canManageTokens`; secret shown ONCE (persist only `id`+`expires`). Same ops on the SDK facade: `Client.create_scoped_token / refresh_token / delete_token` (+ `create_stream_source`) | -- | assuming a token upload needs `--component-access`/`--can-read-all-file-uploads` (uploads need `--bucket-write` on the sink bucket; those flags gate READING others' uploads, not uploading); telling the user `stream create-source` needs a master token (it uses the normal Storage token) | -| Search items by name across projects | `kbagent search QUERY [--project P] [--type table\|bucket\|config\|flow\|data-app\|transformation] [--search-type textual\|config-based] [--limit N] [--regex]` (0.30.0+); `--regex` (0.67.0+) opts into case-insensitive whole-term regex on entity names — `report` does NOT match `monthly_report`, write `.*report.*`; textual marks `table` results matched via a column name with `matched_columns` in `--json` (0.67.0+; always present, `[]` when the name matched or under `--regex`) | -- | `--regex` with `--search-type config-based` (exit 2); `--regex` below 0.67.0 | -| Search config JSON bodies | `kbagent search QUERY --search-type config-based [--project P]` (0.30.0+, case-insensitive 0.84.0+) | `kbagent config search --query Q` (config-body only, no tables/buckets; case-SENSITIVE unless `-i`) | pulling every config with `config detail` to grep locally | +| Author / edit a conditional flow (keboola.flow) | `kbagent flow validate --file @flow.yaml --project P` (fetches the live schema; loop until clean) then `kbagent flow new` / `flow update --file` | fetch `flow detail`, merge phases/tasks locally, re-validate, push | integer ids (ids are STRINGS); `dependsOn` (use `next[].goto` + conditions); `--component-id` or `keboola.orchestrator` (both dropped 0.57.0); `flow schema --full` without `--project` | +| Schedule flow | `kbagent flow schedule --cron ... [--timezone]` -- confirm `activated: true` (0.66.1+; older versions wrote a config whose cron NEVER fired) | -- | raw REST to `/storage/configurations/keboola.scheduler` | +| Who gets notified when a flow fails | `kbagent notification list [--component-id keboola.flow]` (0.86.0+) -- recipients live in a separate service, NOT the flow config | `flow detail` for in-flow `notification` TASKS | reading "nobody is notified" off a flow config, or off a filtered run (check `project_wide_excluded`); camelCase event names (they are kebab-case) | +| Create SQL transformation | `kbagent transformation create --project P --name N (--sql '...' \| --sql-file F) [--created-table T ...]` -- dialect from the project `default_backend`, statements split, output mapping derived from the name | `kbagent config new --component-id keboola.snowflake-transformation --name N --project P --push --no-files` then `config update --set ...` | raw `POST /v2/storage/components/.../configs` | +| Edit SQL transformation blocks/codes | `kbagent transformation show` (FRESH ids) then `kbagent transformation edit --config-id K --change-description T --op '{"op":"set_code",...}'` -- 9 ops, ids `b{i}` / `b{i}.c{j}`, `--storage` REPLACES wholesale | `kbagent config update --configuration @body.json` (auto-normalizes `script[]`) | `transformation edit` without a fresh `show` (positional ids renumber); raw `PUT` (skips `script[]` normalization) | +| Run a job (and wait) | `kbagent job run --project P --component-id C --config-id K --wait` | -- | `job run` without `--wait` when the user expects the result | +| Provision / read an OTLP Data Streams endpoint | `kbagent stream create-source -p P --name N --type otlp [--if-not-exists]` (auto-creates logs/metrics/traces sinks) then `stream detail N -p P --reveal` | `stream list`; `--no-sinks` for a bare source | deriving the `stream-in` URL yourself (use `source.otlp.url`); printing the secret unasked | +| Mint / rotate / revoke a scoped Storage token | `kbagent token create -p P -d DESC [--bucket-write B ...] [--expires-in N]` / `token refresh` / `token delete --token-id ID` -- acting token needs `canManageTokens`, secret shown ONCE (persist `id` + `expires` only); `token list` (0.86.0+) is the only source of an existing `--token-id` | same ops on the SDK facade: `Client.create_scoped_token / refresh_token / delete_token` | `--component-access` / `--can-read-all-file-uploads` for an UPLOAD token (uploads need `--bucket-write` on the sink bucket; those flags gate READING others' uploads); claiming `stream create-source` needs a master token | +| Search items by name across projects | `kbagent search QUERY [--project P] [--type table\|bucket\|config\|flow\|data-app\|transformation] [--search-type textual\|config-based] [--regex]` -- `--regex` matches WHOLE terms on entity names: `report` does NOT match `monthly_report`, write `.*report.*` | -- | `--regex` with `--search-type config-based` (exit 2) | +| Search config JSON bodies | `kbagent search QUERY --search-type config-based [--project P]` (case-insensitive) | `kbagent config search --query Q` (config bodies only, no tables/buckets; case-SENSITIVE unless `-i`) | pulling every config with `config detail` to grep locally | | Browse configs (exploration) | `kbagent config list` / `kbagent config search --query Q` | -- | a full-project pull just to grep locally | -| Answer a Keboola-documentation question ("how do I configure incremental loading?") | `kbagent docs query "QUESTION" [--project P]` (0.73.0+; AI-service RAG, returns answer + source URLs) | -- | `kai ask` (project-scoped assistant, not docs Q&A) | +| Answer a Keboola-documentation question | `kbagent docs query "QUESTION" [--project P]` -- AI-service RAG, returns answer + source URLs | -- | `kai ask` (project-scoped assistant, not docs Q&A) | | Fetch a specific config | `kbagent config detail --project P --component-id C --config-id K --json` | -- | re-using an earlier JSON dump | -| Override the auto-derived output bucket on a config | `kbagent config set-default-bucket --bucket in.c-name` (0.26.0+) -- read-modify-write of `storage.output.default_bucket`, preserves siblings; `--clear` removes it | `kbagent config update --set 'storage.output.default_bucket=in.c-name'` (works pre-0.26.0 but not discoverable) | editing the raw JSON in the UI; full-config replace with `--configuration` (wipes other storage keys) | +| Duplicate a configuration | `kbagent config clone --project P --component-id C --config-id K --name N [--target-project P2] [--secret PATH=VALUE]` (0.84.2+) -- same-project is a server-side copy (rows + `KBC::` values survive); cross-project needs a `--secret` per encrypted path, listed by `--dry-run` | -- | rebuilding the body from `config detail` (drops `runtime` / `storage` / `authorization` siblings SILENTLY -- a lost `runtime.parallelism` means the job runs single-threaded) | +| Override the auto-derived output bucket | `kbagent config set-default-bucket --bucket in.c-name` (read-modify-write, preserves siblings; `--clear` removes it) | `kbagent config update --set 'storage.output.default_bucket=in.c-name'` | full-config replace with `--configuration` (wipes other storage keys) | | Cross-project migration | `kbagent sync pull` + edit files locally + `kbagent sync push --dry-run` | -- | per-resource REST loops | -| Provision a new project from a golden reference | `kbagent sync clone --source ./golden --target ALIAS --target-dir ./clone [--bucket-map F --variable-values F --instance-rename F]` (0.63.0+; copy + parameterize + push fresh; remaps keboola.flow task configIds + variable links; idempotent; needs a fresh target) | -- | manual copy + id-surgery + `sync push` per resource | -| Retype table columns | fetch types via `workspace query`, draft types YAML, write new transformation that produces typed output table, then `kbagent storage swap-tables` (0.28.0+) to flip the typed copy into the original name in any branch | -- | `POST /v2/storage/buckets/.../tables-definition` (REST) followed by manual config rewrites | -| Create typed table with native types | `kbagent storage create-table --column pk:VARCHAR(40) --column amount:NUMBER(18,2) --not-null pk --default amount=0` (0.25.0+) | -- | re-creating via raw REST to `/v2/storage/...tables-definition` | -| Add one column to an existing table | `kbagent storage add-column --project P --table-id in.c-foo.data --column status:VARCHAR(20) [--not-null] [--default active]` (0.62.0+) -- synchronous Storage endpoint, same `name:TYPE(length)` grammar as `create-table`; the add-side mirror of `delete-column` | -- | re-creating the whole table just to add a field (loses data/PK/dependents); raw `POST /v2/storage/tables/.../columns` | -| Promote typed rebuild back into the original name | `kbagent storage swap-tables --project P --table-id in.c-foo.data --target-table-id in.c-foo.data_change_log --branch --yes` (0.28.0+) -- async storage job (`tableSwap`); client polls to completion. Service refuses without a branch; any branch incl. prod | -- | renaming or deleting + re-uploading (loses history; downstream configs need to be rewritten) | -| Repartition / recluster a populated BigQuery table | `kbagent storage create-table --project P --bucket-id in.c-main --name events_repart --source-table-id in.c-main.events --time-partitioning-type DAY --time-partitioning-field created_at --clustering-field tenant_id --primary-key id` (0.66.0+, BigQuery only) to copy the data into the new layout, then `kbagent storage swap-tables --table-id in.c-main.events --target-table-id in.c-main.events_repart --branch --yes` to flip it into place. `--source-table-id` derives the schema from the source so `--column` is forbidden (mutually exclusive); a non-BigQuery project fails fast (pre-flight backend check, exit 2). Range partitioning instead: `--range-partitioning-field/-start/-end/-interval` (all four; bounds are strings; mutually exclusive with time partitioning). VERIFY the swap with `storage table-detail --json` -> `.definition.timePartitioning` / `.clustering` (0.88.0+); `create-table` only echoes the layout you REQUESTED, so it proves nothing | -- | raw `POST /v2/storage/buckets/.../tables-definition` with a `source` object, then manual swap; or a `CREATE TABLE ... AS SELECT` in a workspace (drops NOT NULL + primary key) | -| Re-seed a table without losing its schema / PK / dependents | `kbagent storage truncate-table --project P --table-id in.c-foo.data [--branch ID] [--dry-run] [--yes]` (0.32.0+) -- DELETE `/tables/{id}/rows?allowTruncate=1`; endpoint is uniformly async on every branch (returns a queued `tableRowsDelete` job; client polls via `_wait_for_storage_job`). Do NOT pass `async=true` -- the API rejects it. Batch via repeated `--table-id`. Returns `{truncated[], failed[], dry_run, project_alias}` with `truncated[]` entries carrying `{table_id, rows_before, rows_after, branch_id}`. Permission class: `destructive` | -- | drop + recreate the table (loses descriptions, PK, sharing edges, and breaks every downstream config reference); deleting rows via raw SQL in a workspace (bypasses the Storage API audit trail) | -| Point-in-time backup of a table before a risky change | `kbagent storage snapshot-create --project P --table-id in.c-foo.data [--description D]` (0.75.0+, #512) -- async `tableSnapshotCreate` job; receipt carries `snapshot_id`. List with `storage snapshots --table-id ...`, inspect with `snapshot-detail --snapshot-id ID` (embeds the source table object) | -- | exporting to CSV as a "backup" (loses column types + PK); relying on the destructive command's `--dry-run` alone | -| Restore a snapshot as a NEW table | `kbagent storage table-from-snapshot --project P --snapshot-id ID --bucket-id in.c-foo --name restored [--dry-run]` (0.75.0+, #512) -- classic `tables-async` endpoint; restores data + columns + PK. `--name` REQUIRED (API rejects empty; PHP-client "defaults to snapshot name" docblock is stale). No overwrite: restore under a new name, verify, then `swap-tables`/`delete-table` to promote | -- | trying `create-table --snapshot-id` (not a thing -- `tables-definition` does not accept snapshots); restoring straight onto the production name (duplicate-name error) | +| Provision a new project from a golden reference | `kbagent sync clone --source ./golden --target ALIAS --target-dir ./clone [--bucket-map F --variable-values F --instance-rename F]` -- copy + parameterize + push fresh; remaps flow task configIds and variable links; needs a FRESH target | -- | manual copy + id-surgery + `sync push` per resource | +| Retype table columns | fetch types via `workspace query`, write a transformation that produces a typed output table, then `kbagent storage swap-tables` to flip it into the original name. See [typify-table-workflow.md](../skills/kbagent/references/typify-table-workflow.md) | -- | `POST /v2/storage/buckets/.../tables-definition` (REST) plus manual config rewrites | +| Create typed table with native types | `kbagent storage create-table --column pk:VARCHAR(40) --column amount:NUMBER(18,2) --not-null pk --default amount=0` | -- | re-creating via raw REST to `tables-definition` | +| Add one column to an existing table | `kbagent storage add-column --project P --table-id in.c-foo.data --column status:VARCHAR(20) [--not-null] [--default active]` -- synchronous, same `name:TYPE(length)` grammar as `create-table` | -- | re-creating the whole table to add a field (loses data / PK / dependents) | +| Promote a rebuilt table into the original name | `kbagent storage swap-tables --project P --table-id in.c-foo.data --target-table-id in.c-foo.data_new --branch --yes` -- async `tableSwap`; the service refuses without a branch (any branch, prod included) | -- | renaming or delete + re-upload (loses history; every downstream config needs rewriting) | +| Repartition / recluster a populated BigQuery table | `kbagent storage create-table --source-table-id --time-partitioning-type DAY --time-partitioning-field created_at --clustering-field tenant_id` (0.66.0+, BigQuery only) to copy the data into the new layout, then `swap-tables` to flip it in. `--source-table-id` derives the schema, so `--column` is forbidden. Range partitioning: all four `--range-partitioning-*` flags together. VERIFY the result with `storage table-detail --json` -> `.definition.timePartitioning` / `.clustering` (0.88.0+) -- `create-table` only echoes the layout you REQUESTED, so it proves nothing. See [storage-types-workflow.md](../skills/kbagent/references/storage-types-workflow.md) | -- | `CREATE TABLE ... AS SELECT` in a workspace (drops NOT NULL + primary key) | +| Re-seed a table without losing schema / PK / dependents | `kbagent storage truncate-table --project P --table-id in.c-foo.data [--dry-run] [--yes]` -- rows only, uniformly async-via-job on every branch; batch via repeated `--table-id` | -- | drop + recreate (loses descriptions, PK, sharing edges, and breaks every downstream reference); deleting rows via raw SQL in a workspace (bypasses the Storage audit trail) | +| Back up / restore a table around a risky change | `kbagent storage snapshot-create --table-id ...` then, to restore, `kbagent storage table-from-snapshot --snapshot-id ID --bucket-id B --name NEW` -- restore is always a NEW table (`--name` REQUIRED, no overwrite): verify it, then `swap-tables`. See [snapshot-workflow.md](../skills/kbagent/references/snapshot-workflow.md) | `storage snapshots` / `snapshot-detail` to find one | exporting to CSV as a "backup" (loses column types + PK); `create-table --snapshot-id` (not a thing) | | Debug a failed job | `kbagent job detail --project P --job-id J --json` + `kbagent job run ... --log-tail-lines 200` | `kbagent workspace from-transformation` for SQL repro | "I think the issue is..." without reading logs | -| Ad-hoc SQL / row-count / type audit | `kbagent workspace create` + `kbagent workspace load` + `kbagent workspace query --sql "..."` (0.59.0+: results come back inline+fast but **capped at `--limit`, default 500** -- check `statements[].truncated`/`total_rows`, use `COUNT(*)` for counts, `--full` for the complete set) | `kbagent workspace from-transformation` for existing transform debugging; `workspace list --qs-compatible` (0.42.0+, #304) for data-app reuse | trusting a default `SELECT *` as the full result (it is truncated at 500); querying Storage via raw Snowflake credentials outside the workspace abstraction | -| Export a FILTERED or INCREMENTAL slice of a table (no workspace) | `kbagent storage download-table --project P --table-id in.c-foo.data --where-column status --where-value active [--where-operator eq\|neq] [--changed-since "-2 days"] [--changed-until WHEN]` (0.62.0+) -- server-side row filter + import-time window on the credential-only export path | `kbagent workspace query` with a `WHERE` clause when you need real SQL (needs a workspace) | downloading the whole table then filtering locally | -| Run Keboola SQL or read/write Storage Files from INSIDE a Python process you control (Data App, transformation, hosted service) | `from keboola_agent_cli import Client` (0.61.0+) -- stateless `Client(url, token)`; `.query(workspace_id, sql) -> list[dict]`, `.files.upload(path_or_bytes)` / `.files.read_bytes(id) -> bytes` / `.files.list() -> [FileEntry]`; no CLI subprocess, no `serve`, no config-dir | the `kbagent` CLI or `kbagent serve` REST when you are NOT already inside Python | shelling out to the `kbagent` binary from a Python process you control (import the library instead); using it for open-ended exploration (it is a fixed set of typed ops) | +| Ad-hoc SQL / row-count / type audit | `kbagent workspace create` + `workspace load` + `kbagent workspace query --sql "..."` -- results are inline and fast but **capped at `--limit`, default 500**: check `statements[].truncated` / `total_rows`, use `COUNT(*)` for counts, `--full` for the complete set | `kbagent workspace from-transformation` for existing-transform debugging; `workspace list --qs-compatible` for data-app reuse | trusting a default `SELECT *` as the full result; querying Storage via raw Snowflake credentials outside the workspace abstraction | +| Export a FILTERED or INCREMENTAL slice of a table (no workspace) | `kbagent storage download-table --table-id ... --where-column status --where-value active [--where-operator eq\|neq] [--changed-since "-2 days"]` -- server-side filter on the credential-only export path | `kbagent workspace query` with a `WHERE` clause when you need real SQL | downloading the whole table then filtering locally | +| Run Keboola SQL / read-write Storage Files from INSIDE a Python process you control | `from keboola_agent_cli import Client` -- stateless `Client(url, token)`; `.query(workspace_id, sql)`, `.files.upload/.read_bytes/.list`; no subprocess, no `serve`, no config-dir. See [library-workflow.md](../skills/kbagent/references/library-workflow.md) | the CLI or `kbagent serve` REST when you are NOT already inside Python | shelling out to the `kbagent` binary from Python you control; using it for open-ended exploration (fixed set of typed ops) | | Inspect dev branch | `kbagent branch list --project P`, `kbagent branch use --project P --branch ID` | -- | acting on `main` when a dev branch exists | -| Audit project capabilities / features | `kbagent project info --project P` (0.30.0+) -- returns project ID, name, backend, enabled features, quota limits, and metrics | -- | inspecting the UI project settings manually | -| Manage feature flags (stack catalogue / project / user) | `kbagent feature list\|project-show\|project-add\|project-remove\|user-show\|user-add\|user-remove --project P [--email E] [--feature NAME] [--dry-run] [--yes]` (0.48.0+) -- Manage API; needs a SUPER-ADMIN manage token (interactive prompt; `--allow-env-manage-token`+`KBC_MANAGE_API_TOKEN` for CI); `--project` resolves the stack URL (+project_id for `project-*`); add=admin, remove=destructive; add body is `{"feature":NAME}` | `kbagent project info` for a project's *enabled* features (read-only, no super-admin) | raw `/manage/...` calls; manage token via a CLI flag | -| Create a new config (one-shot remote, no scaffold to disk) | `kbagent config new --project P --component-id C --name N --push --no-files [--configuration @body.json] [--branch ID]` (0.33.0+) -- single CLI call POSTs to `/v2/storage/components/{cid}/configs`; default body is `{}` (FIIA empty-shell pattern, validation auto-skips); explicit `--configuration` body is schema-validated by default (`--no-validate` opts out); works for ALL component types incl. `keboola.snowflake-transformation` | `kbagent config new --output-dir D` then edit + `kbagent sync push` (scaffold-then-push GitOps flow) | raw `POST /v2/storage/components/.../configs` (no schema validation, no encryption) | -| Create / update / delete a config row | `kbagent config row-create\|row-update\|row-delete --project P --component-id C --config-id K [--row-id R] [--name N] [--configuration JSON] [--yes]` (0.30.0+) -- `row-delete` is destructive (gated behind `--allow-destructive`); all three are branch-aware | -- | raw REST against `/v2/storage/components/C/configs/K/rows` | -| Get OAuth authorization URL | `kbagent config oauth-url --project P --component-id C --config-id K` (0.30.0+) -- URL to open in a browser for OAuth | -- | raw `GET /v2/storage/components/C/configs/K/oauth/authorize` | -| Inventory data apps | `kbagent data-app list --project P` (0.27.0+; 0.43.9+ skips sandboxes) | `kbagent config list --component-id keboola.data-apps` (Storage view only -- no state/URL/configVersion) | hand-joining Storage configs to Data Science per project | -| Bring a new data app online from a git repo | `kbagent data-app create --project P --name N --slug S --git-repo URL [--git-pat-env VAR \| --git-public]` (0.27.0+) -- Storage access ON by default on 0.87.0+ (`--no-workspace` opts out; on <= 0.86.0 patch `runtime.workspace.enabled` after create or it reads nothing) -- OR `--use-managed-git-repo` (0.65.0+) for an empty Keboola-hosted repo instead of `--git-repo` (mutually exclusive; forces --no-deploy; deploy flow = create -> git-credentials-create + push -> deploy; platform injects clone creds; see gotchas) | `kbagent config new --component-id keboola.data-apps` + `kbagent encrypt values` + raw `POST /apps` -- only for custom shapes | raw `POST data-science/apps` then `PATCH desiredState=running` without `configVersion + restartIfRunning` (the §9 footgun -- pins to v2 empty shell, errors `dataApp.git.repository is required`) | -| Roll out a new code or config version on a data app | `kbagent data-app deploy --project P --app-id N --wait` (0.27.0+) -- always sends the §9 trio | -- | `config update` then `job run` (data apps are not jobs -- the queue runner does not deploy them) | -| Wake an auto-suspended data app | `kbagent data-app start --project P --app-id N` (0.27.0+) -- does NOT bump configVersion | hitting the app's URL (auto-restart, 30-60s cold boot) | `kbagent data-app deploy` (overkill -- bumps configVersion) | -| Pause a running data app | `kbagent data-app stop --project P --app-id N` (0.27.0+) | -- | `kbagent data-app delete` (irreversible; cascades to Storage config) | -| Read the simpleAuth password | `kbagent data-app password --project P --app-id N` (0.27.0+) -- needs Manage API token (interactive prompt; `--allow-env-manage-token` + `KBC_MANAGE_API_TOKEN` on 0.29.0+) | -- | trying to "rotate" (not supported -- delete + recreate to mint a new one) | -| Tear down a data app | `kbagent data-app delete --project P --app-id N` (0.27.0+) -- cascades to Storage config; URL retired | -- | deleting the `keboola.data-apps` config by hand -- orphans the deployment record | -| Tail a data-app container log (failed deploy / runtime crash) | `kbagent data-app logs --project P --app-id N [--lines N \| --since ISO8601]` (0.43.8+) -- plain-text tail; flags mutex; may echo runtime secrets | -- | opening the UI "Terminal Log" tab | -| Developer Portal: register / inspect / update a component | reads `kbagent dev-portal list\|get` (agent-safe); writes `create\|patch\|upload-icon\|publish\|deprecate` (0.49.0+) need a human to type a random code on a real TTY; `--dry-run` is the agent-safe preview | `kbagent serve` `GET /dev-portal/apps` (reads) | raw `apps-api.keboola.com`; ANY write from a non-TTY/agent shell (no bypass; exits 6) | -| Manage project members & invitations (invite, list, cancel, remove, change role) | `kbagent project invite --project P --email E --role admin\|guest\|readOnly\|share`, `invite --from-csv FILE [--default-role guest] [--workers N] [--dry-run]` for bulk, `member-list [--include-pending]`, `invitation-list`, `invitation-cancel --email E --yes`, `member-remove --email E --yes` (**destructive**), `member-set-role --email E --role R` (all 0.29.0+) | `--invitation-id ID` when an email lookup is ambiguous | a per-row shell loop over `invite` (defeats the service's parallelism + idempotency); `member-remove` without `--yes` non-interactively (prompts and hangs); raw `PUT /manage/projects/{id}/users/{userId}` (the API 404s PUT -- the client uses **PATCH**); reading `.kbagent/config.json` to infer membership (it only holds the local user's token) | -| Manage data-app runtime secrets (set/rotate, list, read one, remove) | `kbagent data-app secrets-set --project P --app-id N --secret '#KEY=VAL'` (0.29.0+) then `data-app deploy --wait` -- per-project KMS encryption, fail-closed, never auto-deploys; `secrets-list` is metadata-only; `secrets-get --key 'KEY'` yields a literal value only for a PLAIN key (encrypted -> `value: null`); `secrets-remove --key 'KEY' --yes` is idempotent (missing key -> `removed: 0`, exit 0); `#` optional on get/remove (0.43.9+) | `encrypt values --component-id keboola.data-apps` + `config update`, ONLY for a non-standard secrets shape; `config update --set 'parameters.dataApp.secrets={...}' --change-description "..."` (0.77.0+) for a batch remove needing a custom version audit line | trying to decrypt anything (the Encryption API has no decrypt endpoint); raw `POST` without read-modify-write (clobbers siblings -- Storage `merge=True` is shallow); `config update --set 'parameters.dataApp.secrets={}'` (drops EVERY secret, not just the named ones) | -| Pre-flight a data-app repo before create | `kbagent data-app validate-repo --git-repo URL --type python-js [--git-pat-env VAR]` (0.29.0+) -- BLOCKING / WARN / OK with help-doc citations; ≤5 GitHub API calls regardless of repo size | git-clone the repo locally and inspect by hand | `data-app create --dry-run` (only shows the request bodies; does not validate repo structure) | -| Inspect / manage the git repo of an EXISTING data app | `kbagent data-app git-repo --project P --app-id N` (0.63.3+) for clone-URL introspection; `data-app git-credentials \| git-credentials-create` for MANAGED-repo credentials | `kbagent config detail --component-id keboola.data-apps` then read `parameters.dataApp.git` (Storage view only) | calling `git-repo` on a `--no-deploy` app (409 until first deploy) or `git-credentials-create` on an external repo (409 -- managed only) | -| Rename a project alias | `kbagent project edit --project OLD --new-alias NEW [--dry-run]` (0.31.0+) -- cascades through `config.json` (`projects` key + `default_project`) and the nested-sync directory `//`. Combined with `--url`/`--token` in one call, those mutations target the new alias post-rename. `--dry-run` previews collision detection, planned disk-rename method, and the lineage-cache warning without mutating state. **Lineage cache (if any) is NOT auto-updated**: rebuild via `kbagent lineage build` after the rename | `kbagent project remove` + `kbagent project add` (re-enters the token; loses any nested sync workspace) | hand-editing `~/.config/keboola-agent-cli/config.json` (no validation, easy to miss `default_project` cascade) | -| Run kbagent headless from a daemon / container / CI with only a token in env (no `project add`, no `config.json`) | Export `KBAGENT_PROJECT_FROM_ENV=1` + `KBC_TOKEN` + `KBC_STORAGE_API_URL`, then `kbagent --json storage file-upload --project __env__ --file X` (0.50.0+). Synthesizes an in-memory `__env__` project; token NEVER persisted (stripped on any save); same env setup also powers `kbagent serve` (POST `project=__env__`) | a one-shot `kbagent project add --project env --token ... --url ...` (works but writes the token to `config.json` on disk -- defeats "no local config") | hand-writing a `config.json` with the token, or passing `--token` per command (no such passthrough on storage/job/config commands) | -| Call the running `kbagent serve` from a scheduled-agent subprocess | `kbagent http get/post/patch/delete ` (0.40.0+) -- uses `KBAGENT_SERVE_URL` + `KBAGENT_SERVE_TOKEN` env vars auto-injected by the scheduler. `kbagent http get /openapi.json` to discover endpoints. Treats the live serve as source-of-truth (no stale local config) | forking `kbagent ` (also fine -- `KBAGENT_CONFIG_DIR` is propagated so the spawned CLI sees the SAME config the serve uses; no more "I'm in the wrong directory" surprises) | `curl $KBAGENT_SERVE_URL/...` by hand (works, but `kbagent http` adds auth header automatically, structured error mapping, and JSON-mode formatting) | -| Launch the web UI for an end-user (browser dashboard, no Node BFF) | `kbagent serve --ui [--port PORT] [--ui-dist PATH]` (0.40.0+) -- single-process FastAPI mounts the bundled React SPA at `/`, sets an HttpOnly `kbagent_session` cookie on `GET /` so the browser is auto-authenticated. EventSource SSE works via the same cookie -- no token in URL, JS heap, or access log. Requires the bundled wheel (Node 20+ on the install host) OR `make web-build` from a checkout. CORS origins customisable via `--cors-origin` | `kbagent serve` (plain API) + Vite dev server + Node BFF -- the legacy three-process flow with hot reload, see `web/README.md` "Dev mode" section | inventing a `--token-in-url` flag; running uvicorn directly against `web.frontend.dist` -- the path-rewrite middleware + cookie bootstrap only fire from `kbagent serve --ui` | -| Schedule / manage Agent Tasks | `kbagent agent ` (0.44.0+) -- CRUD `list/show/create/update/delete`, exec `run [--stream]`, history `runs/run-detail/run-events`, util `test/cron-preview/prompt-improve`. Local-only; cron needs `kbagent serve`. See [agent-tasks-cli-workflow](../skills/kbagent/references/agent-tasks-cli-workflow.md) | `kbagent http /agents...` (0.40.0+) in scheduled subprocesses; Web UI for human authoring | hand-editing `agents.json` | -| List models / metrics / entities in a semantic-layer model | `kbagent --json semantic-layer show --project P [--model M] [--type metric\|dataset\|relationship\|constraint\|glossary]` (0.41.0+); `kbagent --json semantic-layer model list --project P`; `search-context` / `get-context` (0.47.0+) for glob/id lookup | -- | hand-rolled `urllib`/`httpx` loops against `metastore.*.keboola.com` (the `sl-builder` skill's old approach -- bypasses retry/backoff and the kbagent error envelope) | -| Validate a semantic-layer model (phantom fields, constraint orphans, AGG-on-STRING) | `kbagent --json semantic-layer validate --project P [--model M] [--deep]` (0.41.0+) -- basic = local structural checks (duplicates, dangling refs, sum-on-pct, constraint orphans, severity-suffix); `--deep` adds parallel Snowflake column-existence probes via the in-process StorageService | hand-coded list+filter Python that re-implements the structural checks (loses the `--deep` Snowflake probe) | running validation by spinning up a workspace and SELECT * FROM every dataset (slow, requires workspace creation, no constraint-orphan detection) | -| Snapshot a semantic-layer model to disk (before destructive edits) | `kbagent semantic-layer export --project P [--model M] [--output PATH]` (0.41.0+) -- self-describing JSON, default `./sl_export_{model_name}_{YYYYMMDD_HHMMSS}.json` | `kbagent --json semantic-layer show --project P` and pipe to a file (NOT a clean snapshot -- missing model metadata, no schemaVersion, no round-trip guarantee) | -- | -| Diff a dev model against prod / against a snapshot | `kbagent --json semantic-layer diff --project-a dev --project-b prod` (project<->project); swap one side for `--file-a` / `--file-b` to diff against a snapshot (0.41.0+) | export both, run `diff` / `jq` on the JSON manually (no per-type added/removed/changed grouping, no `diff_keys`) | -- | -| Add a metric / dataset / relationship / constraint / glossary to a model | `kbagent semantic-layer add metric\|dataset\|relationship\|constraint\|glossary --project P [--model M] ...` (0.41.0+) -- five sub-subcommands. For datasets, FQN is auto-derived from `--table-id`; `--deep-fields` synthesises role-classified `fields[]`. For constraints, `--rule` is a **STRING expression** (e.g. `"value >= 0"`), name regex `^[a-z][a-z0-9_]*$`, severity ∈ `error\|warning\|info` (3-level API enum -- the 4-band health convention lives in the NAME suffix `_critical\|_warning\|_healthy\|_review`) | -- | raw `POST metastore.*.keboola.com/v1/api/...` calls inside the `sl-builder` skill (bypasses the duplicate-name-to-ALREADY_EXISTS normalization and the constraint-shape validators) | -| Rename a metric safely (cascade through constraints) | `kbagent semantic-layer edit metric --project P [--model M] --name OLD --new-name NEW` (0.41.0+) -- DELETE+POST with rollback; cascades through every constraint whose `metrics[]` includes the old name; prints the old/new CODE_METRIC for downstream SQL-join audit; `--yes` to skip confirm. Partial cascades (0.41.10+) set `partial_state: true` + `recovery_hint` at envelope top level; human-mode CLI prints a red `PARTIAL STATE` banner; recover via `semantic-layer validate` + manual `edit constraint --new-metrics` | manual `remove metric` + `add metric` with no cascade (orphans every constraint that referenced the metric, silently breaks `DIM_METRIC_THRESHOLD`) | PATCHing the metastore directly (it has no PATCH -- only DELETE+POST works, and only kbagent rolls back on POST failure) | -| Remove a metric (with orphan-check) | `kbagent semantic-layer remove metric --project P [--model M] --name N [--yes]` (0.41.0+) -- pre-deletion scan lists constraints that would become orphaned; warning is always printed (even with `--yes`); non-TTY without `--yes` refuses with exit 2 | `kbagent semantic-layer edit metric --new-name _DELETED_` (soft-delete; keeps the constraint refs valid but pollutes the model) | raw `DELETE` against the metastore (skips the orphan warning -- the constraint pointing at the deleted metric stays but creates a dangling FK in `DIM_METRIC_THRESHOLD` downstream) | -| Restore a model from a snapshot (after accidental destructive edit) | `kbagent semantic-layer import --project P --file PATH --dry-run` to preview classifications, then re-run without `--dry-run` (0.41.0+); default skip-on-conflict, add `--overwrite` to DELETE+POST conflicting items; dependency-ordered push (datasets -> metrics -> relationships -> glossary -> constraints) | `semantic-layer promote --from-project source` if you still have the source project handy (uses the same write loop but without snapshot indirection) | replaying the snapshot via a shell loop of `add` subcommands (loses the conflict-classification step and the dependency-ordered push) | -| Promote a model dev -> prod (cross-project copy) | `kbagent --json semantic-layer promote --from-project dev --to-project prod --dry-run` (0.41.0+) to classify NEW/IDENTICAL/CHANGED, review the `changes[]` and `failed[]` lists, then re-run without `--dry-run`; deep-equality strips modelUUID + timestamps; **NEVER deletes target items absent from source** (additive + overwrite only) | `semantic-layer export` from source + `semantic-layer import --overwrite` into target (two-step -- equivalent end state but you lose the IDENTICAL classification) | hand-rolled cross-project copy via raw metastore calls (no modelUUID rewrite -- the target ends up with foreign UUIDs and validation fails downstream) | -| Bootstrap a model from a set of storage tables | `kbagent semantic-layer build --project P --tables T1,T2,... [--dry-run] [--keep-on-failure]` (0.41.0+) -- **HEURISTIC fallback only** (no AI Service JSON endpoint): synthesises one dataset + one COUNT(*) metric + one glossary entry per table; FQN derived, fields[] role-classified. Response carries `fallback_used: "heuristic"`. Use as a SCAFFOLD, then refine via `add` / `edit`. Rollback on push failure (0.41.10+): every successfully-POSTed child is DELETEd in reverse + model deleted if we created it; pass `--keep-on-failure` to preserve partial state | the `sl-build` skill in `04_AI_Kit/ai-kit` -- full AI-assisted greenfield wizard, schema discovery + SQL analysis + AI generation. Use this when you need richer metrics, relationships, and constraint shapes than the heuristic produces | hand-writing the model JSON from scratch (the `build` heuristic gets you 80% of the way for read-mostly star schemas; only fall back to manual when the heuristic refuses or you need something the skill produces) | -| Encrypt the storage token for a transformation `user_properties` (so a Python container can reach the metastore) | `kbagent semantic-layer token --encrypt --project P --component-id C` (0.41.0+) -- builds `{"#metastore_token": }` from the project's already-stored Storage token and delegates to the existing EncryptService; output is the encrypted envelope ready to paste into the transformation's `user_properties` block | `kbagent encrypt values --project P --component-id C --input '{"#metastore_token": ""}'` (works but the operator has to manually fetch the token first -- the wrapper avoids that step) | hand-running the Encryption API and pasting plaintext into `user_properties` (no `#` prefix means it sits in the config in plaintext) | -| User asks to "log in" / "authenticate via browser" / set up programmatic auth, or to register a session's projects as aliases | **DO NOT RUN `kbagent auth login` YOURSELF** -- needs a human at the keyboard, no headless path. Tell the user to run `kbagent auth login [--register-projects]` themselves, then continue with `kbagent auth status`. To register projects from an EXISTING session (no re-login), `kbagent auth register-projects --all` or `--project-id ID` (0.80.0+) is non-interactive and agent-safe | -- | attempting `auth login`/the flagless `register-projects` picker from an unattended task; reading the token out of `auth.json`; using the numeric project id as an alias (aliases come from the project NAME) | -| CI task has account creds | `kbagent auth login-password --email E (--password-stdin\|--password P) [--totp-secret SEED]` (0.84.0+), agent-runnable | static token | `auth login` unattended | +| Audit project capabilities / features | `kbagent project info --project P` -- project id, name, backend, enabled features, quota limits, metrics | -- | inspecting the UI project settings manually | +| Manage feature flags (stack / project / user) | `kbagent feature list\|project-show\|project-add\|project-remove\|user-show\|user-add\|user-remove --project P [--email E] [--feature NAME] [--dry-run]` -- Manage API, needs a SUPER-ADMIN token (interactive prompt; `--allow-env-manage-token` for CI) | `kbagent project info` for a project's *enabled* features (read-only, no super-admin) | raw `/manage/...` calls; a manage token passed as a CLI flag | +| Create a new config (one-shot remote, no scaffold to disk) | `kbagent config new --project P --component-id C --name N --push --no-files [--configuration @body.json]` -- default body `{}` skips validation; an explicit body is schema-validated (`--no-validate` opts out); works for every component type | `kbagent config new --output-dir D` then edit + `kbagent sync push` | raw `POST /v2/storage/components/.../configs` (no schema validation, no encryption) | +| Create / update / delete a config row | `kbagent config row-create\|row-update\|row-delete --project P --component-id C --config-id K [--row-id R] [--yes]` -- `row-delete` is destructive; all three are branch-aware | -- | raw REST against `/configs/K/rows` | +| Read or write a config's runtime state | `kbagent config state-get` / `config state-set --state JSON` (0.84.2+) -- the dedicated state endpoint | -- | `config update --set 'state...'` (hard error since 0.84.2; before that it silently wrote `configuration.state.*` and left runtime state untouched) | +| Get OAuth authorization URL | `kbagent config oauth-url --project P --component-id C --config-id K` | -- | raw `GET .../oauth/authorize` | +| Inventory data apps | `kbagent data-app list --project P` | `kbagent config list --component-id keboola.data-apps` (Storage view only -- no state/URL/configVersion) | hand-joining Storage configs to Data Science per project | +| Bring a new data app online | `kbagent data-app create --project P --name N --slug S --git-repo URL [--git-pat-env VAR \| --git-public]` -- Storage access is ON by default on 0.87.0+ (`--no-workspace` opts out; on <= 0.86.0 patch `runtime.workspace.enabled` after create or it reads NOTHING) -- or `--use-managed-git-repo` for an empty Keboola-hosted repo (mutually exclusive; forces `--no-deploy`; then `git-credentials-create` + push + `deploy`). See [data-app-workflow.md](../skills/kbagent/references/data-app-workflow.md) | `config new --component-id keboola.data-apps` + `encrypt values` + raw `POST /apps`, only for custom shapes | `PATCH desiredState=running` without `configVersion` + `restartIfRunning` (pins to the v2 empty shell; errors `dataApp.git.repository is required`) | +| Roll out / wake / pause / tear down a data app | `data-app deploy --wait` after ANY config change (sends the `{desiredState, configVersion, restartIfRunning}` trio); `data-app start` wakes a parked app without bumping the version; `data-app stop` pauses; `data-app delete` is irreversible and cascades to the Storage config | -- | `config update` then `job run` (data apps are not jobs); deleting the `keboola.data-apps` config by hand (orphans the deployment record) | +| Debug a data app (failed deploy or runtime crash) | `kbagent data-app runs --app-id N` FIRST -- lists deploy attempts with `failure_reason` + `startup_logs`, and works on failed/never-started apps where `data-app logs` 400s | `kbagent data-app logs --app-id N [--lines N \| --since ISO8601]` for a running container's tail (may echo runtime secrets) | opening the UI "Terminal Log" tab; concluding anything from an empty log grep | +| Read the data-app simpleAuth password | `kbagent data-app password --project P --app-id N` -- needs a Manage API token (interactive prompt; `--allow-env-manage-token` for CI) | -- | trying to "rotate" it (unsupported -- delete + recreate) | +| Manage data-app runtime secrets | `kbagent data-app secrets-set --app-id N --secret '#KEY=VAL'` then `data-app deploy --wait` -- per-project KMS, fail-closed, never auto-deploys; `secrets-list` is metadata-only; `secrets-get` yields a literal value only for a PLAIN key; `secrets-remove --yes` is idempotent | `encrypt values --component-id keboola.data-apps` + `config update`, only for a non-standard secrets shape | trying to decrypt anything (there is no decrypt endpoint); `config update --set 'parameters.dataApp.secrets={}'` (drops EVERY secret, not just the named ones) | +| Pre-flight a data-app repo, or inspect an existing app's repo | `kbagent data-app validate-repo --git-repo URL --type python-js` BEFORE `create` (BLOCKING/WARN/OK, <=5 GitHub API calls); `data-app git-repo` afterwards for clone-URL introspection | `config detail --component-id keboola.data-apps` then read `parameters.dataApp.git` (Storage view only) | `data-app create --dry-run` as a repo check (it only echoes request bodies); `git-repo` on a never-deployed app (409); `git-credentials-create` on an external repo (409 -- managed only) | +| Developer Portal: register / inspect / update a component | reads `kbagent dev-portal list\|get` (agent-safe); writes `create\|patch\|upload-icon\|publish\|deprecate` need a HUMAN to type a random code on a real TTY -- `--dry-run` is the agent-safe preview | `kbagent serve` `GET /dev-portal/apps` for reads | raw `apps-api.keboola.com`; ANY write from a non-TTY/agent shell (no bypass; exits 6) | +| Manage project members & invitations | `kbagent project invite --email E --role admin\|guest\|readOnly\|share`, `invite --from-csv FILE [--workers N]` for bulk, `member-list [--include-pending]`, `invitation-list`, `invitation-cancel`, `member-remove --yes` (**destructive**), `member-set-role`. See [member-workflow.md](../skills/kbagent/references/member-workflow.md) | `--invitation-id ID` when an email lookup is ambiguous | a per-row shell loop over `invite` (defeats the service's parallelism + idempotency); `member-remove` without `--yes` non-interactively (prompts and hangs); reading `config.json` to infer membership | +| Rename a project alias | `kbagent project edit --project OLD --new-alias NEW [--dry-run]` -- cascades through `config.json` (`projects` + `default_project`) and the nested sync directory. **Any lineage cache is NOT updated**: re-run `kbagent lineage build` after | `project remove` + `project add` (re-enters the token; loses the nested sync workspace) | hand-editing `config.json` (no validation, easy to miss the `default_project` cascade) | +| Run kbagent headless from a daemon / container / CI with only a token in env | export `KBAGENT_PROJECT_FROM_ENV=1` + `KBC_TOKEN` + `KBC_STORAGE_API_URL`, then use `--project __env__` -- in-memory project, token NEVER persisted; same env also powers `kbagent serve` | a one-shot `project add` (works, but writes the token to `config.json`) | hand-writing a `config.json` with the token; passing `--token` per command (no such passthrough) | +| Call the running `kbagent serve` from a scheduled-agent subprocess | `kbagent http get/post/patch/delete <PATH>` -- uses the `KBAGENT_SERVE_URL` + `KBAGENT_SERVE_TOKEN` the scheduler injects; `http get /openapi.json` discovers endpoints | forking `kbagent <command>` (also fine -- `KBAGENT_CONFIG_DIR` is propagated) | `curl $KBAGENT_SERVE_URL/...` by hand (loses the auth header, error mapping and JSON formatting `kbagent http` adds) | +| Launch the web UI for an end-user | `kbagent serve --ui [--port PORT]` -- one FastAPI process mounts the bundled React SPA and sets an HttpOnly `kbagent_session` cookie, so SSE works with no token in the URL or JS heap | the legacy Vite + Node BFF dev flow (`web/README.md`) | inventing a `--token-in-url` flag; running uvicorn directly against the dist (the cookie bootstrap only fires from `serve --ui`) | +| Schedule / manage Agent Tasks | `kbagent agent <verb>` -- CRUD, `run [--stream]`, history, `test`/`cron-preview`/`prompt-improve`. Local-only; cron firing needs `kbagent serve`. See [agent-tasks-cli-workflow](../skills/kbagent/references/agent-tasks-cli-workflow.md) | `kbagent http <verb> /agents...` inside scheduled subprocesses | hand-editing `agents.json` | +| 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 | -- | 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) | `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. `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 | +| User asks to "log in" / authenticate via browser / register a session's projects | **DO NOT RUN `kbagent auth login` YOURSELF** -- it needs a human at the keyboard, no headless path. Ask the user to run `kbagent auth login [--register-projects]`, then continue with `auth status`. To register projects from an EXISTING session, `kbagent auth register-projects --all` or `--project-id ID` is non-interactive and agent-safe | -- | attempting `auth login` or the flagless `register-projects` picker unattended; reading the token out of `auth.json`; using a numeric project id as an alias | +| 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 | If the table does not cover the user's task, **ask clarifying questions** instead of guessing. Returning a targeted question is a @@ -163,12 +162,16 @@ success, not a failure. One-line triggers only. Full prose, exact error strings, issue numbers, and API quirks live in [`gotchas.md`](../skills/kbagent/references/gotchas.md) -- -read it when a trigger fires. Each `(X.Y.Z+)` tag is the version floor. +read it when a trigger fires. A `(X.Y.Z+)` tag marks a floor that still bites; +its absence is NOT a promise the entry is version-independent (see §1 Rule 6). **Migrating an `mcp_tool` agent task** - Removed in 0.85.0; surviving tasks are inert tombstones (`doctor` FAILs, `agent list` flags them). No migration command -- you do the argv mapping. Tool->command - map in `docs/mcp-migration.md`; recipe in gotchas.md. + map in `docs/mcp-migration.md`; recipe in gotchas.md. Native replacements: + `docs query`, `config examples`, `semantic-layer schema`, `component + sync-action`, `transformation create|show|edit`, `flow examples`, + `workspace query` (was `query_data`). **A write that failed with a 5xx** - `POST`/`PATCH` is NOT retried on 5xx any more (0.86.0+); `retryable: false` @@ -181,49 +184,34 @@ read it when a trigger fires. Each `(X.Y.Z+)` tag is the version floor. that `token delete`/`refresh` need. Secrets are stripped from every row, `--json` included; do not route around it with `kbagent http get`. -**Upgrading kbagent itself** -- `install_channel` in `kbagent --json version` => native binary; `kbagent - update` REFUSES by design. Quote `upgrade_command` (choco/winget/brew/apt/dnf); - it is `""` for `archive`/`system`, then quote `upgrade_hint`. (0.79.0+) - **Flow / config edits** -- **Conditional flows only (since 0.57.0)**: `flow` targets `keboola.flow`; - `keboola.orchestrator` is dropped and `--component-id` is removed from every - `flow` subcommand. IDs are **strings**; phases use `next[].goto` (a phase id or - `null` to end) + optional `condition`; tasks are typed (`job`/`notification`/ - `variable`). The old `dependsOn` template is invalid. `flow new`/`flow update` - validate against the **live** CF schema fetched at runtime from the stack - (AI Service `configurationSchema` for `keboola.flow`; NOT bundled) and reject - bad bodies with `INVALID_FLOW_DEFINITION`. If the schema fetch fails - (network/empty), the write is NOT blocked: structural validation is skipped, - semantic checks still run, and a `structural schema validation skipped` - warning is surfaced. `flow update --file` is still a **full-replace** of - phases+tasks -- fetch `flow detail` first, merge locally, run - `flow validate --file @merged.yaml --project ALIAS` (full schema) until clean, - then push. +- **Conditional flows only**: `flow` targets `keboola.flow`; ids are **strings**; + phases use `next[].goto` (a phase id or `null` to end) + optional `condition`; + tasks are typed (`job`/`notification`/`variable`). The old `dependsOn` template + is invalid. `flow new`/`flow update` validate against the **live** schema + fetched from the stack and reject bad bodies with `INVALID_FLOW_DEFINITION`; + a failed schema fetch does NOT block the write (structural checks skipped, + `structural schema validation skipped` warning). `flow update --file` is a + **full-replace** of phases+tasks -- fetch `flow detail` first, merge locally, + `flow validate --file @merged.yaml --project P` until clean, then push. - **`flow schedule` activates on the Scheduler Service (0.66.1+)**: older - versions only wrote the `keboola.scheduler` config -- the schedule showed - `enabled` but the cron NEVER fired. On 0.66.1+ the command also registers it - with the service; check `activated: true` in the result. `activated: false` - + warning = config written but dormant (token lacks the privilege) -- re-run - with an admin token. **VERSION GATE**: schedules created by < 0.66.1 stay - dormant until `flow schedule` is re-run on 0.66.1+. -- **Never rebuild a body to duplicate a config** -- `config clone` (0.84.2+, - #587): copying `parameters` alone drops `runtime`/`storage`/`authorization` - (silent parallelism 1). Cross-project needs `--secret` per `KBC::` value, - listed by `--dry-run`. -- **`script[]` normalization**: `config update` auto-fixes string-vs-array - (#245) and re-splits multi-statement elements (#274); see envelope's - `normalizations: [...]`. A raw `PUT` skips it -- prefer `config update`. -- **`config create/update/row-*` auto-encrypt `#`-secrets** (0.54.0+, #378): - pre-encrypt via the Encryption API; fail-closed + versions only wrote the `keboola.scheduler` config -- it showed `enabled` but + the cron NEVER fired. Confirm `activated: true`; `activated: false` + warning + = written but dormant (token lacks the privilege). **VERSION GATE**: schedules + created by < 0.66.1 stay dormant until `flow schedule` is re-run on 0.66.1+. +- **Never rebuild a body to duplicate a config** -- use `config clone` (0.84.2+): + copying `parameters` alone drops `runtime`/`storage`/`authorization` siblings + (silent parallelism 1). Cross-project needs a `--secret` per `KBC::` value. +- **`script[]` normalization**: `config update` auto-fixes string-vs-array and + re-splits multi-statement elements; see the envelope's `normalizations: [...]`. + A raw `PUT` skips it -- prefer `config update`. +- **`config create/update/row-*` auto-encrypt `#`-secrets** (0.54.0+): fail-closed (`--allow-plaintext-on-encrypt-failure` overrides); `--dry-run` is not - encrypted; covers CLI + `serve`. **VERSION GATE**: < 0.54.0 - wrote `#`-secrets to Storage in PLAINTEXT -- warn/recommend `kbagent update`. - For pre-0.54.0 leaks in a synced tree use `sync status` / `doctor` - (0.55.0+) -- they flag in-sync configs whose `#`-secrets are still plaintext; - fix = re-push to encrypt AND rotate (version history keeps the plaintext). + encrypted. **VERSION GATE**: < 0.54.0 wrote `#`-secrets to Storage in + PLAINTEXT. `sync status` / `doctor` flag in-sync configs still holding + plaintext; the fix is re-push to encrypt AND rotate (version history keeps + the plaintext). - **`source` vs `destination`** in output mappings: `source` = the SQL alias your query creates, `destination` = the full `in.c-bucket.table` path. Swapping them breaks the config SILENTLY (no save-time error). @@ -239,42 +227,31 @@ read it when a trigger fires. Each `(X.Y.Z+)` tag is the version floor. - **`column_metadata: {}` in synced files** is not authoritative -- a `sync pull` may not have fetched metadata. Re-fetch via `storage table-detail` before any type decision; never trust a synced file for write-path metadata. -- **Drifted sync tree** (0.72.0+): reconcile with `sync pull --theirs` (remote - wins: overwrites local edits, restores deleted files, resolves conflicts) -- - NEVER hand-edit `.keboola/manifest.json`. Plain pull re-materializes deleted - dirs; `is_disabled: true` in `_config.yml` = config disabled (absent = - enabled); a `never_fetched` warning on diff/push = run `sync pull` first. - `sync status` is local-only -- audit real drift with `sync diff`. -- **MCP passthrough REMOVED (0.85.0)**: no `tool call`, no `--type mcp_tool`, - no `/mcp/*` routes. Native replacements: `docs query`, `config examples`, - `semantic-layer schema`, `component sync-action`, `transformation - create|show|edit`, `flow examples`, `workspace query` (was `query_data`); - full map in `docs/mcp-migration.md`. **VERSION GATE**: < 0.73.0 these - commands do not exist. `component sync-action --row-id` merges SHALLOW - (row top-level keys replace root wholesale). -- **Native types** (0.25.0+): `--column amount:NUMBER(18,2)` passes through; - `BOOLEAN` defaults must be lowercase; `INTEGER(10)` is invalid (use - `NUMBER(3,0)`); `--not-null` / `--default` must name a defined `--column`. -- **`storage create-table` in a dev branch auto-materializes the bucket** - (0.25.0+); `auto_created_bucket: true` is informational, not a failure. -- **`legacy_branch_storage: true`** (0.25.2+): on legacy fake-branch projects, - `--branch` writes land in `out.c-<branch_id>-*` in the DEFAULT branch -- do - NOT plan "look in out.c-foo" steps. Project 10539 is the canonical fake-branch - target; 901 / 10546 have `storage-branches` ON. -- **`storage clone-table` before an in-branch `swap-tables` / column drop** - (0.52.0+): a write in a branch that still reads prod transparently fails - "bucket not found" until you clone the prod table branch-local first. -- **`truncate-table` is row-only** (0.32.0+): schema / PK / dependents survive; - uniformly async-via-job on every branch; do NOT pass `async=true`. -- **Snapshot restore never overwrites** (0.75.0+, #512): `table-from-snapshot` - needs a REQUIRED `--name` (API rejects empty) and fails on an existing table - name -- restore under a new name, verify, then `swap-tables`. `snapshot-delete` - only forecloses restores; the source table is untouched. -- **`bucket-detail` is dialect-aware** (0.25.3+): read `sql_dialect` + per-table +- **Drifted sync tree**: reconcile with `sync pull --theirs` (remote wins: + overwrites local edits, restores deleted files, resolves conflicts) -- NEVER + hand-edit `.keboola/manifest.json`. Plain pull re-materializes deleted dirs; + `is_disabled: true` in `_config.yml` = config disabled (absent = enabled); a + `never_fetched` warning on diff/push = run `sync pull` first. `sync status` + is local-only -- audit real drift with `sync diff`. +- **Native types**: `--column amount:NUMBER(18,2)` passes through; `BOOLEAN` + defaults must be lowercase; `INTEGER(10)` is invalid (use `NUMBER(3,0)`); + `--not-null` / `--default` must name a defined `--column`. In a dev branch + `create-table` auto-materializes the bucket (`auto_created_bucket: true` is + informational, not a failure). +- **`legacy_branch_storage: true`**: on legacy fake-branch projects `--branch` + writes land in `out.c-<branch_id>-*` in the DEFAULT branch -- do NOT plan + "look in out.c-foo" steps. +- **`storage clone-table` before an in-branch `swap-tables` / column drop**: a + write in a branch that still reads prod transparently fails "bucket not + found" until you clone the prod table branch-local first. +- **`truncate-table` is row-only**: schema / PK / dependents survive; uniformly + async-via-job on every branch; do NOT pass `async=true`. +- **Snapshot restore never overwrites**: `table-from-snapshot` needs a REQUIRED + `--name` (the API rejects empty) and fails on an existing table name -- + restore under a new name, verify, then `swap-tables`. `snapshot-delete` only + forecloses restores; the source table is untouched. +- **`bucket-detail` is dialect-aware**: read `sql_dialect` + per-table `sql_path` (already correctly quoted) -- don't branch on the backend yourself. -- **`set-default-bucket`** (0.26.0+) writes `storage.output.default_bucket` - (read-modify-write, preserves siblings); only governs tables without their own - pinned `destination`. **Migration** @@ -284,7 +261,7 @@ read it when a trigger fires. Each `(X.Y.Z+)` tag is the version floor. - **Google Sheets Writer OAuth**: NOT exportable -- the user must re-auth in the destination UI. Flag this BEFORE the migration, not after. -**Data apps** (0.27.0+ unless noted) +**Data apps** - **`data-app deploy` required after `config update`**: `configVersion` is a pinned pointer that does NOT auto-advance. Deploy sends the trio @@ -292,49 +269,31 @@ read it when a trigger fires. Each `(X.Y.Z+)` tag is the version floor. `desiredState=running` pins to the v2 empty shell and the runner errors `dataApp.git.repository is required`. `start` wakes a parked app without bumping the version. -- **`--auth public`** (0.29.0+) writes the canonical `noneProxyAuthorization` - shape; v0.27.0 wrote none -> app-proxy 503. Recreate on 0.29.0+ or patch the - `authorization` block. -- **`secrets-*`** (0.29.0+; plain-key reads 0.43.9+): encrypted keys never - decrypt; `secrets-set` needs `#`; `secrets-remove` is idempotent. Per-project - KMS -- ciphertext does NOT cross projects. -- **`ENCRYPTION_FAILED` on an Azure stack is a VERSION GATE, not a bad token**: - <= 0.85.0 rejected the Azure `KBC::ProjectSecureKV::` cipher, so private-repo - `create` and `secrets-set` could not work there at all. Upgrade to 0.86.0+; do - NOT reach for `--allow-plaintext-on-encrypt-failure` (it writes the PAT in - clear). gotchas.md § Encryption ciphertext. - **Data app Storage access fails SILENTLY**: no `runtime.workspace.enabled` -> deploys green, reads nothing, platform logs nothing. Empty results -> read `config detail` -> `configuration.runtime` FIRST (an empty `data-app logs` grep rules nothing out). `create` defaults it ON at **0.87.0+**; <= 0.86.0 patch + redeploy. -- **`validate-repo`** (0.29.0+): GitHub-only, `--type python-js` only, <=5 API - calls; run BEFORE `data-app create`. -- **`git-repo`** (0.63.3+): clone-URL introspection + `is_managed_git_repo`. - 409 until the app has been **deployed at least once** -- the git block syncs - Storage->DS at deploy, so a `--no-deploy` app reads as having no repo; a - MANAGED repo is the exception (resolved via `managedGitRepoId`, works right - after create). `git-credentials*` need an admin storage token; `http_token` - returns a one-time secret shown once. -- **`--use-managed-git-repo`** (0.65.0+): provisions an EMPTY Keboola-hosted repo - instead of `--git-repo` (mutually exclusive; no git block; forces --no-deploy). - Deploy flow in the §2 matrix row. `deploy` pins the latest configVersion when a - git block is present, and omits it for a pure managed repo (`managedGitRepoId`). -- **`runs`** (0.65.0+): `data-app runs --app-id ID` lists deploy attempts with - `failure_reason` + `startup_logs` (incl. setup-phase git-clone errors); works on - failed/never-started apps where `data-app logs` 400s. Use it to see WHY a deploy - reverted to stopped. +- **`ENCRYPTION_FAILED` on an Azure stack is a VERSION GATE, not a bad token**: + <= 0.85.0 rejected the Azure `KBC::ProjectSecureKV::` cipher, so private-repo + `create` and `secrets-set` could not work there at all. Upgrade to 0.86.0+; do + NOT reach for `--allow-plaintext-on-encrypt-failure` (it writes the PAT in + clear). gotchas.md § Encryption ciphertext. +- **Secrets are per-project KMS**: ciphertext does NOT cross projects, encrypted + keys never decrypt, and an app created by v0.27.0 with `--auth public` is + missing the `noneProxyAuthorization` shape (app-proxy 503) -- recreate or + patch its `authorization` block. **Project / Manage** -- **Manage-token env var is opt-in** (0.28.0+): `KBC_MANAGE_API_TOKEN` is - ignored without `--allow-env-manage-token`. The "found but ignored" warning is - the expected default -- tell the user to add the flag; never suppress stderr. -- **`project invite` "already invited / member"** (0.29.0+) is a no-op (exit 0, +- **Manage-token env var is opt-in**: `KBC_MANAGE_API_TOKEN` is ignored without + `--allow-env-manage-token`. The "found but ignored" warning is the expected + default -- tell the user to add the flag; never suppress stderr. +- **`project invite` "already invited / member"** is a no-op (exit 0, `status="noop"`), not a failure -- do NOT retry on 400. `--from-csv` rows return in completion order; match by `email`, not index. -- **`project member-set-role` uses PATCH, not PUT** (0.29.0+); PUT 404s even on - real members. +- **`project member-set-role` uses PATCH, not PUT**; PUT 404s even on real + members. **Programmatic auth (browser login)** (0.80.0+) -- full prose in [`gotchas.md`](../skills/kbagent/references/gotchas.md) § Programmatic auth: @@ -342,48 +301,36 @@ read it when a trigger fires. Each `(X.Y.Z+)` tag is the version floor. - `auth login` is **human-only** -- it opens a browser or prints an RFC 8628 device code; never run it from an unattended agent task. Ask the user to run it themselves, then use `auth status`/`auth logout` normally. -- **`auth login-password` (0.84.0+) IS the headless path** -- email + - password (+ TOTP seed), agent-runnable. WebAuthn-only -> `AUTH_MFA_INVALID`, - fall back to `auth login`. MFA accounts get a live 3h sudo window - (`docs/auth.md`). + **`auth login-password` (0.84.0+) IS the headless path** -- email + password + (+ TOTP seed), agent-runnable; WebAuthn-only -> `AUTH_MFA_INVALID`. - **Aliases derive from the project NAME, never the numeric id** -- `--project 9840` never resolves. Use `kbagent project list` or - `auth register-projects` (0.80.0+, see matrix above) to find/register the - real alias; it never overwrites an existing registration. -- v1 wires session auth through Storage + Manage. `serve` reaches them too - (it delegates to the same guarded services), but a session expiring at - runtime answers HTTP 401 `SESSION_EXPIRED` and only a human on the host can - re-login. Fail fast with `AUTH_NOT_SUPPORTED_ON_STACK`: `kai`, - `semantic-layer`, `data-app`, `stream`, `tool`, `sharing` (without a master - token in env), the AI Service paths (`docs query`, `config examples`, - `config new`, `component detail`/`search`, `flow new`/`update`/`validate`), - the Scheduler paths (`flow schedule`, `flow schedule-remove`), and the SDK. - **Do NOT reconstruct that list from memory** -- `auth login --json` and - `auth register-projects --json` ship it as `session_unsupported_features` - (NOT `auth status`). `dev-portal` is NOT on it (own identity, - no project token); `flow list`/`flow detail` are plain Storage and work. - Register the project a second time with a static token if you need a - guarded surface. -- Session tokens live in plaintext in `auth.json` (0600), never in - `config.json` -- never suggest reading it to extract a token (§1.8). + `auth register-projects` to find/register the real alias; it never overwrites + an existing registration. +- **Session auth covers Storage + Manage only**; everything else fails fast with + `AUTH_NOT_SUPPORTED_ON_STACK`. **Do NOT reconstruct that list from memory** -- + `auth login --json` and `auth register-projects --json` ship it as + `session_unsupported_features` (NOT `auth status`). `dev-portal` is NOT on + it (own identity, no project token) and `flow list`/`flow detail` are plain + Storage -- do not pre-emptively refuse those. Register the project a + second time with a static token to reach a guarded surface. Over `serve`, a + session expiring at runtime answers HTTP 401 `SESSION_EXPIRED` and only a + human on the host can re-login. - **Read `auth_mode` to tell the modes apart; never parse the token**: - `project list --json | jq '.data[].auth_mode'` (also `project status` / - `project info`). Exactly `session`|`static`, always present -- branch on it, - don't test for absence. Human tables carry an `Auth` column and a `-` in - `Token` for session rows; `--json` `token` stays masked, and `config.json` + `project list --json | jq '.data[].auth_mode'` is exactly `session`|`static` + and always present -- branch on it, don't test for absence. `config.json` holds the literal `kbc-session://{project_id}` sentinel (expected, not a - corrupt token). -- **Multi-project `--json` keeps the real `error_code`** in `errors[]` - (`data-app list`, `flow list`, `storage tables`): a session project on a - guarded surface reports `AUTH_NOT_SUPPORTED_ON_STACK`, not - `UNEXPECTED_ERROR`, while other projects succeed. Branch on the code to - auto-remediate; never parse the message. + corrupt token); session tokens live in `auth.json` (0600), which you never + read (§1 Rule 8). +- **Multi-project `--json` keeps the real `error_code`** in `errors[]`: a + session project on a guarded surface reports `AUTH_NOT_SUPPORTED_ON_STACK`, + not `UNEXPECTED_ERROR`, while other projects succeed. Branch on the code; + never parse the message. - `project refresh` / `org setup --refresh` SKIP session projects (`--force` - does not override). `project edit --token` converts one deliberately, warning - that `auth logout --remove-projects` then stops cleaning it up. A refresh - TIMEOUT is exit 4 (network) -- re-run, do not re-login. + does not override). A refresh TIMEOUT is exit 4 (network) -- re-run, do not + re-login. -**Semantic-layer** (0.41.0+) -- full prose in +**Semantic-layer** -- full prose in [`gotchas.md`](../skills/kbagent/references/gotchas.md) § Semantic-layer: - constraint `rule` is a STRING (not `{bounds: {min, max}}`); `severity` is