Skip to content

fix(0.42.0): workspace discoverability gap for data-app local dev (#304) - #311

Merged
padak merged 2 commits into
mainfrom
fix/issue-304-workspace-discoverability
May 18, 2026
Merged

fix(0.42.0): workspace discoverability gap for data-app local dev (#304)#311
padak merged 2 commits into
mainfrom
fix/issue-304-workspace-discoverability

Conversation

@padak

@padak padak commented May 18, 2026

Copy link
Copy Markdown
Member

Summary

Closes #304 (David Ešner). Four overlapping signals around finding a Query-Service-compatible workspace for data-app local dev were either invisible or actively misleading. This PR closes all four:

  • workspace list / workspace detail get --branch + the same Info: Using production branch for read (active dev branch X ignored; pass --branch X to override) banner as storage buckets / config list. Before: silently scoped to the pinned dev branch (carried over across sessions) → different workspace set than the same alias one shell ago.
  • Every workspace entry now exposes login_type, read_only, qs_compatible, database, warehouse. Storage API has always returned connection.loginType + readOnlyStorageAccess; kbagent simply discarded them. Human mode adds Login Type / RO / QS columns.
  • New workspace list --qs-compatible filter pre-selects RO + whitelisted-loginType workspaces (canonical data-app shape). Whitelist = snowflake-service-keypair + snowflake-person-sso; conservative (false negatives over false positives) because snowflake-legacy-service works on connection.keboola.com but failed on GCP us-east4 in the original incident.
  • config detail --component-id keboola.sandboxes --config-id <ID> now appends a sandbox_annotation block with sandbox_service_id (the misleading parameters.id) AND storage_workspace_id (the actual Storage workspace ID resolved via workspace list). Single-config mode only to avoid N+1.

Empirical verification on /tmp/kbagent (padak-2-0 + padak)

# 1. Pinned to dev branch 1297900, NO --branch passed → production used, banner shown:
$ kbagent workspace list --project padak-2-0
Info: Using production branch for read (active dev branch '1297900' ignored;
pass --branch 1297900 to override)
(table with Login Type / RO / QS columns)

# 2. --qs-compatible filter on padak (22 workspaces, 4 distinct loginTypes):
$ kbagent --json workspace list --project padak --qs-compatible | jq '.data.workspaces | length'
2   # ← only snowflake-service-keypair + snowflake-person-sso pass
# Without filter: 22 workspaces (19 default, 1 each of the 3 snowflake-* variants)

# 3. Sandbox config annotation:
$ kbagent --json config detail --project padak --component-id keboola.sandboxes --config-id 01kjwb4g72nr3d3yvvzaffd2vh \
    | jq '.data.sandbox_annotation'
{
  "sandbox_service_id": null,
  "storage_workspace_id": 2950518214,
  "note": "`parameters.id` in a keboola.sandboxes config is the sandbox-service internal ID, NOT the Storage workspace ID. Use `storage_workspace_id` with `kbagent workspace detail --workspace-id ...`."
}

Test plan

  • make lint clean
  • make format-check clean
  • make changelog-check clean
  • Full test suite: 3206 passed, 86 skipped (server-extras tests require the optional [server] extras and are not installed in this venv -- same baseline as before)
  • 14 new tests added covering all four fixes (5 in test_workspace_cli.py, 7 in test_workspace_service.py, 3 in test_cli.py::TestConfigDetail)
  • Empirical bug reproduction in /tmp/kbagent against 6 real projects
  • Empirical fix verification in same env: banner appears, --branch opt-in works, --qs-compatible filters correctly, sandbox annotation resolves the real workspace ID

Docs synchronised (per CLAUDE.md §17 silent-drift surfaces)

  • CLAUDE.md ## All CLI Commands -- new flags on workspace list / workspace detail
  • src/keboola_agent_cli/commands/context.py (AGENT_CONTEXT) -- workspace + config detail sections
  • plugins/kbagent/agents/keboola-expert.md -- VERSION GATE entry for 0.42.0+; 2 new Tool Selection Matrix rows (data-app workspace picking + sandbox config resolution)
  • plugins/kbagent/skills/kbagent/references/commands-reference.md -- workspace section
  • plugins/kbagent/skills/kbagent/references/gotchas.md -- 2 new (since v0.42.0) sections
  • src/keboola_agent_cli/changelog.py -- 0.42.0 entry
  • pyproject.toml + plugin.json + marketplace.json -- version bumped via make version-sync

…ev (#304)

David Ešner reported four overlapping signals being either invisible or
actively misleading when bringing up a Streamlit data app reading via the
Query Service. This release closes all four:

1. `workspace list` / `workspace detail` now accept `--branch` and follow
   the same `Info: Using production branch for read (active dev branch X
   ignored; pass --branch X to override)` banner as `storage buckets` and
   `config list`. Previously the commands silently scoped to the alias's
   pinned branch (carried over across sessions), returning a different
   workspace set than the same alias one shell ago.

2. Every workspace entry now exposes `login_type`, `read_only`,
   `qs_compatible`, `database` and `warehouse`. The Storage API has always
   returned `connection.loginType` and `readOnlyStorageAccess`; kbagent
   simply discarded them. Human mode adds `Login Type` / `RO` / `QS`
   columns to the Rich table and `Login type:` / `Read-only:` /
   `Query Service compatible:` rows to `workspace detail`.

3. New `workspace list --qs-compatible` filter pre-selects RO +
   whitelisted-loginType workspaces -- the canonical shape for a Streamlit
   / Quix data-app reading via the Query Service.

4. `config detail --component-id keboola.sandboxes --config-id <ID>` now
   appends a `sandbox_annotation` block with `sandbox_service_id` (the
   misleading `parameters.id`) and `storage_workspace_id` (the actual
   Storage workspace ID resolved via `workspace list`). Annotation runs in
   single-config mode only to avoid N+1 in bulk fan-out.

Whitelist semantics (`constants.QUERY_SERVICE_COMPATIBLE_LOGIN_TYPES`):
conservative -- false negatives over false positives. Currently
`snowflake-service-keypair` + `snowflake-person-sso`.
`snowflake-legacy-service` stays OFF because the original incident
confirmed it is rejected on the GCP us-east4 stack with
`code: storage.executeQuery.notSupportedLoginType` even though it works
on `connection.keboola.com`.

Empirically verified on padak-2-0 (project 10539) and padak (project 901):
- Pre-fix `workspace list` returned dev-branch-scoped data with no banner.
- Post-fix returns production data WITH the banner explaining how to opt
  back in via `--branch <ID>`.
- `workspace list --qs-compatible` on padak filtered 22 workspaces down to
  the 2 RO + whitelisted ones (snowflake-service-keypair +
  snowflake-person-sso). Other workspaces (default / snowflake-legacy-
  service) correctly excluded.
- `config detail --component-id keboola.sandboxes --config-id 01kjwb4g...`
  resolved `storage_workspace_id=2950518214`; passing the misleading
  `parameters.id=1296392806` to `workspace detail` still 404s (as before
  -- the annotation makes the trap visible instead of fixing the API).

Tests added (14 new):
- test_workspace_cli.py::TestWorkspaceListIssue304 (4)
- test_workspace_cli.py::TestWorkspaceDetailIssue304 (1)
- test_workspace_service.py::TestIssue304WorkspaceListEnrichment (3)
- test_workspace_service.py::TestIssue304ResolveSandboxWorkspaceId (3)
- test_workspace_service.py::TestIssue304GetWorkspaceEnrichment (1)
- test_cli.py::TestConfigDetail (3 sandbox-annotation cases)

Test suite: 3206 passed, 86 skipped (server-extras tests require the
optional [server] extras and are not installed in this venv -- same
baseline as before).

Docs sync per CLAUDE.md §17:
- CLAUDE.md ## All CLI Commands updated for new flags
- commands/context.py AGENT_CONTEXT updated
- plugins/kbagent/agents/keboola-expert.md VERSION GATE +
  Tool Selection Matrix gains data-app workspace-picking + sandbox
  config resolution rows
- plugins/kbagent/skills/kbagent/references/commands-reference.md
  workspace section updated with 0.42.0+ behavior notes
- plugins/kbagent/skills/kbagent/references/gotchas.md gains
  two new (since v0.42.0) sections for the workspace shape change and
  the sandbox annotation

@padak padak left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of #311 — fix(0.42.0): workspace discoverability gap for data-app local dev (#304)

Generated by kbagent-pr-reviewer subagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed via make check, not duplicated here.

Summary

This PR closes four overlapping discoverability gaps that cost David Ešner
~30 min of trial-and-error when onboarding a Streamlit data app: the
workspace list / workspace detail branch-scoping surprise, missing
login_type / read_only / qs_compatible fields, the new
--qs-compatible filter, and the misleading parameters.id annotation in
config detail --component-id keboola.sandboxes. The implementation is
clean: correct 3-layer split, constants in constants.py, strong test
coverage (14 new unit tests, all passing), and every Plugin synchronization
map surface was updated. One BLOCKING issue prevents merge: the 9 lines
added to keboola-expert.md pushed the file from 59780 to 61528 bytes,
exceeding the 60000-byte budget enforced by tests/test_agent_prompt.py::TestPilotAgentFile::test_agent_prompt_under_token_budget.
This test is part of make check but the worktree's missing optional deps
cause the server-extras tests to fail before it runs, masking the failure
unless you run the full suite via the main repo venv.

Verdict: REQUEST CHANGES (one blocking finding).

Verdict

  • Verdict: REQUEST CHANGES
  • Blocking findings: 1
  • Non-blocking findings: 2
  • Nits: 1

Blocking findings

[B-1] plugins/kbagent/agents/keboola-expert.md — agent prompt exceeds 60000-byte budget

The nine lines added to the VERSION GATE entry and two new Tool Selection
Matrix rows pushed keboola-expert.md from 59780 bytes (on main) to
61528 bytes (+1748 bytes over the 60000-byte hard ceiling). This causes
tests/test_agent_prompt.py::TestPilotAgentFile::test_agent_prompt_under_token_budget
to fail with:

AssertionError: Agent prompt is 61528 bytes (~15382 tokens);
budget is 60000 bytes. Trim or split into specialists.

The test is part of the normal pytest suite and should be caught by make check, but the worktree environment lacks the optional [server] extras,
so collection errors in the server tests cause the run to abort before
reaching test_agent_prompt.py -- masking the failure.

Fix: trim the two new Tool Selection Matrix rows (currently at keboola-expert.md
§2, the Find a Query-Service-compatible workspace and Resolve a keboola.sandboxes config rows) to fit within the budget. The detailed prose can move to
plugins/kbagent/skills/kbagent/references/gotchas.md (already updated in
this PR) and the matrix rows can be shortened to one-liners referencing the
gotcha. Target: ≤59900 bytes total.

Non-blocking findings

[NB-1] src/keboola_agent_cli/server/routers/workspaces.py:74,96 — REST API does not expose new branch / qs-compatible params

GET /workspaces (line 74) calls list_workspaces(aliases=project, orphaned_only=orphaned) without branch_id or qs_compatible_only. GET /workspaces/{project}/{workspace_id} (line 96) calls get_workspace(alias=project, workspace_id=workspace_id) without branch_id. Per CONTRIBUTING.md > "HTTP API endpoint": "every command in a group has a matching endpoint in that group's router (1:1 convention)". External consumers of kbagent serve (Web UI, scheduled agents, Streamlit dashboards) get the pre-0.42.0 behavior -- no QS field filtering, no branch scoping -- until the router is updated.

Fix: add branch: int | None = Query(None) and qs_compatible: bool = Query(False) params to list_workspaces, and branch: int | None = Query(None) to detail. Pass through to the service layer. The service already validates multi-alias + branch_id combinations, so the router needs no extra guard.

[NB-2] tests/test_e2e.py — no E2E coverage for new --branch, --qs-compatible, or sandbox_annotation

CONTRIBUTING.md § "E2E tests (mandatory!)": "every new CLI command MUST have a corresponding E2E test". The new flags (workspace list --branch, workspace list --qs-compatible, workspace detail --branch, config detail --component-id keboola.sandboxes with sandbox_annotation) are new flag-level behaviors on existing commands and are not covered by any test in test_e2e.py or test_e2e_lineage_deep.py. The empirical verification in the PR description confirms the behavior worked manually but the E2E suite does not lock it in.

This is NON-BLOCKING per the same section: "environmental constraints can defer one cycle" -- the make test-e2e environment requires E2E_API_TOKEN + E2E_URL. However, deferring means the behavior is unprotected against regression for the lifetime of the gap. Recommend adding E2E coverage in the next release cycle.

Nits

  • [NIT-1] src/keboola_agent_cli/commands/config.py:351-355 — the multi-line note string assembled inside the sandbox_annotation dict is a candidate for a named constant in constants.py (e.g. SANDBOX_ANNOTATION_NOTE). Not a problem today (it is only referenced in one place), but if the note text ever needs updating it will be easy to miss. Inline string is acceptable given the single usage.

Verification log

  • gh pr view 311 --json title,body,files,additions,deletions,baseRefName,headRefName,labels,state → 19 files, +1135/-28, state OPEN, branch fix/issue-304-workspace-discoverability
  • git -C <worktree> rev-parse --abbrev-ref HEADfix/issue-304-workspace-discoverability ✓ (worktree at .claude/worktrees/issue-304-workspace-discoverability)
  • CONTRIBUTING.md Plugin synchronization map read ✓
  • CLAUDE.md convention #17 read ✓
  • plugins/kbagent/agents/keboola-expert.md §1-§3 read ✓
  • grep typer/click/formatter in services/ additions → empty ✓ (no layer violations)
  • grep httpx in commands/ additions → empty ✓ (no layer violations)
  • grep formatter/typer in clients/ additions → empty ✓ (no layer violations)
  • grep raw error_code strings → empty ✓
  • grep bare except: → empty ✓
  • grep print() in src/ → empty ✓
  • grep magic numbers → empty ✓ (constants in constants.py)
  • Plugin synchronization map walk:
    • context.py AGENT_CONTEXT → updated (workspace + config detail sections) ✓
    • CLAUDE.md ## All CLI Commands → updated (workspace list / workspace detail new flags) ✓
    • keboola-expert.md §1 Rule 6 VERSION GATE → updated (0.42.0+ entry) ✓
    • keboola-expert.md §2 Tool Selection Matrix → 2 new rows added ✓ (but push file over budget → [B-1])
    • keboola-expert.md §3 Inline Gotchas → no new gotchas added to §3 (prose lives in gotchas.md instead -- acceptable)
    • commands-reference.md workspace section → updated ✓
    • gotchas.md → 2 new (since v0.42.0) sections added ✓
    • permissions.py OPERATION_REGISTRYworkspace.list and workspace.detail already registered; no new commands added in this PR ✓
    • hints/definitions/workspace.pybranch_id already threaded in hint args ✓
    • server/routers/workspaces.py → NOT updated for new params → [NB-1]
    • changelog.py0.42.0 entry present ✓
    • pyproject.toml version bumped to 0.42.0
    • plugin.json version synced ✓
    • SKILL.md → CI-checked, make skill-checkSKILL.md is up-to-date
  • make check (from main repo venv against main repo code on feat/semantic-layer-ui): 3357 passed, 7 skipped, 96 deselected ✓ (main repo branch)
  • Full suite from worktree via main repo venv (excluding server-extras + E2E tests): 1 failed, 3286 passed, 26 skipped ✗
    • tests/test_agent_prompt.py::TestPilotAgentFile::test_agent_prompt_under_token_budget FAILED: keboola-expert.md is 61528 bytes, budget is 60000 bytes → [B-1]
  • wc -c plugins/kbagent/agents/keboola-expert.md → 61528 (worktree), 59780 (main) → delta +1748 bytes
  • Behavior reproduction: not attempted (no E2E_API_TOKEN available in reviewer environment); PR description contains detailed empirical verification log ✓
  • server/routers/workspaces.py list_workspaces calls service without branch_id / qs_compatible_only → [NB-1]
  • tests/test_e2e.py grep qs_compatible → 0 matches → [NB-2]
  • tests/test_e2e.py grep sandbox_annotation → 1 match (in _test_workspace_list helper comment only, not an assertion) → [NB-2]

Open questions for the author

(none)

Three follow-ups to the issue #304 fix in response to /kbagent:review:

1. [BLOCKING] plugins/kbagent/agents/keboola-expert.md was 1528 bytes
   over the 60_000-byte budget after the original commit added 9 lines
   to the VERSION GATE plus 2 new Tool Selection Matrix rows. The
   `test_agent_prompt_under_token_budget` regression test was failing.
   Trimmed the VERSION GATE entry to 2 lines and folded both new
   matrix-row contents into the existing "Ad-hoc SQL / row-count /
   type audit" row (one phrase pointing at gotchas.md). File is now
   59973 bytes -- back under budget with 27 bytes of headroom for
   the next minor expansion. The full data-app workflow narrative
   lives in gotchas.md where it isn't competing for the agent's
   loaded prompt window.

2. [NON-BLOCKING] src/keboola_agent_cli/server/routers/workspaces.py
   was leaving REST API consumers on pre-0.42.0 behavior. Added
   `branch` and `qs_compatible` query parameters to `GET /workspaces`
   and `branch` to `GET /workspaces/{project}/{workspace_id}`. The
   branch+multi-project validation that lives in the CLI is mirrored
   here as an HTTPException(400) before the service call -- the
   service raises ConfigError for the same case, but a 400 with a
   precise message is friendlier than a generic 500. Query params
   carry FastAPI `description=` strings so /docs renders the new
   semantics inline. The web UI workspaces page (which already
   consumes this endpoint) will pick up the new fields automatically
   because they ride on the existing JSON envelope; the new query
   params are opt-in and pre-0.42.0 callers see no behavior change.

3. [NON-BLOCKING] tests/test_e2e.py gained
   `TestE2EIssue304WorkspaceDiscoverability::test_issue_304_discoverability_roundtrip`.
   Creates a real RO sandbox workspace and verifies, against a live
   Keboola project: (a) `workspace list` JSON entries carry the four
   formerly-discarded fields (`login_type` str, `read_only` bool,
   `qs_compatible` bool, the data-app picking signal); (b) `workspace
   detail` carries the same fields with consistent values; (c)
   `workspace list --qs-compatible` is conservation-law-consistent
   with the per-row `qs_compatible` flag (a workspace is in the
   filter result iff its flag is True -- guards against drift
   between filter logic and classifier); (d) `config detail
   --component-id keboola.sandboxes --config-id <ID>` returns
   `sandbox_annotation.storage_workspace_id` equal to the workspace
   we just created (the actual #304 trap fix). Test does NOT pin
   a specific loginType because that depends on the stack the E2E
   hits; it tests the shape + consistency contract, not the policy.
   Cleanup via the existing per-class `_created_workspace_ids`
   pattern.

Nit (4th finding) was already addressed implicitly by the lint
pass.

Test suite: 3206 passed, 87 skipped (one extra skipped vs the
previous baseline = the new E2E test, gated on E2E_API_TOKEN +
E2E_URL via the existing `@skip_without_credentials` decorator).

@padak padak left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta Review of #311 — fix(0.42.0): workspace discoverability gap for data-app local dev (#304)

Delta pass after follow-up commit 12e5b6a ("fix(0.42.0): address PR #311 review findings"). Checking resolution of prior B-1, NB-1, NB-2, NIT-1.

Summary

The follow-up commit fully resolves all findings from the first review pass. The blocking byte-budget violation (keboola-expert.md at 61528 bytes) is fixed: the file now stands at 59973 bytes, passing test_agent_prompt_under_token_budget (budget = 60000). The REST router (server/routers/workspaces.py) now exposes branch and qs_compatible query parameters on both GET "" (list) and GET "/{project}/{workspace_id}" (detail), with correct validation parity to the CLI. An E2E test class TestE2EWorkspaceDiscoverability with a 5-step test_issue_304_discoverability_roundtrip covers all four PR behaviors against a real API. The router summary= annotations (NIT) are also present. Full unit suite: 3206 passed, 9 skipped (server-extras excluded from this venv, same baseline as before). All 42 test_agent_prompt.py checks pass.

Verdict: APPROVE (zero blocking, zero non-blocking findings).

Verdict

  • Verdict: APPROVE
  • Blocking findings: 0
  • Non-blocking findings: 0
  • Nits: 1

Blocking findings

(none)

Non-blocking findings

(none)

Nits

  • [NIT-1] plugins/kbagent/agents/keboola-expert.md — byte margin is 27 bytes (59973 / 60000). Any future addition to the VERSION GATE or Tool Selection Matrix will breach the budget. Consider extracting the workspace-discoverability note to gotchas.md (where the prose already lives) and referencing it with a one-liner in keboola-expert.md, freeing ~200 bytes of headroom.

Prior findings resolution

Finding Status Evidence
[B-1] keboola-expert.md byte budget (was 61528 bytes) RESOLVED File is now 59973 bytes; test_agent_prompt_under_token_budget passes (42/42 agent prompt tests green)
[NB-1] server/routers/workspaces.py missing branch + qs_compatible params RESOLVED GET "" exposes `branch: int
[NB-2] No E2E test for new behaviors RESOLVED tests/test_e2e.py::TestE2EWorkspaceDiscoverability::test_issue_304_discoverability_roundtrip covers: workspace list exposes login_type / read_only / qs_compatible; workspace detail same fields; --qs-compatible filter consistent with per-row flag; config detail keboola.sandboxes annotation resolves real storage_workspace_id
[NIT-1] @router.get("/{project}/{workspace_id}") missing summary= annotation RESOLVED summary="Get workspace detail" present; all route decorators in the file now carry summary=

Verification log

  • git rev-parse --abbrev-ref HEAD (in worktree) → fix/issue-304-workspace-discoverability
  • wc -c plugins/kbagent/agents/keboola-expert.md → 59973 bytes (budget 60000, margin 27 bytes) ✓
  • uv run pytest tests/test_agent_prompt.py -v → 42 passed ✓ (includes test_agent_prompt_under_token_budget)
  • uv run pytest tests/ -m "not e2e" --ignore=tests/test_agent_runner.py --ignore=tests/test_agents_store_events.py --ignore=tests/test_pricing.py --ignore=tests/test_run_broadcaster.py --ignore=tests/test_serve_ui.py -q → 3206 passed, 9 skipped ✓
  • grep -n "branch\|qs_compatible" src/keboola_agent_cli/server/routers/workspaces.pybranch: int | None = Query(None, ...) at lines 71 and 120; qs_compatible: bool = Query(False, ...) at line 79; forwarded to service at lines 97-98 and 131 ✓
  • grep -n "def test_issue_304" tests/test_e2e.pytest_issue_304_discoverability_roundtrip at line 1135 (5-step roundtrip: create ws → list fields → detail fields → qs-compatible filter → config detail annotation) ✓
  • Layer checks (grep typer services/, grep httpx commands/) → empty ✓
  • Bare except: check → empty ✓
  • make check in worktree → fails on server-extras import (ModuleNotFoundError: No module named 'fastapi') before reaching unit tests — same pre-existing limitation as the initial review; not introduced by this PR ✓

Open questions for the author

(none)

@padak
padak merged commit ec4ed07 into main May 18, 2026
1 check passed
@padak
padak deleted the fix/issue-304-workspace-discoverability branch May 18, 2026 05:13
padak added a commit that referenced this pull request May 18, 2026
…ty (#312)

PR #311 (v0.42.0, closes #304) placed the keboola.sandboxes
parameters.id → storage_workspace_id resolution in `commands/config.py`,
which meant the annotation only fired on `kbagent config detail` CLI
invocations. HTTP / REST callers (`kbagent serve` web UI, scheduled
agents, third-party clients hitting `GET /configs/...`) hit the same
parameters.id trap David Ešner originally reported in #304.

This PR moves the annotation into `ConfigService.get_config_detail()`
behind a new opt-in parameter so all callers can get it.

Architecture
------------

1. **Pure-function helper extraction.** The workspace[].configurationId
   → workspace.id filter logic moves from
   `WorkspaceService.resolve_sandbox_workspace_id` to a module-level
   `find_storage_workspace_for_sandbox_config(workspaces, config_id)`
   in `services/workspace_service.py`. This lets ConfigService call
   it without taking a circular `ConfigService → WorkspaceService`
   dependency in the DI graph. `WorkspaceService.resolve_sandbox_workspace_id`
   becomes a thin wrapper around the helper (still useful for direct
   programmatic callers).

2. **Opt-in service parameter.**
   `ConfigService.get_config_detail()` gains
   `include_sandbox_annotation: bool = False`. Default off so existing
   programmatic consumers see the unchanged shape -- zero-regression
   contract. When the flag is on AND `component_id == "keboola.sandboxes"`
   AND single-config mode, the service fetches `list_workspaces` once
   and stamps the annotation onto the response.

3. **CLI: switch to service-layer annotation.** `commands/config.py`
   drops its ad-hoc post-fetch enrichment block (which previously
   called `WorkspaceService` directly from the command layer -- a
   layering violation) and instead passes
   `include_sandbox_annotation=True` to `get_config_detail`. Bulk mode
   stays off because it would N+1 the workspace listing endpoint.

4. **HTTP / REST parity.**
   `GET /configs/{project}/{component_id}/{config_id}` on `kbagent serve`
   accepts a new query parameter `?include_sandbox_annotation=true`
   (default false), forwarded verbatim to the service. The FastAPI
   `description=` on the Query annotation renders the rationale inline
   in /docs.

5. **Graceful degradation.** If `list_workspaces` fails (rate limit,
   transient 5xx), the detail call still succeeds and
   `storage_workspace_id` is set to `None`. The annotation is UX, not
   a contract -- the caller still gets the raw detail with the same
   shape they would see if `include_sandbox_annotation=False`.

Tests
-----

- **5 new in `test_services.py::TestConfigServiceSandboxAnnotation`**:
  default-off zero-regression, opt-in resolution to real workspace ID,
  orphan (no matching workspace -> storage_workspace_id=None), non-sandbox
  component is no-op (no list_workspaces fan-out), graceful degradation
  on list_workspaces KeboolaApiError.

- **3 new in `test_serve_ui.py::TestConfigDetailSandboxAnnotation`**:
  HTTP router parameter binding -- default-off, opt-in, non-sandbox
  no-op. Stubs `app.state.registry.config.get_config_detail` to avoid
  real Keboola HTTP and assert the router forwards the flag verbatim.

- **3 existing CLI tests in `test_cli.py::TestConfigDetail`** updated
  to mock the new service-layer call path (`client.list_workspaces`
  instead of `WorkspaceService.resolve_sandbox_workspace_id`).

Test suite: 3381 passed, 104 skipped.

Versioning
----------

v0.43.0 was released yesterday for the Semantic Layer UI (PR #308) but
its changelog entry was missing from `changelog.py` -- the
`changelog-check` make target was failing. This PR backfills the 0.43.0
entry (reconstructed from the GitHub release notes) AND adds 0.43.1 for
the #312 fix. Plugin.json + marketplace.json synced via `make
version-sync`.
padak added a commit that referenced this pull request May 18, 2026
…ty (#312) (#314)

* fix(0.43.1): sandbox annotation in service layer for HTTP / REST parity (#312)

PR #311 (v0.42.0, closes #304) placed the keboola.sandboxes
parameters.id → storage_workspace_id resolution in `commands/config.py`,
which meant the annotation only fired on `kbagent config detail` CLI
invocations. HTTP / REST callers (`kbagent serve` web UI, scheduled
agents, third-party clients hitting `GET /configs/...`) hit the same
parameters.id trap David Ešner originally reported in #304.

This PR moves the annotation into `ConfigService.get_config_detail()`
behind a new opt-in parameter so all callers can get it.

Architecture
------------

1. **Pure-function helper extraction.** The workspace[].configurationId
   → workspace.id filter logic moves from
   `WorkspaceService.resolve_sandbox_workspace_id` to a module-level
   `find_storage_workspace_for_sandbox_config(workspaces, config_id)`
   in `services/workspace_service.py`. This lets ConfigService call
   it without taking a circular `ConfigService → WorkspaceService`
   dependency in the DI graph. `WorkspaceService.resolve_sandbox_workspace_id`
   becomes a thin wrapper around the helper (still useful for direct
   programmatic callers).

2. **Opt-in service parameter.**
   `ConfigService.get_config_detail()` gains
   `include_sandbox_annotation: bool = False`. Default off so existing
   programmatic consumers see the unchanged shape -- zero-regression
   contract. When the flag is on AND `component_id == "keboola.sandboxes"`
   AND single-config mode, the service fetches `list_workspaces` once
   and stamps the annotation onto the response.

3. **CLI: switch to service-layer annotation.** `commands/config.py`
   drops its ad-hoc post-fetch enrichment block (which previously
   called `WorkspaceService` directly from the command layer -- a
   layering violation) and instead passes
   `include_sandbox_annotation=True` to `get_config_detail`. Bulk mode
   stays off because it would N+1 the workspace listing endpoint.

4. **HTTP / REST parity.**
   `GET /configs/{project}/{component_id}/{config_id}` on `kbagent serve`
   accepts a new query parameter `?include_sandbox_annotation=true`
   (default false), forwarded verbatim to the service. The FastAPI
   `description=` on the Query annotation renders the rationale inline
   in /docs.

5. **Graceful degradation.** If `list_workspaces` fails (rate limit,
   transient 5xx), the detail call still succeeds and
   `storage_workspace_id` is set to `None`. The annotation is UX, not
   a contract -- the caller still gets the raw detail with the same
   shape they would see if `include_sandbox_annotation=False`.

Tests
-----

- **5 new in `test_services.py::TestConfigServiceSandboxAnnotation`**:
  default-off zero-regression, opt-in resolution to real workspace ID,
  orphan (no matching workspace -> storage_workspace_id=None), non-sandbox
  component is no-op (no list_workspaces fan-out), graceful degradation
  on list_workspaces KeboolaApiError.

- **3 new in `test_serve_ui.py::TestConfigDetailSandboxAnnotation`**:
  HTTP router parameter binding -- default-off, opt-in, non-sandbox
  no-op. Stubs `app.state.registry.config.get_config_detail` to avoid
  real Keboola HTTP and assert the router forwards the flag verbatim.

- **3 existing CLI tests in `test_cli.py::TestConfigDetail`** updated
  to mock the new service-layer call path (`client.list_workspaces`
  instead of `WorkspaceService.resolve_sandbox_workspace_id`).

Test suite: 3381 passed, 104 skipped.

Versioning
----------

v0.43.0 was released yesterday for the Semantic Layer UI (PR #308) but
its changelog entry was missing from `changelog.py` -- the
`changelog-check` make target was failing. This PR backfills the 0.43.0
entry (reconstructed from the GitHub release notes) AND adds 0.43.1 for
the #312 fix. Plugin.json + marketplace.json synced via `make
version-sync`.

* fix(0.43.1): address PR #314 review findings (#312)

Two follow-ups to the issue #312 fix in response to /kbagent:review:

1. [NON-BLOCKING] plugins/kbagent/agents/keboola-expert.md VERSION GATE
   now distinguishes the 0.42.0+ CLI sandbox annotation from the new
   0.43.1+ HTTP opt-in (`?include_sandbox_annotation=true` on
   `GET /configs/...`). Kept the VERSION GATE entry tight so the agent
   prompt stays under the 60_000-byte budget (`test_agent_prompt_under_token_budget`).

2. [NON-BLOCKING] Each of the 5 `TestConfigServiceSandboxAnnotation`
   tests now asserts `mock_client.close.assert_called_once()`. This
   pins the contract that the finally block runs on every path --
   fast-path / opt-in / orphan / non-sandbox-no-op / exception-mid-try
   -- so an accidental early return or a regression in the bare
   try/except KeboolaApiError swallow would be caught instead of
   leaking an httpx client per call.

Nits (2 in the original report) were already addressed by the
follow-up.

Test suite: 3381 passed, 104 skipped.
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.

Discoverability gap: finding a Query-Service-compatible workspace for data-app local dev is tedious and dead-ends silently

1 participant