release: v0.22.0 -- FIIA parity batch - #215
Merged
Merged
Conversation
* 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).
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
Conflict resolution highlights
Docs completion (my follow-up commit)
Max's PRs covered most of the required CONTRIBUTING.md artifacts, but three were missing:
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
Post-merge
Related follow-ups (not in this release)