Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,210 @@
|
||||
# Plugin SDK Reference
|
||||
|
||||
> `@toolctrl/plugin-sdk` — the typed surface plugin authors compile
|
||||
> against. Mirrors the runtime capability set so an SDK upgrade is a
|
||||
> straight version bump on the author side.
|
||||
|
||||
## Installing
|
||||
|
||||
The SDK lives in the host monorepo today as `workspace:*`. External
|
||||
plugin authors install it as a regular npm dependency:
|
||||
|
||||
```bash
|
||||
pnpm add -D @toolctrl/plugin-sdk
|
||||
```
|
||||
|
||||
## Top-level exports
|
||||
|
||||
| Export | Kind | Purpose |
|
||||
| ----------------------------------------- | ----- | --------------------------------------------------------------------------- |
|
||||
| `defineHook` | fn | Identity wrapper that captures `Input`/`Output` types at the call site |
|
||||
| `createPluginSettingsClient` | fn | Settings iframe helper around the host `postMessage` protocol |
|
||||
| `HookHandler` | type | `(input, ctx) => Output \| Promise<Output>` |
|
||||
| `PluginContext` | type | Per-invocation context (logger, capabilities, tenant info) |
|
||||
| `HostApi` | type | Aggregate of optional bindings; only the declared ones are bound |
|
||||
| `PluginCapabilityName` | type | Closed enum of capabilities |
|
||||
| `PluginLogger` | type | Pino-subset (info/warn/error) |
|
||||
| `PluginHttpClient` | type | `http.outbound` capability surface |
|
||||
| `PluginStorage` | type | `storage.read` / `storage.write` capability surface |
|
||||
| `PluginAudit` | type | `audit` capability surface; production-available but still capability-gated |
|
||||
| `PluginTenantInfo` | type | Read-only tenant metadata (`tenant.info`) |
|
||||
| `PluginSettingsClient` | type | Settings iframe helper returned by `createPluginSettingsClient` |
|
||||
| `PluginSettingsClientError` | class | Host-declared settings request failure |
|
||||
| `PluginSettingsClientTimeoutError` | class | Settings request timeout failure |
|
||||
| `PluginSettingsFrameMessage` | type | Settings iframe `postMessage` request union |
|
||||
| `PluginSettingsFrameResultMessage` | type | Host response union for settings iframe requests |
|
||||
| `PluginSettingValue` | type | JSON-safe value shape accepted by per-user plugin settings |
|
||||
| `PLUGIN_SETTINGS_FRAME_PROTOCOL_VERSION` | const | Current settings iframe protocol version (`1`) |
|
||||
| `PLUGIN_SETTINGS_FRAME_CHANNEL_META_NAME` | const | Meta tag name that carries the host-generated channel id |
|
||||
|
||||
## Authoring shape
|
||||
|
||||
```ts
|
||||
import { defineHook, type HookHandler } from "@toolctrl/plugin-sdk";
|
||||
|
||||
export const handleHello: HookHandler<{ name: string }, { greeting: string }> = defineHook((input, ctx) => {
|
||||
ctx.log.info({ name: input.name }, "hello");
|
||||
return { greeting: `Hello, ${input.name}!` };
|
||||
});
|
||||
```
|
||||
|
||||
Every hook export resolves to a single WebAssembly function with a
|
||||
`(arg) -> result` shape. The runtime serialises both directions as
|
||||
JSON; complex types fall through the standard `JSON.stringify` lens.
|
||||
|
||||
## Payload ABI
|
||||
|
||||
The foundation runtime exposes the active invocation payload as UTF-8
|
||||
JSON through two raw WebAssembly imports:
|
||||
|
||||
| Import | Signature | Purpose |
|
||||
| --------------------------- | --------------------- | ------------------------------------------------- |
|
||||
| `env.toolctrl_payload_len` | `() -> i32` | Length in bytes of the JSON payload |
|
||||
| `env.toolctrl_payload_byte` | `(index: i32) -> i32` | Byte value at `index`, or `0` outside the payload |
|
||||
|
||||
The SDK exports `PLUGIN_PAYLOAD_ABI_VERSION` and
|
||||
`PLUGIN_PAYLOAD_ABI_IMPORTS` so toolchain bindings can assert the host
|
||||
contract. Higher-level AssemblyScript/Rust helpers are still follow-up
|
||||
work; raw WASM plugins can already decode the JSON bytes themselves.
|
||||
|
||||
## Settings iframe protocol
|
||||
|
||||
When a manifest declares `ui.settings.entry`, the host renders that HTML
|
||||
inside a sandboxed `PluginSettingsFrame`. The iframe has an opaque
|
||||
origin (`sandbox="allow-scripts"`, no `allow-same-origin`) and a deny-by-default
|
||||
CSP, so settings UIs communicate with the host only through the versioned
|
||||
`postMessage` protocol exported by the SDK.
|
||||
|
||||
Use `createPluginSettingsClient()` to read the injected channel id, send
|
||||
`ready`, `get`, and `set` messages to `window.parent`, and accept responses
|
||||
only when the message came from `window.parent` and matches the expected
|
||||
protocol version, plugin id, channel id, and request id:
|
||||
|
||||
```ts
|
||||
import { createPluginSettingsClient, PluginSettingsClientError, PluginSettingsClientTimeoutError } from "@toolctrl/plugin-sdk";
|
||||
|
||||
const settings = createPluginSettingsClient({
|
||||
pluginId: "com.acme.settings",
|
||||
});
|
||||
|
||||
settings.ready();
|
||||
|
||||
try {
|
||||
const theme = await settings.get("theme");
|
||||
applyTheme(theme);
|
||||
|
||||
await settings.set("theme", "dark");
|
||||
} catch (error) {
|
||||
if (error instanceof PluginSettingsClientError) {
|
||||
console.warn("setting request failed", error.code, error.message);
|
||||
} else if (error instanceof PluginSettingsClientTimeoutError) {
|
||||
console.warn("setting request timed out", error.message);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The host validates `pluginId`, `channelId`, `requestId`, keys, and values.
|
||||
Values must be JSON-safe (`PluginSettingValue`) and stay within the host
|
||||
size/depth budgets. Persistent writes are CSRF-protected, tenant/user scoped,
|
||||
and rate-limited by the route action.
|
||||
|
||||
## Per-capability bindings on `ctx`
|
||||
|
||||
Each capability bound to the invocation appears as an optional field on
|
||||
`ctx`. The runtime sets a field only when the manifest declared it AND
|
||||
the gateway has a host implementation for it. Always null-check before
|
||||
use:
|
||||
|
||||
```ts
|
||||
if (!ctx.http) {
|
||||
ctx.log.warn({}, "http capability not bound — manifest must declare http.outbound");
|
||||
return { ok: false };
|
||||
}
|
||||
await ctx.http.fetch({ url: "https://example.com", method: "GET" });
|
||||
```
|
||||
|
||||
This shape gives you compile-time documentation: every capability the
|
||||
plugin uses is visible at the top of the handler.
|
||||
|
||||
## Logger
|
||||
|
||||
`ctx.log` is a structured logger. The host writes the message to its
|
||||
own logging pipeline (pino under the hood). The signature mirrors pino:
|
||||
|
||||
```ts
|
||||
ctx.log.info({ tenantId, recordId }, "entity created");
|
||||
ctx.log.warn({ retries: 3 }, "retrying");
|
||||
ctx.log.error({ err: errSerializable }, "fatal");
|
||||
```
|
||||
|
||||
Never log secrets — output sanitisation is best-effort, not a guarantee.
|
||||
|
||||
## HTTP client (`ctx.http`)
|
||||
|
||||
Reserved SDK shape for `http.outbound`. It is not bound in the current
|
||||
production/foundation runtime; manifests may declare it only as a reviewed
|
||||
future capability, and calls fail closed until the host binding is explicitly
|
||||
enabled after security review.
|
||||
|
||||
```ts
|
||||
const response = await ctx.http!.fetch({
|
||||
url: "https://hooks.slack.com/services/T0/B0/abc",
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: "hello" }),
|
||||
});
|
||||
```
|
||||
|
||||
When enabled, the host validates the URL against `allowedHosts` from the manifest's
|
||||
`http.outbound` scope before issuing the request. Any other host
|
||||
results in a denied audit row.
|
||||
|
||||
## Storage (`ctx.storage`)
|
||||
|
||||
Reserved SDK shape for `storage.read` and `storage.write`. It is not bound in
|
||||
the current production/foundation runtime; tenant-scoped storage remains
|
||||
fail-closed until the capability receives a dedicated security review.
|
||||
|
||||
```ts
|
||||
const channel = await ctx.storage!.get(`slack:${ctx.tenantId}:default-channel`);
|
||||
await ctx.storage!.put(`slack:${ctx.tenantId}:default-channel`, "#alerts", {
|
||||
ttlSeconds: 86_400,
|
||||
});
|
||||
```
|
||||
|
||||
Keys are byte-strings (no length cap today, but treat 256B as a soft
|
||||
budget). Values are byte-strings; encode JSON yourself if you need
|
||||
structured data.
|
||||
|
||||
## Audit (`ctx.audit`)
|
||||
|
||||
Production-available when the plugin manifest declares `audit` and the tenant
|
||||
has granted that capability. Records a tenant-visible audit event:
|
||||
|
||||
```ts
|
||||
await ctx.audit.record({
|
||||
action: "slack.notification-sent",
|
||||
target: `entity:${recordId}`,
|
||||
meta: { channel: "#alerts" },
|
||||
});
|
||||
```
|
||||
|
||||
Use for state-change observability. Do not abuse as a queue —
|
||||
metadata payloads have a hard cap.
|
||||
|
||||
## Errors
|
||||
|
||||
Throwing from a handler aborts the invocation. The runtime captures the
|
||||
exception, records `outcome=error` against every in-flight capability,
|
||||
flushes the audit batch, and marks the invocation `failed`. The plugin
|
||||
crash-tracker auto-disables a plugin after repeated failures.
|
||||
|
||||
## Versioning
|
||||
|
||||
The SDK follows SemVer. Major bumps may add or remove capability
|
||||
surfaces; consult `docs/plugins/versioning.md` before pinning to a new
|
||||
major. The runtime advertises its host range through `minHostVersion` /
|
||||
`maxHostVersion` in the manifest — when in doubt, lint your manifest
|
||||
against the running host.
|
||||
Reference in new issue
Block a user