@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 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
- - `PluginContext` exposes `user_id`, `organization_id`, `plugin_id`, `permissions`, `config`, `storage`, `ctx.db`, `ctx.redis`, and `ctx.vector`.
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, streams, locks
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.xadd("events", {"type": "created"})
536
- await ctx.redis.xread({"events": "0-0"}, count=10)
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)
@@ -25,6 +25,7 @@ KNOWN_PERMISSIONS: frozenset[str] = frozenset(
25
25
  "routines:read",
26
26
  "routines:write",
27
27
  "members:read",
28
+ "members:write",
28
29
  "resources:read",
29
30
  "resources:write",
30
31
  }
@@ -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 { createPaletteClient } from "@palettelab/sdk"
734
-
735
- const palette = createPaletteClient()
776
+ import { apiFetch } from "@palettelab/sdk"
736
777
 
737
778
  async function loadInvoices() {
738
- const res = await palette.apiFetch("/api/v1/plugins/finance-tools/invoices")
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
@@ -18,6 +18,7 @@ const KNOWN_PERMISSIONS = new Set([
18
18
  "routines:read",
19
19
  "routines:write",
20
20
  "members:read",
21
+ "members:write",
21
22
  "resources:read",
22
23
  "resources:write",
23
24
  ])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palettelab/cli",
3
- "version": "0.3.34",
3
+ "version": "0.3.36",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
6
  "pltt": "bin/pltt.js"