@palettelab/cli 0.3.48 → 0.3.50
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 +56 -8
- package/backend-sdk/palette_sdk/__init__.py +12 -0
- package/backend-sdk/palette_sdk/apps.py +12 -0
- package/backend-sdk/palette_sdk/events.py +35 -1
- package/backend-sdk/palette_sdk/manifest.py +48 -3
- package/backend-sdk/palette_sdk/services.py +147 -0
- package/docs/python-backend-sdk.md +65 -35
- package/lib/bundler.js +58 -5
- package/lib/cli.js +10 -0
- package/lib/commands/dev.js +4 -1
- package/lib/commands/doctor.js +9 -5
- package/lib/commands/package.js +4 -1
- package/lib/commands/publish.js +97 -19
- package/lib/commands/services.js +426 -0
- package/lib/commands/test.js +21 -11
- package/lib/css-scope.js +124 -0
- package/lib/dev-simulator.js +34 -4
- package/lib/manifest.js +120 -5
- package/lib/secrets.js +83 -8
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -533,10 +533,35 @@ Environment variables:
|
|
|
533
533
|
| `PALETTE_DEV_LOG_TAIL` | `200` | Initial hosted log events shown by `pltt dev --sandbox` / `--cloud` |
|
|
534
534
|
| `APPSTORE_AUTO_APPROVE_SANDBOX_PREVIEWS` | `false` | Backend setting for hosted sandboxes; auto-approve passing preview publishes so developers can test full OS behavior without manual review |
|
|
535
535
|
|
|
536
|
-
### `
|
|
536
|
+
### `.env` and secrets
|
|
537
537
|
|
|
538
|
-
|
|
539
|
-
|
|
538
|
+
For normal app development, put environment values in root `.env` files and run
|
|
539
|
+
the usual CLI command. No separate input is required:
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
pltt dev
|
|
543
|
+
pltt sandbox --env staging
|
|
544
|
+
pltt preview --env staging
|
|
545
|
+
pltt publish --env staging
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
The CLI automatically reads these files, with later files overriding earlier
|
|
549
|
+
ones:
|
|
550
|
+
|
|
551
|
+
```text
|
|
552
|
+
.env
|
|
553
|
+
.env.local
|
|
554
|
+
.env.<env>
|
|
555
|
+
.env.<env>.local
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Server-only keys such as `OPENAI_API_KEY` or `STRIPE_SECRET` are uploaded during
|
|
559
|
+
hosted preview/publish as encrypted plugin secrets and resolved through
|
|
560
|
+
`ctx.secret("NAME")`. Public keys prefixed with `NEXT_PUBLIC_` are bundled into
|
|
561
|
+
the frontend and are not uploaded as backend secrets.
|
|
562
|
+
|
|
563
|
+
You can still declare secrets explicitly in `palette-plugin.json` when you need
|
|
564
|
+
install-scoped values or labels/help text:
|
|
540
565
|
|
|
541
566
|
```json
|
|
542
567
|
{
|
|
@@ -549,7 +574,7 @@ Palette secrets are declared in `palette-plugin.json` and resolved through
|
|
|
549
574
|
}
|
|
550
575
|
```
|
|
551
576
|
|
|
552
|
-
|
|
577
|
+
Optional management commands:
|
|
553
578
|
|
|
554
579
|
```bash
|
|
555
580
|
pltt secrets init
|
|
@@ -559,10 +584,9 @@ pltt secrets list --env staging
|
|
|
559
584
|
pltt publish --env staging --secrets-file plugin-secrets.env
|
|
560
585
|
```
|
|
561
586
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
Frontend bundles may only receive public values such as `NEXT_PUBLIC_*`.
|
|
587
|
+
`.palette/.env.local` remains supported for older projects. `plugin` secrets
|
|
588
|
+
are encrypted by the platform and attached to the plugin/environment.
|
|
589
|
+
`install` secrets are filled by the installing org.
|
|
566
590
|
|
|
567
591
|
Managed platform services do not require developer Redis/Qdrant keys. Declare
|
|
568
592
|
them and use scoped SDK clients:
|
|
@@ -737,6 +761,30 @@ pltt logs --json
|
|
|
737
761
|
|
|
738
762
|
If no plugin ID is provided, the CLI uses the current `palette-plugin.json` or `.palette/last-publish.json`.
|
|
739
763
|
|
|
764
|
+
### `pltt services`
|
|
765
|
+
|
|
766
|
+
Inspect and generate OS-broker service integrations declared through
|
|
767
|
+
`provides` and `consumes` in `palette-plugin.json`.
|
|
768
|
+
|
|
769
|
+
```bash
|
|
770
|
+
pltt services list --env staging
|
|
771
|
+
pltt services add hr/v1#approvalChain.get --reason "Route approvals through HR"
|
|
772
|
+
pltt services add hr/v1#hierarchy.updated --event --optional
|
|
773
|
+
pltt services pull --env staging
|
|
774
|
+
pltt services scaffold approvalChain.get
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
Subcommands:
|
|
778
|
+
|
|
779
|
+
- `list`: fetches `/api/v1/os-broker/catalog` from the selected environment and prints services/events available to the current org. Use `--json` for the raw catalog.
|
|
780
|
+
- `add <namespace/version#name>`: appends a consumed service target to `consumes.services`. Use `--event` or `--type event` for `consumes.events`, `--optional` for non-required dependencies, and `--reason <text>` to record why the dependency is needed.
|
|
781
|
+
- `pull`: reads `consumes.services` and `consumes.events`, fetches JSON Schemas from `/api/v1/os-broker/schemas`, and generates `.palette/types/services.ts` plus `.palette/python/services.py`.
|
|
782
|
+
- `scaffold <method>`: adds a provider method to `provides.services`, creates `schemas/<method>.in.json` and `schemas/<method>.out.json`, and writes a Python handler stub under `broker/`.
|
|
783
|
+
|
|
784
|
+
Generated TypeScript clients use the SDK's default `palette` client. Generated
|
|
785
|
+
Python clients call `palette_sdk.services(ctx)`, so backend routes still pass
|
|
786
|
+
through Palette's broker permission and install checks.
|
|
787
|
+
|
|
740
788
|
## Global Flags
|
|
741
789
|
|
|
742
790
|
- `--json` emits machine-readable output for `package`, `publish`, `status`, `logs`, and `test`.
|
|
@@ -34,7 +34,14 @@ from palette_sdk.permissions import (
|
|
|
34
34
|
is_known_permission,
|
|
35
35
|
require_permission,
|
|
36
36
|
)
|
|
37
|
+
from palette_sdk import events as events_module # also exposed as `palette_sdk.events`
|
|
37
38
|
from palette_sdk.events import Event, EventPublisher, subscribe_event
|
|
39
|
+
from palette_sdk.services import (
|
|
40
|
+
BrokerCallError,
|
|
41
|
+
ServicesClient,
|
|
42
|
+
service,
|
|
43
|
+
services,
|
|
44
|
+
)
|
|
38
45
|
from palette_sdk.config import get_config, require_config
|
|
39
46
|
from palette_sdk.webhooks import sign_webhook, verify_webhook_signature
|
|
40
47
|
from palette_sdk.testing import route_permission_issues
|
|
@@ -77,6 +84,11 @@ __all__ = [
|
|
|
77
84
|
"Event",
|
|
78
85
|
"EventPublisher",
|
|
79
86
|
"subscribe_event",
|
|
87
|
+
"events_module",
|
|
88
|
+
"service",
|
|
89
|
+
"services",
|
|
90
|
+
"ServicesClient",
|
|
91
|
+
"BrokerCallError",
|
|
80
92
|
"get_config",
|
|
81
93
|
"require_config",
|
|
82
94
|
"sign_webhook",
|
|
@@ -64,3 +64,15 @@ class AppInteropClient:
|
|
|
64
64
|
json_body=json,
|
|
65
65
|
timeout=timeout,
|
|
66
66
|
)
|
|
67
|
+
|
|
68
|
+
async def broker_call(self, target: str, payload: dict[str, Any] | None = None) -> Any:
|
|
69
|
+
"""OS-broker RPC call. Used by the high-level `services(ctx).call(...)`."""
|
|
70
|
+
if self._adapter is None:
|
|
71
|
+
raise MissingAppServiceError("Palette OS broker is not available in this runtime")
|
|
72
|
+
return await self._adapter.broker_call(target, payload or {})
|
|
73
|
+
|
|
74
|
+
async def broker_emit(self, target: str, payload: dict[str, Any] | None = None) -> None:
|
|
75
|
+
"""Emit an event through the OS broker."""
|
|
76
|
+
if self._adapter is None:
|
|
77
|
+
raise MissingAppServiceError("Palette OS broker is not available in this runtime")
|
|
78
|
+
await self._adapter.broker_emit(target, payload or {})
|
|
@@ -44,6 +44,13 @@ _pending: list[tuple[str, Handler]] = []
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def subscribe_event(topic: str, handler: Handler) -> None:
|
|
47
|
+
"""Subscribe to a topic.
|
|
48
|
+
|
|
49
|
+
Accepts either a legacy bare topic (`"task.created"`) or a fully qualified
|
|
50
|
+
broker target (`"core/v1#task.created"` or `"org/v1#member.updated"`). Both
|
|
51
|
+
forms are routed to the same core bus; the broker decodes the qualified
|
|
52
|
+
form on dispatch.
|
|
53
|
+
"""
|
|
47
54
|
_pending.append((topic, handler))
|
|
48
55
|
|
|
49
56
|
|
|
@@ -54,8 +61,31 @@ def drain_pending() -> list[tuple[str, Handler]]:
|
|
|
54
61
|
return out
|
|
55
62
|
|
|
56
63
|
|
|
64
|
+
def on(topic: str) -> Callable[[Handler], Handler]:
|
|
65
|
+
"""Decorator form of `subscribe_event`.
|
|
66
|
+
|
|
67
|
+
Usage::
|
|
68
|
+
|
|
69
|
+
@events.on("org/v1#member.updated")
|
|
70
|
+
async def refresh(event):
|
|
71
|
+
...
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def decorator(fn: Handler) -> Handler:
|
|
75
|
+
subscribe_event(topic, fn)
|
|
76
|
+
return fn
|
|
77
|
+
|
|
78
|
+
return decorator
|
|
79
|
+
|
|
80
|
+
|
|
57
81
|
class EventPublisher:
|
|
58
|
-
"""Publish app-owned events declared in palette-plugin.json provides.events.
|
|
82
|
+
"""Publish app-owned events declared in palette-plugin.json provides.events.
|
|
83
|
+
|
|
84
|
+
Accepts both legacy topics (`task.created`) and qualified broker targets
|
|
85
|
+
(`org/v1#member.updated`). The adapter (injected by the platform) routes
|
|
86
|
+
the call through the broker, which checks that the caller is the declared
|
|
87
|
+
provider and validates the payload against its schema.
|
|
88
|
+
"""
|
|
59
89
|
|
|
60
90
|
def __init__(self, adapter: Any = None):
|
|
61
91
|
self._adapter = adapter
|
|
@@ -64,3 +94,7 @@ class EventPublisher:
|
|
|
64
94
|
if self._adapter is None:
|
|
65
95
|
raise RuntimeError("Palette event publisher is not available in this runtime")
|
|
66
96
|
await self._adapter.publish(topic, payload or {})
|
|
97
|
+
|
|
98
|
+
# Alias: `events.emit(...)` reads more naturally for app-owned events.
|
|
99
|
+
async def emit(self, topic: str, payload: dict[str, Any] | None = None) -> None:
|
|
100
|
+
await self.publish(topic, payload)
|
|
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Literal
|
|
7
|
+
from typing import Any, Literal
|
|
8
8
|
|
|
9
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class AgentDefinition(BaseModel):
|
|
@@ -99,6 +99,22 @@ class AppServiceRouteSpec(BaseModel):
|
|
|
99
99
|
description: str = ""
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
class AppServiceMethodSpec(BaseModel):
|
|
103
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
104
|
+
|
|
105
|
+
id: str = Field(validation_alias=AliasChoices("id", "name"))
|
|
106
|
+
scope: str | None = None
|
|
107
|
+
version: str | None = None
|
|
108
|
+
label: str | None = None
|
|
109
|
+
description: str = ""
|
|
110
|
+
input_schema: dict[str, Any] | str | None = None
|
|
111
|
+
input: dict[str, Any] | str | None = None
|
|
112
|
+
output_schema: dict[str, Any] | str | None = None
|
|
113
|
+
output: dict[str, Any] | str | None = None
|
|
114
|
+
route_method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] | None = None
|
|
115
|
+
route_path: str | None = None
|
|
116
|
+
|
|
117
|
+
|
|
102
118
|
class ProvidedAppServiceSpec(BaseModel):
|
|
103
119
|
id: str
|
|
104
120
|
version: str | None = None
|
|
@@ -106,6 +122,22 @@ class ProvidedAppServiceSpec(BaseModel):
|
|
|
106
122
|
description: str = ""
|
|
107
123
|
permissions: list[str] = Field(default_factory=list)
|
|
108
124
|
routes: list[AppServiceRouteSpec] = Field(default_factory=list)
|
|
125
|
+
methods: list[AppServiceMethodSpec] = Field(default_factory=list)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ProvidedEventSpec(BaseModel):
|
|
129
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
130
|
+
|
|
131
|
+
id: str = Field(validation_alias=AliasChoices("id", "topic"))
|
|
132
|
+
version: str | None = None
|
|
133
|
+
label: str | None = None
|
|
134
|
+
description: str = ""
|
|
135
|
+
payload_schema: dict[str, Any] | str | None = None
|
|
136
|
+
schema_: dict[str, Any] | str | None = Field(
|
|
137
|
+
default=None,
|
|
138
|
+
validation_alias=AliasChoices("schema", "schema_"),
|
|
139
|
+
serialization_alias="schema",
|
|
140
|
+
)
|
|
109
141
|
|
|
110
142
|
|
|
111
143
|
class AppDependencySpec(BaseModel):
|
|
@@ -123,9 +155,16 @@ class ServiceDependencySpec(BaseModel):
|
|
|
123
155
|
provider: str | None = None
|
|
124
156
|
|
|
125
157
|
|
|
158
|
+
class ConsumedBrokerTargetSpec(BaseModel):
|
|
159
|
+
target: str
|
|
160
|
+
required: bool = True
|
|
161
|
+
reason: str = ""
|
|
162
|
+
|
|
163
|
+
|
|
126
164
|
class ProvidesSpec(BaseModel):
|
|
165
|
+
namespace: str | None = None
|
|
127
166
|
services: list[ProvidedAppServiceSpec] = Field(default_factory=list)
|
|
128
|
-
events: list[str] = Field(default_factory=list)
|
|
167
|
+
events: list[str | ProvidedEventSpec] = Field(default_factory=list)
|
|
129
168
|
|
|
130
169
|
|
|
131
170
|
class RequiresSpec(BaseModel):
|
|
@@ -134,6 +173,11 @@ class RequiresSpec(BaseModel):
|
|
|
134
173
|
events: list[str] = Field(default_factory=list)
|
|
135
174
|
|
|
136
175
|
|
|
176
|
+
class ConsumesSpec(BaseModel):
|
|
177
|
+
services: list[str | ConsumedBrokerTargetSpec] = Field(default_factory=list)
|
|
178
|
+
events: list[str | ConsumedBrokerTargetSpec] = Field(default_factory=list)
|
|
179
|
+
|
|
180
|
+
|
|
137
181
|
class PlatformServiceSpec(BaseModel):
|
|
138
182
|
required: bool = False
|
|
139
183
|
billing: Literal["org_wallet", "plugin_owner", "platform"] | None = None
|
|
@@ -167,6 +211,7 @@ class PluginManifest(BaseModel):
|
|
|
167
211
|
connections: list[ConnectionSpec] = Field(default_factory=list)
|
|
168
212
|
provides: ProvidesSpec | None = None
|
|
169
213
|
requires: RequiresSpec | None = None
|
|
214
|
+
consumes: ConsumesSpec | None = None
|
|
170
215
|
platform_services: list[Literal["llm", "redis", "storage", "vector"]] | dict[str, PlatformServiceSpec] = Field(default_factory=list)
|
|
171
216
|
rating: float = 0.0
|
|
172
217
|
reviews: int = 0
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""OS-broker service helpers - provider + consumer side.
|
|
2
|
+
|
|
3
|
+
Provider example::
|
|
4
|
+
|
|
5
|
+
from palette_sdk import service, PluginContext
|
|
6
|
+
|
|
7
|
+
@service("members.list", scope="org/members.read")
|
|
8
|
+
async def list_members(ctx: PluginContext, payload: dict) -> dict:
|
|
9
|
+
return {"items": [...]}
|
|
10
|
+
|
|
11
|
+
Consumer example::
|
|
12
|
+
|
|
13
|
+
from palette_sdk import services
|
|
14
|
+
|
|
15
|
+
members = await services(ctx).call("org/v1#members.list", {"team_id": 7})
|
|
16
|
+
|
|
17
|
+
The decorator captures handlers at import time; the platform's plugin loader
|
|
18
|
+
binds them into the broker registry when the plugin is mounted, prefixing the
|
|
19
|
+
method names with the plugin's declared namespace/version.
|
|
20
|
+
|
|
21
|
+
The consumer accessor goes through `ctx.apps.broker_call()`, which the
|
|
22
|
+
platform injects with the same signed-internal-call transport used elsewhere.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from collections.abc import Awaitable, Callable
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
ServiceHandler = Callable[[Any, dict[str, Any]], Awaitable[Any]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class ServiceDecl:
|
|
36
|
+
name: str
|
|
37
|
+
handler: ServiceHandler
|
|
38
|
+
scope: str | None = None
|
|
39
|
+
label: str | None = None
|
|
40
|
+
description: str | None = None
|
|
41
|
+
input_schema: dict | None = None
|
|
42
|
+
output_schema: dict | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_pending: list[ServiceDecl] = []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def service(
|
|
49
|
+
name: str,
|
|
50
|
+
*,
|
|
51
|
+
scope: str | None = None,
|
|
52
|
+
label: str | None = None,
|
|
53
|
+
description: str | None = None,
|
|
54
|
+
input_schema: dict | None = None,
|
|
55
|
+
output_schema: dict | None = None,
|
|
56
|
+
) -> Callable[[ServiceHandler], ServiceHandler]:
|
|
57
|
+
"""Mark an async function as a broker-callable service method."""
|
|
58
|
+
|
|
59
|
+
def decorator(fn: ServiceHandler) -> ServiceHandler:
|
|
60
|
+
_pending.append(
|
|
61
|
+
ServiceDecl(
|
|
62
|
+
name=name,
|
|
63
|
+
handler=fn,
|
|
64
|
+
scope=scope,
|
|
65
|
+
label=label,
|
|
66
|
+
description=description,
|
|
67
|
+
input_schema=input_schema,
|
|
68
|
+
output_schema=output_schema,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return fn
|
|
72
|
+
|
|
73
|
+
return decorator
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def drain_pending_services() -> list[ServiceDecl]:
|
|
77
|
+
"""Platform-only. Drain and return services registered since last drain."""
|
|
78
|
+
out = list(_pending)
|
|
79
|
+
_pending.clear()
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BrokerCallError(RuntimeError):
|
|
84
|
+
"""Raised when the broker rejects or fails a service call."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ServicesClient:
|
|
88
|
+
"""Returned by `services(ctx)`. Use `.call(target, payload)` or sugar `.proxy(...)`."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, adapter: Any | None):
|
|
91
|
+
self._adapter = adapter
|
|
92
|
+
|
|
93
|
+
async def call(self, target: str, payload: dict[str, Any] | None = None) -> Any:
|
|
94
|
+
if self._adapter is None:
|
|
95
|
+
raise BrokerCallError(
|
|
96
|
+
"Palette OS broker is not available in this runtime. "
|
|
97
|
+
"This call must run inside a plugin request handler."
|
|
98
|
+
)
|
|
99
|
+
return await self._adapter.broker_call(target, payload or {})
|
|
100
|
+
|
|
101
|
+
async def emit(self, target: str, payload: dict[str, Any] | None = None) -> None:
|
|
102
|
+
if self._adapter is None:
|
|
103
|
+
raise BrokerCallError("Palette OS broker is not available in this runtime.")
|
|
104
|
+
await self._adapter.broker_emit(target, payload or {})
|
|
105
|
+
|
|
106
|
+
def proxy(self, namespace_version: str) -> "_Proxy":
|
|
107
|
+
return _Proxy(self, namespace_version)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _Proxy:
|
|
111
|
+
"""Dotted-attribute syntax to broker target string."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, client: ServicesClient, namespace_version: str, segments: tuple[str, ...] = ()):
|
|
114
|
+
if "/" not in namespace_version:
|
|
115
|
+
raise ValueError("namespace_version must look like 'org/v1'")
|
|
116
|
+
self._client = client
|
|
117
|
+
self._ns = namespace_version
|
|
118
|
+
self._segments = segments
|
|
119
|
+
|
|
120
|
+
def __getattr__(self, item: str) -> "_Proxy":
|
|
121
|
+
return _Proxy(self._client, self._ns, self._segments + (item,))
|
|
122
|
+
|
|
123
|
+
async def __call__(self, payload: dict[str, Any] | None = None) -> Any:
|
|
124
|
+
if not self._segments:
|
|
125
|
+
raise BrokerCallError("missing method name on proxy call")
|
|
126
|
+
method = ".".join(self._segments)
|
|
127
|
+
return await self._client.call(f"{self._ns}#{method}", payload)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def services(ctx_or_adapter: Any) -> ServicesClient:
|
|
131
|
+
"""Resolve a `ServicesClient` from either a `PluginContext` or an adapter."""
|
|
132
|
+
if ctx_or_adapter is None:
|
|
133
|
+
return ServicesClient(None)
|
|
134
|
+
apps = getattr(ctx_or_adapter, "apps", None)
|
|
135
|
+
if apps is not None and hasattr(apps, "broker_call"):
|
|
136
|
+
return ServicesClient(apps)
|
|
137
|
+
return ServicesClient(ctx_or_adapter)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
__all__ = [
|
|
141
|
+
"BrokerCallError",
|
|
142
|
+
"ServiceDecl",
|
|
143
|
+
"ServicesClient",
|
|
144
|
+
"drain_pending_services",
|
|
145
|
+
"service",
|
|
146
|
+
"services",
|
|
147
|
+
]
|
|
@@ -114,7 +114,7 @@ Available context values:
|
|
|
114
114
|
| `ctx.data_rooms` | Backend Data Room client |
|
|
115
115
|
| `ctx.connections` | Palette-managed third-party connection client |
|
|
116
116
|
| `ctx.members` | Current organisation member client |
|
|
117
|
-
| `ctx.apps` | Governed app-to-app calls through declared `
|
|
117
|
+
| `ctx.apps` / `services(ctx)` | Governed app-to-app calls through declared `consumes` contracts |
|
|
118
118
|
| `ctx.events` | Publish event topics declared in `provides.events` |
|
|
119
119
|
| `ctx.redis` | Plugin/org-scoped Redis-style service when `platform_services` includes `redis` |
|
|
120
120
|
| `ctx.vector` | Plugin/org-scoped vector service when `platform_services` includes `vector` |
|
|
@@ -141,13 +141,15 @@ These are the public Python helpers exported by `palette_sdk`.
|
|
|
141
141
|
| `KNOWN_PERMISSIONS`, `is_known_permission(...)` | Permission vocabulary checks for manifests/tools |
|
|
142
142
|
| `DataRoomsClient`, `ctx.data_rooms` | Backend Data Room room/folder/file helpers |
|
|
143
143
|
| `OrganizationMembersClient`, `ctx.members` | Current-organization member lookup, invite, and role helpers |
|
|
144
|
-
| `
|
|
144
|
+
| `PluginConnectionsClient`, `ConnectionStatus`, `MissingConnectionError`, `ctx.connections` | Third-party connection status and token helpers |
|
|
145
|
+
| `AppInteropClient`, `AppServiceClient`, `MissingAppServiceError`, `ctx.apps` | Call required apps/services without direct database access |
|
|
146
|
+
| `service(...)`, `services(ctx)`, `ServicesClient`, `BrokerCallError` | Provide and consume OS-broker service methods/events |
|
|
145
147
|
| `OrgRepository`, `ctx.repo(Model)` | Org-safe convenience CRUD for app-owned models |
|
|
146
148
|
| `PluginBase`, `OrgScopedTable` | SQLAlchemy declarative bases for plugin-owned tables |
|
|
147
149
|
| `ensure_org_rls(op, table)` | Alembic helper that enables org row-level security |
|
|
148
150
|
| `plugin_safe_id(...)`, `plugin_schema(...)`, `plugin_table_prefix(...)` | Manifest id to database-safe naming helpers |
|
|
149
151
|
| `get_config(ctx, key)`, `require_config(ctx, key)` | Functional form of config reads when not using `ctx.config_value(...)` |
|
|
150
|
-
| `LocalRedisService`, `LocalVectorService` | Local `pltt dev` service emulators and test fakes |
|
|
152
|
+
| `LocalRedisService`, `LocalVectorService`, `LocalStorageService` | Local `pltt dev` service emulators and test fakes |
|
|
151
153
|
| `PlatformServiceUnavailable`, `UnavailablePlatformService` | Clear errors when an undeclared platform service is used |
|
|
152
154
|
| `LifecycleHooks` | Install/update/enable/disable/uninstall callbacks |
|
|
153
155
|
| `Event`, `EventPublisher`, `subscribe_event(...)` | In-process event subscriptions and declared app event publishing |
|
|
@@ -631,9 +633,10 @@ Declare secret ownership in `palette-plugin.json`:
|
|
|
631
633
|
```
|
|
632
634
|
|
|
633
635
|
`ctx.secret("KEY")` resolves declared secrets from the configured scope:
|
|
634
|
-
install config, plugin-scope encrypted secrets, or local `.
|
|
635
|
-
during `pltt dev`.
|
|
636
|
-
|
|
636
|
+
install config, plugin-scope encrypted secrets, or local root `.env` files
|
|
637
|
+
during `pltt dev`. During hosted preview/publish, server-only `.env` keys are
|
|
638
|
+
uploaded automatically as encrypted plugin secrets. Undeclared keys still fall
|
|
639
|
+
back to the process environment for local compatibility.
|
|
637
640
|
|
|
638
641
|
Managed Redis, vector, and storage services are declared in the manifest:
|
|
639
642
|
|
|
@@ -689,57 +692,84 @@ token = await ctx.connections.access_token("google_calendar")
|
|
|
689
692
|
|
|
690
693
|
## 11. App-To-App Services
|
|
691
694
|
|
|
692
|
-
Apps can expose governed services and
|
|
693
|
-
apps
|
|
695
|
+
Apps can expose governed broker services and events, then consume them from
|
|
696
|
+
other installed apps without knowing another app's URL.
|
|
697
|
+
|
|
698
|
+
Provider apps declare a namespace and callable methods with `provides`:
|
|
694
699
|
|
|
695
700
|
```json
|
|
696
701
|
{
|
|
697
702
|
"provides": {
|
|
703
|
+
"namespace": "hr",
|
|
698
704
|
"services": [
|
|
699
705
|
{
|
|
700
|
-
"id": "
|
|
701
|
-
"
|
|
702
|
-
|
|
703
|
-
{ "method": "GET", "path": "/hierarchy/approval-chain/{user_id}" }
|
|
706
|
+
"id": "hr.directory",
|
|
707
|
+
"methods": [
|
|
708
|
+
{ "name": "approvalChain.get", "input_schema": "schemas/approval-chain.input.json" }
|
|
704
709
|
]
|
|
705
710
|
}
|
|
706
711
|
],
|
|
707
|
-
"events": ["hierarchy.updated"]
|
|
712
|
+
"events": [{ "topic": "hierarchy.updated" }]
|
|
708
713
|
}
|
|
709
714
|
}
|
|
710
715
|
```
|
|
711
716
|
|
|
712
|
-
|
|
717
|
+
Expose the handler with `@service`:
|
|
718
|
+
|
|
719
|
+
```python
|
|
720
|
+
from palette_sdk import PluginContext, service
|
|
721
|
+
|
|
722
|
+
@service("approvalChain.get")
|
|
723
|
+
async def approval_chain(ctx: PluginContext, payload: dict) -> dict:
|
|
724
|
+
return {"approvers": [{"user_id": ctx.user_id, "step": 1}]}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Consumers declare qualified targets with `consumes`:
|
|
713
728
|
|
|
714
729
|
```json
|
|
715
730
|
{
|
|
716
|
-
"
|
|
731
|
+
"consumes": {
|
|
717
732
|
"services": [
|
|
718
|
-
{
|
|
733
|
+
{
|
|
734
|
+
"target": "hr/v1#approvalChain.get",
|
|
735
|
+
"required": true,
|
|
736
|
+
"reason": "Route leave approvals through the HR hierarchy"
|
|
737
|
+
}
|
|
738
|
+
],
|
|
739
|
+
"events": [
|
|
740
|
+
{ "target": "hr/v1#hierarchy.updated", "required": false }
|
|
719
741
|
]
|
|
720
742
|
}
|
|
721
743
|
}
|
|
722
744
|
```
|
|
723
745
|
|
|
724
|
-
|
|
725
|
-
the same app emits `leave.requested`, declare that topic under its own
|
|
726
|
-
`provides.events` first:
|
|
746
|
+
The CLI can update the manifest and generate typed clients for those targets:
|
|
727
747
|
|
|
728
|
-
```
|
|
729
|
-
|
|
730
|
-
|
|
748
|
+
```bash
|
|
749
|
+
pltt services add hr/v1#approvalChain.get --reason "Route leave approvals through HR"
|
|
750
|
+
pltt services add hr/v1#hierarchy.updated --event --optional
|
|
751
|
+
pltt services pull --env staging
|
|
731
752
|
```
|
|
732
753
|
|
|
733
|
-
|
|
754
|
+
`pltt services pull` writes `.palette/types/services.ts` for frontend code and
|
|
755
|
+
`.palette/python/services.py` for backend code from the schemas returned by the
|
|
756
|
+
selected Palette environment.
|
|
734
757
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
758
|
+
Then call through the OS broker; never read another app's database directly.
|
|
759
|
+
If the same app emits `leave.requested`, declare that topic under its own
|
|
760
|
+
`provides.events` first and emit with `ctx.events.emit`:
|
|
761
|
+
|
|
762
|
+
```python
|
|
763
|
+
from palette_sdk import services
|
|
764
|
+
|
|
765
|
+
chain = await services(ctx).call("hr/v1#approvalChain.get", {"user_id": ctx.user_id})
|
|
766
|
+
await ctx.events.emit("leave/v1#leave.requested", {"approval_chain": chain})
|
|
741
767
|
```
|
|
742
768
|
|
|
769
|
+
At install time, Palette checks required `consumes` targets and can show the
|
|
770
|
+
provider apps that must also be installed. The org-level grant is saved on the
|
|
771
|
+
install and the broker checks it on every call, emit, or event stream.
|
|
772
|
+
|
|
743
773
|
App storage is separate from Data Rooms. Use `ctx.storage` and `palette.storage` for app-owned files that go directly to the OS-configured storage backend, currently GCS in hosted environments. Use `ctx.data_rooms` or `palette.dataRooms` only when the file should be visible and governed as a Data Room document.
|
|
744
774
|
|
|
745
775
|
Palette scopes storage the same way. Files written through `ctx.storage` or the
|
|
@@ -886,7 +916,7 @@ point = ctx.vector.scoped_point(
|
|
|
886
916
|
await client.upsert(collection_name=collection, points=[point])
|
|
887
917
|
```
|
|
888
918
|
|
|
889
|
-
##
|
|
919
|
+
## 12. Lifecycle Hooks
|
|
890
920
|
|
|
891
921
|
Lifecycle hooks let an app seed defaults or clean up app-owned data when the
|
|
892
922
|
platform installs, updates, enables, disables, or uninstalls the app.
|
|
@@ -908,7 +938,7 @@ async def on_enable(ctx: PluginContext):
|
|
|
908
938
|
ctx.logger.info("finance tools enabled for org %s", ctx.organization_id)
|
|
909
939
|
```
|
|
910
940
|
|
|
911
|
-
##
|
|
941
|
+
## 13. Calling Backend APIs From Frontend
|
|
912
942
|
|
|
913
943
|
Frontend code should call backend routes through the platform API helper, not by
|
|
914
944
|
hardcoding backend origins.
|
|
@@ -925,7 +955,7 @@ async function loadInvoices() {
|
|
|
925
955
|
During `pltt dev`, the simulator routes this to the local backend. In hosted
|
|
926
956
|
sandbox or OS runtime, it goes through the real platform shell and auth context.
|
|
927
957
|
|
|
928
|
-
##
|
|
958
|
+
## 14. Local Development
|
|
929
959
|
|
|
930
960
|
Create and run an app:
|
|
931
961
|
|
|
@@ -947,7 +977,7 @@ The CLI checks manifest shape, SDK compatibility, frontend bundling, backend
|
|
|
947
977
|
imports, backend route permission gates, declared permissions, migration safety,
|
|
948
978
|
package dependency policy, and backend package size.
|
|
949
979
|
|
|
950
|
-
##
|
|
980
|
+
## 15. Test In Real Palette OS Without Docker
|
|
951
981
|
|
|
952
982
|
Configure the hosted sandbox once:
|
|
953
983
|
|
|
@@ -977,7 +1007,7 @@ APPSTORE_AUTO_APPROVE_SANDBOX_PREVIEWS=true
|
|
|
977
1007
|
|
|
978
1008
|
That lets passing preview publishes become active without manual approval.
|
|
979
1009
|
|
|
980
|
-
##
|
|
1010
|
+
## 16. Common Mistakes
|
|
981
1011
|
|
|
982
1012
|
- Do not import `@palettelab/sdk` from Python. That package is for frontend
|
|
983
1013
|
JavaScript/React code only.
|
|
@@ -992,7 +1022,7 @@ That lets passing preview publishes become active without manual approval.
|
|
|
992
1022
|
- Use hosted sandbox when you need real Data Rooms, login, organization,
|
|
993
1023
|
install, logs, and OS behavior without Docker.
|
|
994
1024
|
|
|
995
|
-
##
|
|
1025
|
+
## 17. Complete Minimal Example
|
|
996
1026
|
|
|
997
1027
|
`backend/api/main.py`:
|
|
998
1028
|
|