mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-15 03:51:22 +02:00
## Thinking Path > - Paperclip manages AI agents and their work. > - AI Connections separate account access from models and harnesses. > - A pool must act as one connection while retaining each task’s account. > - Core must enforce member access and preserve session and recovery rules. > - A plugin supplies rotation policy without receiving credentials. > - This change adds durable routing and native connector setup and management. ## Linked Issues or Issue Description **Subsystem affected** AI Connections, Connectors, plugins, run dispatch, and session compatibility. **Problem or motivation** Operators need to rotate new tasks across saved accounts while each task keeps its account and session. Pool setup must fit the existing connector catalog and account workflow. **Proposed solution** Add an experimental router binding, a capability-gated plugin hook, and transactional task pins. Plugins declare native pooled connectors through `aiConnectionRouter`. Core hosts the existing-account picker, ordering step, and account settings. Related usage contract: #14936. Companion private plugin: https://github.com/paperclipai/paperclip-cloud/pull/643. **Roadmap alignment** This extends Apps and AI Connections. Core supplies generic enforcement and native connector UI; the private plugin owns rotation and quota policy. The prior duplicate search found no matching router implementation. ## What Changed - Add a router binding without changing existing concrete bindings. Keep the instance flag and new pools disabled by default. Require manual operator configuration. Show no routing toggle in Experimental settings on either open-source or Cloud installs, even after routing is enabled. - Persist company-scoped pools, one shared cursor per pool, and pins keyed by company, pool, agent, and task. Commit pins and cursor advances together with revision checks and bounded retries. Persist run-ID affinity before allocation. - Pass only authorized metadata and normalized usage to plugins. Core retains credential handling, member access checks, runtime qualification, and recovery evidence. Probe outside locks with a shared 15-second budget and freshness cache. - Resolve routing before credential preparation and backend selection. Preserve pins through turns, session resets, removed members, and quota waits. Retain admitted recovery after disable or uninstall. - Separate credential session epochs from token generations. Verified refresh preserves the epoch; reconnect and manual replacement change it. Include the credential slot ID in session and usage-cache identity, so reconnecting an indexed legacy account invalidates its old session even when both epochs are zero. - Validate pool member installations before accepting saved-agent bindings and recheck compatibility when the harness changes. Install only authorized members in the new-agent transaction and record their IDs in local activity. Pool membership cannot install a restricted shared connection. - Preserve pool bindings when agents hire teammates through either creation API or native caller runtime inheritance. Block stale manager credential references; retain explicit child authentication precedence and reject incompatible inherited pools. - Add native connector registration through plugin metadata. Reuse the Connectors catalog, setup header, account header, sidebar, dialogs, and usage display. Setup selects and orders saved connections. Advanced settings hold usage rules and member runtime defaults. New-account setup opens in another tab. - Use revision-checked pool archival from the Connectors catalog and account page. Keep task pins, cursors, recovery evidence, and underlying connections. Reject ordinary connection updates or removals that bypass pool revisions. - Add pool selectors, composer models, override notes, quota status, run details, activity records, and local run-log records. Keep session-adoption copy minimal. - Show **Used by** below the pool connections. List current company agents with shared avatars and profile links. Include paused agents; exclude terminated agents and agents using another pool. - Add Core stories for the generic connector workflow and runtime surfaces. Cloud stories reuse these production routes and tokens through a preview-only alias. ## Verification - Final head `73cb953bca30ed83e4505dd820edd9b5edffd28b`: full workspace `pnpm -r typecheck`, `pnpm build`, and `pnpm check:token-gates` pass locally. - All 422 focused connector/settings/shared-contract/migration tests and all 156 database-backed AI connection, hiring, reconnect, and durable-routing cases pass (69 hiring cases rerun after the final auth-precedence fix). The merged shared contract retains connection instructions and pool metadata. The pool migration is generated at sequence 0299 after the latest upstream migrations; this PR makes no lockfile changes. - All four full-app Playwright tests pass on the final head after a cold restart and migration, against the installed private plugin and isolated database, with no route or pool-API mocks. They cover hidden routing controls after manual opt-in, native pool creation, ordering, membership edits, rename, paused defaults, enabling/save/refresh persistence, stale edits, cancellation/removal, preserved underlying accounts, unavailable routers, and Used by avatars and profile links. Exact command: `PAPERCLIP_CONNECTION_POOL_E2E=1 AI_CONNECTIONS_TEST_COMPANY_ID=a37b9625-5ecf-4e29-8081-04df3d6e7d6f AI_CONNECTIONS_TEST_URL=http://127.0.0.1:3108 pnpm exec playwright test --config tests/ai-connections-app/playwright.config.ts connection-pools.spec.ts`. - [Native setup, ordering, and management screenshots](https://github.com/paperclipai/paperclip/pull/14967#issuecomment-6006976278) address the review follow-up. [Earlier selector, quota, and run-detail screenshots](https://github.com/paperclipai/paperclip/pull/14967#issuecomment-5971537316) show the runtime surfaces. Core previews: `pnpm --filter @paperclipai/ui storybook`, then **Connectors / Pool host** or **AI Connections / Connection pools**. Cloud owns its host-backed plugin stories; both repositories’ Operator Setup Required story assertions pass. - Live acceptance used OpenAI/Codex and Anthropic/Claude ACPX, resumed both exact sessions after restart, preserved pinned accounts through explicit reset and controlled quota deferral/recovery, and committed only two allocations across fourteen runs. A later UI-created task test again rotated OpenAI then Anthropic and resumed OpenAI through follow-up/restart/quota recovery. That later Anthropic execution was blocked by its saved OAuth token expiring (provider 401). No live usage probes ran. - The full local `pnpm test:run` was attempted earlier and did not complete because of macOS embedded PostgreSQL bootstrap/shared-memory failures and the 40,000-file Git fixture timeout. The focused database suites above now pass; full-suite verification is provided by the split CI lanes. The preceding CI run had one runtime readiness timeout; it passes locally both alone and inside the larger runtime suite. That larger local suite also encountered an embedded PostgreSQL setup failure and two macOS temporary-path alias assertions; those two assertions pass with canonical TMPDIR=/private/tmp. All final-head CI checks are terminal green, including full general/serialized server suites, Runner checks, browser E2E shards, canary verification, build, and typecheck. Greptile is 5/5 on that exact head with no unresolved threads. ## Risks - The migration adds routing tables and a credential epoch column. Install the private plugin only with the compatible Core contract. - Routing and each pool require opt-in. Production distribution and fleet defaults remain unchanged. - Unknown usage stays eligible. Known pinned exhaustion waits; revoked access requires operator repair. - Legacy adapters require compatible members. Runner model and effort overrides remain limited by qualified backend support. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository editing, code execution, and browser testing. The exact deployment model ID and context window are not exposed in this session. ## 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 - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass (targeted suites; full-suite limitations are reported above) - [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: Paperclip <noreply@paperclip.ing>
577 lines
21 KiB
TypeScript
577 lines
21 KiB
TypeScript
import type { AiConnectionRouterRequest, AiConnectionRouterResult } from "@paperclipai/shared";
|
|
/**
|
|
* `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,
|
|
PluginEnvironmentRunnerIngressEndpointParams,
|
|
PluginEnvironmentRunnerIngressEndpoint,
|
|
PluginEnvironmentSyncInParams,
|
|
PluginEnvironmentSyncOutParams,
|
|
PluginEnvironmentSyncResult,
|
|
PluginEnvironmentStartInteractiveSetupParams,
|
|
PluginEnvironmentInteractiveSetupSession,
|
|
PluginEnvironmentGetInteractiveSetupParams,
|
|
PluginEnvironmentCaptureTemplateParams,
|
|
PluginEnvironmentCaptureTemplateResult,
|
|
PluginEnvironmentCancelInteractiveSetupParams,
|
|
PluginEnvironmentCancelInteractiveSetupResult,
|
|
PluginEnvironmentDeleteTemplateParams,
|
|
PluginEnvironmentDeleteTemplateResult,
|
|
PluginEnvironmentLease,
|
|
PluginEnvironmentProbeParams,
|
|
PluginEnvironmentProbeResult,
|
|
PluginEnvironmentRealizeWorkspaceParams,
|
|
PluginEnvironmentRealizeWorkspaceResult,
|
|
PluginEnvironmentReleaseLeaseParams,
|
|
PluginEnvironmentTerminationReceipt,
|
|
PluginEnvironmentResumeLeaseParams,
|
|
PluginEnvironmentValidateConfigParams,
|
|
PluginEnvironmentValidationResult,
|
|
DetectExternalObjectsParams,
|
|
DetectExternalObjectsResult,
|
|
ResolveExternalObjectParams,
|
|
PluginExternalObjectResolveResult,
|
|
RefreshExternalObjectsParams,
|
|
RefreshExternalObjectsResult,
|
|
PluginLoginPtyOpenParams,
|
|
PluginLoginPtyOpenResult,
|
|
PluginLoginPtyInputParams,
|
|
PluginLoginPtyStopParams,
|
|
PluginLoginPtyCloseParams,
|
|
PluginLoginPtyCloseResult,
|
|
PluginDuplexChannelOpenParams,
|
|
PluginDuplexChannelOpenResult,
|
|
PluginDuplexChannelWriteParams,
|
|
PluginDuplexChannelStopParams,
|
|
PluginDuplexChannelCloseParams,
|
|
PluginDuplexChannelCloseResult,
|
|
} 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;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config change context
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Scope metadata delivered alongside a `configChanged` RPC so the worker knows
|
|
* *which company's* configuration changed.
|
|
*
|
|
* The host→worker `configChanged` message has always carried the company scope,
|
|
* but the SDK historically dropped it before invoking `onConfigChanged`, leaving
|
|
* proactive plugins to keep a single worker-global config. That is safe for a
|
|
* single-tenant plugin but silently collapses a multi-company plugin onto
|
|
* whichever company's config was delivered last. Threading the scope through
|
|
* lets a `multiCompanyConfig` plugin maintain per-company state.
|
|
*
|
|
* @see PLUGIN_SPEC.md §13.4 — `configChanged`
|
|
*/
|
|
export interface PluginConfigChangeContext {
|
|
/**
|
|
* The company whose configuration changed, or `null` for an instance/global
|
|
* save that is not bound to a specific company.
|
|
*/
|
|
companyId: string | null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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>;
|
|
|
|
/**
|
|
* When true, this plugin's worker correctly serves configuration from more
|
|
* than one company inside a single worker process — for example by keying its
|
|
* state on `context.companyId` in `onConfigChanged` and running one connection
|
|
* / subscription set per company.
|
|
*
|
|
* When false or omitted (the default), the plugin is treated as single-tenant.
|
|
* The host then **fails closed** if `configChanged` would ever deliver a
|
|
* second, distinct company's configuration to the same worker: instead of
|
|
* silently collapsing the worker onto whichever company arrived last (a
|
|
* cross-tenant identity/secret confusion bug), the delivery is rejected with
|
|
* `PLUGIN_RPC_ERROR_CODES.CROSS_TENANT_CONFIG`. Re-delivering an unchanged
|
|
* config for a different company (idempotent replay) is still allowed.
|
|
*/
|
|
multiCompanyConfig?: boolean;
|
|
|
|
/**
|
|
* 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
|
|
* @param context - Scope of the change. `context.companyId` identifies the
|
|
* company whose config changed (null for an instance/global save). A
|
|
* multi-company plugin (`multiCompanyConfig: true`) MUST key its per-company
|
|
* state on this value rather than assuming a single global config.
|
|
* @see PLUGIN_SPEC.md §13.4 — `configChanged`
|
|
*/
|
|
onConfigChanged?(
|
|
newConfig: Record<string, unknown>,
|
|
context?: PluginConfigChangeContext,
|
|
): 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`.
|
|
*/
|
|
/** Propose a member from host-authorized candidates. Requires ai.connections.route. */
|
|
onRouteAiConnection?(params: AiConnectionRouterRequest): Promise<AiConnectionRouterResult>;
|
|
|
|
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<PluginEnvironmentTerminationReceipt | void>;
|
|
|
|
/** Stop this exact allocation and retain all files, regardless of release
|
|
* policy. Throw if stop cannot be confirmed; never destroy as a fallback.
|
|
* Separate worker discovery lets the host safely defer older providers. */
|
|
onEnvironmentStopLease?(
|
|
params: PluginEnvironmentReleaseLeaseParams,
|
|
): Promise<PluginEnvironmentTerminationReceipt>;
|
|
|
|
/** Called when the host needs to force-destroy provider state. */
|
|
onEnvironmentDestroyLease?(
|
|
params: PluginEnvironmentDestroyLeaseParams,
|
|
): Promise<PluginEnvironmentTerminationReceipt | 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>;
|
|
|
|
/** Return an authenticated private WebSocket ingress for runnerd. */
|
|
onEnvironmentRunnerIngressEndpoint?(
|
|
params: PluginEnvironmentRunnerIngressEndpointParams,
|
|
): Promise<PluginEnvironmentRunnerIngressEndpoint>;
|
|
|
|
/**
|
|
* 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>;
|
|
|
|
/**
|
|
* Called to open one live Claude `setup-token` login pseudo-terminal.
|
|
* The worker registers the terminal under the host route identifier and returns a
|
|
* worker session identifier for the output notification binding only. The worker
|
|
* streams output and the exit through `ctx.loginPty`, never as a reply.
|
|
* Defining the four `onLoginPty*` hooks advertises the four methods.
|
|
*/
|
|
onLoginPtyOpen?(
|
|
params: PluginLoginPtyOpenParams,
|
|
): Promise<PluginLoginPtyOpenResult>;
|
|
|
|
/** Called to write delayed input to an open login pseudo-terminal, keyed by the worker session identifier. */
|
|
onLoginPtyInput?(params: PluginLoginPtyInputParams): Promise<void>;
|
|
|
|
/** Called to stop an open login pseudo-terminal child, keyed by the worker session identifier. */
|
|
onLoginPtyStop?(params: PluginLoginPtyStopParams): Promise<void>;
|
|
|
|
/**
|
|
* Called to close an open login pseudo-terminal by the host route identifier. The
|
|
* worker closes the exact terminal registered under that identifier and returns a
|
|
* close acknowledgement that carries the same identifier.
|
|
*/
|
|
onLoginPtyClose?(
|
|
params: PluginLoginPtyCloseParams,
|
|
): Promise<PluginLoginPtyCloseResult>;
|
|
|
|
/**
|
|
* Called to open one persistent duplex channel. The worker registers the
|
|
* channel under the host route identifier and returns a worker session
|
|
* identifier for the data notification binding only. The worker streams data
|
|
* and the exit through worker→host notifications, never as a reply. Defining
|
|
* the four `onDuplexChannel*` hooks advertises the four methods. The host reads
|
|
* the open verb to gate the `duplexCommandStream` capability.
|
|
*
|
|
* HTTP/2 is the preferred transport. `queue_v1` is the soft-deprecated fallback.
|
|
*/
|
|
onDuplexChannelOpen?(
|
|
params: PluginDuplexChannelOpenParams,
|
|
): Promise<PluginDuplexChannelOpenResult>;
|
|
|
|
/** Called to write raw input to an open duplex channel, keyed by the worker session identifier. */
|
|
onDuplexChannelWrite?(params: PluginDuplexChannelWriteParams): Promise<void>;
|
|
|
|
/** Called to stop an open duplex channel child, keyed by the worker session identifier. */
|
|
onDuplexChannelStop?(params: PluginDuplexChannelStopParams): Promise<void>;
|
|
|
|
/**
|
|
* Called to close an open duplex channel by the host route identifier. The
|
|
* worker closes the exact channel registered under that identifier and returns
|
|
* a close acknowledgement that carries the same identifier.
|
|
*/
|
|
onDuplexChannelClose?(
|
|
params: PluginDuplexChannelCloseParams,
|
|
): Promise<PluginDuplexChannelCloseResult>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 });
|
|
}
|