@palettelab/cli 0.3.34 → 0.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/backend-sdk/palette_sdk/__init__.py +2 -0
- package/backend-sdk/palette_sdk/members.py +45 -0
- package/backend-sdk/palette_sdk/permissions.py +1 -0
- package/backend-sdk/palette_sdk/plugin_context.py +7 -0
- package/docs/python-backend-sdk.md +48 -7
- package/lib/manifest.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -275,17 +275,28 @@ async def create_invoice(body: InvoiceIn, ctx: PluginContext = Depends(get_plugi
|
|
|
275
275
|
|
|
276
276
|
Backend SDK features for app-owned data:
|
|
277
277
|
|
|
278
|
-
- `
|
|
278
|
+
- `PluginRouter`, `PluginContext`, and `get_plugin_context` provide the FastAPI route and request-context surface.
|
|
279
|
+
- `PluginContext` exposes `user_id`, `organization_id`, `org_role`, `plugin_id`, `permissions`, `storage`, `ctx.db`, `ctx.data_rooms`, `ctx.members`, `ctx.redis`, `ctx.vector`, `ctx.config`, and `ctx.logger`.
|
|
279
280
|
- `ctx.db` is the full scoped SQLAlchemy `AsyncSession` for app-owned database data.
|
|
280
281
|
- `ctx.repo(Model)` gives org-safe CRUD helpers for app tables.
|
|
281
282
|
- `ctx.data_rooms` gives backend access to Palette Data Rooms without importing platform internals.
|
|
283
|
+
- `ctx.members` gives backend access to current organisation members; it exposes list/get/invite/update-role helpers, but no delete/remove helper.
|
|
282
284
|
- `ctx.has_permission("...")`, `ctx.has_any_permission([...])`, and `ctx.has_all_permissions([...])` check declared permissions.
|
|
283
285
|
- `ctx.config_value("key")` and `ctx.require_config("key")` read app install/config values.
|
|
284
286
|
- `ctx.secret("KEY")` reads app secrets from config or environment variables.
|
|
287
|
+
- `get_config(ctx, key)` and `require_config(ctx, key)` are functional config helper forms.
|
|
288
|
+
- `require_permission(permission)`, `KNOWN_PERMISSIONS`, and `is_known_permission(permission)` support route and manifest permission checks.
|
|
285
289
|
- `ctx.redis` gives a Redis-backed, plugin/org-scoped Redis API when `"redis"` is declared in `platform_services`.
|
|
286
290
|
- `ctx.vector` gives a Qdrant-backed, plugin/org-scoped vector API when `"vector"` is declared in `platform_services`.
|
|
287
291
|
- `LifecycleHooks` lets apps define install/update/enable/disable/uninstall hooks.
|
|
288
292
|
- `OrgScopedTable` and `PluginBase` keep app data inside the plugin schema model set.
|
|
293
|
+
- `plugin_safe_id(...)`, `plugin_schema(...)`, `plugin_table_prefix(...)`, and `ensure_org_rls(...)` keep database names and row-level security consistent.
|
|
294
|
+
- `Event` and `subscribe_event(...)` register in-process platform event handlers.
|
|
295
|
+
- `sign_webhook(...)` and `verify_webhook_signature(...)` handle HMAC-SHA256 webhook signing checks.
|
|
296
|
+
- `ToolDefinition` is the base class for custom agent tools.
|
|
297
|
+
- `PluginManifest` and `load_manifest(...)` parse and validate `palette-plugin.json`.
|
|
298
|
+
- `SuccessResponse`, `ErrorResponse`, and `PaginatedResponse` are reusable response schemas.
|
|
299
|
+
- `route_permission_issues(router, public_routes=None)` is the test helper for detecting ungated backend routes.
|
|
289
300
|
|
|
290
301
|
Python backend Data Room example:
|
|
291
302
|
|
|
@@ -523,7 +534,7 @@ await ctx.redis.incr("counter")
|
|
|
523
534
|
await ctx.redis.decr("counter")
|
|
524
535
|
await ctx.redis.scan(prefix="cache:", limit=100)
|
|
525
536
|
|
|
526
|
-
# Redis hashes, lists, sets, sorted sets,
|
|
537
|
+
# Redis hashes, lists, sets, sorted sets, queues, locks
|
|
527
538
|
await ctx.redis.hset("hash", "field", {"value": 1})
|
|
528
539
|
await ctx.redis.hgetall("hash")
|
|
529
540
|
await ctx.redis.lpush("queue", {"job": 1})
|
|
@@ -532,8 +543,8 @@ await ctx.redis.sadd("tags", "red", "blue")
|
|
|
532
543
|
await ctx.redis.smembers("tags")
|
|
533
544
|
await ctx.redis.zadd("scores", {"alice": 10})
|
|
534
545
|
await ctx.redis.zrange("scores", 0, -1, with_scores=True)
|
|
535
|
-
await ctx.redis.
|
|
536
|
-
await ctx.redis.
|
|
546
|
+
await ctx.redis.enqueue("jobs", {"task": "sync"})
|
|
547
|
+
await ctx.redis.dequeue("jobs")
|
|
537
548
|
await ctx.redis.lock("invoice:1", token, ttl=30)
|
|
538
549
|
await ctx.redis.unlock("invoice:1", token)
|
|
539
550
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from palette_sdk.plugin_router import PluginRouter
|
|
4
4
|
from palette_sdk.plugin_context import MissingSecretError, PluginContext, get_plugin_context
|
|
5
5
|
from palette_sdk.data_rooms import DataRoomsClient
|
|
6
|
+
from palette_sdk.members import OrganizationMembersClient
|
|
6
7
|
from palette_sdk.platform_services import (
|
|
7
8
|
LocalRedisService,
|
|
8
9
|
LocalVectorService,
|
|
@@ -42,6 +43,7 @@ __all__ = [
|
|
|
42
43
|
"MissingSecretError",
|
|
43
44
|
"get_plugin_context",
|
|
44
45
|
"DataRoomsClient",
|
|
46
|
+
"OrganizationMembersClient",
|
|
45
47
|
"LocalRedisService",
|
|
46
48
|
"LocalVectorService",
|
|
47
49
|
"PlatformServiceUnavailable",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Backend organization member helpers for plugin Python code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OrganizationMembersClient:
|
|
9
|
+
"""Thin wrapper around the platform-injected organization member service."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, service: Any = None, permissions: list[str] | None = None):
|
|
12
|
+
self._service = service
|
|
13
|
+
self._permissions = permissions or []
|
|
14
|
+
|
|
15
|
+
def _require_service(self) -> Any:
|
|
16
|
+
if self._service is None:
|
|
17
|
+
raise RuntimeError(
|
|
18
|
+
"Organization member service is not available in this runtime. "
|
|
19
|
+
"Run inside Palette OS/hosted sandbox or inject a fake service in tests."
|
|
20
|
+
)
|
|
21
|
+
return self._service
|
|
22
|
+
|
|
23
|
+
def _require_permission(self, permission: str) -> None:
|
|
24
|
+
if permission not in self._permissions:
|
|
25
|
+
raise PermissionError(f"App does not declare required permission: {permission}")
|
|
26
|
+
|
|
27
|
+
async def list(self) -> list[dict[str, Any]]:
|
|
28
|
+
self._require_permission("members:read")
|
|
29
|
+
return await self._require_service().list_members()
|
|
30
|
+
|
|
31
|
+
async def get(self, user_id: str) -> dict[str, Any] | None:
|
|
32
|
+
self._require_permission("members:read")
|
|
33
|
+
return await self._require_service().get_member(user_id)
|
|
34
|
+
|
|
35
|
+
async def get_by_email(self, email: str) -> dict[str, Any] | None:
|
|
36
|
+
self._require_permission("members:read")
|
|
37
|
+
return await self._require_service().get_member_by_email(email)
|
|
38
|
+
|
|
39
|
+
async def invite(self, email: str, role: str = "member") -> dict[str, Any]:
|
|
40
|
+
self._require_permission("members:write")
|
|
41
|
+
return await self._require_service().invite_member(email, role)
|
|
42
|
+
|
|
43
|
+
async def update_role(self, user_id: str, role: str) -> dict[str, Any]:
|
|
44
|
+
self._require_permission("members:write")
|
|
45
|
+
return await self._require_service().update_member_role(user_id, role)
|
|
@@ -11,6 +11,7 @@ from fastapi import Depends, Request
|
|
|
11
11
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
12
|
|
|
13
13
|
from palette_sdk.data_rooms import DataRoomsClient
|
|
14
|
+
from palette_sdk.members import OrganizationMembersClient
|
|
14
15
|
from palette_sdk.platform_services import UnavailablePlatformService
|
|
15
16
|
|
|
16
17
|
|
|
@@ -35,6 +36,7 @@ class PluginContext:
|
|
|
35
36
|
plugin_id: The plugin's ID from its manifest
|
|
36
37
|
permissions: List of permissions declared in the manifest
|
|
37
38
|
storage: Storage service for file upload/download
|
|
39
|
+
members: Organization member helpers for the current org
|
|
38
40
|
"""
|
|
39
41
|
db: AsyncSession
|
|
40
42
|
user_id: str
|
|
@@ -44,6 +46,7 @@ class PluginContext:
|
|
|
44
46
|
permissions: list[str] = field(default_factory=list)
|
|
45
47
|
storage: Any = None # Platform storage service injected at runtime
|
|
46
48
|
data_rooms: DataRoomsClient = field(default_factory=DataRoomsClient)
|
|
49
|
+
members: OrganizationMembersClient = field(default_factory=OrganizationMembersClient)
|
|
47
50
|
redis: Any = field(default_factory=lambda: UnavailablePlatformService("redis"))
|
|
48
51
|
vector: Any = field(default_factory=lambda: UnavailablePlatformService("vector"))
|
|
49
52
|
config: dict[str, Any] = field(default_factory=dict)
|
|
@@ -110,6 +113,10 @@ async def get_plugin_context(request: Request) -> PluginContext:
|
|
|
110
113
|
permissions=getattr(state, "plugin_permissions", []),
|
|
111
114
|
storage=getattr(state, "storage", None),
|
|
112
115
|
data_rooms=DataRoomsClient(getattr(state, "data_rooms", None)),
|
|
116
|
+
members=OrganizationMembersClient(
|
|
117
|
+
getattr(state, "org_members", None),
|
|
118
|
+
getattr(state, "plugin_permissions", []),
|
|
119
|
+
),
|
|
113
120
|
redis=getattr(state, "redis", None) or UnavailablePlatformService("redis"),
|
|
114
121
|
vector=getattr(state, "vector", None) or UnavailablePlatformService("vector"),
|
|
115
122
|
config=getattr(state, "plugin_config", {}),
|
|
@@ -112,6 +112,7 @@ Available context values:
|
|
|
112
112
|
| `ctx.permissions` | Permissions granted from `palette-plugin.json` |
|
|
113
113
|
| `ctx.storage` | Runtime storage service, when available |
|
|
114
114
|
| `ctx.data_rooms` | Backend Data Room client |
|
|
115
|
+
| `ctx.members` | Current organisation member client |
|
|
115
116
|
| `ctx.redis` | Plugin/org-scoped Redis-style service when `platform_services` includes `redis` |
|
|
116
117
|
| `ctx.vector` | Plugin/org-scoped vector service when `platform_services` includes `vector` |
|
|
117
118
|
| `ctx.config` | App install/config values |
|
|
@@ -124,6 +125,34 @@ Available context values:
|
|
|
124
125
|
| `ctx.require_config(key)` | Read required config or raise |
|
|
125
126
|
| `ctx.secret(key, default)` | Read a secret from app config or environment |
|
|
126
127
|
|
|
128
|
+
## 4a. Backend Helper API Index
|
|
129
|
+
|
|
130
|
+
These are the public Python helpers exported by `palette_sdk`.
|
|
131
|
+
|
|
132
|
+
| Helper | Use it for |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `PluginRouter` | FastAPI router mounted under `/api/v1/plugins/<plugin-id>` |
|
|
135
|
+
| `PluginContext`, `get_plugin_context` | Authenticated request context dependency |
|
|
136
|
+
| `MissingSecretError` | Handling missing required declared secrets from `ctx.secret(...)` |
|
|
137
|
+
| `require_permission(permission)` | Route-level permission gate required for protected routes |
|
|
138
|
+
| `KNOWN_PERMISSIONS`, `is_known_permission(...)` | Permission vocabulary checks for manifests/tools |
|
|
139
|
+
| `DataRoomsClient`, `ctx.data_rooms` | Backend Data Room room/folder/file helpers |
|
|
140
|
+
| `OrganizationMembersClient`, `ctx.members` | Current-organization member lookup, invite, and role helpers |
|
|
141
|
+
| `OrgRepository`, `ctx.repo(Model)` | Org-safe convenience CRUD for app-owned models |
|
|
142
|
+
| `PluginBase`, `OrgScopedTable` | SQLAlchemy declarative bases for plugin-owned tables |
|
|
143
|
+
| `ensure_org_rls(op, table)` | Alembic helper that enables org row-level security |
|
|
144
|
+
| `plugin_safe_id(...)`, `plugin_schema(...)`, `plugin_table_prefix(...)` | Manifest id to database-safe naming helpers |
|
|
145
|
+
| `get_config(ctx, key)`, `require_config(ctx, key)` | Functional form of config reads when not using `ctx.config_value(...)` |
|
|
146
|
+
| `LocalRedisService`, `LocalVectorService` | Local `pltt dev` service emulators and test fakes |
|
|
147
|
+
| `PlatformServiceUnavailable`, `UnavailablePlatformService` | Clear errors when an undeclared platform service is used |
|
|
148
|
+
| `LifecycleHooks` | Install/update/enable/disable/uninstall callbacks |
|
|
149
|
+
| `Event`, `subscribe_event(...)` | In-process platform event subscriptions |
|
|
150
|
+
| `sign_webhook(...)`, `verify_webhook_signature(...)` | HMAC-SHA256 webhook signing and verification |
|
|
151
|
+
| `ToolDefinition` | Base class for custom agent tools |
|
|
152
|
+
| `PluginManifest`, `load_manifest(...)` | Typed manifest parsing from `palette-plugin.json` |
|
|
153
|
+
| `SuccessResponse`, `ErrorResponse`, `PaginatedResponse` | Common response schemas for plugin APIs |
|
|
154
|
+
| `route_permission_issues(router, public_routes=None)` | Test helper that reports routes missing `require_permission(...)` |
|
|
155
|
+
|
|
127
156
|
## 5. Permissions
|
|
128
157
|
|
|
129
158
|
Use route-level permission guards for normal APIs:
|
|
@@ -167,10 +196,26 @@ data_rooms:read, data_rooms:write
|
|
|
167
196
|
agents:read, agents:write
|
|
168
197
|
chat:read, chat:write
|
|
169
198
|
routines:read, routines:write
|
|
170
|
-
members:read
|
|
199
|
+
members:read, members:write
|
|
171
200
|
resources:read, resources:write
|
|
172
201
|
```
|
|
173
202
|
|
|
203
|
+
Organisation member helpers:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
@router.get("/members", dependencies=[require_permission("members:read")])
|
|
207
|
+
async def members(ctx: PluginContext = Depends(get_plugin_context)):
|
|
208
|
+
return await ctx.members.list()
|
|
209
|
+
|
|
210
|
+
@router.post("/members/invite", dependencies=[require_permission("members:write")])
|
|
211
|
+
async def invite_member(email: str, ctx: PluginContext = Depends(get_plugin_context)):
|
|
212
|
+
return await ctx.members.invite(email, role="member")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`ctx.members` exposes `list()`, `get(user_id)`, `get_by_email(email)`,
|
|
216
|
+
`invite(email, role="member")`, and `update_role(user_id, role)`. Member
|
|
217
|
+
deletion/removal is intentionally not exposed through the app SDK.
|
|
218
|
+
|
|
174
219
|
## 6. App-Owned Data
|
|
175
220
|
|
|
176
221
|
Apps can ship their own tables and migrations. Use `OrgScopedTable` for data
|
|
@@ -649,8 +694,6 @@ await ctx.redis.zrem("scores", "bob")
|
|
|
649
694
|
|
|
650
695
|
await ctx.redis.enqueue("jobs", {"task": "sync"})
|
|
651
696
|
await ctx.redis.dequeue("jobs")
|
|
652
|
-
await ctx.redis.xadd("events", {"type": "created"})
|
|
653
|
-
await ctx.redis.xread({"events": "0-0"}, count=10)
|
|
654
697
|
await ctx.redis.lock("invoice:1", token, ttl=30)
|
|
655
698
|
await ctx.redis.unlock("invoice:1", token)
|
|
656
699
|
```
|
|
@@ -730,12 +773,10 @@ Frontend code should call backend routes through the platform API helper, not by
|
|
|
730
773
|
hardcoding backend origins.
|
|
731
774
|
|
|
732
775
|
```tsx
|
|
733
|
-
import {
|
|
734
|
-
|
|
735
|
-
const palette = createPaletteClient()
|
|
776
|
+
import { apiFetch } from "@palettelab/sdk"
|
|
736
777
|
|
|
737
778
|
async function loadInvoices() {
|
|
738
|
-
const res = await
|
|
779
|
+
const res = await apiFetch("/api/v1/plugins/finance-tools/invoices")
|
|
739
780
|
return res.json()
|
|
740
781
|
}
|
|
741
782
|
```
|
package/lib/manifest.js
CHANGED