@palettelab/cli 0.3.50 → 0.3.52
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 +28 -2
- package/backend-sdk/palette_sdk/__init__.py +4 -0
- package/backend-sdk/palette_sdk/events.py +16 -0
- package/backend-sdk/palette_sdk/manifest.py +2 -0
- package/backend-sdk/palette_sdk/services.py +26 -2
- package/lib/bundler.js +22 -2
- package/lib/cli.js +8 -1
- package/lib/commands/init.js +1 -1
- package/lib/commands/services.js +255 -17
- package/lib/commands/version.js +20 -0
- package/lib/dev-simulator.js +12 -0
- package/lib/manifest.js +18 -2
- package/package.json +1 -1
- package/template-fallback/package.json +1 -1
- package/template-fallback/templates/consumer-app/README.md +10 -0
- package/template-fallback/templates/consumer-app/backend/api/main.py +15 -0
- package/template-fallback/templates/consumer-app/frontend/src/index.tsx +26 -0
- package/template-fallback/templates/consumer-app/package.json +9 -0
- package/template-fallback/templates/consumer-app/palette-plugin.json +45 -0
- package/template-fallback/templates/consumer-app/pyproject.toml +11 -0
- package/template-fallback/templates/dashboard/package.json +1 -1
- package/template-fallback/templates/database/package.json +1 -1
- package/template-fallback/templates/external-service/package.json +1 -1
- package/template-fallback/templates/frontend-only/package.json +1 -1
- package/template-fallback/templates/next/package.json +1 -1
- package/template-fallback/templates/palette-app/package.json +1 -1
- package/template-fallback/templates/provider-app/README.md +10 -0
- package/template-fallback/templates/provider-app/backend/api/main.py +25 -0
- package/template-fallback/templates/provider-app/package.json +8 -0
- package/template-fallback/templates/provider-app/palette-plugin.json +49 -0
- package/template-fallback/templates/provider-app/pyproject.toml +11 -0
- package/template-fallback/templates/provider-app/schemas/approvalChain.get.in.json +8 -0
- package/template-fallback/templates/provider-app/schemas/approvalChain.get.out.json +13 -0
- package/template-fallback/templates/provider-app/schemas/hierarchy.updated.json +8 -0
package/README.md
CHANGED
|
@@ -21,6 +21,13 @@ npx @palettelab/cli <command>
|
|
|
21
21
|
pltt <command>
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Check the installed CLI version with:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pltt version
|
|
28
|
+
pltt --version
|
|
29
|
+
```
|
|
30
|
+
|
|
24
31
|
## Quick Start: Build And Test A Palette App
|
|
25
32
|
|
|
26
33
|
Use this flow when a developer wants to create an app, run it locally, then test
|
|
@@ -667,6 +674,16 @@ pltt login --env staging --url https://sandbox.pltt.ai --token <publish-token>
|
|
|
667
674
|
pltt dev --sandbox --env staging
|
|
668
675
|
```
|
|
669
676
|
|
|
677
|
+
### `pltt version`
|
|
678
|
+
|
|
679
|
+
Show the installed `pltt` CLI version.
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
pltt version
|
|
683
|
+
pltt version --json
|
|
684
|
+
pltt --version
|
|
685
|
+
```
|
|
686
|
+
|
|
670
687
|
### `pltt doctor`
|
|
671
688
|
|
|
672
689
|
Check local tooling and common setup problems.
|
|
@@ -767,9 +784,14 @@ Inspect and generate OS-broker service integrations declared through
|
|
|
767
784
|
`provides` and `consumes` in `palette-plugin.json`.
|
|
768
785
|
|
|
769
786
|
```bash
|
|
787
|
+
pltt init employee-management --template provider-app
|
|
788
|
+
pltt init leave-management --template consumer-app
|
|
789
|
+
pltt services sync
|
|
770
790
|
pltt services list --env staging
|
|
771
791
|
pltt services add hr/v1#approvalChain.get --reason "Route approvals through HR"
|
|
772
792
|
pltt services add hr/v1#hierarchy.updated --event --optional
|
|
793
|
+
pltt services mock hr/v1#approvalChain.get
|
|
794
|
+
pltt services test --offline
|
|
773
795
|
pltt services pull --env staging
|
|
774
796
|
pltt services scaffold approvalChain.get
|
|
775
797
|
```
|
|
@@ -778,7 +800,10 @@ Subcommands:
|
|
|
778
800
|
|
|
779
801
|
- `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
802
|
- `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
|
-
- `
|
|
803
|
+
- `sync`: scans backend `@service(...)` decorators and updates `provides.services` so provider contracts do not drift from code.
|
|
804
|
+
- `mock <namespace/version#name>`: adds a local response to `.palette/app-services.local.json` for `pltt dev`.
|
|
805
|
+
- `test`: checks local consumes/provides/mocks and, unless `--offline` is set, verifies consumed targets against `/api/v1/os-broker/schemas`.
|
|
806
|
+
- `pull`: reads `consumes.services` and `consumes.events`, fetches JSON Schemas from `/api/v1/os-broker/schemas`, and generates nested `.palette/types/services.ts` plus Python helpers in `.palette/python/services.py`.
|
|
782
807
|
- `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
808
|
|
|
784
809
|
Generated TypeScript clients use the SDK's default `palette` client. Generated
|
|
@@ -787,7 +812,8 @@ through Palette's broker permission and install checks.
|
|
|
787
812
|
|
|
788
813
|
## Global Flags
|
|
789
814
|
|
|
790
|
-
- `--json` emits machine-readable output for `package`, `publish`, `status`, `logs`, and `
|
|
815
|
+
- `--json` emits machine-readable output for `package`, `publish`, `status`, `logs`, `test`, and `version`.
|
|
816
|
+
- `-v, --version` prints the installed `pltt` CLI version.
|
|
791
817
|
- `--env <name>` selects a configured publish/status/logs environment.
|
|
792
818
|
- `-y, --yes` skips production publish confirmation.
|
|
793
819
|
|
|
@@ -38,6 +38,8 @@ from palette_sdk import events as events_module # also exposed as `palette_sdk.
|
|
|
38
38
|
from palette_sdk.events import Event, EventPublisher, subscribe_event
|
|
39
39
|
from palette_sdk.services import (
|
|
40
40
|
BrokerCallError,
|
|
41
|
+
CrossAppGrantError,
|
|
42
|
+
MissingDependencyError,
|
|
41
43
|
ServicesClient,
|
|
42
44
|
service,
|
|
43
45
|
services,
|
|
@@ -89,6 +91,8 @@ __all__ = [
|
|
|
89
91
|
"services",
|
|
90
92
|
"ServicesClient",
|
|
91
93
|
"BrokerCallError",
|
|
94
|
+
"CrossAppGrantError",
|
|
95
|
+
"MissingDependencyError",
|
|
92
96
|
"get_config",
|
|
93
97
|
"require_config",
|
|
94
98
|
"sign_webhook",
|
|
@@ -98,3 +98,19 @@ class EventPublisher:
|
|
|
98
98
|
# Alias: `events.emit(...)` reads more naturally for app-owned events.
|
|
99
99
|
async def emit(self, topic: str, payload: dict[str, Any] | None = None) -> None:
|
|
100
100
|
await self.publish(topic, payload)
|
|
101
|
+
|
|
102
|
+
async def publish_durable(self, topic: str, payload: dict[str, Any] | None = None) -> None:
|
|
103
|
+
"""Publish a business event through the broker durable-event API.
|
|
104
|
+
|
|
105
|
+
Current platform versions route this through the standard broker event
|
|
106
|
+
publisher. The method is intentionally stable so apps can adopt durable
|
|
107
|
+
delivery without changing call sites when queue-backed delivery is
|
|
108
|
+
enabled by the platform.
|
|
109
|
+
"""
|
|
110
|
+
if hasattr(self._adapter, "publish_durable"):
|
|
111
|
+
await self._adapter.publish_durable(topic, payload or {})
|
|
112
|
+
return
|
|
113
|
+
await self.publish(topic, payload)
|
|
114
|
+
|
|
115
|
+
async def emit_durable(self, topic: str, payload: dict[str, Any] | None = None) -> None:
|
|
116
|
+
await self.publish_durable(topic, payload)
|
|
@@ -113,6 +113,8 @@ class AppServiceMethodSpec(BaseModel):
|
|
|
113
113
|
output: dict[str, Any] | str | None = None
|
|
114
114
|
route_method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] | None = None
|
|
115
115
|
route_path: str | None = None
|
|
116
|
+
deprecated: bool = False
|
|
117
|
+
replacement: str | None = None
|
|
116
118
|
|
|
117
119
|
|
|
118
120
|
class ProvidedAppServiceSpec(BaseModel):
|
|
@@ -35,11 +35,15 @@ ServiceHandler = Callable[[Any, dict[str, Any]], Awaitable[Any]]
|
|
|
35
35
|
class ServiceDecl:
|
|
36
36
|
name: str
|
|
37
37
|
handler: ServiceHandler
|
|
38
|
+
namespace: str | None = None
|
|
39
|
+
version: str | None = None
|
|
38
40
|
scope: str | None = None
|
|
39
41
|
label: str | None = None
|
|
40
42
|
description: str | None = None
|
|
41
43
|
input_schema: dict | None = None
|
|
42
44
|
output_schema: dict | None = None
|
|
45
|
+
deprecated: bool = False
|
|
46
|
+
replacement: str | None = None
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
_pending: list[ServiceDecl] = []
|
|
@@ -48,11 +52,17 @@ _pending: list[ServiceDecl] = []
|
|
|
48
52
|
def service(
|
|
49
53
|
name: str,
|
|
50
54
|
*,
|
|
55
|
+
namespace: str | None = None,
|
|
56
|
+
version: str | None = None,
|
|
51
57
|
scope: str | None = None,
|
|
52
58
|
label: str | None = None,
|
|
53
59
|
description: str | None = None,
|
|
54
60
|
input_schema: dict | None = None,
|
|
61
|
+
input: dict | None = None,
|
|
55
62
|
output_schema: dict | None = None,
|
|
63
|
+
output: dict | None = None,
|
|
64
|
+
deprecated: bool = False,
|
|
65
|
+
replacement: str | None = None,
|
|
56
66
|
) -> Callable[[ServiceHandler], ServiceHandler]:
|
|
57
67
|
"""Mark an async function as a broker-callable service method."""
|
|
58
68
|
|
|
@@ -61,11 +71,15 @@ def service(
|
|
|
61
71
|
ServiceDecl(
|
|
62
72
|
name=name,
|
|
63
73
|
handler=fn,
|
|
74
|
+
namespace=namespace,
|
|
75
|
+
version=version,
|
|
64
76
|
scope=scope,
|
|
65
77
|
label=label,
|
|
66
78
|
description=description,
|
|
67
|
-
input_schema=input_schema,
|
|
68
|
-
output_schema=output_schema,
|
|
79
|
+
input_schema=input_schema or input,
|
|
80
|
+
output_schema=output_schema or output,
|
|
81
|
+
deprecated=deprecated,
|
|
82
|
+
replacement=replacement,
|
|
69
83
|
)
|
|
70
84
|
)
|
|
71
85
|
return fn
|
|
@@ -84,6 +98,14 @@ class BrokerCallError(RuntimeError):
|
|
|
84
98
|
"""Raised when the broker rejects or fails a service call."""
|
|
85
99
|
|
|
86
100
|
|
|
101
|
+
class MissingDependencyError(BrokerCallError):
|
|
102
|
+
"""Raised by generated clients when a required provider app is missing."""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CrossAppGrantError(BrokerCallError):
|
|
106
|
+
"""Raised by generated clients when an org admin revoked a cross-app grant."""
|
|
107
|
+
|
|
108
|
+
|
|
87
109
|
class ServicesClient:
|
|
88
110
|
"""Returned by `services(ctx)`. Use `.call(target, payload)` or sugar `.proxy(...)`."""
|
|
89
111
|
|
|
@@ -139,6 +161,8 @@ def services(ctx_or_adapter: Any) -> ServicesClient:
|
|
|
139
161
|
|
|
140
162
|
__all__ = [
|
|
141
163
|
"BrokerCallError",
|
|
164
|
+
"CrossAppGrantError",
|
|
165
|
+
"MissingDependencyError",
|
|
142
166
|
"ServiceDecl",
|
|
143
167
|
"ServicesClient",
|
|
144
168
|
"drain_pending_services",
|
package/lib/bundler.js
CHANGED
|
@@ -247,6 +247,25 @@ function currentSearchParams() {
|
|
|
247
247
|
return new URLSearchParams(window.location.search)
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
function preserveCurrentPreviewParams(path, pluginId) {
|
|
251
|
+
if (typeof window === "undefined" || !pluginId || /^https?:\\/\\//i.test(path)) return path
|
|
252
|
+
|
|
253
|
+
const currentParams = new URLSearchParams(window.location.search)
|
|
254
|
+
const previewPublishId = currentParams.get("preview_publish_id")
|
|
255
|
+
const previewToken = currentParams.get("preview_token")
|
|
256
|
+
if (!previewPublishId || !previewToken) return path
|
|
257
|
+
|
|
258
|
+
const [beforeHash, hash = ""] = path.split("#", 2)
|
|
259
|
+
const [pathname, query = ""] = beforeHash.split("?", 2)
|
|
260
|
+
if (pathname !== "/apps/" + pluginId && !pathname.startsWith("/apps/" + pluginId + "/")) return path
|
|
261
|
+
|
|
262
|
+
const params = new URLSearchParams(query)
|
|
263
|
+
params.set("preview_publish_id", previewPublishId)
|
|
264
|
+
params.set("preview_token", previewToken)
|
|
265
|
+
const serialized = params.toString()
|
|
266
|
+
return pathname + (serialized ? "?" + serialized : "") + (hash ? "#" + hash : "")
|
|
267
|
+
}
|
|
268
|
+
|
|
250
269
|
function renderRoute(route) {
|
|
251
270
|
const page = createElement(route.page)
|
|
252
271
|
return (route.layouts || []).reduceRight((children, Layout) => createElement(Layout, null, children), page)
|
|
@@ -284,8 +303,9 @@ export function PaletteAppRouter({ routes, notFound: NotFound }) {
|
|
|
284
303
|
const osPath = inPalettePath && platform.pluginId
|
|
285
304
|
? "/apps/" + platform.pluginId + (pathname === "/" ? "" : pathname) + (queryPart ? "?" + queryPart : "")
|
|
286
305
|
: next
|
|
287
|
-
|
|
288
|
-
|
|
306
|
+
const preservedOsPath = preserveCurrentPreviewParams(osPath, platform.pluginId)
|
|
307
|
+
if (replace && typeof window !== "undefined") window.history.replaceState(null, "", preservedOsPath)
|
|
308
|
+
else platform.navigate(preservedOsPath)
|
|
289
309
|
}, [platform])
|
|
290
310
|
const state = useMemo(() => ({
|
|
291
311
|
pathname: location.pathname,
|
package/lib/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ const status = require("./commands/status")
|
|
|
12
12
|
const logs = require("./commands/logs")
|
|
13
13
|
const secrets = require("./commands/secrets")
|
|
14
14
|
const services = require("./commands/services")
|
|
15
|
+
const version = require("./commands/version")
|
|
15
16
|
|
|
16
17
|
const COMMANDS = {
|
|
17
18
|
init: { run: init, help: "Scaffold a new plugin directory from the template" },
|
|
@@ -55,6 +56,7 @@ const COMMANDS = {
|
|
|
55
56
|
run: services,
|
|
56
57
|
help: "Inspect / pull / scaffold OS-broker services (list, pull, scaffold)",
|
|
57
58
|
},
|
|
59
|
+
version: { run: version, help: "Show the installed pltt CLI version" },
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
function printHelp() {
|
|
@@ -65,7 +67,8 @@ function printHelp() {
|
|
|
65
67
|
console.log(` ${name.padEnd(8)} ${help}`)
|
|
66
68
|
}
|
|
67
69
|
console.log("\nGlobal flags:")
|
|
68
|
-
console.log(" --json Emit machine-readable JSON output (status, logs, package, publish, test)")
|
|
70
|
+
console.log(" --json Emit machine-readable JSON output (status, logs, package, publish, test, version)")
|
|
71
|
+
console.log(" -v, --version Show the installed pltt CLI version")
|
|
69
72
|
console.log("\nPublish flags:")
|
|
70
73
|
console.log(" --env <name> Target environment from ~/.palette/config.json (default: local)")
|
|
71
74
|
console.log(" -y, --yes Skip interactive confirmation for production pushes")
|
|
@@ -108,6 +111,10 @@ function printHelp() {
|
|
|
108
111
|
|
|
109
112
|
async function run(argv) {
|
|
110
113
|
const cmd = argv[0]
|
|
114
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
115
|
+
version(argv.slice(1))
|
|
116
|
+
return
|
|
117
|
+
}
|
|
111
118
|
if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
112
119
|
printHelp()
|
|
113
120
|
return
|
package/lib/commands/init.js
CHANGED
|
@@ -10,7 +10,7 @@ const DEFAULT_TEMPLATE_REPO = "palette-lab/plugin-template"
|
|
|
10
10
|
const TEMPLATE_REPO = process.env.PALETTE_TEMPLATE_REPO || DEFAULT_TEMPLATE_REPO
|
|
11
11
|
const TEMPLATE_REF = process.env.PALETTE_TEMPLATE_REF || "main"
|
|
12
12
|
|
|
13
|
-
const KNOWN_TEMPLATES = ["frontend-only", "palette-app", "next", "dashboard", "agent-tool", "external-service", "database"]
|
|
13
|
+
const KNOWN_TEMPLATES = ["frontend-only", "palette-app", "next", "dashboard", "agent-tool", "external-service", "database", "provider-app", "consumer-app"]
|
|
14
14
|
|
|
15
15
|
function toSlug(name) {
|
|
16
16
|
return name
|
package/lib/commands/services.js
CHANGED
|
@@ -180,6 +180,54 @@ function safeIdent(s) {
|
|
|
180
180
|
return s.replace(/[^A-Za-z0-9_]/g, "_")
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
function safePythonIdent(s) {
|
|
184
|
+
return safeIdent(s).replace(/^(?=\d)/, "_")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pythonMethodName(s) {
|
|
188
|
+
return safePythonIdent(
|
|
189
|
+
s
|
|
190
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
191
|
+
.replace(/[.-]+/g, "_")
|
|
192
|
+
.toLowerCase(),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function nestedAssign(root, dotted, value) {
|
|
197
|
+
const parts = dotted.split(".").filter(Boolean)
|
|
198
|
+
let cursor = root
|
|
199
|
+
while (parts.length > 1) {
|
|
200
|
+
const part = parts.shift()
|
|
201
|
+
cursor[part] = cursor[part] || {}
|
|
202
|
+
cursor = cursor[part]
|
|
203
|
+
}
|
|
204
|
+
cursor[parts[0] || dotted] = value
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderTsObject(node, indent = " ") {
|
|
208
|
+
const lines = ["{"]
|
|
209
|
+
for (const [key, value] of Object.entries(node)) {
|
|
210
|
+
const prop = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key)
|
|
211
|
+
if (value && value.__service) {
|
|
212
|
+
if (value.scope) lines.push(`${indent}/** scope: ${value.scope} */`)
|
|
213
|
+
lines.push(`${indent}async ${prop}(input: ${value.inT}): Promise<${value.outT}> {`)
|
|
214
|
+
lines.push(`${indent} return (await palette.broker.call(${JSON.stringify(value.target)}, input)) as ${value.outT}`)
|
|
215
|
+
lines.push(`${indent}},`)
|
|
216
|
+
} else if (value && value.__event) {
|
|
217
|
+
lines.push(`${indent}${prop}(handler: (payload: ${value.payT}) => void) {`)
|
|
218
|
+
lines.push(`${indent} return palette.events.on(${JSON.stringify(value.target)}, (payload) => handler(payload as ${value.payT}))`)
|
|
219
|
+
lines.push(`${indent}},`)
|
|
220
|
+
} else {
|
|
221
|
+
const rendered = renderTsObject(value, indent + " ").split("\n")
|
|
222
|
+
lines.push(`${indent}${prop}: ${rendered[0]}`)
|
|
223
|
+
for (const line of rendered.slice(1)) lines.push(line)
|
|
224
|
+
lines[lines.length - 1] += ","
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
lines.push(`${indent.slice(0, -2)}}`)
|
|
228
|
+
return lines.join("\n")
|
|
229
|
+
}
|
|
230
|
+
|
|
183
231
|
function tsHeader() {
|
|
184
232
|
return (
|
|
185
233
|
"// AUTOGENERATED by `pltt services pull` — do not edit by hand.\n" +
|
|
@@ -194,7 +242,7 @@ function pyHeader() {
|
|
|
194
242
|
"from __future__ import annotations\n\n" +
|
|
195
243
|
"from typing import Any\n\n" +
|
|
196
244
|
"from pydantic import BaseModel\n" +
|
|
197
|
-
"from palette_sdk import services as
|
|
245
|
+
"from palette_sdk import services as _palette_services\n\n"
|
|
198
246
|
)
|
|
199
247
|
}
|
|
200
248
|
|
|
@@ -240,24 +288,28 @@ async function runPull(argv, { cwd }) {
|
|
|
240
288
|
const services = items.filter((i) => i.kind === "service")
|
|
241
289
|
const events = items.filter((i) => i.kind === "event")
|
|
242
290
|
if (services.length) {
|
|
243
|
-
|
|
291
|
+
const tree = {}
|
|
244
292
|
for (const svc of services) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
293
|
+
nestedAssign(tree, svc.method, {
|
|
294
|
+
__service: true,
|
|
295
|
+
target: `${ns}#${svc.method}`,
|
|
296
|
+
inT: svc.inT,
|
|
297
|
+
outT: svc.outT,
|
|
298
|
+
scope: svc.scope,
|
|
299
|
+
})
|
|
250
300
|
}
|
|
251
|
-
tsLines.push(`}\n`)
|
|
301
|
+
tsLines.push(`export const ${safe} = ${renderTsObject(tree)}\n`)
|
|
252
302
|
}
|
|
253
303
|
if (events.length) {
|
|
254
|
-
|
|
304
|
+
const tree = {}
|
|
255
305
|
for (const evt of events) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
306
|
+
nestedAssign(tree, evt.topic, {
|
|
307
|
+
__event: true,
|
|
308
|
+
target: `${ns}#${evt.topic}`,
|
|
309
|
+
payT: evt.payT,
|
|
310
|
+
})
|
|
259
311
|
}
|
|
260
|
-
tsLines.push(`}\n`)
|
|
312
|
+
tsLines.push(`export const ${safe}Events = ${renderTsObject(tree)}\n`)
|
|
261
313
|
}
|
|
262
314
|
}
|
|
263
315
|
const tsDir = path.join(cwd, ".palette", "types")
|
|
@@ -270,7 +322,7 @@ async function runPull(argv, { cwd }) {
|
|
|
270
322
|
const entry = byTarget.get(target)
|
|
271
323
|
if (!entry) continue
|
|
272
324
|
const safeNs = safeIdent(`${entry.namespace}_${entry.version}`)
|
|
273
|
-
const safeMethod =
|
|
325
|
+
const safeMethod = pythonMethodName(entry.method || entry.topic)
|
|
274
326
|
if (entry.kind === "service") {
|
|
275
327
|
const { code: inCode, ref: inRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Input`, entry.input_schema)
|
|
276
328
|
const { code: outCode, ref: outRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Output`, entry.output_schema)
|
|
@@ -280,7 +332,7 @@ async function runPull(argv, { cwd }) {
|
|
|
280
332
|
`async def ${safeNs}_${safeMethod}(ctx, payload: ${inRef}) -> ${outRef}:\n` +
|
|
281
333
|
` """Call ${entry.namespace}/${entry.version}#${entry.method}.\n\n` +
|
|
282
334
|
(entry.scope ? ` Scope: ${entry.scope}\n """\n` : ` """\n`) +
|
|
283
|
-
` return await
|
|
335
|
+
` return await _palette_services(ctx).call(${JSON.stringify(target)}, payload if isinstance(payload, dict) else payload.model_dump())\n`,
|
|
284
336
|
)
|
|
285
337
|
} else if (entry.kind === "event") {
|
|
286
338
|
const { code: payCode, ref: payRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Payload`, entry.payload_schema)
|
|
@@ -303,6 +355,185 @@ async function runPull(argv, { cwd }) {
|
|
|
303
355
|
if (missing.length) process.exit(2)
|
|
304
356
|
}
|
|
305
357
|
|
|
358
|
+
// ---- sync provider contracts from Python decorators -------------------------
|
|
359
|
+
|
|
360
|
+
function listPythonFiles(dir, out = []) {
|
|
361
|
+
if (!fs.existsSync(dir)) return out
|
|
362
|
+
for (const name of fs.readdirSync(dir)) {
|
|
363
|
+
const p = path.join(dir, name)
|
|
364
|
+
const stat = fs.statSync(p)
|
|
365
|
+
if (stat.isDirectory()) {
|
|
366
|
+
if (![".venv", "__pycache__", ".palette", "node_modules"].includes(name)) listPythonFiles(p, out)
|
|
367
|
+
} else if (name.endsWith(".py")) {
|
|
368
|
+
out.push(p)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return out
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function argValue(source, name) {
|
|
375
|
+
const match = source.match(new RegExp(`${name}\\s*=\\s*["']([^"']+)["']`))
|
|
376
|
+
return match ? match[1] : null
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function boolArg(source, name) {
|
|
380
|
+
return new RegExp(`${name}\\s*=\\s*True`).test(source)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function discoverServiceDecorators(cwd) {
|
|
384
|
+
const files = listPythonFiles(path.join(cwd, "backend"))
|
|
385
|
+
const found = []
|
|
386
|
+
for (const file of files) {
|
|
387
|
+
const text = fs.readFileSync(file, "utf8")
|
|
388
|
+
const re = /@service\s*\(([\s\S]*?)\)\s*(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)/g
|
|
389
|
+
let match
|
|
390
|
+
while ((match = re.exec(text))) {
|
|
391
|
+
const args = match[1]
|
|
392
|
+
const name = (args.match(/["']([^"']+)["']/) || [])[1]
|
|
393
|
+
if (!name) continue
|
|
394
|
+
found.push({
|
|
395
|
+
name,
|
|
396
|
+
fn: match[2],
|
|
397
|
+
scope: argValue(args, "scope"),
|
|
398
|
+
label: argValue(args, "label"),
|
|
399
|
+
description: argValue(args, "description"),
|
|
400
|
+
namespace: argValue(args, "namespace"),
|
|
401
|
+
version: argValue(args, "version") || "v1",
|
|
402
|
+
input_schema: argValue(args, "input_schema") || argValue(args, "input"),
|
|
403
|
+
output_schema: argValue(args, "output_schema") || argValue(args, "output"),
|
|
404
|
+
deprecated: boolArg(args, "deprecated"),
|
|
405
|
+
replacement: argValue(args, "replacement"),
|
|
406
|
+
file: path.relative(cwd, file),
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return found
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function runSync(argv, { cwd }) {
|
|
414
|
+
const manifest = readManifest(cwd)
|
|
415
|
+
const decorators = discoverServiceDecorators(cwd)
|
|
416
|
+
if (!decorators.length) {
|
|
417
|
+
console.log("[pltt] no @service(...) decorators found under backend/.")
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
manifest.provides = manifest.provides || {}
|
|
421
|
+
manifest.provides.namespace = valueAfter(argv, "--namespace") || decorators.find(d => d.namespace)?.namespace || manifest.provides.namespace || manifest.id
|
|
422
|
+
manifest.provides.services = manifest.provides.services || []
|
|
423
|
+
for (const decl of decorators) {
|
|
424
|
+
const serviceId = safeIdent(decl.name.split(".")[0] || decl.name).toLowerCase().replace(/_/g, ".")
|
|
425
|
+
let svc = manifest.provides.services.find(s => s && s.id === serviceId)
|
|
426
|
+
if (!svc) {
|
|
427
|
+
svc = { id: serviceId, version: decl.version || "v1", methods: [] }
|
|
428
|
+
manifest.provides.services.push(svc)
|
|
429
|
+
}
|
|
430
|
+
svc.version = svc.version || decl.version || "v1"
|
|
431
|
+
svc.methods = svc.methods || []
|
|
432
|
+
let method = svc.methods.find(m => m && m.name === decl.name)
|
|
433
|
+
if (!method) {
|
|
434
|
+
method = { name: decl.name }
|
|
435
|
+
svc.methods.push(method)
|
|
436
|
+
}
|
|
437
|
+
if (decl.scope) method.scope = decl.scope
|
|
438
|
+
if (decl.label) method.label = decl.label
|
|
439
|
+
if (decl.description) method.description = decl.description
|
|
440
|
+
if (decl.input_schema) method.input_schema = decl.input_schema
|
|
441
|
+
else method.input_schema = method.input_schema || `schemas/${decl.name}.in.json`
|
|
442
|
+
if (decl.output_schema) method.output_schema = decl.output_schema
|
|
443
|
+
else method.output_schema = method.output_schema || `schemas/${decl.name}.out.json`
|
|
444
|
+
if (decl.deprecated) method.deprecated = true
|
|
445
|
+
if (decl.replacement) method.replacement = decl.replacement
|
|
446
|
+
}
|
|
447
|
+
writeManifest(cwd, manifest)
|
|
448
|
+
console.log(`[pltt] synced ${decorators.length} service method(s) into palette-plugin.json`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---- mock -------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
function sampleFromSchema(schema) {
|
|
454
|
+
if (!schema || typeof schema !== "object") return {}
|
|
455
|
+
if (schema.example !== undefined) return schema.example
|
|
456
|
+
if (schema.type === "string") return "string"
|
|
457
|
+
if (schema.type === "integer" || schema.type === "number") return 1
|
|
458
|
+
if (schema.type === "boolean") return true
|
|
459
|
+
if (schema.type === "array") return [sampleFromSchema(schema.items)]
|
|
460
|
+
const props = schema.properties || {}
|
|
461
|
+
const out = {}
|
|
462
|
+
for (const [key, prop] of Object.entries(props)) out[key] = sampleFromSchema(prop)
|
|
463
|
+
return out
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function runMock(argv, { cwd }) {
|
|
467
|
+
const target = argv.find((arg) => !arg.startsWith("-"))
|
|
468
|
+
if (!target || !brokerTargetLooksValid(target)) {
|
|
469
|
+
console.error("[pltt] usage: pltt services mock <namespace/version#name>")
|
|
470
|
+
process.exit(1)
|
|
471
|
+
}
|
|
472
|
+
const mockPath = path.join(cwd, ".palette", "app-services.local.json")
|
|
473
|
+
ensureDir(path.dirname(mockPath))
|
|
474
|
+
const current = fs.existsSync(mockPath) ? JSON.parse(fs.readFileSync(mockPath, "utf8")) : {}
|
|
475
|
+
current[target] = current[target] || {
|
|
476
|
+
ok: true,
|
|
477
|
+
target,
|
|
478
|
+
items: [],
|
|
479
|
+
}
|
|
480
|
+
fs.writeFileSync(mockPath, JSON.stringify(current, null, 2) + "\n")
|
|
481
|
+
console.log(`[pltt] wrote local mock for ${target} -> ${path.relative(cwd, mockPath)}`)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ---- test -------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
function validateLocalMocks(cwd, targets) {
|
|
487
|
+
const mockPath = path.join(cwd, ".palette", "app-services.local.json")
|
|
488
|
+
if (!fs.existsSync(mockPath)) return []
|
|
489
|
+
const mocks = JSON.parse(fs.readFileSync(mockPath, "utf8"))
|
|
490
|
+
const errors = []
|
|
491
|
+
for (const key of Object.keys(mocks)) {
|
|
492
|
+
const target = key.split(" ")[0]
|
|
493
|
+
if (!brokerTargetLooksValid(target)) errors.push(`mock key is not a broker target: ${key}`)
|
|
494
|
+
if (!targets.has(target)) errors.push(`mock target is not declared in consumes: ${target}`)
|
|
495
|
+
}
|
|
496
|
+
return errors
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function runTest(argv, { cwd }) {
|
|
500
|
+
const manifest = readManifest(cwd)
|
|
501
|
+
const { services, events } = consumeTargets(manifest)
|
|
502
|
+
const targets = new Set([...services, ...events])
|
|
503
|
+
const errors = []
|
|
504
|
+
for (const target of targets) {
|
|
505
|
+
if (!brokerTargetLooksValid(target)) errors.push(`invalid broker target: ${target}`)
|
|
506
|
+
}
|
|
507
|
+
errors.push(...validateLocalMocks(cwd, targets))
|
|
508
|
+
const decorators = discoverServiceDecorators(cwd)
|
|
509
|
+
const provided = new Set()
|
|
510
|
+
const provides = manifest.provides || {}
|
|
511
|
+
const ns = provides.namespace || manifest.id
|
|
512
|
+
for (const svc of provides.services || []) {
|
|
513
|
+
for (const method of svc.methods || []) {
|
|
514
|
+
if (method.name) provided.add(`${ns}/${svc.version || "v1"}#${method.name}`)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const decl of decorators) {
|
|
518
|
+
const target = `${decl.namespace || ns}/${decl.version || "v1"}#${decl.name}`
|
|
519
|
+
if (!provided.has(target)) errors.push(`@service ${target} is not declared in provides; run pltt services sync`)
|
|
520
|
+
}
|
|
521
|
+
if (!argv.includes("--offline") && targets.size) {
|
|
522
|
+
const { flags } = parseFlags(argv)
|
|
523
|
+
const env = resolveEnvironment({ cwd, flags })
|
|
524
|
+
const schemas = await authFetch(env, `/api/v1/os-broker/schemas?targets=${encodeURIComponent(Array.from(targets).join(","))}`)
|
|
525
|
+
const returned = new Set(schemas.map(s => s.target))
|
|
526
|
+
for (const target of targets) {
|
|
527
|
+
if (!returned.has(target)) errors.push(`target not found in ${env.name || env.url}: ${target}`)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (errors.length) {
|
|
531
|
+
for (const error of errors) console.error(`[pltt] ${error}`)
|
|
532
|
+
process.exit(1)
|
|
533
|
+
}
|
|
534
|
+
console.log(`[pltt] services contract check passed (${targets.size} consumed, ${decorators.length} provider decorator(s)).`)
|
|
535
|
+
}
|
|
536
|
+
|
|
306
537
|
// ---- scaffold provider stub --------------------------------------------------
|
|
307
538
|
|
|
308
539
|
async function runScaffold(argv, { cwd }) {
|
|
@@ -318,9 +549,10 @@ async function runScaffold(argv, { cwd }) {
|
|
|
318
549
|
provides.namespace = namespace
|
|
319
550
|
provides.services = provides.services || []
|
|
320
551
|
// Ensure a default service slot.
|
|
321
|
-
|
|
552
|
+
const serviceId = safeIdent(method.split(".")[0] || method).toLowerCase().replace(/_/g, ".")
|
|
553
|
+
let svc = provides.services.find((s) => (s.id || "") === serviceId) || null
|
|
322
554
|
if (!svc) {
|
|
323
|
-
svc = { id:
|
|
555
|
+
svc = { id: serviceId, version: "v1", methods: [] }
|
|
324
556
|
provides.services.push(svc)
|
|
325
557
|
}
|
|
326
558
|
svc.methods = svc.methods || []
|
|
@@ -410,11 +642,17 @@ async function run(argv, ctx) {
|
|
|
410
642
|
const rest = argv.slice(1)
|
|
411
643
|
if (sub === "list") return runList(rest, ctx)
|
|
412
644
|
if (sub === "pull") return runPull(rest, ctx)
|
|
645
|
+
if (sub === "sync") return runSync(rest, ctx)
|
|
646
|
+
if (sub === "mock") return runMock(rest, ctx)
|
|
647
|
+
if (sub === "test") return runTest(rest, ctx)
|
|
413
648
|
if (sub === "scaffold") return runScaffold(rest, ctx)
|
|
414
649
|
if (sub === "add") return runAdd(rest, ctx)
|
|
415
650
|
console.log("pltt services <command>\n")
|
|
416
651
|
console.log(" list Show services/events available to the current org.")
|
|
417
652
|
console.log(" pull Generate typed TS+Python clients from consumes block.")
|
|
653
|
+
console.log(" sync Update provides from backend @service decorators.")
|
|
654
|
+
console.log(" mock Add a local .palette/app-services.local.json response.")
|
|
655
|
+
console.log(" test Validate consumed targets, local mocks, and provider declarations.")
|
|
418
656
|
console.log(" scaffold Add a new provider method + schemas + handler stub.")
|
|
419
657
|
console.log(" add Add a consumed service/event target to palette-plugin.json.")
|
|
420
658
|
if (sub && !["help", "--help", "-h"].includes(sub)) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const pkg = require("../../package.json")
|
|
4
|
+
|
|
5
|
+
function version(args = []) {
|
|
6
|
+
if (args.includes("--json")) {
|
|
7
|
+
console.log(
|
|
8
|
+
JSON.stringify({
|
|
9
|
+
name: pkg.name,
|
|
10
|
+
bin: "pltt",
|
|
11
|
+
version: pkg.version,
|
|
12
|
+
}),
|
|
13
|
+
)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(`pltt ${pkg.version}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = version
|
package/lib/dev-simulator.js
CHANGED
|
@@ -272,6 +272,18 @@ class LocalAppInteropService:
|
|
|
272
272
|
return DEV_APP_MOCKS[key]
|
|
273
273
|
raise RuntimeError("No local app service mock configured for " + key)
|
|
274
274
|
|
|
275
|
+
async def broker_call(self, target: str, payload=None):
|
|
276
|
+
payload = payload or {}
|
|
277
|
+
payload_key = target + " " + json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
278
|
+
if payload_key in DEV_APP_MOCKS:
|
|
279
|
+
return DEV_APP_MOCKS[payload_key]
|
|
280
|
+
if target in DEV_APP_MOCKS:
|
|
281
|
+
return DEV_APP_MOCKS[target]
|
|
282
|
+
raise RuntimeError("No local broker mock configured for " + target)
|
|
283
|
+
|
|
284
|
+
async def broker_emit(self, target: str, payload=None):
|
|
285
|
+
print("[palette-broker-event]", target, json.dumps(payload or {}, sort_keys=True))
|
|
286
|
+
|
|
275
287
|
class LocalAppServiceClient:
|
|
276
288
|
def __init__(self, service_id: str):
|
|
277
289
|
self.service_id = service_id
|
package/lib/manifest.js
CHANGED
|
@@ -108,6 +108,12 @@ function requireString(obj, key, label, errors) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function requireSchema(obj, key, label, errors) {
|
|
112
|
+
if (obj[key] !== undefined && typeof obj[key] !== "string" && !isObject(obj[key])) {
|
|
113
|
+
errors.push(`${label}.${key} must be a JSON schema object or schema file path`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
function validateArray(value, label, errors) {
|
|
112
118
|
if (value !== undefined && !Array.isArray(value)) errors.push(`${label} must be an array`)
|
|
113
119
|
}
|
|
@@ -294,6 +300,8 @@ function validateServiceMethods(value, label, errors) {
|
|
|
294
300
|
"output",
|
|
295
301
|
"route_method",
|
|
296
302
|
"route_path",
|
|
303
|
+
"deprecated",
|
|
304
|
+
"replacement",
|
|
297
305
|
]),
|
|
298
306
|
methodLabel,
|
|
299
307
|
errors,
|
|
@@ -304,9 +312,14 @@ function validateServiceMethods(value, label, errors) {
|
|
|
304
312
|
errors.push(`duplicate provided method name: ${method.name}`)
|
|
305
313
|
}
|
|
306
314
|
seen.add(method.name)
|
|
307
|
-
for (const key of ["scope", "label", "description", "
|
|
315
|
+
for (const key of ["scope", "label", "description", "route_path"]) {
|
|
308
316
|
requireString(method, key, methodLabel, errors)
|
|
309
317
|
}
|
|
318
|
+
requireBoolean(method, "deprecated", methodLabel, errors)
|
|
319
|
+
requireString(method, "replacement", methodLabel, errors)
|
|
320
|
+
for (const key of ["input_schema", "input", "output_schema", "output"]) {
|
|
321
|
+
requireSchema(method, key, methodLabel, errors)
|
|
322
|
+
}
|
|
310
323
|
if (method.route_method !== undefined && !methods.has(String(method.route_method).toUpperCase())) {
|
|
311
324
|
errors.push(`${methodLabel}.route_method must be one of ${Array.from(methods).join(", ")}`)
|
|
312
325
|
}
|
|
@@ -374,9 +387,12 @@ function validateProvides(value, errors) {
|
|
|
374
387
|
} else if (isObject(event)) {
|
|
375
388
|
unknownKeys(event, new Set(["topic", "name", "version", "payload_schema", "schema", "description"]), label, errors)
|
|
376
389
|
topic = event.topic || event.name
|
|
377
|
-
for (const key of ["version", "
|
|
390
|
+
for (const key of ["version", "description"]) {
|
|
378
391
|
requireString(event, key, label, errors)
|
|
379
392
|
}
|
|
393
|
+
for (const key of ["payload_schema", "schema"]) {
|
|
394
|
+
requireSchema(event, key, label, errors)
|
|
395
|
+
}
|
|
380
396
|
} else {
|
|
381
397
|
errors.push(`${label} must be a topic string or object`)
|
|
382
398
|
return
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from fastapi import Depends
|
|
2
|
+
from palette_sdk import PluginContext, PluginRouter, get_plugin_context, services
|
|
3
|
+
|
|
4
|
+
router = PluginRouter()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@router.post("/leave-requests")
|
|
8
|
+
async def create_leave_request(payload: dict, ctx: PluginContext = Depends(get_plugin_context)):
|
|
9
|
+
chain = await services(ctx).call("hr/v1#approvalChain.get", {
|
|
10
|
+
"user_id": payload.get("user_id", ctx.user_id),
|
|
11
|
+
})
|
|
12
|
+
return {
|
|
13
|
+
"status": "submitted",
|
|
14
|
+
"approval_chain": chain,
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { createRoot } from "react-dom/client"
|
|
3
|
+
import { createPaletteClient, usePlatform } from "@palettelab/sdk"
|
|
4
|
+
|
|
5
|
+
function App() {
|
|
6
|
+
const platform = usePlatform()
|
|
7
|
+
const palette = createPaletteClient(platform)
|
|
8
|
+
const [result, setResult] = useState<string>("")
|
|
9
|
+
|
|
10
|
+
async function previewApprover() {
|
|
11
|
+
const chain = await palette.services("hr/v1").approvalChain.get({
|
|
12
|
+
user_id: platform.user.id,
|
|
13
|
+
})
|
|
14
|
+
setResult(JSON.stringify(chain, null, 2))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<main style={{ padding: 24, fontFamily: "system-ui, sans-serif" }}>
|
|
19
|
+
<h1>Consumer App</h1>
|
|
20
|
+
<button onClick={previewApprover}>Preview approver</button>
|
|
21
|
+
{result && <pre>{result}</pre>}
|
|
22
|
+
</main>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
createRoot(document.getElementById("root")!).render(<App />)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "1",
|
|
3
|
+
"id": "consumer-app",
|
|
4
|
+
"name": "Consumer App",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"developer": "Palette",
|
|
7
|
+
"category": "Productivity",
|
|
8
|
+
"tagline": "Consume governed services from other apps",
|
|
9
|
+
"description": "Example consumer app that asks an HR provider for approval routing.",
|
|
10
|
+
"icon": "ClipboardText",
|
|
11
|
+
"gradient": {
|
|
12
|
+
"bg": "linear-gradient(135deg, #7C3AED, #DB2777)",
|
|
13
|
+
"text": "#ffffff"
|
|
14
|
+
},
|
|
15
|
+
"capabilities": {
|
|
16
|
+
"frontend": true,
|
|
17
|
+
"backend": true
|
|
18
|
+
},
|
|
19
|
+
"frontend": {
|
|
20
|
+
"entry": "./frontend/src/index.tsx"
|
|
21
|
+
},
|
|
22
|
+
"backend": {
|
|
23
|
+
"entry": "./backend/api/main.py"
|
|
24
|
+
},
|
|
25
|
+
"permissions": [
|
|
26
|
+
"resources:read",
|
|
27
|
+
"resources:write"
|
|
28
|
+
],
|
|
29
|
+
"consumes": {
|
|
30
|
+
"services": [
|
|
31
|
+
{
|
|
32
|
+
"target": "hr/v1#approvalChain.get",
|
|
33
|
+
"required": true,
|
|
34
|
+
"reason": "Leave approvals follow the employee reporting hierarchy."
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"events": [
|
|
38
|
+
{
|
|
39
|
+
"target": "hr/v1#hierarchy.updated",
|
|
40
|
+
"required": false,
|
|
41
|
+
"reason": "Refresh approval routing when hierarchy changes."
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from fastapi import Depends
|
|
2
|
+
from palette_sdk import PluginContext, PluginRouter, get_plugin_context, service
|
|
3
|
+
|
|
4
|
+
router = PluginRouter()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@service(
|
|
8
|
+
"approvalChain.get",
|
|
9
|
+
namespace="hr",
|
|
10
|
+
version="v1",
|
|
11
|
+
input_schema="schemas/approvalChain.get.in.json",
|
|
12
|
+
output_schema="schemas/approvalChain.get.out.json",
|
|
13
|
+
)
|
|
14
|
+
async def approval_chain(ctx: PluginContext, payload: dict) -> dict:
|
|
15
|
+
user_id = payload["user_id"]
|
|
16
|
+
return {
|
|
17
|
+
"user_id": user_id,
|
|
18
|
+
"next_approver_id": "manager-dev",
|
|
19
|
+
"levels": ["manager-dev", "hr-dev"],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get("/health")
|
|
24
|
+
async def health(ctx: PluginContext = Depends(get_plugin_context)):
|
|
25
|
+
return {"ok": True, "plugin_id": ctx.plugin_id}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "1",
|
|
3
|
+
"id": "provider-app",
|
|
4
|
+
"name": "Provider App",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"developer": "Palette",
|
|
7
|
+
"category": "Productivity",
|
|
8
|
+
"tagline": "Expose governed services to other apps",
|
|
9
|
+
"description": "Example provider app exposing employee hierarchy through the OS broker.",
|
|
10
|
+
"icon": "UsersThree",
|
|
11
|
+
"gradient": {
|
|
12
|
+
"bg": "linear-gradient(135deg, #0F766E, #2563EB)",
|
|
13
|
+
"text": "#ffffff"
|
|
14
|
+
},
|
|
15
|
+
"capabilities": {
|
|
16
|
+
"backend": true
|
|
17
|
+
},
|
|
18
|
+
"backend": {
|
|
19
|
+
"entry": "./backend/api/main.py"
|
|
20
|
+
},
|
|
21
|
+
"permissions": [
|
|
22
|
+
"resources:read"
|
|
23
|
+
],
|
|
24
|
+
"provides": {
|
|
25
|
+
"namespace": "hr",
|
|
26
|
+
"services": [
|
|
27
|
+
{
|
|
28
|
+
"id": "approval",
|
|
29
|
+
"version": "v1",
|
|
30
|
+
"label": "Approval hierarchy",
|
|
31
|
+
"description": "Resolve the next approver for an employee.",
|
|
32
|
+
"methods": [
|
|
33
|
+
{
|
|
34
|
+
"name": "approvalChain.get",
|
|
35
|
+
"input_schema": "schemas/approvalChain.get.in.json",
|
|
36
|
+
"output_schema": "schemas/approvalChain.get.out.json"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"events": [
|
|
42
|
+
{
|
|
43
|
+
"topic": "hierarchy.updated",
|
|
44
|
+
"version": "v1",
|
|
45
|
+
"schema": "schemas/hierarchy.updated.json"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"user_id": { "type": "string" },
|
|
6
|
+
"next_approver_id": { "type": "string" },
|
|
7
|
+
"levels": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": { "type": "string" }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": ["user_id", "next_approver_id", "levels"]
|
|
13
|
+
}
|