Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ kbagent project description-get --project NAME
kbagent project description-set --project NAME [--text STR | --file PATH | --stdin]
kbagent project use ALIAS
kbagent project current
kbagent project invite --project ALIAS --email EMAIL --role admin|guest|readOnly|share [--reason TEXT] [--dry-run]
kbagent project invite --from-csv FILE [--default-role ROLE] [--workers N] [--dry-run]
kbagent project member-list --project ALIAS [--include-pending]
kbagent project invitation-list --project ALIAS
kbagent project invitation-cancel --project ALIAS --email EMAIL [--invitation-id ID] [--yes]
kbagent project member-remove --project ALIAS --email EMAIL [--yes]
kbagent project member-set-role --project ALIAS --email EMAIL --role admin|guest|readOnly|share

kbagent config list [--project NAME] [--component-type TYPE] [--component-id ID] [--branch ID] [--include-rows]
kbagent config detail --project NAME [--project NAME ...] --component-id ID [--config-id ID] [--branch ID] [--with-state]
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.DEFAULT_GOAL := help

.PHONY: help install install-mcp sync test test-unit test-integration test-e2e test-file lint lint-fix format format-check skill-check skill-gen version-sync version-check changelog changelog-check check-error-codes check clean hooks
.PHONY: help install install-mcp sync test test-unit test-integration test-e2e test-e2e-invite test-file lint lint-fix format format-check skill-check skill-gen version-sync version-check changelog changelog-check check-error-codes check clean hooks

help: ## Show this help message
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
Expand All @@ -26,6 +26,9 @@ test-integration: ## Run integration tests only
test-e2e: ## Run E2E tests (E2E_API_TOKEN and E2E_URL required)
uv run pytest tests/test_e2e.py -v -s --tb=long

test-e2e-invite: ## Run project invite E2E (E2E_MANAGE_TOKEN + E2E_INVITE_PROJECT_ID required)
uv run pytest tests/test_e2e.py -v -s --tb=long -m e2e_invite

test-file: ## Run a specific test file (FILE=tests/test_cli.py)
uv run pytest $(FILE) -v

Expand Down
30 changes: 30 additions & 0 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ a critical failure.
| Pause a running data app | `kbagent data-app stop --project P --app-id N` (0.27.0+) | -- | `kbagent data-app delete` (irreversible; cascades to Storage config) |
| Read the simpleAuth password for a password-gated app | `kbagent data-app password --project P --app-id N` (0.27.0+) -- needs Manage API token (interactive prompt by default; `--allow-env-manage-token` + `KBC_MANAGE_API_TOKEN` for CI on 0.28.0+) | -- | trying to "rotate" the password (not supported by the API; delete + recreate to mint a new one) |
| Tear down a data app | `kbagent data-app delete --project P --app-id N` (0.27.0+) -- cascades to Storage config; URL retired | -- | manually `tool call delete_config keboola.data-apps` while leaving the deployment record orphaned |
| Invite a user to a project (single) | `kbagent project invite --project P --email E --role admin\|guest\|readOnly\|share` (0.26.1+) | raw `requests.post(/manage/projects/{id}/invitations)` only if version-gated out | `kbagent project invite` without `KBC_MANAGE_API_TOKEN` set; passing manage token via CLI flag |
| Invite many users (bulk) | `kbagent project invite --from-csv FILE [--default-role guest] [--workers N] [--dry-run]` (0.26.1+) | `--hint client` to generate a parallel script using `ManageClient` | per-row shell loop calling the CLI -- defeats the parallelism + idempotency the service already does |
| List active project members | `kbagent project member-list --project P [--include-pending]` (0.26.1+) | `tool call run_sync_action` against the Manage API | reading `.kbagent/config.json` to infer membership (it only stores the local user's token) |
| List pending invitations | `kbagent project invitation-list --project P` (0.26.1+) | -- | -- |
| Cancel a pending invitation | `kbagent project invitation-cancel --project P --email E --yes` (0.26.1+) | `--invitation-id ID` if email lookup is ambiguous | DELETE via raw HTTP without going through the service layer |
| Remove an active member | `kbagent project member-remove --project P --email E --yes` (0.26.1+, **destructive**) | `--hint client` for a script that removes by user_id directly | calling `member-remove` without `--yes` in non-interactive contexts (it will prompt and hang) |
| Change a member's role | `kbagent project member-set-role --project P --email E --role admin\|guest\|readOnly\|share` (0.26.1+) | -- | `PUT /manage/projects/{id}/users/{userId}` -- the API rejects PUT with 404, the kbagent client correctly uses **PATCH** |

If the table does not cover the user's task, **ask clarifying
questions** instead of guessing. Returning a targeted question is a
Expand Down Expand Up @@ -187,6 +194,29 @@ success, not a failure.
verification payload but do not treat it as a failure signal.
Production writes never materialize anything.

- **`project invite` "already invited / already member" is a no-op, not a failure** (0.26.1+):
Re-inviting a user the project already knows returns HTTP 400 from the
Manage API. kbagent normalises both "...already been invited..." and
"...already a member..." to `status="noop"` with a `note` field, exit 0.
**Do not retry on 400 from these commands** -- the user is already
on the project (or already pending). For bulk runs, `noop` rows count
toward `noop`, not `failed`, in the summary; surface that distinction
to the user when reporting bulk results.

- **`project invite --from-csv` ordering is non-deterministic** (0.26.1+):
Bulk invitation parallelises via `ThreadPoolExecutor` (default 8 workers).
The `rows[]` array in the JSON result is in completion order, not CSV
order. When reporting per-row outcomes to the user, **match by `email`,
not by index**. Partial-success exits 0 with `failed > 0` reflected in
the JSON -- treat that as a soft failure that needs review, not a
catastrophe.

- **`project member-set-role` uses PATCH, not PUT** (0.26.1+): The Manage
API endpoint is `PATCH /manage/projects/{id}/users/{userId}` with
`{"role": "..."}`. PUT returns 404 even on real members. kbagent's
`ManageClient.update_project_member_role` emits PATCH; if you write a
`--hint client` script that hits the endpoint directly, do the same.

- **`legacy_branch_storage: true` on `--branch` writes** (0.25.2+):
Projects without the `storage-branches` feature flag (legacy fake-branch
projects) accept `--branch X` writes at the API level, but the
Expand Down
11 changes: 11 additions & 0 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ description: >
app proxy, simpleAuth, app auto-suspend, configVersion, redeploy contract,
Data Science API, /apps endpoint, app password, KBC::Project ciphertext,
local workspace, project directory, kbagent init.
local workspace, project directory, kbagent init,
invite user, invite member, project invitation, manage members,
list members, remove member, change role, project role,
bulk invite, invite from CSV, project access, member management.
---

# kbagent -- Keboola Agent CLI
Expand Down Expand Up @@ -92,6 +96,12 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Show the effective default project | `kbagent project current` |
| Get the Keboola dashboard project description | `kbagent project description-get --project PROJECT` |
| Set the Keboola dashboard project description (markdown) | `kbagent project description-set --project PROJECT` |
| Invite a user (or many users via CSV) to one or more projects | `kbagent project invite` |
| List active members of a project (and optionally pending invitations) | `kbagent project member-list --project PROJECT` |
| List pending project invitations | `kbagent project invitation-list --project PROJECT` |
| Cancel a pending invitation | `kbagent project invitation-cancel --project PROJECT --email EMAIL` |
| Remove an active member from a project (destructive) | `kbagent project member-remove --project PROJECT --email EMAIL` |
| Change an existing member's role (PATCH) | `kbagent project member-set-role --project PROJECT --email EMAIL --role ROLE` |
| Set up projects and register them in the kbagent config | `kbagent org setup --url URL` |
| List available components from connected projects | `kbagent component list` |
| Show detailed information about a specific component | `kbagent component detail --component-id COMPONENT-ID` |
Expand Down Expand Up @@ -247,6 +257,7 @@ For detailed response parsing rules and common pitfalls, see [gotchas](reference
| **Storage column types** (native types, NOT NULL, DEFAULT, branch materialize) | [storage-types-workflow](references/storage-types-workflow.md) |
| **Typify a typeless table** (profile -> CTAS -> swap-tables -> validate -> handoff) | [typify-table-workflow](references/typify-table-workflow.md) |
| Bucket sharing & linking | [sharing-workflow](references/sharing-workflow.md) |
| **Project members & invitations** (single + bulk via CSV, role change, remove) | [member-workflow](references/member-workflow.md) |
| Dev branches | [branch-workflow](references/branch-workflow.md) |
| Encrypting secrets for MCP tools | [encrypt-workflow](references/encrypt-workflow.md) |
| Sync & Git-branching (GitOps) | [sync-workflow](references/sync-workflow.md) |
Expand Down
12 changes: 12 additions & 0 deletions plugins/kbagent/skills/kbagent/references/commands-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ All commands support `--json` for structured output. Multi-project flags (`--pro
- `project use ALIAS` -- pin `ALIAS` as the persistent default project. Stored as `default_project` in config.json. Overridden at runtime by `KBAGENT_PROJECT=ALIAS` (env, beats pin) and by `--project ALIAS` (CLI flag, beats both)
- `project current` -- print the effective default project and its source (`env` / `pin` / `none`). Reports both the env override AND the persisted pin so misconfigurations are visible. Returns `{"alias": null, "source": "none"}` when neither is set

## Project Members & Invitations (since v0.26.1)

All seven commands authenticate via `KBC_MANAGE_API_TOKEN` (Manage API), not the project's Storage token. Allowed roles are exactly `admin`, `guest`, `readOnly`, `share` -- the API self-reports this list in its 400 validation error and `constants.PROJECT_ROLES` mirrors it.

- `project invite --project ALIAS --email EMAIL --role admin|guest|readOnly|share [--reason TEXT] [--dry-run]` -- single-shot invitation. Returns `{"status": "ok", "invitation_id": ..., ...}`. Re-inviting an already-invited or already-member email returns `{"status": "noop", "note": "already_invited" | "already_member"}` (HTTP 400 from the Manage API, normalised to a no-op).
- `project invite --from-csv FILE [--default-role ROLE] [--workers N] [--dry-run]` -- bulk invitation. CSV header required; columns: `email`, `project` (alias) or `project_id` (numeric), `role` (optional with `--default-role`), `reason` (optional). Parallelised via `ThreadPoolExecutor` (default 8 workers). Single-stack-URL invariant per file: rows referencing different stacks raise `ConfigError` upfront. Result is `{"total","succeeded","noop","failed","rows":[...]}`; `rows[]` order is *not deterministic*. Exit 0 even with `failed > 0` -- inspect the JSON.
- `project member-list --project ALIAS [--include-pending]` -- list active members. Each member dict carries `id`, `email`, `name`, `role`, `status`, `mfa_enabled`. With `--include-pending`, the response also includes `pending_invitations: [...]`.
- `project invitation-list --project ALIAS` -- list pending (unaccepted) invitations only.
- `project invitation-cancel --project ALIAS --email EMAIL [--invitation-id ID] [--yes]` -- cancel a pending invitation. Without `--invitation-id`, the service resolves it by listing pending invitations and matching `--email` (case-insensitive). 204 No Content on success; `KeboolaApiError(NOT_FOUND)` if the email has no pending invitation.
- `project member-remove --project ALIAS --email EMAIL [--yes]` -- destructive: remove an active member. The service resolves `--email` to the numeric `user_id` (case-insensitive) and DELETEs `/manage/projects/{id}/users/{userId}`. Re-add the user via `project invite`.
- `project member-set-role --project ALIAS --email EMAIL --role admin|guest|readOnly|share` -- change an existing member's role. Uses **PATCH** `/manage/projects/{id}/users/{userId}` with `{"role": "..."}`. PUT does *not* work on this endpoint -- pre-v0.26.1 implementations that tried PUT got a misleading 404.

## Permission flags (top-level, session-only)
- `--deny-writes` -- block all write/destructive/admin operations for this single invocation. Merges with any persisted permission policy; never written to config.json. Exit code 6 (PERMISSION_DENIED) on blocked operations
- `--deny-destructive` -- block only destructive operations (delete-table, delete-bucket, terminate-job, etc.) for this invocation. Pure-write ops like create-table stay allowed. Use this when you want to keep build-up capabilities but lock out tear-downs
Expand Down
30 changes: 30 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,36 @@
container after `autoSuspendAfterSeconds` of inactivity. Hit the URL
to wake it (auto-restart triggers a 30-60s cold boot) or run
`kbagent data-app start --app-id N`.
## `project invite` "already invited / already member" returns HTTP 400, not 422 (since v0.26.1)

- Re-inviting a user the project already knows about returns HTTP **400** with
one of two error strings:
- `"This user has already been invited to this project."` (pending invitation)
- `"This user is already a member of this project."` (active member)
- `MemberService.invite()` translates both cases to `status="noop"` with
`note="already_invited"` / `"already_member"` -- they are *not* exit-1
failures. Bulk runs (`--from-csv`) count them as `noop` in the summary, not
`failed`.
- The 422 heuristic in pre-v0.26.1 orchestrator scripts (`invite_participants.py:25`)
is **wrong** for this API. If you write a parallel implementation, key off
status_code 400 + the substring marker, not 422.

## `project member-set-role` is PATCH, not PUT (since v0.26.1)

- The Manage API role-change endpoint is `PATCH /manage/projects/{id}/users/{userId}`
with body `{"role": "..."}`. **PUT returns 404** ("resource not found") even
on a real, currently-active member -- the endpoint shape is PATCH-only.
- The kbagent `ManageClient.update_project_member_role` method emits PATCH;
any custom code re-implementing the call must do the same.

## `project invite --from-csv` order is not deterministic (since v0.26.1)

- Bulk invitation parallelises via `ThreadPoolExecutor` (default 8 workers).
The `rows[]` array in the result is in completion order, not CSV order.
- Per-row parsing of `failed_rows` should match by `email`, not by index.
- A failed row never aborts the run -- the executor accumulates results and
the command exits 0 with `failed > 0` reflected in the JSON summary. Mirror
the `org setup` partial-success exit semantics.

## `default_bucket` is per-config and only an output prefix (since 0.26.0)

Expand Down
Loading