Skip to content
Merged
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Official Python SDK for [Norbix](https://norbix.ai).
Use split clients with flat module access:

- `NorbixApi` for API scope (`client.database`, `client.membership`, ...)
- `NorbixApi` for API scope (`client.database`, `client.membership`, `client.public`, ...)
- `NorbixHub` for Hub scope (`client.database`, `client.account`, ...)

## Install
Expand Down Expand Up @@ -143,6 +143,32 @@ and `norbix.hub.ai` (`get_embedding_integrations`, `save_embedding_integration`,
[docs/api/ai.md](./docs/api/ai.md), [docs/hub/ai.md](./docs/hub/ai.md),
[docs/hub/account.md](./docs/hub/account.md).

## Project settings, public pages, MCP and AI service users

Project owners manage the Admin Portal and legal pages on the Hub,
`norbix.hub.account`: `update_project_admin_url`, `update_project_legal_documents`,
`update_project_expose_legal`, `get_admin_portal_structure`,
`assign_admin_portal_service_user`. Two switches choose what the public Admin
Portal config shows: `update_project_expose_brand` (brand, on by default) and
`update_project_expose_auth` (sign-in methods and password policy, off by default).

Once the legal pages are exposed, anyone can read them — and the project's
public config — from the API host with no sign-in, `norbix.api.public`:

```python
norbix.hub.account.update_project_legal_documents("proj_123", termsMarkdown="# Terms", privacyMarkdown="# Privacy")
norbix.hub.account.update_project_expose_legal("proj_123", exposed=True)
norbix.api.public.get_public_project_legal("proj_123", "terms") # {"kind", "title", "body", "available"}
norbix.api.public.get_public_project_config("proj_123")
```

AI agents reach Norbix through the developer MCP endpoint with an AI service
user key: `create_ai_service_user`, `list_ai_service_users`,
`rotate_ai_service_user_key`, `revoke_ai_service_user_key`,
`delete_ai_service_user`, then `mcp` / `mcp_stream` / `mcp_end_session`.
Full tables and an MCP example: [docs/hub/account.md](./docs/hub/account.md),
[docs/api/public.md](./docs/api/public.md).

## Errors

```python
Expand Down
1 change: 1 addition & 0 deletions docs/api/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
| [`echo`](./echo.md) | 1 |
| [`files`](./files.md) | 10 |
| [`membership`](./membership.md) | 18 |
| [`public`](./public.md) | 2 |
16 changes: 16 additions & 0 deletions docs/api/public.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# API · Public

Access with `norbix.api.public` (or `client.public` on `NorbixApi`).

These routes need **no sign-in**: the gateway does not authenticate them, and
the SDK sends no `Authorization` header even when the client has a token.

| Method | Verb | Path | Scope |
| --- | --- | --- | --- |
| `get_public_project_config` | `GET` | `/{version}/public/projects/{ProjectId}/config` | `unauthenticated` |
| `get_public_project_legal` | `GET` | `/{version}/public/projects/{ProjectId}/legal/{Kind}` | `unauthenticated` |

`get_public_project_legal` takes `kind` = `"terms"` or `"privacy"` and gives
back `{kind, title, body, available}` (Markdown in `body`). `available` is
`False` until the project turns the legal pages on with
`norbix.hub.account.update_project_expose_legal(project_id, exposed=True)`.
2 changes: 1 addition & 1 deletion docs/hub/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

| Module | Endpoints |
| --- | ---: |
| [`account`](./account.md) | 36 |
| [`account`](./account.md) | 58 |
| [`ai`](./ai.md) | 14 |
| [`database`](./database.md) | 41 |
| [`echo`](./echo.md) | 1 |
Expand Down
39 changes: 39 additions & 0 deletions docs/hub/account.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,42 @@ Access with `norbix.hub.account`.
| `delete_project_ai_assistant` | `DELETE` | `/{version}/account/projects/{projectId}/ai/assistants/{assistantId}` | `project` |
| `get_project_ai_usage` | `GET` | `/{version}/account/projects/{projectId}/ai/usage` | `project` |
| `set_admin_portal_enabled` | `PUT` | `/{version}/account/projects/{projectId}/admin-portal/enabled` | `project` |
| `update_project_admin_url` | `PATCH` | `/{version}/account/projects/{projectId}/settings/admin-url` | `project` |
| `update_project_legal_documents` | `PATCH` | `/{version}/account/projects/{projectId}/settings/legal` | `project` |
| `update_project_expose_legal` | `PATCH` | `/{version}/account/projects/{projectId}/settings/legal/expose` | `project` |
| `update_project_expose_brand` | `PATCH` | `/{version}/account/projects/{projectId}/settings/brand/expose` | `project` |
| `update_project_expose_auth` | `PATCH` | `/{version}/account/projects/{projectId}/settings/auth/expose` | `project` |
| `get_admin_portal_structure` | `GET` | `/{version}/account/projects/{projectId}/admin-portal/structure` | `project` |
| `assign_admin_portal_service_user` | `PUT` | `/{version}/account/projects/{projectId}/settings/admin-portal/service-user` | `project` |
| `create_ai_service_user` | `POST` | `/{version}/account/ai/service-users` | `project` |
| `list_ai_service_users` | `GET` | `/{version}/account/ai/service-users` | `project` |
| `rotate_ai_service_user_key` | `POST` | `/{version}/account/ai/service-users/{Id}/keys` | `project` |
| `revoke_ai_service_user_key` | `DELETE` | `/{version}/account/ai/service-users/{Id}/keys/{KeyId}` | `project` |
| `delete_ai_service_user` | `DELETE` | `/{version}/account/ai/service-users/{Id}` | `project` |
| `mcp` | `POST` | `/{version}/account/mcp` | `project` |
| `mcp_stream` | `GET` | `/{version}/account/mcp` | `project` |
| `mcp_end_session` | `DELETE` | `/{version}/account/mcp` | `project` |

## The developer MCP endpoint

`mcp`, `mcp_stream` and `mcp_end_session` speak MCP Streamable HTTP
(revision 2025-11-25). They give back `{"status", "sessionId", "body", "events"}`:
`sessionId` is read from the `Mcp-Session-Id` answer header of `initialize`
and must be passed as `session_id` on every later call; `body` is the JSON-RPC
answer; `events` holds the messages of an SSE answer (a streamed `tools/call`,
or the `mcp_stream` server stream). `mcp_stream` does not stream: it returns
when the server closes the stream or `timeout` runs out.

```python
init = norbix.hub.account.mcp({"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {},
"clientInfo": {"name": "my-agent", "version": "1.0"}}})
sid = init["sessionId"]
norbix.hub.account.mcp({"jsonrpc": "2.0", "method": "notifications/initialized"}, session_id=sid)
tools = norbix.hub.account.mcp({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}, session_id=sid)
norbix.hub.account.mcp_end_session(sid)
```

An AI service user key (`nbsu_…`, from `create_ai_service_user` or
`rotate_ai_service_user_key`) works as the client's `api_key` / `bearer_token`
and narrows the tools the agent sees to the service user's scope.
120 changes: 120 additions & 0 deletions docs/tasks/project-audit-python.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Project audit — Python SDK: Project module completeness
This file: /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/tasks/project-audit-python.md (branch audit/project)

## Goal
Give the Python SDK every Project-module route the TypeScript SDK already has: admin portal and legal settings, the public project config and legal pages, the developer MCP endpoint, and AI service users — sync and async, each with a test and a docs row.
Not in scope: AI plans, knowledge search, AI credits (decided internal); changes to the gateway or the TypeScript SDK.

## Plan
1. [done] docs(sdk-python:project): write this task file before any code
2. [done] feat(sdk-python:account:settings): admin URL, legal documents, expose legal, admin portal structure, admin portal service user
3. [done] feat(sdk-python:account:ai-service-users): create, list, delete AI service users, rotate and revoke their keys
4. [done] feat(sdk-python:account:mcp): the developer MCP endpoint — send a message (POST), read the server stream (GET), end the session (DELETE)
5. [done] feat(sdk-python:api:public): new `api.public` module — public project config and public legal document, no sign-in
6. [done] docs(sdk-python:project): docs pages, docs index and README section for all of the above
7. [done] release(sdk-python:project): ruff, mypy, pytest green; push branch; open one pull request to main — https://github.com/norbix-code/sdk-python/pull/25
8. [done] feat(sdk-python:account:settings): expose brand and expose auth switches (item B3b; gateway routes from item B1) — `update_project_expose_brand`, `update_project_expose_auth`, sync + async, tests, docs; pushed to pull request #25

## Checks
- `uv run ruff check .` — All checks passed
- `uv run mypy src` — Success: no issues found in 37 source files
- `uv run pytest` — 799 passed (31 new: 20 settings/service-user shapes, 7 MCP, 4 public)
- venv lives outside the repo: `UV_PROJECT_ENVIRONMENT=~/scratch/project-python/venv`
- Step 8 re-run (venv `~/scratch/project-b3b/venv`): ruff All checks passed; mypy no issues in 37 source files; pytest 803 passed (4 new: brand + auth, sync + async)

## Changes
| file (absolute, branch audit/project) | what changed | step |
|------|--------------|------|
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/tasks/project-audit-python.md | this task file | 1–7 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/hub/account.py | 5 settings methods, 5 AI service user methods, 3 MCP methods + MCP helpers (sync + async) | 2, 3, 4 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/tests/hub/test_account_project.py | new: one table, 10 routes, each sent through the sync and async client (verb, path, body, headers) | 2, 3 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/transport.py | additive send options: `extra_headers`, `query`, `response_type="envelope"` (sync + async) | 4 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/tests/hub/test_account_mcp.py | new: initialize session header, SSE answer parsing, 202 notification, stream resume, end session, 404, async twins | 4 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/api/public.py | new module `api.public`: config + legal, no sign-in (sync + async) | 5 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/api/__init__.py | `public` added to both API namespaces | 5 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/client.py | `NorbixApi.public` / `.Public` | 5 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/tests/api/test_public.py | new: API host, path, no Authorization even when signed in, NorbixApi flat access, async twin | 5 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/hub/account.md | 13 rows + "The developer MCP endpoint" section | 6 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/api/public.md | new page | 6 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/api/_index.md, /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/hub/_index.md | `public` row; account count 36 → 56 | 6 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/README.md | module list mentions `client.public`; new section "Project settings, public pages, MCP and AI service users" | 6 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/hub/account.py | `update_project_expose_brand`, `update_project_expose_auth` (sync + async) | 8 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/tests/hub/test_account_project.py | 2 table rows (brand sends `exposed: false`, so a false body is checked too) | 8 |
| /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/hub/account.md, /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/hub/_index.md, /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/README.md | 2 rows; account count 56 → 58; README sentence on the two switches | 8 |

## Findings
fix(sdk-python:client:api): the flat `NorbixApi` client has no `ai`, although `api.ai` exists since the AI pull request #23 — open, not fixed here
where: /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/client.py:243 (branch audit/project)
```python
# src/norbix_python/client.py:242-248 (audit/project)
api = ApiNamespace(self._transport)
self.database = api.database
self.membership = api.membership
self.echo = api.echo
self.files = api.files
self.public = api.public # added in step 5
# <-- missing: self.ai = api.ai (and self.Ai)
```

decision(sdk-python:account:settings): new methods use scope `project`; the older project-settings methods use `account`, which needlessly demands `account_id` on the client — open
where: /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/hub/account.py:325 (branch audit/project)
```python
# src/norbix_python/hub/account.py:325-333 (audit/project)
def update_project_accent_color(self, project_id: str, *, ...) -> Any:
"""PATCH /{version}/account/projects/{projectId}/settings/accent-color"""
return self._transport.send(
target="hub",
path="/{version}/account/projects/{projectId}/settings/accent-color",
method="PATCH",
path_params={"projectId": project_id},
request=request,
scope="account", # <-- here: gateway reads the account from the session; TS uses 'project'
```

fix(sdk-ts:api:public): the TypeScript `getPublicProjectConfig` / `getPublicProjectLegal` demand a token, but the gateway routes are anonymous — open, other repo
where: /Users/djovaisas/Projects/norbix/sdks/norbix-js src/api/public.ts:28 and :47 (branch origin/main b7230dc, not edited)
```ts
// norbix-js src/api/public.ts:24-29 (origin/main)
path: '/{version}/public/projects/{ProjectId}/config',
method: 'GET',
request,
pathParams: ['ProjectId'],
scope: 'project', // <-- here: needs a token; gateway has no [Authenticate] on this route
```

fix(sdk-ts:account:mcp): the TypeScript `mcp` only sends POST and cannot read the `Mcp-Session-Id` answer header, so no session can follow `initialize` — open, other repo
where: /Users/djovaisas/Projects/norbix/sdks/norbix-js src/hub/account.ts:2055-2068 (branch origin/main b7230dc, not edited)
```ts
// norbix-js src/hub/account.ts:2059-2066 (origin/main)
return this.transport.send<string>({
target: 'hub',
path: '/{version}/account/mcp',
method: 'POST', // <-- here: GET (stream) and DELETE (end session) aliases unreachable
request,
pathParams: [],
scope: 'project',
```

fix(sdk-python:transport): the body drops every `None` value, so a caller cannot send an explicit null (admin URL reset needs `url=""` instead) — open, documented in the docstring
where: /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/src/norbix_python/transport.py:393 (branch audit/project)
```python
# src/norbix_python/transport.py:393 (audit/project)
remaining = {k: v for k, v in request.items() if v is not None} # <-- here: explicit null never reaches the gateway
```

docs(sdk-python:docs:index): endpoint counts in the docs index pages are stale (hub ai says 14, page has 20; hub email 1 vs 2; api database 18 vs 20; api files 10 vs 9; push and sms pages missing from the index) — open; only account and public fixed here
where: /Users/djovaisas/Projects/norbix/worktrees/sdks/norbix-python/audit/project/docs/hub/_index.md:6 (branch audit/project)

docs(sdk-python:hub:account): the two new switches have no matching request DTO class in the SDK — none needed, the Python SDK sends `**request` keyword bodies with no generated types — no action

## Rejected / moved out
- decision(sdk-python:account:ai): AI plans, knowledge search and AI credits not added — rejected — decided internal by the campaign brief — none
- decision(sdk-python:account:mcp): one TS-style `mcp` for all three verbs not copied; three methods instead (`mcp` POST, `mcp_stream` GET, `mcp_end_session` DELETE), because each verb needs different headers and arguments — rejected (by design) — none
- decision(sdk-python:account:mcp): no real streaming for `mcp_stream`; it returns when the server closes the stream or the timeout ends, because the SDK transport is request/response only — rejected (by design) — none

## Needs you
- [ ] release(sdk-python:project): review and squash-merge the pull request to main (expected next version: minor, no breaking change) — needs you · action: merge the PR listed in the final message
- [ ] decision(sdk-python:findings): route the findings above (NorbixApi `ai`, settings scope, TS public scope, TS mcp, null bodies, docs counts) — needs you · action: pick fix-here / own branch / hand out

## Open questions
- none
3 changes: 3 additions & 0 deletions src/norbix_python/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .echo import AsyncEchoModule, EchoModule
from .files import AsyncFilesModule, FilesModule
from .membership import AsyncMembershipModule, MembershipModule
from .public import AsyncPublicModule, PublicModule


class ApiNamespace:
Expand All @@ -15,6 +16,7 @@ def __init__(self, transport: Transport) -> None:
self.echo = EchoModule(transport)
self.files = FilesModule(transport)
self.membership = MembershipModule(transport)
self.public = PublicModule(transport)


class AsyncApiNamespace:
Expand All @@ -24,3 +26,4 @@ def __init__(self, transport: AsyncTransport) -> None:
self.echo = AsyncEchoModule(transport)
self.files = AsyncFilesModule(transport)
self.membership = AsyncMembershipModule(transport)
self.public = AsyncPublicModule(transport)
Loading
Loading