From 4523f6d3fe745de3ccb6fd87b516a97955265f6d Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 25 Aug 2026 16:25:42 +0200 Subject: [PATCH] chore(release): 0.91.0 Bump pyproject to 0.91.0, add the changelog entry covering every PR merged since v0.90.1 (#627, #681, #691, #692, #693, #694, #695, #696, #697, #698), resolve all 54 vNEXT version-gate placeholders, and run version-sync + skill-gen. No web/frontend changes in this batch, so no whatsnew.ts entry. --- .claude-plugin/marketplace.json | 2 +- CLAUDE.md | 18 ++--- docs/TUTORIAL.md | 2 +- docs/guide.md | 2 +- docs/web-server.md | 4 +- plugins/kbagent/.claude-plugin/plugin.json | 2 +- plugins/kbagent/agents/keboola-expert.md | 10 +-- .../kbagent/references/commands-reference.md | 20 ++--- .../skills/kbagent/references/gotchas.md | 20 ++--- .../references/permissions-workflow.md | 4 +- .../kbagent/references/sync-workflow.md | 12 +-- .../kbagent/references/workspace-workflow.md | 2 +- pyproject.toml | 2 +- src/keboola_agent_cli/changelog.py | 81 +++++++++++++++++++ src/keboola_agent_cli/commands/context.py | 14 ++-- uv.lock | 2 +- 16 files changed, 139 insertions(+), 58 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 740af97f..b9b4cc8a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "kbagent", - "version": "0.90.1", + "version": "0.91.0", "source": "./plugins/kbagent", "description": "DEPRECATED — install from keboola/ai-kit: /plugin marketplace add keboola/ai-kit && /plugin install kbagent@keboola-claude-kit — AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, sync configs as files, manage dev branches, and debug SQL in workspaces", "category": "development" diff --git a/CLAUDE.md b/CLAUDE.md index 5474f41c..bc648889 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -414,7 +414,7 @@ kbagent auth register-projects [--stack URL|alias] [--all] [--project-id ID ...] # `Depends(require_permission(...))`, so a denial answers HTTP 403 `PERMISSION_DENIED` over REST # exactly as on the CLI. The other ~30 routers do not check the engine yet. # The policy comes from the config dir `serve` RESOLVES (its own `--config-dir`, then -- since -# vNEXT, #679 -- an explicit root `kbagent --config-dir`, then KBAGENT_CONFIG_DIR, then the +# 0.91.0, #679 -- an explicit root `kbagent --config-dir`, then KBAGENT_CONFIG_DIR, then the # local/global chain) plus the session flags of the invocation. On 0.90.1 the root-level # `--config-dir` was IGNORED here, so a policy stored beside the projects the caller named was # silently not the one enforced; on that version pass --config-dir to `serve` itself. @@ -687,7 +687,7 @@ kbagent token refresh --project NAME --token-id ID [--yes] # like cli:read, cli:write, cli:destructive). `tool:*` patterns are INERT since 0.85.0 (the MCP # passthrough is gone): they load but match nothing, so a mode=deny policy whose only allowance was # tool:read now denies everything. The agent guards rails against mistakes; not a sandbox. -# `permissions set` (since vNEXT, issue #688) validates every --allow/--deny pattern BEFORE the +# `permissions set` (since 0.91.0, issue #688) validates every --allow/--deny pattern BEFORE the # interactive confirmation: each must be a cli:* category, an exact operation name (incl. a # flag-escalated string like "auth.logout --remove-projects"), or a glob matching >=1 known # operation -- an unknown pattern (typo, fabricated category) is rejected with VALIDATION_ERROR, @@ -724,7 +724,7 @@ kbagent workspace detail --project ALIAS --workspace-id ID [--branch ID] kbagent workspace delete --project ALIAS --workspace-id ID kbagent workspace password --project ALIAS --workspace-id ID kbagent workspace load --project ALIAS --workspace-id ID --tables TABLE_ID [--tables ...] [--preserve] [--load-type clone|copy|view] [--force] [--timeout SECONDS] -# workspace load (since vNEXT, #687): DEFAULT is now per-table AUTO-DECIDE, mirroring the +# workspace load (since 0.91.0, #687): DEFAULT is now per-table AUTO-DECIDE, mirroring the # server's LoadTypeDecider -- zero-copy CLONE when eligible (same backend as the workspace, # backend snowflake/bigquery, full load -- kbagent never sends filters here, no # external-schema bucket, alias only when column-auto-sync is ON and unfiltered, and on @@ -838,20 +838,20 @@ kbagent sync pull --project ALIAS [--all-projects] [--force] [--theirs] [--dry-r kbagent sync status [--directory DIR] kbagent sync diff --project ALIAS [--all-projects] [--directory DIR] [--branch ID] kbagent sync push --project ALIAS [--all-projects] [--dry-run] [--force] [--allow-plaintext-on-encrypt-failure] [--branch ID] [--no-name-drift-warnings] -# sync push (since vNEXT, #686): the manifest baseline `pull_config_hash` is stamped from the API +# sync push (since 0.91.0, #686): the manifest baseline `pull_config_hash` is stamped from the API # response (or a read-back), never from disk -- push-deployed multi-statement SQL transformations # (and anything disabled in the UI whose local YAML lacks `is_disabled`) no longer show permanent # phantom `~ REMOTE MODIFIED` drift in `sync diff`. Unreadable-after-write leaves the baseline # UNTOUCHED + a `warnings[]` entry (never a disk-derived fallback). One canonical script[] shape now # (one element = one statement) and `transform.sql` gains `/* ===== STATEMENT ===== */` markers when -# semicolons cannot recover the boundaries -- which also closes a SILENT pre-vNEXT rewrite that +# semicolons cannot recover the boundaries -- which also closes a SILENT pre-0.91.0 rewrite that # collapsed such scripts to one statement (MULTI_STATEMENT_COUNT=1) while diff said "in sync". -# Migration: entries carry `metadata.config_hash_version`; unversioned ones match leniently (pre-vNEXT +# Migration: entries carry `metadata.config_hash_version`; unversioned ones match leniently (pre-0.91.0 # hash of the SAME remote counts as in sync, nothing else), and ONE `sync pull` per project migrates. # A pre-markers tree whose only difference from the remote is the lost boundaries is REFUSED per-change # with SYNC_LEGACY_BOUNDARY telling you to pull first; genuine edits push normally. # sync diff/push (0.89.0+, #649): local side read from exactly ONE tree (target branch subtree, else main/); entries tracked on another branch's tree are excluded from the changeset and reported under orphaned[] + summary.orphaned (reasons + reconcile hints); fix with sync pull. Adopt-by-id is branch-aware. -# Ignored components (since vNEXT, #689): keboola.mcp-server-tool joins keboola.sandboxes on ALWAYS_IGNORED_COMPONENTS (the MCP server's auto-created empty mcp-workspace- configs); the manifest field ignoredComponents (.keboola/manifest.json) is now LIVE and unions with the hardcoded set, honored by pull/diff/push. pull drops manifest entries + local dirs for a newly-ignored component, reported with action "ignored" (distinct from "removed" = deleted on remote); diff filters the local side too, so a stale dir for an ignored component can never classify as DELETED -- closes the delete-dir-then-push trap that used to destroy production keboola.mcp-server-tool configs. +# Ignored components (since 0.91.0, #689): keboola.mcp-server-tool joins keboola.sandboxes on ALWAYS_IGNORED_COMPONENTS (the MCP server's auto-created empty mcp-workspace- configs); the manifest field ignoredComponents (.keboola/manifest.json) is now LIVE and unions with the hardcoded set, honored by pull/diff/push. pull drops manifest entries + local dirs for a newly-ignored component, reported with action "ignored" (distinct from "removed" = deleted on remote); diff filters the local side too, so a stale dir for an ignored component can never classify as DELETED -- closes the delete-dir-then-push trap that used to destroy production keboola.mcp-server-tool configs. kbagent sync clone --source DIR --target ALIAS --target-dir DIR [--bucket-map FILE] [--variable-values FILE] [--instance-rename FILE] [--dry-run] [--branch ID] # `sync clone` (0.63.0+) copies a reference synced tree into a fresh target project + parameterizes it: applies bucket_map / variable_values / instance_rename overrides (JSON/YAML files), then pushes so every config CREATEs fresh -- keboola.flow task configIds and transformation variable links are remapped reference->ULID by push Phase C/D. Idempotent: re-run with an existing --target-dir reports no_changes. Fails fast if the target already contains the reference's configs (clone needs a fresh target). Override files must be flat {id: scalar} mappings (0.89.0+): a nested mapping/list/null value is rejected with CONFIG_ERROR naming the key + actual type, instead of being silently stringified into a bogus ID. kbagent sync branch-link --project ALIAS (--branch-id ID | --branch-name NAME) [--directory DIR] @@ -1008,7 +1008,7 @@ kbagent notification detail --project NAME --subscription-id ID kbagent notification create --project ALIAS --event NAME --channel email|webhook --address ADDR [--component-id ID] [--config-id ID] [--branch ID] [--expires-at TS] kbagent notification delete --project ALIAS --subscription-id ID [--yes] kbagent notification replace-recipient --project ALIAS --subscription-id ID --address NEW_ADDR [--channel email|webhook] [--yes] -# notification WRITE path (since vNEXT, #690): the group is no longer read-only. +# notification WRITE path (since 0.91.0, #690): the group is no longer read-only. # --address carries the email address (--channel email) OR the webhook URL (--channel webhook) -- # channel-discriminated on the wire, same as the read path's single `address` column. An invalid # --channel is a structured INVALID_ARGUMENT, exit 2. `create` WITHOUT --branch writes NO branch.id @@ -1076,7 +1076,7 @@ kbagent update [--beta] kbagent changelog [--limit N] [--full] # Default shows a one-line summary (first sentence) per version; --full / -v expands every note. kbagent serve [--host HOST] [--port PORT] [--ui] [--ui-dist PATH] [--reload] [--log-level LVL] [--cors-origin ORIGIN] [--config-dir DIR] [--no-banner] -# `--config-dir` on serve (since vNEXT, #679): `serve` is the only subcommand with a --config-dir of +# `--config-dir` on serve (since 0.91.0, #679): `serve` is the only subcommand with a --config-dir of # its own, and most specific wins -- `serve --config-dir X` beats a root `kbagent --config-dir Y`, # which in turn beats KBAGENT_CONFIG_DIR / the .kbagent walk-up / global. Passing both is NOT an # error. Up to 0.90.1 the ROOT flag was ignored by serve entirely, silently: `kbagent --config-dir A diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 0bfa0ffb..19eb7073 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -495,7 +495,7 @@ kbagent sync pull --project padak-2-0 git add -A && git commit -m "initial sync" ``` -`manifest.json`'s `ignoredComponents` field (since vNEXT) lets you exclude +`manifest.json`'s `ignoredComponents` field (since 0.91.0) lets you exclude project-specific components from every sync operation, on top of the always-ignored `keboola.sandboxes` and `keboola.mcp-server-tool`. diff --git a/docs/guide.md b/docs/guide.md index 585d091c..682d69e5 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -68,7 +68,7 @@ The agent can browse configs, list jobs and trace lineage -- but cannot create b | `cli:read` | All read-only CLI commands | | `branch.delete` | Exact command | | `sync.*` | All sync subcommands | -| `tool:*` | Nothing -- inert since 0.85.0 (the MCP passthrough is gone); a `--mode deny` policy that only allowed `tool:read` now denies everything; `permissions set` REJECTS it as input (since vNEXT) -- only an already-persisted `tool:*` sticks around | +| `tool:*` | Nothing -- inert since 0.85.0 (the MCP passthrough is gone); a `--mode deny` policy that only allowed `tool:read` now denies everything; `permissions set` REJECTS it as input (since 0.91.0) -- only an already-persisted `tool:*` sticks around | ### Management commands diff --git a/docs/web-server.md b/docs/web-server.md index e1bebc9a..aebfcc51 100644 --- a/docs/web-server.md +++ b/docs/web-server.md @@ -517,7 +517,7 @@ carried. Two consequences worth knowing before you reach for a flag: you want to keep), or the server will not start for the same reason as above. Either spelling of `--config-dir` selects the served directory *(since - vNEXT)* — see [Which config directory `serve` + 0.91.0)* — see [Which config directory `serve` uses](#which-config-directory-serve-uses) below. On **0.90.1 and older**, only `serve --config-dir` did: a root-level `kbagent --config-dir ... serve` was ignored, so the policy above was silently not the one enforced. @@ -604,7 +604,7 @@ is captured into the run history. ### Which config directory `serve` uses `serve` is the only subcommand with a `--config-dir` of its own, so there are -two places the flag can appear. Most specific wins *(since vNEXT)*: +two places the flag can appear. Most specific wins *(since 0.91.0)*: 1. `kbagent serve --config-dir X` → serves `X`. 2. `kbagent --config-dir Y serve` → serves `Y`. diff --git a/plugins/kbagent/.claude-plugin/plugin.json b/plugins/kbagent/.claude-plugin/plugin.json index 8b5384cb..995c6e9e 100644 --- a/plugins/kbagent/.claude-plugin/plugin.json +++ b/plugins/kbagent/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "kbagent", - "version": "0.90.1", + "version": "0.91.0", "description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, sync configs as files, manage dev branches, and debug SQL in workspaces", "author": { "name": "Keboola", diff --git a/plugins/kbagent/agents/keboola-expert.md b/plugins/kbagent/agents/keboola-expert.md index 3b419966..fd9380bc 100644 --- a/plugins/kbagent/agents/keboola-expert.md +++ b/plugins/kbagent/agents/keboola-expert.md @@ -101,7 +101,7 @@ been retired, so its absence is NOT a promise (see §1 Rule 6). | 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) | -| Add / remove / re-point a notification recipient | `kbagent notification create --event job-failed --channel email\|webhook --address A [--config-id K] [--branch ID]` / `notification delete --subscription-id ID` / `notification replace-recipient --subscription-id ID --address NEW` (vNEXT+) | -- | caching the old id after a replace (it is delete+recreate -- a NEW `subscription_id` is always minted; a failed delete leaves a duplicate as `old_deleted: false`); omitting `--branch` and expecting the UI's behavior (no `--branch` = NO `branch.id` filter = fires on every branch) | +| Add / remove / re-point a notification recipient | `kbagent notification create --event job-failed --channel email\|webhook --address A [--config-id K] [--branch ID]` / `notification delete --subscription-id ID` / `notification replace-recipient --subscription-id ID --address NEW` (0.91.0+) | -- | caching the old id after a replace (it is delete+recreate -- a NEW `subscription_id` is always minted; a failed delete leaves a duplicate as `old_deleted: false`); omitting `--branch` and expecting the UI's behavior (no `--branch` = NO `branch.id` filter = fires on every branch) | | 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 | @@ -124,7 +124,7 @@ been retired, so its absence is NOT a promise (see §1 Rule 6). | 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` + `workspace load` (since vNEXT auto-CLONEs eligible tables, else COPY; `--load-type` forces one and fails loudly if ineligible; COPY > 1 GiB needs `--force` outside a TTY; on a `--timeout` [default 300s] the job keeps running server-side and now exits 4/retryable, not 1 -- retry or poll `GET /v2/storage/jobs/{id}`, don't treat it as a hard failure) + `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; read-only input-mapping (`KBC__`) to query prod with no load at all | trusting a default `SELECT *` as the full result; querying Storage via raw Snowflake credentials outside the workspace abstraction | +| Ad-hoc SQL / row-count / type audit | `kbagent workspace create` + `workspace load` (since 0.91.0 auto-CLONEs eligible tables, else COPY; `--load-type` forces one and fails loudly if ineligible; COPY > 1 GiB needs `--force` outside a TTY; on a `--timeout` [default 300s] the job keeps running server-side and now exits 4/retryable, not 1 -- retry or poll `GET /v2/storage/jobs/{id}`, don't treat it as a hard failure) + `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; read-only input-mapping (`KBC__`) to query prod with no load at all | 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 | @@ -259,10 +259,10 @@ its absence is NOT a promise the entry is version-independent (see §1 Rule 6). branch's tree -- `sync pull` to re-target, never push. `sync status` is local-only -- audit real drift with `sync diff`. On <= 0.90.1 a `~ REMOTE MODIFIED ... codes changed` on a config nobody touched is usually - PHANTOM (issue #686: push stamped the baseline from disk); fixed in vNEXT -- + PHANTOM (issue #686: push stamped the baseline from disk); fixed in 0.91.0 -- one `sync pull` per project migrates a tree pulled by an older version, and a `SYNC_LEGACY_BOUNDARY` push error means exactly that: pull first. -- **Ignored components** (vNEXT, #689): `keboola.sandboxes` + +- **Ignored components** (0.91.0, #689): `keboola.sandboxes` + `keboola.mcp-server-tool` are always excluded from `pull`/`diff`/`push`, unioned with the manifest's `ignoredComponents` list. A component newly ignored is cleaned up on the next pull (manifest entry + local dir removed, @@ -385,7 +385,7 @@ its absence is NOT a promise the entry is version-independent (see §1 Rule 6). (`fallback_used: "heuristic"`), not the full AI wizard (that is the `sl-build` skill). -**`permissions set --allow/--deny` validates patterns (since vNEXT)** +**`permissions set --allow/--deny` validates patterns (since 0.91.0)** - A typo'd or fabricated pattern (`tool.admin`, `stroage.*`) fails fast with `VALIDATION_ERROR`, exit 2, instead of persisting silently -- valid inputs are `cli:*` categories, exact operation names, or globs matching >=1 diff --git a/plugins/kbagent/skills/kbagent/references/commands-reference.md b/plugins/kbagent/skills/kbagent/references/commands-reference.md index 77ebeb39..efd7ba75 100644 --- a/plugins/kbagent/skills/kbagent/references/commands-reference.md +++ b/plugins/kbagent/skills/kbagent/references/commands-reference.md @@ -93,8 +93,8 @@ All seven commands authenticate via `KBC_MANAGE_API_TOKEN` (Manage API), not the ## Permissions (session firewall commands) The `permissions` subcommands persist a write/destructive policy to config.json (the `--deny-*` flags above are the one-shot form). The engine guards against agent mistakes; it is not a sandbox. - `permissions list [--category read|write|destructive|admin]` -- list all operations with their risk category and current allowed/denied status -- `permissions show` -- show the current active permission policy. *(since vNEXT)* also flags ANY persisted pattern matching zero known operations, not only the retired `tool:*` namespace (same generalization applies to `kbagent doctor`) -- a typo like `cli:reed` or `stroage.upload-table` is surfaced too, with a generic "check for typos" hint instead of the MCP-migration one -- `permissions set --mode allow|deny [--allow PATTERN ...] [--deny PATTERN ...]` -- set the permission policy (firewall rules); patterns like `cli:read`, `cli:write`, `cli:destructive`. `tool:*` patterns are INERT since v0.85.0 (the MCP passthrough is gone) -- they load but match nothing, so a `--mode deny` policy whose only allowance was `tool:read` now denies everything. *(since vNEXT, issue #688)* Every pattern is validated BEFORE the interactive confirmation: it must be a `cli:*` category, an exact operation name, or a glob matching at least one known operation, or the call fails fast with `VALIDATION_ERROR` (exit 2, `error.details.invalid_patterns` in `--json`) and nothing is written +- `permissions show` -- show the current active permission policy. *(since 0.91.0)* also flags ANY persisted pattern matching zero known operations, not only the retired `tool:*` namespace (same generalization applies to `kbagent doctor`) -- a typo like `cli:reed` or `stroage.upload-table` is surfaced too, with a generic "check for typos" hint instead of the MCP-migration one +- `permissions set --mode allow|deny [--allow PATTERN ...] [--deny PATTERN ...]` -- set the permission policy (firewall rules); patterns like `cli:read`, `cli:write`, `cli:destructive`. `tool:*` patterns are INERT since v0.85.0 (the MCP passthrough is gone) -- they load but match nothing, so a `--mode deny` policy whose only allowance was `tool:read` now denies everything. *(since 0.91.0, issue #688)* Every pattern is validated BEFORE the interactive confirmation: it must be a `cli:*` category, an exact operation name, or a glob matching at least one known operation, or the call fails fast with `VALIDATION_ERROR` (exit 2, `error.details.invalid_patterns` in `--json`) and nothing is written - `permissions reset` -- remove all permission restrictions - `permissions check OPERATION` -- check if a specific operation is allowed (e.g. `permissions check storage.delete-table`) @@ -243,7 +243,7 @@ Bucket sharing + linking across projects in the same organization. `sharing edge - `workspace detail --project ALIAS --workspace-id ID [--branch ID]` -- show connection details. **Since v0.42.0 (#304)**: response carries `login_type`, `read_only`, `qs_compatible`; human mode adds `Login type:` / `Read-only:` / `Query Service compatible:` rows. **Updated v0.58.0**: BigQuery `default` workspaces now report `qs_compatible: true` (was `false`). `--branch` opt-in mirrors `workspace list` - `workspace delete --project ALIAS --workspace-id ID` -- delete workspace - `workspace password --project ALIAS --workspace-id ID` -- reset and return new password -- `workspace load --project ALIAS --workspace-id ID --tables TABLE_ID [...] [--preserve] [--load-type clone|copy|view] [--force] [--timeout SECONDS]` -- load storage tables. **Since vNEXT (#687)**: default is per-table auto-decide -- zero-copy `clone` when eligible (same backend, full load, no external-schema bucket), else plain `copy`; `--load-type` forces one type for every table and fails loudly (never silently degrades) if a table is ineligible for it. A `copy` over 1 GiB needs interactive confirmation or `--force` in non-interactive mode. `--timeout` defaults to 300s (other storage-job commands keep 60s); on timeout the job keeps running server-side and the error now exits 4, not 1 +- `workspace load --project ALIAS --workspace-id ID --tables TABLE_ID [...] [--preserve] [--load-type clone|copy|view] [--force] [--timeout SECONDS]` -- load storage tables. **Since 0.91.0 (#687)**: default is per-table auto-decide -- zero-copy `clone` when eligible (same backend, full load, no external-schema bucket), else plain `copy`; `--load-type` forces one type for every table and fails loudly (never silently degrades) if a table is ineligible for it. A `copy` over 1 GiB needs interactive confirmation or `--force` in non-interactive mode. `--timeout` defaults to 300s (other storage-job commands keep 60s); on timeout the job keeps running server-side and the error now exits 4, not 1 - `workspace query --project ALIAS --workspace-id ID --sql "..." [--file F] [--transactional] [--full] [--limit N]` -- run SQL via Query Service. **Fast inline results since v0.59.0**: default reads the result set inline via `GET /api/v1/queries/{job}/{stmt}/results` (JSON `columns`+`rows`, no CSV-file materialization), capped at `--limit` rows (default 500) and marked `truncated` when there are more; pass `--full` for the complete CSV export (slower, uncapped). Each statement still carries `csv_data` (synthesized from the inline rows) so older parsers keep working. **Backend-agnostic since v0.58.0**: runs against both Snowflake and BigQuery workspaces. Mind the dialect: Snowflake quotes identifiers with `"..."`, BigQuery with backticks `` `...` `` - `workspace gc [--project NAME ...] [--dry-run] [--yes]` -- garbage-collect orphaned workspaces (and any lingering `keboola.sandboxes` configs). `--dry-run` previews without deleting; `--project` repeatable, omit to GC across all connected projects - `workspace from-transformation --project ALIAS --component-id ID --config-id ID [--row-id ID]` -- workspace from existing transform @@ -317,18 +317,18 @@ Requires the project to be added with its **master ('owner') Storage API token** - `--component-id` / `--config-id` filter **client-side** too. They match the subscription's own `job.component.id` / `job.configuration.id` filter values (dotted paths into the event payload -- not flat `componentId`/`configurationId` keys) - `branch_id` is populated on **every** row, production included -- the Flow Builder always writes a `branch.id` filter and uses the default branch's numeric id for production. A filled Branch column does NOT mean "dev-branch only"; cross-check `kbagent branch list` - A subscription with **no filters at all** is project-wide and fires for every job. Those are excluded by `--component-id`/`--config-id` and reported as `project_wide_excluded` (plus a warning in human mode), so "who gets paged when this flow breaks" is never silently under-reported -- see [gotchas.md](gotchas.md) -- `notification create --project ALIAS --event NAME --channel email|webhook --address ADDR [--component-id ID] [--config-id ID] [--branch ID] [--expires-at TS]` *(since vNEXT)* -- create a subscription. `--address` carries the **email address** (`--channel email`) or the **webhook URL** (`--channel webhook`); the wire shape is channel-discriminated exactly as the read path's single `address` column is. An invalid `--channel` is a structured `INVALID_ARGUMENT` (exit 2). **Without `--branch` no `branch.id` filter is written** -- unlike the UI, which always writes one; an absent filter does not constrain matching, so the subscription fires for jobs on every branch. See [gotchas.md](gotchas.md) -- `notification delete --project ALIAS --subscription-id ID [--yes]` *(since vNEXT)* -- delete a subscription; confirmation guard unless `--yes` (or `--json`) -- `notification replace-recipient --project ALIAS --subscription-id ID --address NEW_ADDR [--channel email|webhook] [--yes]` *(since vNEXT)* -- swap a subscription's recipient, keeping event, filters and expiry. **Delete+recreate under the hood** -- the API has no update primitive -- so a **new `subscription_id` is always minted**; read `old_subscription_id` / `new_subscription_id` off the result and never re-use a cached id. The new subscription is created *first*, so a failed delete leaves a recoverable duplicate (`old_deleted: false` + a warning), never a lost subscription. `--channel` defaults to the old subscription's channel. Confirmation guard unless `--yes` (or `--json`). See [gotchas.md](gotchas.md) +- `notification create --project ALIAS --event NAME --channel email|webhook --address ADDR [--component-id ID] [--config-id ID] [--branch ID] [--expires-at TS]` *(since 0.91.0)* -- create a subscription. `--address` carries the **email address** (`--channel email`) or the **webhook URL** (`--channel webhook`); the wire shape is channel-discriminated exactly as the read path's single `address` column is. An invalid `--channel` is a structured `INVALID_ARGUMENT` (exit 2). **Without `--branch` no `branch.id` filter is written** -- unlike the UI, which always writes one; an absent filter does not constrain matching, so the subscription fires for jobs on every branch. See [gotchas.md](gotchas.md) +- `notification delete --project ALIAS --subscription-id ID [--yes]` *(since 0.91.0)* -- delete a subscription; confirmation guard unless `--yes` (or `--json`) +- `notification replace-recipient --project ALIAS --subscription-id ID --address NEW_ADDR [--channel email|webhook] [--yes]` *(since 0.91.0)* -- swap a subscription's recipient, keeping event, filters and expiry. **Delete+recreate under the hood** -- the API has no update primitive -- so a **new `subscription_id` is always minted**; read `old_subscription_id` / `new_subscription_id` off the result and never re-use a cached id. The new subscription is created *first*, so a failed delete leaves a recoverable duplicate (`old_deleted: false` + a warning), never a lost subscription. `--channel` defaults to the old subscription's channel. Confirmation guard unless `--yes` (or `--json`). See [gotchas.md](gotchas.md) - Permission classes: `notification.create` and `notification.replace-recipient` are **write**, `notification.delete` is **destructive** (`list`/`detail` stay read). `--deny-writes` blocks all three; `--deny-destructive` blocks only `delete` - Exposed over `kbagent serve` as `GET /notifications`, `GET /notifications/{project}/{subscription_id}`, `POST /notifications/{project}`, `DELETE /notifications/{project}/{subscription_id}`, and `POST /notifications/{project}/{subscription_id}/replace-recipient` ## Sync (GitOps) - `sync init --project ALIAS [--directory DIR] [--git-branching] [--adopt-existing]` -- initialize sync working directory; `--adopt-existing` (since v0.22.0) adopts a `.keboola/manifest.json` already written by the kbc Go CLI without overwriting (idempotent; validates `project_id` against the alias token) -- `sync pull --project ALIAS [--all-projects] [--force] [--theirs] [--dry-run] [--with-samples] [--no-storage] [--no-jobs] [--job-limit N] [--branch ID]` -- download configs to local files. For large projects (>100 configs), automatically fetches jobs per-config when the grouped API limit is insufficient. `--force` is conflict-aware (since 0.53.0): a locally-modified config whose remote is unchanged is **preserved** (pending delta stays pushable, never silently re-stamped); a true merge conflict (local AND remote both changed since last pull) **aborts** the pull (exit 1, `SYNC_CONFLICT`; `--json` lists `details.conflicts`); local-untouched + remote-changed takes remote. `--theirs` (since v0.72.0) is the supported "discard local, take production" reconcile path: overwrites locally-modified configs/rows, restores deleted/missing files, resolves conflicts by taking remote (no abort, no manifest surgery). Since v0.72.0 plain pull also re-materializes a tracked config whose local dir was deleted (manifest<->disk invariant), so delete-dir-then-pull refetches. Config-level `isDisabled` round-trips (since v0.72.0) as sparse `is_disabled: true` in `_config.yml` -- absent key = enabled. `--branch` (0.47.0+) per-invocation dev-branch override, beats every other branch source. Ignored components (since vNEXT): `keboola.sandboxes` + `keboola.mcp-server-tool` are always excluded, unioned with the manifest's `ignoredComponents` list; a component newly ignored has its manifest entry dropped and local directory removed, reported with pull action `"ignored"` (distinct from `"removed"` = genuinely deleted on remote). -- `sync push --project ALIAS [--all-projects] [--dry-run] [--force] [--allow-plaintext-on-encrypt-failure] [--branch ID] [--no-name-drift-warnings]` -- push local changes (auto-encrypts secrets, fails if encryption fails). Fresh-CREATE writeback updates placeholder manifest entries in place (since 0.47.0) and propagates any `KBC.configuration.*` metadata via `set_config_metadata`. Fresh-CREATE variable binding (since 0.47.2): when a `keboola.variables` config + its values row are created alongside a transformation in the same push, the transformation's `variables_id` / `variables_values_id` placeholders are rebound to the assigned ULIDs and the row's `values` are hoisted even without a `_keboola` block, so `job run` succeeds with no post-push `config variables-set` step (unresolvable/ambiguous links surface a `variable_link` entry in `errors[]`, never a broken link). Never-fetched guard (since v0.72.0): a manifest entry with an empty `pull_hash` and no local files (pre-0.72 name-collision phantom) is **never** planned as a remote DELETE -- diff/push exclude it and report it under `never_fetched` with a warning (run `sync pull` to materialize); local deletion of a properly-pulled config still deletes on push. Adopted-by-id writeback (since v0.72.0): pushing an untracked file whose `_keboola.config_id` resolves on the branch also writes the manifest entry, so follow-up diffs are stable. `--branch` (0.47.0+) per-invocation override; when no `/` subtree exists on disk (since 0.47.2) the local default tree (`main/`) is promoted to the target branch (API writes still target the branch id); `--no-name-drift-warnings` (0.47.0+) drops the cosmetic warnings array. Branch-scoped since v0.89.0 (issue #649): push consumes the diff's changeset, so configs tracked on another branch's tree are never planned as creates -- they ride along on the result envelope under `orphaned` instead (see `sync diff`). **Since vNEXT (#686)** the manifest baseline `pull_config_hash` is stamped from the API response (or a read-back), not from the files on disk, so a pushed multi-statement SQL transformation -- or anything disabled in the UI whose local YAML lacks `is_disabled` -- no longer shows permanent phantom `REMOTE MODIFIED` drift; if the config cannot be read back after the write the baseline is left UNTOUCHED and a `warnings[]` entry says to run `sync pull` (never a disk-derived fallback). One legacy change is refused per-change with `SYNC_LEGACY_BOUNDARY`: a tree pulled before statement-boundary markers existed whose only difference from the remote is the lost boundaries (pushing it would collapse separate SQL statements into one) -- run `sync pull` for that project first. Ignored components (since vNEXT) are filtered out on both sides of the diff push builds on, so a stale local directory for an ignored component (e.g. `keboola.mcp-server-tool`) is never classified as `DELETED` and can never be pushed as a remote deletion. +- `sync pull --project ALIAS [--all-projects] [--force] [--theirs] [--dry-run] [--with-samples] [--no-storage] [--no-jobs] [--job-limit N] [--branch ID]` -- download configs to local files. For large projects (>100 configs), automatically fetches jobs per-config when the grouped API limit is insufficient. `--force` is conflict-aware (since 0.53.0): a locally-modified config whose remote is unchanged is **preserved** (pending delta stays pushable, never silently re-stamped); a true merge conflict (local AND remote both changed since last pull) **aborts** the pull (exit 1, `SYNC_CONFLICT`; `--json` lists `details.conflicts`); local-untouched + remote-changed takes remote. `--theirs` (since v0.72.0) is the supported "discard local, take production" reconcile path: overwrites locally-modified configs/rows, restores deleted/missing files, resolves conflicts by taking remote (no abort, no manifest surgery). Since v0.72.0 plain pull also re-materializes a tracked config whose local dir was deleted (manifest<->disk invariant), so delete-dir-then-pull refetches. Config-level `isDisabled` round-trips (since v0.72.0) as sparse `is_disabled: true` in `_config.yml` -- absent key = enabled. `--branch` (0.47.0+) per-invocation dev-branch override, beats every other branch source. Ignored components (since 0.91.0): `keboola.sandboxes` + `keboola.mcp-server-tool` are always excluded, unioned with the manifest's `ignoredComponents` list; a component newly ignored has its manifest entry dropped and local directory removed, reported with pull action `"ignored"` (distinct from `"removed"` = genuinely deleted on remote). +- `sync push --project ALIAS [--all-projects] [--dry-run] [--force] [--allow-plaintext-on-encrypt-failure] [--branch ID] [--no-name-drift-warnings]` -- push local changes (auto-encrypts secrets, fails if encryption fails). Fresh-CREATE writeback updates placeholder manifest entries in place (since 0.47.0) and propagates any `KBC.configuration.*` metadata via `set_config_metadata`. Fresh-CREATE variable binding (since 0.47.2): when a `keboola.variables` config + its values row are created alongside a transformation in the same push, the transformation's `variables_id` / `variables_values_id` placeholders are rebound to the assigned ULIDs and the row's `values` are hoisted even without a `_keboola` block, so `job run` succeeds with no post-push `config variables-set` step (unresolvable/ambiguous links surface a `variable_link` entry in `errors[]`, never a broken link). Never-fetched guard (since v0.72.0): a manifest entry with an empty `pull_hash` and no local files (pre-0.72 name-collision phantom) is **never** planned as a remote DELETE -- diff/push exclude it and report it under `never_fetched` with a warning (run `sync pull` to materialize); local deletion of a properly-pulled config still deletes on push. Adopted-by-id writeback (since v0.72.0): pushing an untracked file whose `_keboola.config_id` resolves on the branch also writes the manifest entry, so follow-up diffs are stable. `--branch` (0.47.0+) per-invocation override; when no `/` subtree exists on disk (since 0.47.2) the local default tree (`main/`) is promoted to the target branch (API writes still target the branch id); `--no-name-drift-warnings` (0.47.0+) drops the cosmetic warnings array. Branch-scoped since v0.89.0 (issue #649): push consumes the diff's changeset, so configs tracked on another branch's tree are never planned as creates -- they ride along on the result envelope under `orphaned` instead (see `sync diff`). **Since 0.91.0 (#686)** the manifest baseline `pull_config_hash` is stamped from the API response (or a read-back), not from the files on disk, so a pushed multi-statement SQL transformation -- or anything disabled in the UI whose local YAML lacks `is_disabled` -- no longer shows permanent phantom `REMOTE MODIFIED` drift; if the config cannot be read back after the write the baseline is left UNTOUCHED and a `warnings[]` entry says to run `sync pull` (never a disk-derived fallback). One legacy change is refused per-change with `SYNC_LEGACY_BOUNDARY`: a tree pulled before statement-boundary markers existed whose only difference from the remote is the lost boundaries (pushing it would collapse separate SQL statements into one) -- run `sync pull` for that project first. Ignored components (since 0.91.0) are filtered out on both sides of the diff push builds on, so a stale local directory for an ignored component (e.g. `keboola.mcp-server-tool`) is never classified as `DELETED` and can never be pushed as a remote deletion. - `sync clone --source DIR --target ALIAS --target-dir DIR [--bucket-map FILE] [--variable-values FILE] [--instance-rename FILE] [--dry-run] [--branch ID]` -- clone a reference synced project into a **fresh** target project and parameterize it (since v0.63.0). Copies the reference tree at `--source` into `--target-dir`, applies declarative overrides from JSON/YAML files (`--bucket-map` `{old_bucket_id: new_bucket_id}` rewrites storage input/output table refs; `--variable-values` `{var_name: value}` overrides `keboola.variables` rows; `--instance-rename` `{old_path_prefix: new_path_prefix}` renames config dirs + manifest paths), re-points the manifest at the target project, and pushes. Because the reference's config ids do not exist in the fresh target, every config is CREATEd fresh and **keboola.flow task `configId`s + transformation variable links are remapped reference->ULID** by push Phase C/D (the push result carries `flow_task_remaps`). **Idempotent**: re-running with an existing `--target-dir` skips copy/overrides and just pushes, reporting `no_changes` / `created: 0`. Fails fast (`CONFIG_ERROR`) if the target already contains the reference's configs -- clone requires a fresh/empty target. `SyncService.clone_project(...)` returns a typed `CloneResult` for in-process SDK callers. Override files must be flat `{id: scalar}` mappings *(since v0.89.0)* -- a nested mapping, list, or null value is rejected with `CONFIG_ERROR` (exit 5) naming the key and its actual type. -- `sync diff --project ALIAS [--all-projects] [--branch ID]` -- 3-way diff (local vs base vs remote), detects conflicts. `--branch` (0.47.0+) per-invocation dev-branch override. Branch-scoped since v0.89.0 (issue #649): the local side is read from exactly ONE tree (the target branch's subtree, or `main/` when the target has none). Manifest entries belonging to another branch's tree -- what `sync pull --branch ` leaves behind when it re-targets the manifest -- are excluded from the changeset and reported under `orphaned` (`summary.orphaned` + details with `component_id`, `config_id`, `path`, `branch_id`, `branch_path`, `exists_on_target`, `reason`, `hint`); human mode previews the first 10. An orphaned FILE whose `_keboola.config_id` still resolves on the target is adopted (diffed as `unchanged`/`modified`), never re-created; same-tree id claims keep the #482/#497 fork-by-copy CREATE. Fix a non-zero `summary.orphaned` with `sync pull`. **Since vNEXT (#686)** a manifest entry without `metadata.config_hash_version` (written by a pre-vNEXT kbagent) is compared leniently: a stored hash equal to the pre-vNEXT hash of the SAME remote config counts as in sync, so the phantom `codes changed` entries disappear immediately; every other field is still pinned by that hash, so real remote drift is unaffected. One `sync pull` per project stamps the version and ends the leniency. Ignored components (since vNEXT) -- `keboola.sandboxes`, `keboola.mcp-server-tool`, and anything listed in the manifest's `ignoredComponents` -- are excluded from BOTH sides of the comparison, so a stale local directory for one of them never shows up as `DELETED`. +- `sync diff --project ALIAS [--all-projects] [--branch ID]` -- 3-way diff (local vs base vs remote), detects conflicts. `--branch` (0.47.0+) per-invocation dev-branch override. Branch-scoped since v0.89.0 (issue #649): the local side is read from exactly ONE tree (the target branch's subtree, or `main/` when the target has none). Manifest entries belonging to another branch's tree -- what `sync pull --branch ` leaves behind when it re-targets the manifest -- are excluded from the changeset and reported under `orphaned` (`summary.orphaned` + details with `component_id`, `config_id`, `path`, `branch_id`, `branch_path`, `exists_on_target`, `reason`, `hint`); human mode previews the first 10. An orphaned FILE whose `_keboola.config_id` still resolves on the target is adopted (diffed as `unchanged`/`modified`), never re-created; same-tree id claims keep the #482/#497 fork-by-copy CREATE. Fix a non-zero `summary.orphaned` with `sync pull`. **Since 0.91.0 (#686)** a manifest entry without `metadata.config_hash_version` (written by a pre-0.91.0 kbagent) is compared leniently: a stored hash equal to the pre-0.91.0 hash of the SAME remote config counts as in sync, so the phantom `codes changed` entries disappear immediately; every other field is still pinned by that hash, so real remote drift is unaffected. One `sync pull` per project stamps the version and ends the leniency. Ignored components (since 0.91.0) -- `keboola.sandboxes`, `keboola.mcp-server-tool`, and anything listed in the manifest's `ignoredComponents` -- are excluded from BOTH sides of the comparison, so a stale local directory for one of them never shows up as `DELETED`. - `sync status [--directory DIR]` -- show locally modified/added/deleted configs. Also surfaces `plaintext_secret_warnings` (since 0.55.0): in-sync configs/rows whose `#`-secrets are still plaintext on the remote (a leftover from pre-0.54.0 writes; #378). Pending (un-pushed) edits are not flagged. Fix = re-push on >=0.54.0 + rotate (version history keeps the plaintext). - `sync branch-link --project ALIAS [--branch-id ID] [--branch-name NAME]` -- link git branch to Keboola dev branch - `sync branch-unlink [--directory DIR]` -- remove git-to-Keboola branch mapping @@ -443,7 +443,7 @@ CLI parity for the `/agents` REST surface. Reads/writes `/agents.jso - `doctor` -- health checks (no `--fix` since v0.85.0 -- it only installed the MCP server). Includes a `sync_secrets` check (since 0.55.0): when run inside a sync working tree (`.keboola/manifest.json`), warns if any in-sync config holds plaintext `#`-secrets (#378); `skip` outside a sync tree. Also an `mcp_tool_tasks` check: FAILs when `agents.json` holds tasks using the removed `mcp_tool` action, listing each task in `details.tasks[]` -- map the tool to its native command via `docs/mcp-migration.md` and recreate the task as `--type cli_command` - `version` -- show the kbagent version and check for kbagent updates (kbagent only since v0.85.0); on a standalone binary it advertises the native channel's upgrade command instead of `kbagent update` (v0.79.0+) - `context` -- full usage instructions for AI agents -- `serve [--host HOST] [--port PORT] [--ui] [--ui-dist PATH] [--reload] [--log-level LVL] [--cors-origin ORIGIN] [--config-dir DIR] [--no-banner]` -- launch the FastAPI HTTP server backing the web UI. `--ui` mounts the built SPA at `/` and authenticates the browser with an HttpOnly `kbagent_session` cookie (nothing is injected into `index.html`). `--no-banner` *(since 0.90.0)* suppresses the UI's "What's new" popup -- a curated per-version highlights modal shown once per version, dismissal persisted in localStorage `kbagent.whatsnew.seen`. The SPA reads the switch from `GET /ui-config` -> `{"banner": bool}`; it gates only the UNSOLICITED popup, so the command palette's "What's new" action still opens it on request. `--config-dir` picks the directory the server exposes (projects AND the `permissions` policy the `/auth/*` routes enforce); *(since vNEXT)* an explicit root-level `kbagent --config-dir Y serve` is honored too, with the serve-level flag winning when both are given -- on 0.90.1 and older the root-level one was silently ignored, so pass it to `serve` itself there. Requires the optional `server` extra +- `serve [--host HOST] [--port PORT] [--ui] [--ui-dist PATH] [--reload] [--log-level LVL] [--cors-origin ORIGIN] [--config-dir DIR] [--no-banner]` -- launch the FastAPI HTTP server backing the web UI. `--ui` mounts the built SPA at `/` and authenticates the browser with an HttpOnly `kbagent_session` cookie (nothing is injected into `index.html`). `--no-banner` *(since 0.90.0)* suppresses the UI's "What's new" popup -- a curated per-version highlights modal shown once per version, dismissal persisted in localStorage `kbagent.whatsnew.seen`. The SPA reads the switch from `GET /ui-config` -> `{"banner": bool}`; it gates only the UNSOLICITED popup, so the command palette's "What's new" action still opens it on request. `--config-dir` picks the directory the server exposes (projects AND the `permissions` policy the `/auth/*` routes enforce); *(since 0.91.0)* an explicit root-level `kbagent --config-dir Y serve` is honored too, with the serve-level flag winning when both are given -- on 0.90.1 and older the root-level one was silently ignored, so pass it to `serve` itself there. Requires the optional `server` extra ## Global Flags | Flag | Description | diff --git a/plugins/kbagent/skills/kbagent/references/gotchas.md b/plugins/kbagent/skills/kbagent/references/gotchas.md index 834e4606..4c5177dc 100644 --- a/plugins/kbagent/skills/kbagent/references/gotchas.md +++ b/plugins/kbagent/skills/kbagent/references/gotchas.md @@ -458,7 +458,7 @@ Versioning convention: `--allow-plaintext-on-encrypt-failure`, which would write the PAT in plaintext into Storage. -## `sync push` no longer leaves phantom `REMOTE MODIFIED` drift; `transform.sql` carries statement boundaries (since vNEXT, #686) +## `sync push` no longer leaves phantom `REMOTE MODIFIED` drift; `transform.sql` carries statement boundaries (since 0.91.0, #686) `pull_config_hash` in `.keboola/manifest.json` is the 3-way diff's base, and it means "the hash of this config **as the API returns it**". `sync pull` and the @@ -494,7 +494,7 @@ it, and the next deploy re-created it (field report: 18 phantom configs hiding statement array -- i.e. when the API's elements carry no trailing `;`. A `;`-terminated file is byte-identical to what earlier versions wrote, so existing trees produce no spurious diff. This closes a SILENT failure: before - vNEXT, a script like `["SELECT 1", "SELECT 2"]` lost its boundary on pull and + 0.91.0, a script like `["SELECT 1", "SELECT 2"]` lost its boundary on pull and push rewrote production as ONE statement (the `MULTI_STATEMENT_COUNT=1` crash shape) while `sync diff` reported "in sync". - Markers are guaranteed boundaries, but `;` splitting still runs INSIDE each @@ -527,7 +527,7 @@ it, and the next deploy re-created it (field report: 18 phantom configs hiding statements into one. Run `sync pull` for that project, then push again. Genuine SQL edits are never blocked by this guard. -## `sync push` runs the same script-shape guard as `config update` (since vNEXT) +## `sync push` runs the same script-shape guard as `config update` (since 0.91.0) `sync push` now runs `normalize_blocks_codes_script` -- the runtime-safety guard `config update` and `transformation edit/create` have always run -- on every @@ -1424,7 +1424,7 @@ events and emits a final `done` SSE frame mirroring the same record. subscription is inactive. ## Notification subscriptions can now be written, and the write path has sharp edges -*(since vNEXT)* +*(since 0.91.0)* - **`notification replace-recipient` always mints a NEW `subscription_id`.** The Notification Service has **no update primitive** -- there is no PATCH or @@ -4601,7 +4601,7 @@ though single-project `job list` looked correctly time-ordered. `create_app` builds a `PermissionEngine` from the persisted `permissions` policy of **the config dir `serve` resolves** (its own `--config-dir`, then -- -since vNEXT -- an explicit root-level `kbagent --config-dir`, then +since 0.91.0 -- an explicit root-level `kbagent --config-dir`, then `KBAGENT_CONFIG_DIR`, then the local/global chain), and `kbagent serve` forwards only the session FLAGS of the invocation on top. But of the ~30 routers, only the three `/auth/*` routes (`server/routers/auth.py`) declare @@ -4647,7 +4647,7 @@ shapes. ## `serve` honors the root-level `--config-dir` -*(since vNEXT)* `serve` is the only subcommand carrying a `--config-dir` of its own, so the +*(since 0.91.0)* `serve` is the only subcommand carrying a `--config-dir` of its own, so the flag has two possible positions. The precedence is **most specific wins**, matching what `kbagent repl` does with the root flags: @@ -4672,7 +4672,7 @@ resolution is left to the server, which lands on the same directory anyway. ## `workspace load` now auto-CLONEs eligible tables instead of always COPYing -*(since vNEXT, closes #687)* Before this, `workspace load` always sent a plain `copy` (the API's own +*(since 0.91.0, closes #687)* Before this, `workspace load` always sent a plain `copy` (the API's own default when `loadType` is omitted) -- a 282 GB table load burned warehouse credits for hours where `clone` is metadata-only and finishes in seconds. kbagent now mirrors the server's `LoadTypeDecider` per table. @@ -4729,7 +4729,7 @@ materialized inside the workspace. ## `permissions set` now rejects unknown patterns instead of silently persisting them -*(since vNEXT)* Before this fix (issue #688), `kbagent permissions set --allow/--deny PATTERN` +*(since 0.91.0)* Before this fix (issue #688), `kbagent permissions set --allow/--deny PATTERN` accepted any string with zero validation. A typo like `tool.admin` or `stroage.upload-table` (missing the `-` in `storage`) was written straight to `config.json` as a dead rule that would never match anything -- the failure @@ -4766,13 +4766,13 @@ was silent, and the only way to notice was reading `permissions show` (or persist the pattern. - **`doctor --json`'s `inert_permission_patterns` check carries `details.inert_since` only when at least one offending pattern is - `tool:`-prefixed** (since vNEXT) -- a purely typo'd policy (e.g. + `tool:`-prefixed** (since 0.91.0) -- a purely typo'd policy (e.g. `stroage.upload-table`) omits the key entirely, so JSON consumers must not assume `inert_since` is always present on `status: "warn"`. ## `keboola.mcp-server-tool` is now always excluded from sync; `ignoredComponents` is live -*(since vNEXT)* `ALWAYS_IGNORED_COMPONENTS` (`constants.py`) now includes `keboola.mcp-server-tool` +*(since 0.91.0)* `ALWAYS_IGNORED_COMPONENTS` (`constants.py`) now includes `keboola.mcp-server-tool` alongside `keboola.sandboxes`. The Keboola MCP server auto-creates one empty workspace-record config per project it touches (`configuration: {}`, name like `mcp-workspace-`); those configs carry no configuration and are managed diff --git a/plugins/kbagent/skills/kbagent/references/permissions-workflow.md b/plugins/kbagent/skills/kbagent/references/permissions-workflow.md index 85acf0a2..78d1615f 100644 --- a/plugins/kbagent/skills/kbagent/references/permissions-workflow.md +++ b/plugins/kbagent/skills/kbagent/references/permissions-workflow.md @@ -28,7 +28,7 @@ The agent CANNOT: > show` names any such pattern (key `inert_patterns` in `--json`) and `kbagent > doctor` WARNs via its `inert_permission_patterns` check. > -> **(since vNEXT)** `permissions set` now validates every `--allow`/`--deny` +> **(since 0.91.0)** `permissions set` now validates every `--allow`/`--deny` > pattern before persisting it: it must be a `cli:*` category, an exact > operation name, or a glob matching >=1 known operation, else the call fails > with `VALIDATION_ERROR`, exit 2, before the confirmation prompt is even @@ -106,7 +106,7 @@ kbagent --json permissions list | `cli:read` | All read-only CLI commands | | `branch.delete` | Exact command match | | `sync.*` | All sync subcommands (glob) | -| `tool:*` | Nothing -- inert since v0.85.0 (the MCP passthrough is gone); `permissions set` REJECTS it as input (since vNEXT) -- only an already-persisted `tool:*` sticks around | +| `tool:*` | Nothing -- inert since v0.85.0 (the MCP passthrough is gone); `permissions set` REJECTS it as input (since 0.91.0) -- only an already-persisted `tool:*` sticks around | ## Session firewall flags diff --git a/plugins/kbagent/skills/kbagent/references/sync-workflow.md b/plugins/kbagent/skills/kbagent/references/sync-workflow.md index 4867ba62..b7903010 100644 --- a/plugins/kbagent/skills/kbagent/references/sync-workflow.md +++ b/plugins/kbagent/skills/kbagent/references/sync-workflow.md @@ -416,13 +416,13 @@ Stored in `.keboola/branch-mapping.json`: - **Pull protects local edits**: locally-modified files are skipped by default - **`--force` is conflict-aware (since 0.53.0)**: see below -- it no longer blindly overwrites - **Push only sends local changes**: remote_modified and conflict changes are skipped -- **Push records the API's own view of what it wrote (since vNEXT, #686)**: the +- **Push records the API's own view of what it wrote (since 0.91.0, #686)**: the manifest baseline (`pull_config_hash`) comes from the API response (or a - read-back), never from the files on disk. Before vNEXT the two producers + read-back), never from the files on disk. Before 0.91.0 the two producers disagreed and every pushed multi-statement SQL transformation showed permanent phantom `REMOTE MODIFIED` drift - **Push runs the same runtime-safety script normalization as `config update` - (since vNEXT)**: `parameters.blocks[].codes[].script` is normalized to the + (since 0.91.0)**: `parameters.blocks[].codes[].script` is normalized to the runtime's shape (one element = one executable statement) before every write. A no-op for a normally pulled tree; it catches a hand-authored `_config.yml` with inline `parameters.blocks` and no `transform.sql`. Any fix is reported @@ -437,7 +437,7 @@ Stored in `.keboola/branch-mapping.json`: ## Ignored components -*(since vNEXT)* Some components are excluded from every sync operation (`pull`, `diff`, +*(since 0.91.0)* Some components are excluded from every sync operation (`pull`, `diff`, `push`) because they are managed through separate APIs and have volatile internal state: @@ -489,7 +489,7 @@ the 3-way diff state per config (and per row): > stops you loudly instead of losing work. To intentionally drop a local edit, > delete the file (or the config directory) and pull. -## Migrating a tree pulled before vNEXT (#686) +## Migrating a tree pulled before 0.91.0 (#686) `sync push` used to stamp the manifest baseline from the files on disk while `sync pull` / `sync diff` computed it from the API. Any config the two @@ -508,7 +508,7 @@ gained markers) and the phantom entries are gone for good -- no more content-free "refresh the baseline" PR after each deploy. Until that pull happens, kbagent is lenient with unversioned entries: a stored -hash that matches the pre-vNEXT hash of the same remote config is treated as in +hash that matches the pre-0.91.0 hash of the same remote config is treated as in sync. The leniency covers ONLY that difference -- a genuinely changed remote still reports `REMOTE MODIFIED`. diff --git a/plugins/kbagent/skills/kbagent/references/workspace-workflow.md b/plugins/kbagent/skills/kbagent/references/workspace-workflow.md index b955b697..53754579 100644 --- a/plugins/kbagent/skills/kbagent/references/workspace-workflow.md +++ b/plugins/kbagent/skills/kbagent/references/workspace-workflow.md @@ -104,7 +104,7 @@ kbagent --json workspace query \ ## Load types: clone / copy / view -*(since vNEXT, #687)* **Cheapest first**: skip the load entirely if you can +*(since 0.91.0, #687)* **Cheapest first**: skip the load entirely if you can (see "Don't load at all" below) > `clone` (zero-copy, seconds, any table size) > `view` (zero-storage, narrower eligibility) > `copy` (real data movement, size-guarded). `workspace load` supports all three as diff --git a/pyproject.toml b/pyproject.toml index 5b469e2f..b7ba59fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keboola-cli" -version = "0.90.1" +version = "0.91.0" description = "AI-friendly CLI for managing Keboola projects" readme = "README.md" requires-python = ">=3.12" diff --git a/src/keboola_agent_cli/changelog.py b/src/keboola_agent_cli/changelog.py index 19afccf8..b585aca3 100644 --- a/src/keboola_agent_cli/changelog.py +++ b/src/keboola_agent_cli/changelog.py @@ -24,6 +24,87 @@ # Ordered newest-first. Each value is a list of brief one-line descriptions. CHANGELOG: dict[str, list[str]] = { + "0.91.0": [ + "New (#692): `workspace load` now picks a zero-copy CLONE per table by default " + "instead of always copying. Eligibility mirrors the server's own `LoadTypeDecider` " + "(same backend, snowflake/bigquery, full load, no external-schema bucket, alias only " + "when column auto-sync is on, and on BigQuery not an Analytics-Hub-linked bucket); an " + "ineligible table falls back to COPY individually and reports a per-table " + "`clone_ineligible_reason`. `--load-type clone|copy|view` forces the type for every " + "table and fails loudly on an ineligible choice rather than silently degrading. A " + "table resolved or forced to COPY above 1 GiB now needs an interactive confirmation " + "or `--force`, so a 282 GB copy can no longer start unnoticed. `--timeout SECONDS` " + "(default 300 here) replaces the hard-coded 60 s wait, the timeout message names the " + "job id and warns that the job keeps running server-side, and " + "`STORAGE_JOB_TIMEOUT` now exits 4 (retryable) instead of 1.", + "New (#695): the `notification` group is no longer read-only -- `create`, `delete` " + "and `replace-recipient` can now fix what the #600 audit finds. `--address` carries " + "the email address or the webhook URL, discriminated by `--channel`. " + "`replace-recipient` is delete+recreate (the API has no update primitive): the new " + "subscription is created first, so a new `subscription_id` is always minted and a " + "failed delete leaves a recoverable duplicate flagged as `old_deleted: false` rather " + "than a lost subscription. `create` without `--branch` writes no branch filter, so " + "the subscription fires for jobs on every branch. Permission classes: `create` and " + "`replace-recipient` are writes, `delete` is destructive.", + "Fix (#686, #694, #696): `sync push` now stamps the manifest baseline from the API " + "response, ending permanent phantom `~ REMOTE MODIFIED` drift. Pushed " + "multi-statement SQL transformations were the reported symptom: push hashed the " + "files on disk while " + "`pull`/`diff` hash the API response, so the two sides disagreed forever. Triage " + "found a worse sibling: with no trailing semicolons in the API's `script[]`, push " + "silently collapsed N statements into one -- the `MULTI_STATEMENT_COUNT=1` crash " + "shape -- while `diff` reported in sync. There is now one canonical `script[]` shape " + "(one element = one statement), `transform.sql` carries " + "`/* ===== STATEMENT ===== */` markers when semicolons cannot recover the " + "boundaries, and the runtime-safety script guard also runs on the push path. An " + "unreadable-after-write leaves the baseline untouched plus a `warnings[]` entry, " + "never a disk-derived fallback. Entries carry `metadata.config_hash_version`; one " + "`sync pull` per project migrates a pre-0.91.0 tree, and a tree whose only " + "difference is the lost statement boundaries is refused per-change with " + "`SYNC_LEGACY_BOUNDARY` telling you to pull first.", + "Fix (#689, #691): `sync` now ignores `keboola.mcp-server-tool` and honors the " + "manifest's `ignoredComponents` field. That field was declared in the schema and " + "read by nothing. The MCP server's auto-created empty `mcp-workspace-` configs join " + "`keboola.sandboxes` on the always-ignored list, and the manifest field unions with " + "it across pull, diff and push. `pull` drops manifest entries and local directories " + "for a newly-ignored component under the new action `ignored` (distinct from " + "`removed` = deleted on the remote), and `diff` filters the local side too -- so a " + "stale directory for an ignored component can no longer classify as a delete, " + "closing the trap that destroyed production `keboola.mcp-server-tool` configs.", + "Fix (#688, #693): `permissions set` validates every `--allow`/`--deny` pattern " + "before persisting it. A typo or a fabricated category used to be written to " + "`config.json` and echoed back as a normal rule while matching nothing at runtime, " + "so the operator walked away believing the firewall had changed. Each pattern must " + "now be a `cli:*` category, an exact operation name (including a flag-escalated " + "string like `auth.logout --remove-projects`), or a glob matching at least one known " + "operation; anything else is a `VALIDATION_ERROR`, exit 2, listing every offending " + "pattern, and nothing is persisted. `permissions show` and `kbagent doctor` now flag " + "any persisted pattern matching zero operations, not just the retired `tool:*` " + "namespace. `PermissionEngine` itself stays lenient at evaluation time.", + "Fix (#679, #681): `kbagent --config-dir X serve` now actually serves X. `serve` is " + "the only subcommand with a `--config-dir` of its own and it read only that one, so " + "the root flag was silently ignored: the wrong projects were exposed, and since " + "0.90.1's `/auth/*` enforcement the wrong directory's permission policy applied. " + "Precedence is now most-specific-wins -- `serve --config-dir` beats the root flag, " + "which beats `KBAGENT_CONFIG_DIR` / the `.kbagent` walk-up / global. Only an " + "explicit root flag propagates; an env-var or walk-up resolution is left to the " + "server, which reaches the identical directory on its own.", + "Change (#627): the kbagent Claude Code plugin is now published through the " + "`keboola-claude-kit` marketplace in `keboola/ai-kit`. Keboola had two competing " + "marketplaces and no way for a user to tell which was real; this leaves one. Install " + "with `/plugin marketplace add keboola/ai-kit` then " + "`/plugin install kbagent@keboola-claude-kit`. The plugin source stays in this repo " + "(four CI gates validate it against the live command tree), and a new " + "`ai-kit-marketplace` release job repoints ai-kit's entry on every stable tag. This " + "repo's own marketplace becomes a deprecated shim so existing installs keep " + "resolving updates. `kbagent doctor` also now reports the newest plugin copy across " + "both marketplace cache directories.", + "Note (#697, #698): documentation and plugin guidance were brought in line with the " + "above. Version tags moved out of markdown headings (a resolved tag changes the " + "anchor slug and breaks inbound links), and the workspace-load skill guidance gained " + "cheapest-first ordering, a verify-what-happened pointer at the per-table " + "`tables[].load_type` report, and a correction of a misleading `--force` example.", + ], "0.90.1": [ "New (#675): the `serve --ui` web UI gained cross-project All Jobs and All Tokens " "views. All Jobs (`#/jobs-all`) is one chronological feed over every registered " diff --git a/src/keboola_agent_cli/commands/context.py b/src/keboola_agent_cli/commands/context.py index 651aca7f..86b11194 100644 --- a/src/keboola_agent_cli/commands/context.py +++ b/src/keboola_agent_cli/commands/context.py @@ -1125,7 +1125,7 @@ --channel email|webhook --address ADDR [--component-id ID] [--config-id ID] [--branch ID] [--expires-at TS] - Create a subscription (since vNEXT). --address carries the email address + Create a subscription (since 0.91.0). --address carries the email address (--channel email) or the webhook URL (--channel webhook) -- the wire shape is channel-discriminated, the CLI takes one flag either way. An invalid --channel is a structured INVALID_ARGUMENT, exit 2. @@ -1134,13 +1134,13 @@ a subscription fires for jobs on EVERY branch. kbagent notification delete --project ALIAS --subscription-id ID [--yes] - Delete a subscription (since vNEXT). Confirms unless --yes or --json. + Delete a subscription (since 0.91.0). Confirms unless --yes or --json. kbagent notification replace-recipient --project ALIAS --subscription-id ID --address NEW_ADDR [--channel email|webhook] [--yes] - Swap a subscription's recipient (since vNEXT), keeping the event, filters + Swap a subscription's recipient (since 0.91.0), keeping the event, filters and expiry. This is delete+recreate -- the API has NO update primitive. The NEW subscription is created FIRST, then the old one deleted, so a NEW subscription_id is ALWAYS minted: never cache the old id, read @@ -1216,7 +1216,7 @@ clone_ineligible_reason). --load-type forces clone/copy/view for every table -- an ineligible explicit choice fails loudly, it never silently degrades. A COPY over 1 GiB needs interactive confirmation or --force. --timeout defaults to 300s; on timeout the - job keeps running server-side (exit 4, not 1). (since vNEXT) + job keeps running server-side (exit 4, not 1). (since 0.91.0) kbagent workspace query --project ALIAS --workspace-id ID --sql "SQL" [--file F] [--transactional] [--full] [--limit N] Execute SQL via Query Service. No Snowflake credentials needed. @@ -1423,7 +1423,7 @@ automatically falls back to per-config job fetching to ensure all configs get job history. Auto-detects renamed configs and renames local directories to match (uses git mv in git repos). --branch (since 0.47.0): per-invocation dev-branch override. Same semantics as sync push/diff. - Ignored components (since vNEXT, #689): keboola.sandboxes + keboola.mcp-server-tool are + Ignored components (since 0.91.0, #689): keboola.sandboxes + keboola.mcp-server-tool are always excluded, unioned with the manifest's ignoredComponents list (.keboola/manifest.json) -- a per-tree exclusion knob honored by pull/diff/push. A component newly ignored has its manifest entry dropped and local dir removed on the next @@ -1455,7 +1455,7 @@ never re-created; a same-tree id claim keeps the #482 fork-by-copy CREATE. Fix a non-zero summary.orphaned with `sync pull`; promote dev-only configs with `branch merge`, never by pushing them to production. - Ignored components (since vNEXT, #689) are excluded from BOTH sides of the comparison, + Ignored components (since 0.91.0, #689) are excluded from BOTH sides of the comparison, so a stale manifest entry or leftover dir for one of them contributes nothing -- not added, not deleted, not orphaned. Push builds on this diff, so it plans nothing for them either (closes the delete-dir-then-push trap for ignored components). @@ -1982,7 +1982,7 @@ they matched is gone. A persisted policy still loads with them, but they match nothing, so a mode=deny policy whose only allowance was `tool:read` now denies everything. Rewrite such a policy with `cli:read`. - Since vNEXT (issue #688): every pattern is validated BEFORE the + Since 0.91.0 (issue #688): every pattern is validated BEFORE the interactive confirmation -- it must be a cli:* category, an exact operation name, or a glob matching >=1 known operation, or the whole call fails with VALIDATION_ERROR (exit 2, invalid patterns listed) and diff --git a/uv.lock b/uv.lock index 577ab241..6c73a22f 100644 --- a/uv.lock +++ b/uv.lock @@ -590,7 +590,7 @@ wheels = [ [[package]] name = "keboola-cli" -version = "0.90.1" +version = "0.91.0" source = { editable = "." } dependencies = [ { name = "croniter" },