Skip to content

release: v0.22.0 -- FIIA parity batch - #215

Merged
padak merged 7 commits into
mainfrom
release/0.22.0
Apr 23, 2026
Merged

release: v0.22.0 -- FIIA parity batch#215
padak merged 7 commits into
mainfrom
release/0.22.0

Conversation

@padak

@padak padak commented Apr 23, 2026

Copy link
Copy Markdown
Member

Summary

Integration PR for v0.22.0 feature release, bundling 6 squash-merged PRs from @ottomansky plus a docs completion commit. All 6 PRs were rebased locally on top of v0.21.2 to resolve merge conflicts (changelog.py + test_e2e.py + errors.py + commands/_helpers.py). Max's review fixes are all preserved.

Types: 15+ new CLI commands, 3 new global flags, 2 new env-var controls, 1 refactor (ErrorCode StrEnum with CI guard). No bug fixes — this is feature-forward.

Contents (in merge order)

PR Type What Originally closes
#203 feat `project use/current` + `KBAGENT_PROJECT` env var + `--deny-writes` / `--deny-destructive` session firewall (FIIA P0-4)
#200 feat `flow list/detail/schema/new/update/delete/schedule/schedule-remove` (8 commands); Kahn DAG validation; schedules stored as `keboola.scheduler` configs partially addresses #195
#204 feat `config metadata-list/get-metadata/set-metadata/delete-metadata` + `config set-folder` (FIIA P1-3); `workspace list --orphaned` + `workspace gc` (FIIA P1-4)
#205 feat `storage describe-bucket/table/column/batch` via `KBC.description` metadata; `bucket-detail` / `table-detail` now return description + metadata fields
#202 feat Queue polling parity: exponential curve (2s×30 → 5s×48 → 15s), `logTail` on terminal non-success, auto-cancel on `--timeout` with exit code 7, `--poll-strategy fixed` for legacy behaviour (FIIA P0-3)
#201 refactor + feat `ErrorCode(StrEnum)` with 49 typed constants, CI guard `check_error_codes.py`, `docs/error-codes.md`; `sync init --adopt-existing` for kbc Go CLI checkouts (P2-1 + P2-2)

Conflict resolution highlights

Docs completion (my follow-up commit)

Max's PRs covered most of the required CONTRIBUTING.md artifacts, but three were missing:

  • sync-workflow.md: New "Adopting an existing kbc Go CLI checkout" section for feat(0.22.0): ErrorCode enum + sync init --adopt-existing #201's `sync init --adopt-existing`.
  • gotchas.md: New entries for exit code 7 (`JOB_TIMEOUT_TERMINATED`), `--poll-strategy fixed`, `--log-tail-lines N`, `logTail` response field, and `--deny-writes` / `--deny-destructive` semantics. Plus brief note on `sync init --adopt-existing`.
  • workspace-workflow.md: New "Orphan detection + garbage collection" section for feat(0.22.0): config metadata CRUD + workspace orphan GC (FIIA P1-3, P1-4) #204's `workspace list --orphaned` / `workspace gc`.
  • commands-reference.md: `--adopt-existing` flag appended to `sync init` line.
  • README.md: Refreshed "What it does" table and "All commands" block with the 15 new commands (flow CRUD, config metadata, workspace GC, storage describe, project pin + firewall, queue polling parity), 2 new env-var controls (`KBAGENT_PROJECT`), 3 new global flags (`--deny-writes` / `--deny-destructive`).

Origin PR status

Because the merges happened locally (squash + rebase on release/0.22.0) rather than through `gh pr merge`, the source PRs are CLOSED on GitHub instead of `MERGED`:

Test plan

  • `make check` locally: ruff + format + skill-check + version-check + changelog-check + error-code-guard + full test suite. 2141 passed, 4 skipped.
  • `scripts/check_error_codes.py` passes — no raw literals left in source.
  • `make version-sync` — `plugin.json` already at 0.22.0.
  • `make test-e2e` against a real Keboola project (reviewer's call)
  • Security review (`/security-review`) after branch settles
  • Reviewer walk-through of the 14 changelog entries + SKILL doc additions

Post-merge

Related follow-ups (not in this release)

ottomansky and others added 7 commits April 23, 2026 13:59
* feat(0.22.0): project pin + firewall flags (FIIA P0-4)

Adds an explicit project-pin UX + session-only firewall sugar that
together close the FIIA P0-4 gap.

New surface:
- `kbagent project use <alias>` -- pins default_project in config.json.
  The field already existed (auto-set on first add, auto-rotated on
  remove) but had no explicit CLI verb. Now it does.
- `kbagent project current` -- prints effective alias + source
  (env / pin / none). Reports both the env override AND the persisted
  pin so misconfigurations surface loudly instead of silently.
- Top-level `--deny-writes` / `--deny-destructive` flags -- synthesize
  a session-only PermissionPolicy and merge with any persisted policy.
  Never written to config.json. `--deny-writes` covers the wide net
  (write+destructive+admin); `--deny-destructive` narrowly covers data
  destruction so callers can keep build-up capabilities while blocking
  tear-down.
- `KBAGENT_PROJECT` env var overrides the persisted pin for a single
  shell. Resolution precedence for single-project ops:
  `--project` > `KBAGENT_PROJECT` > pin > sole-project > fail-hard
  with a repair-friendly CONFIG_ERROR.
- `ProjectService.resolve_pinned_alias()` and the thin
  `commands._helpers.resolve_project_alias()` helper -- public
  contract for future single-project write commands to adopt
  without retrofitting every read-path fan-out.

Wire notes:
- `project.use` registered as 'write', `project.current` as 'read' in
  the permission engine's OPERATION_REGISTRY so persisted policies
  gate them correctly.
- `_apply_firewall_flags()` builds a fresh PermissionPolicy and never
  mutates the persisted deny list.
- Stale pin (default_project pointing at a deleted alias) now raises
  a repair-friendly CONFIG_ERROR instead of silently fanning out.

* fix(repl): forward --deny-writes / --deny-destructive into REPL subcommands

Review caught a silent-elevation bug: starting the CLI with
`kbagent --deny-writes` and then entering the REPL (or typing `repl`)
let inner commands re-parse WITHOUT the firewall flags, restoring full
write/destructive access. The same PR5 firewall flag that a user opted
into was silently dropped the moment they landed at the prompt.

Root cause: `_run_repl()` only forwarded four flags (--json, --verbose,
--no-color, --config-dir) when rebuilding argv for each inner command;
the two new session-firewall flags were never threaded through.

Fix:
- Extend `_run_repl(deny_writes, deny_destructive)` and re-append both
  flags on every inner invocation unless the user already typed them.
- Thread the flags from `cli.py::main()` into both REPL launch paths
  (implicit no-subcommand launch + explicit `kbagent repl`) via
  `ctx.obj["deny_writes"]` / `ctx.obj["deny_destructive"]`.
- Banner advertises the active firewall so users can see what's on.

Tests (`TestReplFirewallPropagation`):
- `test_deny_writes_appended_to_inner_argv` -- guard against the
  regression by asserting every rebuilt argv carries --deny-writes.
- `test_deny_destructive_appended_to_inner_argv` -- same for destructive.
- `test_no_duplicate_when_user_retypes_flag` -- idempotence guard.

Also closes other review-loop gaps (non-REPL):
- `test_project_help` now asserts `use` and `current` subcommands.
- `test_root_help_lists_firewall_flags` guards the global flags.
- `test_list_includes_project_use_and_current` guards the registry.
- `test_hint_on_non_api_project_use/current` cover --hint graceful exit.
- `test_deny_writes_composes_with_persisted_deny_mode` validates the
  runtime composition under default-deny mode.

* fix(test): use callback signature for firewall-flag help assertion

The original `test_root_help_lists_firewall_flags` rendered `kbagent --help`
via CliRunner and string-matched `--deny-writes` / `--deny-destructive`.
CI's narrow default terminal width made Rich's typer help panel truncate
the long option names into `...`, failing the assertion even though the
flags are registered correctly.

Switch to inspecting `cli.main()` function signature. The signature is the
source of truth for registered options and is independent of terminal
width. Still catches the regression (accidental flag removal).

* fix(permissions): show/list now reflect session firewall flags

UX gap surfaced in the review loop: `permissions show` / `permissions list`
previously inspected ONLY the persisted policy. A user running
`kbagent --deny-writes permissions show` (or inside a REPL launched with
those flags) saw misleading output: "No permission policy configured"
even though their writes were in fact blocked for the invocation.

Fix:
- `permissions list` now builds the engine from the MERGED policy
  (`_apply_firewall_flags(persisted, deny_writes=..., deny_destructive=...)`).
  The allowed/denied column matches what the command will actually do.
  Human-mode footer advertises active session flags.
- `permissions show` reports both layers in the JSON envelope:
    {
      "active": bool,              # true if persisted OR session present
      "persisted": {mode, allow, deny} | null,
      "session_flags": [...],
      # legacy top-level keys preserved when persisted != null
      "mode": ..., "allow": ..., "deny": ...
    }
  Human-mode prints a "Session firewall:" line whenever flags are active.
  Legacy JSON consumers that read `.mode` / `.allow` / `.deny` keep working.
- `permissions show` with no persisted policy + no session flags still
  prints the legacy "No permission policy configured" line (empty-state
  contract preserved for existing tests).

Tests added:
- `test_list_reflects_session_deny_writes` -- project.add flips to denied
  when --deny-writes is active.
- `test_show_reports_session_flags_without_persisted_policy` --
  active=true even without a persisted policy.
- `test_show_reports_session_flags_alongside_persisted` -- both layers
  reported + legacy keys still present.
- `test_show_human_mode_no_policy_no_session` -- empty-state regression.

* docs(cli): clarify --deny-destructive is NARROW; admin ops need --deny-writes

Iter-4 review flagged a doc/flag-help ambiguity: the `--deny-destructive`
help string said "destructive (delete/kill/reset) operations" which a user
could reasonably read as covering 'project remove' and 'org setup'. The
actual behavior (correctly) leaves admin-classified ops allowed, because
'admin' is a separate risk category from 'destructive' in the OPERATION
registry. Only --deny-writes catches the wide net.

Fixes:
- `cli.py`: spell out WIDE vs NARROW in each flag's help string. Explicitly
  call out 'project remove' / 'org setup' as NOT blocked by --deny-destructive.
- `context.py`: mirror the same WIDE vs NARROW contract in the AI-agent
  reference text (global flags section).

Tests added (`TestFirewallFlags`):
- `test_deny_destructive_allows_admin_ops` -- contract lock: running
  `kbagent --deny-destructive project remove prod` succeeds (exit != 6).
- `test_deny_writes_blocks_admin_ops` -- complement: --deny-writes IS
  the wide net and DOES block admin ops (exit 6).

Test hygiene (`TestPermissionsShow`):
- `test_show_reports_session_flags_without_persisted_policy` now also
  asserts the legacy `mode`/`allow`/`deny` top-level keys are ABSENT
  when there is no persisted policy. Documents the new shape contract
  for downstream JSON consumers.

* docs(references): add Session firewall flags section to permissions-workflow.md

Document --deny-writes (WIDE) vs --deny-destructive (NARROW) semantics,
REPL forwarding behavior, and the additive-merge relationship with any
persisted policy (session flags never relax what is persisted, never
land in config.json). Cross-references the project-pin UX.

* docs(gotchas): document KBAGENT_PROJECT env var semantics and precedence

Record the three non-obvious rules: empty string = unset (Unix
convention), pointing to an unregistered alias fails hard with
CONFIG_ERROR (exit 5), and the full precedence chain
--project > KBAGENT_PROJECT > persisted pin > sole-project fallback >
fail.

* chore(hints): clarify project use/current are local-only (no --hint)

Both commands operate purely on the local ConfigStore (no HTTP or
service call to render), so registering HintRegistry entries would be
meaningless. Add a single-line comment at each command so future
contributors don't rediscover this by accident.

* refactor(cli): promote _apply_firewall_flags to public apply_firewall_flags

The function is imported from commands/permissions.py, which made the
underscore prefix misleading (it looked private but wasn't). Drop the
leading underscore at the definition, both call sites (cli.py,
commands/permissions.py), and the test suite so the API boundary is
honest.
New: kbagent flow list/detail/schema/new/update/delete/schedule/schedule-remove
covering both keboola.orchestrator (classic) and keboola.flow (new) component
types. Schedules stored as keboola.scheduler Storage API configs; client-side
Kahn DAG validation rejects cycles and unknown phase refs before API write.

Max's review fixes: --dry-run on delete + schedule-remove, nested task shape
matching keboola-as-code convention, narrow except tuples, gotchas docs.
New: config metadata-list/get/set/delete-metadata -- CRUD for arbitrary
key/value pairs on any configuration via branch-aware Storage API metadata
endpoint (FIIA P1-3).

New: config set-folder -- sugar over set-metadata for KBC.configuration.
folderName; organises configs into named folder groups.

New: workspace list --orphaned and workspace gc -- detect and clean up
sandbox-backed workspaces whose sandbox config no longer exists (FIIA P1-4).

Max's review fixes: CLAUDE.md + commands-reference docs, Rich markup escape
in GC output, logger.exception on gc catch, config-metadata-workflow.md.
New: storage describe-bucket/table/column -- set descriptions via KBC
metadata (provider=user); readable via bucket-detail / table-detail.

New: storage describe-batch --from-file YAML -- apply bucket/table/column
descriptions in one shot; failures collected, remaining items continue.

Fix: bucket-detail / table-detail now return description + metadata fields;
KBC.description metadata wins over native bucket description field.

Max's review fixes: commands-reference docs, metadata-extraction tests
pinning precedence, gotchas for description conventions, progress spinner
for describe-batch human mode, storage-describe-workflow.md.
…cel (#202)

Exit code 7 (JOB_TIMEOUT_TERMINATED) distinguishes local-timeout-killed from
queue-side failure (exit 4). Log tail N events attached to logTail on failure
or dict on WARNING/TERMINATED. Legacy 1s fixed polling preserved behind
--poll-strategy fixed for tests and very short jobs.

Polling curve: 2s x 30 -> 5s x 48 -> 15s forever.

KeboolaClient.wait_for_queue_job gains poll_strategy kwarg.
KeboolaClient.fetch_job_events wraps Storage API /v2/storage/events?runId=.
JobService._handle_wait_error centralises failure enrichment.

Max's review fixes: existing error codes reused (no new literal strings).
P2-1: ErrorCode(StrEnum) in errors.py with 49 typed constants (46 original
+ 3 new: JOB_TIMEOUT_TERMINATED, INVALID_FLOW_DAG, SCHEDULE_DELETE_FAILED).
All error_code='STRING' literals across 30+ source files replaced with
ErrorCode.<MEMBER>. Wire format unchanged (StrEnum subclasses str).

P2-2: sync init --adopt-existing idempotently adopts a .keboola/manifest.json
written by the kbc Go CLI without overwriting it. Validates manifest
project_id against the alias token; rejects mismatch with ConfigError.

scripts/check_error_codes.py CI guard rejects new raw literals.
docs/error-codes.md documents semver policy.
- sync-workflow.md: new "Adopting an existing kbc Go CLI checkout" section for sync init --adopt-existing (#201).
- gotchas.md: new entries for exit code 7 (JOB_TIMEOUT_TERMINATED), --poll-strategy fixed, --log-tail-lines N, logTail response semantics (#202); new section for --deny-writes / --deny-destructive (#203); new section for sync init --adopt-existing (#201).
- workspace-workflow.md: new "Orphan detection + garbage collection" section for workspace list --orphaned + workspace gc (#204).
- commands-reference.md: sync init --adopt-existing flag documented.
- README.md: "What it does" table + "All commands" block updated with the 15 new commands shipped in 0.22.0 (flow CRUD, config metadata, workspace GC, storage describe, project pin + firewall, queue polling parity).
@padak
padak merged commit 2509c0d into main Apr 23, 2026
1 check passed
@padak
padak deleted the release/0.22.0 branch April 23, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flow schedules / triggers are not exposed by CLI or MCP tools

2 participants