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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ called out explicitly under **Changed** or **Removed**.

## [Unreleased]

### Added

- **`kizen service-accounts`** lists, creates, updates, deletes and shows
the history of integration service accounts, with permission groups by
name. API keys are never printed.

## [0.3.0] — 2026-10-21

### Changed
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ src/kizen_builder/
records.py # record-level reads/writes (custom objects + built-ins)
saved_views.py # filter groups / quick filters / column templates
schema.py # schema lookups backing the filtering DSL
service_accounts.py # service-account CRUD + history (strips the key from create)
smart_connectors.py # smart-connector CRUD, scripts, executions
team.py # team-member reads + role changes
tools/ # Tool functions. What the CLI mostly calls.
Expand All @@ -132,6 +133,7 @@ src/kizen_builder/
plans.py # Plan / PlanOperation models, apply_plan()
records.py # get_record(), search_records()
saved_views.py # list/get filter groups, quick filters, column templates
service_accounts.py # resolve a service account; list/get/history
smart_connectors/ # inspection + authoring + local pull/run/push dev loop
__init__.py # re-export facade — `from ... import smart_connectors as sct`
_common.py # MARKER_NAME, execution-metadata keys, shared helpers
Expand Down Expand Up @@ -165,6 +167,7 @@ src/kizen_builder/
pipeline_stages.py # plan_create/update/remove stages, record stage-move
records.py # plan_create/update/delete records (data, not schema)
saved_views.py # plan_create/update/delete the three saved-view kinds
service_accounts.py # plan_create/update/delete integration service accounts
team.py # plan_update_team_member_roles
filtering.py # Filter DSL (Field, All/Any, search/filter-config rendering).
translate.py # Automation GET → PUT wire translation.
Expand All @@ -187,6 +190,7 @@ src/kizen_builder/
filters.py # kizen filters (the operator reference)
team.py # kizen team
permissions.py # kizen roles + kizen permissions
service_accounts.py # kizen service-accounts
automations.py # kizen automations — reads, start, diagnostics
steps.py # kizen automations steps
messages.py # kizen messages
Expand Down
146 changes: 145 additions & 1 deletion scripts/cli-tree-baseline.txt

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions src/kizen_builder/api/service_accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Service accounts against the Kizen API (``/api/service-accounts``).

A service account is a non-human employee that holds API keys and permission
groups. Integration accounts (``account_type: service_integration``) are the
ones a business creates; automation and plugin accounts are Kizen's own.

Nothing here touches ``…/api-tokens/*``: the CLI never reads, reveals or
rotates a key.
"""

from __future__ import annotations

from typing import Any

from kizen_builder.api.client import KizenClient

_BASE = "/api/service-accounts"


def list_service_accounts(
client: KizenClient, search: str | None = None
) -> list[dict[str, Any]]:
"""GET /api/service-accounts — every account, previews only.

``search`` filters by name; the schema doesn't declare it, but it works
(confirmed live 2026-10-27).
"""
params: dict[str, Any] = {"page_size": 100}
if search:
params["search"] = search
return client.paginate(_BASE, params)


def get_service_account(client: KizenClient, account_id: str) -> dict[str, Any]:
"""GET /api/service-accounts/{id} — the list row shape, never a key."""
return client.get(f"{_BASE}/{account_id}")


def create_service_account(
client: KizenClient, payload: dict[str, Any]
) -> dict[str, Any]:
"""POST /api/service-accounts — ``{name, permission_groups}``.

The response carries the new account's API key in full under an
``api_key`` object the schema doesn't declare (confirmed live 2026-10-27).
It is removed here, before anything else can see it, so no plan result,
table, log or ``--json`` output can contain it.
"""
resp = client.post(_BASE, json=payload)
if isinstance(resp, dict):
resp.pop("api_key", None)
return resp


def update_service_account(
client: KizenClient, account_id: str, payload: dict[str, Any]
) -> dict[str, Any]:
"""PATCH /api/service-accounts/{id} — only the supplied keys change.

A body that includes ``name`` re-derives ``api_name`` from it unless
``api_name`` is sent too (confirmed live 2026-10-27).
"""
return client.patch(f"{_BASE}/{account_id}", json=payload)


def delete_service_account(client: KizenClient, account_id: str) -> None:
"""DELETE /api/service-accounts/{id} — archives it; no read can see it after."""
client.delete(f"{_BASE}/{account_id}")


def list_service_account_history(
client: KizenClient, account_id: str, limit: int
) -> list[dict[str, Any]]:
"""GET /api/service-accounts/{id}/history — one page of up to ``limit`` events."""
resp = client.get(f"{_BASE}/{account_id}/history", params={"page_size": limit})
if isinstance(resp, dict):
return list(resp.get("results") or [])
return list(resp or [])
1 change: 1 addition & 0 deletions src/kizen_builder/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from kizen_builder.cli import filters # noqa: F401
from kizen_builder.cli import team # noqa: F401
from kizen_builder.cli import permissions # noqa: F401
from kizen_builder.cli import service_accounts # noqa: F401
from kizen_builder.cli import automations # noqa: F401
from kizen_builder.cli import steps # noqa: F401
from kizen_builder.cli import messages # noqa: F401
Expand Down
Loading
Loading