mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 02:00:52 +01:00
## Thinking Path > - Paperclip is an open-source AI-agent management platform; agents run tasks inside sandboxed environments (Daytona, Kubernetes, E2B, etc.) > - The control-plane ↔ sandbox file-transfer path flows through the `environmentExecute` seam in `protocol.ts` — the only verb available to plugins — which forces a base64-over-exec chunked loop for every file move: workspace files, assets, Codex home sync > - This transport is correct and safe, but it bypasses provider-native bulk/streaming APIs (Daytona `uploadFiles`, K8s `FastUploadInterceptor` / volume mounts), leaving significant throughput on the table for large workspaces > - The right fix is an opt-in seam extension: providers with faster native transfer declare two optional verbs; providers that do not opt in stay on the existing fallback with zero code or behavior change required > - This PR adds the first layer of that extension — two optional verbs (`environmentSyncIn` / `environmentSyncOut`) in the plugin SDK, the runtime plumbing to prefer the native path for the two clean destroy-then-replace cases, and a doc for the contract > - The core correctness invariant is byte-identical fallback: if no provider opts in, execution is exactly what ships today; `assertSyncOperationsConfined` enforces host-side path confinement for providers that do opt in > - No provider advertises the verbs yet → zero production behavior change; future PRs wire up Daytona and K8s providers against this contract ## Linked Issues or Issue Description No public GitHub issue exists for this feature. Description follows the `feature_request` issue template: **Subsystem affected:** packages/plugins — plugin system; packages/adapter-utils — adapter runtime; server/ — EnvironmentRuntimeService **Problem or motivation:** Sandbox file transfers currently always use a base64-over-exec chunked loop regardless of what the underlying provider supports. For workspaces larger than a few MB this becomes the dominant wall-clock cost of every sandbox run, and it bypasses bulk/stream APIs that providers like Daytona already expose natively. **Proposed solution:** Add two optional, opt-in plugin hooks — `onEnvironmentSyncIn` / `onEnvironmentSyncOut` — to the plugin SDK. When a provider defines both hooks and both are advertised via the existing `supportedMethods` negotiation, the runtime prefers the native path for the two clean destroy-then-replace transfer cases; all other cases fall back to the existing byte-identical base64 transport. **Alternatives considered:** An unconditional verb would require every provider to implement or stub the verb. The opt-in / `METHOD_NOT_IMPLEMENTED` pattern (already used by `environmentExecute`) preserves backward compatibility with zero provider changes required. **Roadmap alignment:** Consistent with the ✅ "Cloud / Sandbox agents" and ✅ "Plugin system" milestones; extends the plugin seam rather than adding control-plane-level logic. **Additional context:** Searched open pull requests and issues for duplicate sandbox file-sync / native-transfer work; none found. ## What Changed - **`packages/plugins/sdk`** - `protocol.ts`: two new optional `HostToWorkerMethods` — `environmentSyncIn` / `environmentSyncOut` — plus generic `SyncOperation`, `SyncFileMapping`, and `SyncOutcome` types - `define-plugin.ts`: optional `onEnvironmentSyncIn` / `onEnvironmentSyncOut` fields on `PluginDefinition`; worker advertises each verb only when its hook is defined (else `METHOD_NOT_IMPLEMENTED`, mirroring `environmentExecute`) - `worker-rpc-host.ts`: route new verbs to plugin hooks - `index.ts`: re-export new public types - **`packages/adapter-utils`** - `command-managed-runtime.ts`: expose optional `syncIn` / `syncOut` on `CommandManagedRuntimeRunner` (available only when both verbs are advertised); add `assertSyncOperationsConfined` host-side path-confinement guard - `sandbox-managed-runtime.ts`: `SandboxManagedRuntimeClient` gains optional `syncIn` / `syncOut`; orchestrator prefers native path for default-provision asset inbound and workspace-download-into-fresh-dir outbound; all other paths keep the existing base64 fallback - `sandbox-file-sync.test.ts` (new): 234-line characterization suite — native-opt-in branch, fallback branch, `assertSyncOperationsConfined` escape-path rejection, `followSymlinks` → tar `-h` - `command-managed-runtime.test.ts`: negotiation + native-sync + confinement tests - **`server/src/services/environment-runtime.ts`**: `EnvironmentRuntimeService` delegates to `syncIn` / `syncOut`, gated on advertised support - **`server/src/services/environment-execution-target.ts`**: minor typing fix alongside the new verbs - **`doc/plugins/SANDBOX_FILE_SYNC_HOOKS.md`** (new): documents the full contract — opt-in / no-op guarantee, operation ordering, provider-may-tar, atomicity, `followSymlinks`, secret modes (0600, no window), path confinement, `operationId` opacity, resource bounds, shell-quoting ## Verification ```bash # SDK suite pnpm --filter packages/plugins/sdk test # Adapter-utils suite (includes new sandbox-file-sync characterization tests) pnpm --filter packages/adapter-utils test # Expected: 255 pass / 4 skip # Type-check across affected packages pnpm --filter packages/plugins/sdk typecheck pnpm --filter packages/adapter-utils typecheck # Server changed-file spot check: cd server && npx tsc --noEmit --skipLibCheck 2>&1 | grep -E "environment-(runtime|execution-target)" | head -20 ``` Key behavioral invariant to spot-check: with no provider opting in (the current state), run any sandbox task and confirm file-transfer behavior is byte-for-byte identical to what the pre-PR code produces. The characterization tests assert this at the unit level. ## Risks - **Zero production risk today**: no provider advertises `environmentSyncIn` / `environmentSyncOut`, so the new code paths are unreachable in production; all real traffic stays on the existing base64 fallback - **Path confinement**: `assertSyncOperationsConfined` rejects any `targetPath` that escapes the declared root — this is the primary security boundary for future providers. The test suite covers escape-path rejection - **Atomicity**: the contract delegates atomicity to providers; the doc explicitly calls out that directory-level ops are not guaranteed atomic - **Secret transport**: credential assets (e.g., Codex `auth.json`, directory mappings) continue to use the existing tar path — they do not go through the new verbs in any current provider > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used Provider: Anthropic Model: `claude-sonnet-4-6` (Claude Sonnet 4.6) Context window: 200 K tokens Capabilities: extended tool use, multi-file code generation, agentic reasoning via the Paperclip agent framework ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes: #` / `Refs: #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Harold Kim <harold@paperclip.ing> Co-authored-by: Paperclip <noreply@paperclip.ing>
443 lines
15 KiB
TypeScript
443 lines
15 KiB
TypeScript
/**
|
|
* `definePlugin` — the top-level helper for authoring a Paperclip plugin.
|
|
*
|
|
* Plugin authors call `definePlugin()` and export the result as the default
|
|
* export from their worker entrypoint. The host imports the worker module,
|
|
* calls `setup()` with a `PluginContext`, and from that point the plugin
|
|
* responds to events, jobs, webhooks, and UI requests through the context.
|
|
*
|
|
* @see PLUGIN_SPEC.md §14.1 — Example SDK Shape
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* // dist/worker.ts
|
|
* import { definePlugin } from "@paperclipai/plugin-sdk";
|
|
*
|
|
* export default definePlugin({
|
|
* async setup(ctx) {
|
|
* ctx.logger.info("Linear sync plugin starting");
|
|
*
|
|
* // Subscribe to events
|
|
* ctx.events.on("issue.created", async (event) => {
|
|
* const companyId = event.companyId;
|
|
* const config = await ctx.config.get(companyId);
|
|
* const apiKey = await ctx.secrets.resolve(config.apiKeyRef, { companyId, configPath: "apiKeyRef" });
|
|
* await ctx.http.fetch(`https://api.linear.app/...`, {
|
|
* method: "POST",
|
|
* headers: { Authorization: `Bearer ${apiKey}` },
|
|
* body: JSON.stringify({ title: event.payload.title }),
|
|
* });
|
|
* });
|
|
*
|
|
* // Register a job handler
|
|
* ctx.jobs.register("full-sync", async (job) => {
|
|
* ctx.logger.info("Running full-sync job", { runId: job.runId });
|
|
* // ... sync logic
|
|
* });
|
|
*
|
|
* // Register data for the UI
|
|
* ctx.data.register("sync-health", async ({ companyId }) => {
|
|
* const state = await ctx.state.get({
|
|
* scopeKind: "company",
|
|
* scopeId: String(companyId),
|
|
* stateKey: "last-sync",
|
|
* });
|
|
* return { lastSync: state };
|
|
* });
|
|
* },
|
|
* });
|
|
* ```
|
|
*/
|
|
|
|
import type { PluginContext } from "./types.js";
|
|
import type {
|
|
PluginEnvironmentAcquireLeaseParams,
|
|
PluginEnvironmentDestroyLeaseParams,
|
|
PluginEnvironmentExecuteParams,
|
|
PluginEnvironmentExecuteResult,
|
|
PluginEnvironmentSyncInParams,
|
|
PluginEnvironmentSyncOutParams,
|
|
PluginEnvironmentSyncResult,
|
|
PluginEnvironmentStartInteractiveSetupParams,
|
|
PluginEnvironmentInteractiveSetupSession,
|
|
PluginEnvironmentGetInteractiveSetupParams,
|
|
PluginEnvironmentCaptureTemplateParams,
|
|
PluginEnvironmentCaptureTemplateResult,
|
|
PluginEnvironmentCancelInteractiveSetupParams,
|
|
PluginEnvironmentCancelInteractiveSetupResult,
|
|
PluginEnvironmentDeleteTemplateParams,
|
|
PluginEnvironmentDeleteTemplateResult,
|
|
PluginEnvironmentLease,
|
|
PluginEnvironmentProbeParams,
|
|
PluginEnvironmentProbeResult,
|
|
PluginEnvironmentRealizeWorkspaceParams,
|
|
PluginEnvironmentRealizeWorkspaceResult,
|
|
PluginEnvironmentReleaseLeaseParams,
|
|
PluginEnvironmentResumeLeaseParams,
|
|
PluginEnvironmentValidateConfigParams,
|
|
PluginEnvironmentValidationResult,
|
|
DetectExternalObjectsParams,
|
|
DetectExternalObjectsResult,
|
|
ResolveExternalObjectParams,
|
|
PluginExternalObjectResolveResult,
|
|
RefreshExternalObjectsParams,
|
|
RefreshExternalObjectsResult,
|
|
} from "./protocol.js";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Health check result
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Optional plugin-reported diagnostics returned from the `health()` RPC method.
|
|
*
|
|
* @see PLUGIN_SPEC.md §13.2 — `health`
|
|
*/
|
|
export interface PluginHealthDiagnostics {
|
|
/** Machine-readable status: `"ok"` | `"degraded"` | `"error"`. */
|
|
status: "ok" | "degraded" | "error";
|
|
/** Human-readable description of the current health state. */
|
|
message?: string;
|
|
/** Plugin-reported key-value diagnostics (e.g. connection status, queue depth). */
|
|
details?: Record<string, unknown>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config validation result
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Result returned from the `validateConfig()` RPC method.
|
|
*
|
|
* @see PLUGIN_SPEC.md §13.3 — `validateConfig`
|
|
*/
|
|
export interface PluginConfigValidationResult {
|
|
/** Whether the config is valid. */
|
|
ok: boolean;
|
|
/** Non-fatal warnings about the config. */
|
|
warnings?: string[];
|
|
/** Validation errors (populated when `ok` is `false`). */
|
|
errors?: string[];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Webhook handler input
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Input received by the plugin worker's `handleWebhook` handler.
|
|
*
|
|
* @see PLUGIN_SPEC.md §13.7 — `handleWebhook`
|
|
*/
|
|
export interface PluginWebhookInput {
|
|
/** Endpoint key matching the manifest declaration. */
|
|
endpointKey: string;
|
|
/** Inbound request headers. */
|
|
headers: Record<string, string | string[]>;
|
|
/** Raw request body as a UTF-8 string. */
|
|
rawBody: string;
|
|
/** Parsed JSON body (if applicable and parseable). */
|
|
parsedBody?: unknown;
|
|
/** Unique request identifier for idempotency checks. */
|
|
requestId: string;
|
|
}
|
|
|
|
export interface PluginApiRequestInput {
|
|
routeKey: string;
|
|
method: string;
|
|
path: string;
|
|
params: Record<string, string>;
|
|
query: Record<string, string | string[]>;
|
|
body: unknown;
|
|
actor: {
|
|
actorType: "user" | "agent";
|
|
actorId: string;
|
|
agentId?: string | null;
|
|
userId?: string | null;
|
|
runId?: string | null;
|
|
};
|
|
companyId: string;
|
|
headers: Record<string, string>;
|
|
}
|
|
|
|
export interface PluginApiResponse {
|
|
status?: number;
|
|
headers?: Record<string, string>;
|
|
body?: unknown;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Plugin definition
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* The plugin definition shape passed to `definePlugin()`.
|
|
*
|
|
* The only required field is `setup`, which receives the `PluginContext` and
|
|
* is where the plugin registers its handlers (events, jobs, data, actions,
|
|
* tools, etc.).
|
|
*
|
|
* All other lifecycle hooks are optional. If a hook is not implemented the
|
|
* host applies default behaviour (e.g. restarting the worker on config change
|
|
* instead of calling `onConfigChanged`).
|
|
*
|
|
* @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
|
|
*/
|
|
export interface PluginDefinition {
|
|
/**
|
|
* Called once when the plugin worker starts up, after `initialize` completes.
|
|
*
|
|
* This is where the plugin registers all its handlers: event subscriptions,
|
|
* job handlers, data/action handlers, and tool registrations. Registration
|
|
* must be synchronous after `setup` resolves — do not register handlers
|
|
* inside async callbacks that may resolve after `setup` returns.
|
|
*
|
|
* @param ctx - The full plugin context provided by the host
|
|
*/
|
|
setup(ctx: PluginContext): Promise<void>;
|
|
|
|
/**
|
|
* Called when the host wants to know if the plugin is healthy.
|
|
*
|
|
* The host polls this on a regular interval and surfaces the result in the
|
|
* plugin health dashboard. If not implemented, the host infers health from
|
|
* worker process liveness.
|
|
*
|
|
* @see PLUGIN_SPEC.md §13.2 — `health`
|
|
*/
|
|
onHealth?(): Promise<PluginHealthDiagnostics>;
|
|
|
|
/**
|
|
* Called when the operator updates this plugin's company-scoped configuration
|
|
* at runtime, without restarting the worker.
|
|
*
|
|
* If not implemented, the host restarts the worker to apply the new config.
|
|
*
|
|
* @param newConfig - The newly resolved configuration
|
|
* @see PLUGIN_SPEC.md §13.4 — `configChanged`
|
|
*/
|
|
onConfigChanged?(newConfig: Record<string, unknown>): Promise<void>;
|
|
|
|
/**
|
|
* Called when the host is about to shut down the plugin worker.
|
|
*
|
|
* The worker has at most 10 seconds (configurable via plugin config) to
|
|
* finish in-flight work and resolve this promise. After the deadline the
|
|
* host sends SIGTERM, then SIGKILL.
|
|
*
|
|
* @see PLUGIN_SPEC.md §12.5 — Graceful Shutdown Policy
|
|
*/
|
|
onShutdown?(): Promise<void>;
|
|
|
|
/**
|
|
* Called to validate the current plugin configuration.
|
|
*
|
|
* The host calls this:
|
|
* - after the plugin starts (to surface config errors immediately)
|
|
* - after the operator saves a new config (to validate before persisting)
|
|
* - via the "Test Connection" button in the settings UI
|
|
*
|
|
* @param config - The configuration to validate
|
|
* @see PLUGIN_SPEC.md §13.3 — `validateConfig`
|
|
*/
|
|
onValidateConfig?(config: Record<string, unknown>): Promise<PluginConfigValidationResult>;
|
|
|
|
/**
|
|
* Called to handle an inbound webhook delivery.
|
|
*
|
|
* The host routes `POST /api/plugins/:pluginId/webhooks/:endpointKey` to
|
|
* this handler. The plugin is responsible for signature verification using
|
|
* a resolved secret ref.
|
|
*
|
|
* If not implemented but webhooks are declared in the manifest, the host
|
|
* returns HTTP 501 for webhook deliveries.
|
|
*
|
|
* @param input - Webhook delivery metadata and payload
|
|
* @see PLUGIN_SPEC.md §13.7 — `handleWebhook`
|
|
*/
|
|
onWebhook?(input: PluginWebhookInput): Promise<void>;
|
|
|
|
/**
|
|
* Called for manifest-declared scoped JSON API routes under
|
|
* `/api/plugins/:pluginId/api/*` after the host has enforced auth, company
|
|
* access, capabilities, and checkout policy.
|
|
*/
|
|
onApiRequest?(input: PluginApiRequestInput): Promise<PluginApiResponse>;
|
|
|
|
/**
|
|
* Called when Paperclip scans issue/comment/document content and asks this
|
|
* plugin whether any sanitized URL candidates belong to its external object
|
|
* providers. The host has already stripped URL userinfo, query strings, and
|
|
* fragments unless provider-safe identity components were explicitly hashed.
|
|
*
|
|
* Requires `external.objects.detect`.
|
|
*/
|
|
onDetectExternalObjects?(
|
|
params: DetectExternalObjectsParams,
|
|
): Promise<DetectExternalObjectsResult>;
|
|
|
|
/**
|
|
* Called when Paperclip needs the current normalized status for one external
|
|
* object owned by a manifest-declared provider.
|
|
*
|
|
* Requires `external.objects.read`.
|
|
*/
|
|
onResolveExternalObject?(
|
|
params: ResolveExternalObjectParams,
|
|
): Promise<PluginExternalObjectResolveResult>;
|
|
|
|
/**
|
|
* Optional batch resolver used by providers that can refresh many objects
|
|
* more efficiently than individual `onResolveExternalObject` calls.
|
|
*
|
|
* Requires `external.objects.refresh`.
|
|
*/
|
|
onRefreshExternalObjects?(
|
|
params: RefreshExternalObjectsParams,
|
|
): Promise<RefreshExternalObjectsResult>;
|
|
|
|
/**
|
|
* Called to validate provider-specific configuration for a plugin-hosted
|
|
* environment driver.
|
|
*/
|
|
onEnvironmentValidateConfig?(
|
|
params: PluginEnvironmentValidateConfigParams,
|
|
): Promise<PluginEnvironmentValidationResult>;
|
|
|
|
/** Called to test reachability or readiness of a plugin-hosted environment. */
|
|
onEnvironmentProbe?(
|
|
params: PluginEnvironmentProbeParams,
|
|
): Promise<PluginEnvironmentProbeResult>;
|
|
|
|
/** Called before a run starts to acquire a provider lease. */
|
|
onEnvironmentAcquireLease?(
|
|
params: PluginEnvironmentAcquireLeaseParams,
|
|
): Promise<PluginEnvironmentLease>;
|
|
|
|
/** Called to reconnect to a previously acquired provider lease. */
|
|
onEnvironmentResumeLease?(
|
|
params: PluginEnvironmentResumeLeaseParams,
|
|
): Promise<PluginEnvironmentLease>;
|
|
|
|
/** Called when a run finishes and the provider lease can be released. */
|
|
onEnvironmentReleaseLease?(
|
|
params: PluginEnvironmentReleaseLeaseParams,
|
|
): Promise<void>;
|
|
|
|
/** Called when the host needs to force-destroy provider state. */
|
|
onEnvironmentDestroyLease?(
|
|
params: PluginEnvironmentDestroyLeaseParams,
|
|
): Promise<void>;
|
|
|
|
/** Called to materialize the run workspace inside the provider lease. */
|
|
onEnvironmentRealizeWorkspace?(
|
|
params: PluginEnvironmentRealizeWorkspaceParams,
|
|
): Promise<PluginEnvironmentRealizeWorkspaceResult>;
|
|
|
|
/** Called to execute a command inside the provider lease. */
|
|
onEnvironmentExecute?(
|
|
params: PluginEnvironmentExecuteParams,
|
|
): Promise<PluginEnvironmentExecuteResult>;
|
|
|
|
/**
|
|
* Optional, opt-in: called before execution to place host files/directories at
|
|
* target sandbox paths using a provider-native transport instead of the default
|
|
* base64-over-exec fallback. Defining this hook (together with
|
|
* `onEnvironmentSyncOut`) advertises `environmentSyncIn`; leaving it undefined
|
|
* keeps the byte-identical fallback. See `doc/plugins/SANDBOX_FILE_SYNC_HOOKS.md`.
|
|
*/
|
|
onEnvironmentSyncIn?(
|
|
params: PluginEnvironmentSyncInParams,
|
|
): Promise<PluginEnvironmentSyncResult>;
|
|
|
|
/**
|
|
* Optional, opt-in: called after execution to copy sandbox files/directories
|
|
* back to target host paths using a provider-native transport. Defining this
|
|
* hook (together with `onEnvironmentSyncIn`) advertises `environmentSyncOut`.
|
|
* See `doc/plugins/SANDBOX_FILE_SYNC_HOOKS.md`.
|
|
*/
|
|
onEnvironmentSyncOut?(
|
|
params: PluginEnvironmentSyncOutParams,
|
|
): Promise<PluginEnvironmentSyncResult>;
|
|
|
|
/** Called to start an interactive setup sandbox and return redacted connection metadata. */
|
|
onEnvironmentStartInteractiveSetup?(
|
|
params: PluginEnvironmentStartInteractiveSetupParams,
|
|
): Promise<PluginEnvironmentInteractiveSetupSession>;
|
|
|
|
/** Called to read setup status and, when authorized, a one-time connection payload. */
|
|
onEnvironmentGetInteractiveSetup?(
|
|
params: PluginEnvironmentGetInteractiveSetupParams,
|
|
): Promise<PluginEnvironmentInteractiveSetupSession>;
|
|
|
|
/** Called to capture a reusable provider template from a live setup sandbox. */
|
|
onEnvironmentCaptureTemplate?(
|
|
params: PluginEnvironmentCaptureTemplateParams,
|
|
): Promise<PluginEnvironmentCaptureTemplateResult>;
|
|
|
|
/** Called to cancel and clean up a setup sandbox without promoting a template. */
|
|
onEnvironmentCancelInteractiveSetup?(
|
|
params: PluginEnvironmentCancelInteractiveSetupParams,
|
|
): Promise<PluginEnvironmentCancelInteractiveSetupResult>;
|
|
|
|
/** Called for optional best-effort cleanup of a captured provider template. */
|
|
onEnvironmentDeleteTemplate?(
|
|
params: PluginEnvironmentDeleteTemplateParams,
|
|
): Promise<PluginEnvironmentDeleteTemplateResult>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PaperclipPlugin — the sealed object returned by definePlugin()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* The sealed plugin object returned by `definePlugin()`.
|
|
*
|
|
* Plugin authors export this as the default export from their worker
|
|
* entrypoint. The host imports it and calls the lifecycle methods.
|
|
*
|
|
* @see PLUGIN_SPEC.md §14 — SDK Surface
|
|
*/
|
|
export interface PaperclipPlugin {
|
|
/** The original plugin definition passed to `definePlugin()`. */
|
|
readonly definition: PluginDefinition;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// definePlugin — top-level factory
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Define a Paperclip plugin.
|
|
*
|
|
* Call this function in your worker entrypoint and export the result as the
|
|
* default export. The host will import the module and call lifecycle methods
|
|
* on the returned object.
|
|
*
|
|
* @param definition - Plugin lifecycle handlers
|
|
* @returns A sealed `PaperclipPlugin` object for the host to consume
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* import { definePlugin } from "@paperclipai/plugin-sdk";
|
|
*
|
|
* export default definePlugin({
|
|
* async setup(ctx) {
|
|
* ctx.logger.info("Plugin started");
|
|
* ctx.events.on("issue.created", async (event) => {
|
|
* // handle event
|
|
* });
|
|
* },
|
|
*
|
|
* async onHealth() {
|
|
* return { status: "ok" };
|
|
* },
|
|
* });
|
|
* ```
|
|
*
|
|
* @see PLUGIN_SPEC.md §14.1 — Example SDK Shape
|
|
*/
|
|
export function definePlugin(definition: PluginDefinition): PaperclipPlugin {
|
|
return Object.freeze({ definition });
|
|
}
|