2175 lines
65 KiB
TypeScript
2175 lines
65 KiB
TypeScript
/**
|
||
* JSON-RPC 2.0 message types and protocol helpers for the host ↔ worker IPC
|
||
* channel.
|
||
*
|
||
* The Paperclip plugin runtime uses JSON-RPC 2.0 over stdio to communicate
|
||
* between the host process and each plugin worker process. This module defines:
|
||
*
|
||
* - Core JSON-RPC 2.0 envelope types (request, response, notification, error)
|
||
* - Standard and plugin-specific error codes
|
||
* - Typed method maps for host→worker and worker→host calls
|
||
* - Helper functions for creating well-formed messages
|
||
*
|
||
* @see PLUGIN_SPEC.md §12.1 — Process Model
|
||
* @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
|
||
* @see https://www.jsonrpc.org/specification
|
||
*/
|
||
|
||
import type {
|
||
PaperclipPluginManifestV1,
|
||
PluginLauncherBounds,
|
||
PluginLauncherRenderContextSnapshot,
|
||
PluginLauncherRenderEnvironment,
|
||
PluginStateScopeKind,
|
||
Company,
|
||
Project,
|
||
Issue,
|
||
IssueComment,
|
||
IssueDocument,
|
||
IssueDocumentSummary,
|
||
IssueAssigneeAdapterOverrides,
|
||
IssueAttachment,
|
||
IssueThreadInteraction,
|
||
CreateIssueThreadInteraction,
|
||
Approval,
|
||
PluginManagedAgentResolution,
|
||
PluginManagedProjectResolution,
|
||
PluginManagedRoutineResolution,
|
||
PluginManagedSkillResolution,
|
||
Routine,
|
||
RoutineRun,
|
||
Agent,
|
||
Goal,
|
||
PluginLocalFolderDeclaration,
|
||
PrincipalPermissionGrant,
|
||
ExternalObjectStatusCategory,
|
||
ExternalObjectStatusTone,
|
||
ExternalObjectLivenessState,
|
||
ExternalObjectMentionConfidence,
|
||
ExternalObjectMentionSourceKind,
|
||
EnvSecretRefBinding,
|
||
} from "@paperclipai/shared";
|
||
export type { PluginLauncherRenderContextSnapshot } from "@paperclipai/shared";
|
||
|
||
import type {
|
||
PluginEvent,
|
||
PluginIssueCheckoutOwnership,
|
||
PluginIssueOrchestrationSummary,
|
||
PluginIssueRelationSummary,
|
||
PluginIssueSubtree,
|
||
PluginIssueAttachmentContent,
|
||
PluginIssueWakeupBatchResult,
|
||
PluginIssueWakeupResult,
|
||
PluginJobContext,
|
||
PluginExecutionWorkspaceMetadata,
|
||
PluginWorkspace,
|
||
ToolRunContext,
|
||
ToolResult,
|
||
PluginLocalFolderListing,
|
||
PluginLocalFolderStatus,
|
||
PluginAccessInvite,
|
||
PluginAccessMember,
|
||
PluginAssignmentPreviewInput,
|
||
PluginAuthorizationAuditEntry,
|
||
PluginAuthorizationDecisionResult,
|
||
PluginAuthorizationPolicyRecord,
|
||
PluginAuthorizationPolicySummary,
|
||
} from "./types.js";
|
||
import type {
|
||
PluginHealthDiagnostics,
|
||
PluginApiRequestInput,
|
||
PluginApiResponse,
|
||
PluginConfigValidationResult,
|
||
PluginWebhookInput,
|
||
} from "./define-plugin.js";
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// JSON-RPC 2.0 — Core Protocol Types
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** The JSON-RPC protocol version. Always `"2.0"`. */
|
||
export const JSONRPC_VERSION = "2.0" as const;
|
||
|
||
/**
|
||
* A unique request identifier. JSON-RPC 2.0 allows strings or numbers;
|
||
* we use strings (UUIDs or monotonic counters) for all Paperclip messages.
|
||
*/
|
||
export type JsonRpcId = string | number;
|
||
|
||
/**
|
||
* Host-owned scope attached to a host→worker invocation. Workers may echo the
|
||
* invocation id on nested worker→host calls, but they never author this scope.
|
||
*/
|
||
export interface JsonRpcInvocationScope {
|
||
readonly companyId?: string | null;
|
||
}
|
||
|
||
export interface JsonRpcInvocationContext {
|
||
readonly id: string;
|
||
readonly scope: JsonRpcInvocationScope;
|
||
}
|
||
|
||
/**
|
||
* A JSON-RPC 2.0 request message.
|
||
*
|
||
* The host sends requests to the worker (or vice versa) and expects a
|
||
* matching response with the same `id`.
|
||
*/
|
||
export interface JsonRpcRequest<
|
||
TMethod extends string = string,
|
||
TParams = unknown,
|
||
> {
|
||
readonly jsonrpc: typeof JSONRPC_VERSION;
|
||
/** Unique request identifier. Must be echoed in the response. */
|
||
readonly id: JsonRpcId;
|
||
/** The RPC method name to invoke. */
|
||
readonly method: TMethod;
|
||
/** Structured parameters for the method call. */
|
||
readonly params: TParams;
|
||
/**
|
||
* Host-issued metadata for the top-level plugin invocation that is currently
|
||
* executing. The worker treats this as opaque and echoes only the id on
|
||
* worker→host calls made from the same async execution context.
|
||
*/
|
||
readonly paperclipInvocation?: PluginInvocationContext;
|
||
/** Opaque top-level invocation id echoed by worker→host requests. */
|
||
readonly paperclipInvocationId?: string;
|
||
}
|
||
|
||
/**
|
||
* A JSON-RPC 2.0 success response.
|
||
*/
|
||
export interface JsonRpcSuccessResponse<TResult = unknown> {
|
||
readonly jsonrpc: typeof JSONRPC_VERSION;
|
||
/** Echoed request identifier. */
|
||
readonly id: JsonRpcId;
|
||
/** The method return value. */
|
||
readonly result: TResult;
|
||
readonly error?: never;
|
||
}
|
||
|
||
/**
|
||
* A JSON-RPC 2.0 error object embedded in an error response.
|
||
*/
|
||
export interface JsonRpcError<TData = unknown> {
|
||
/** Machine-readable error code. */
|
||
readonly code: number;
|
||
/** Human-readable error message. */
|
||
readonly message: string;
|
||
/** Optional structured error data. */
|
||
readonly data?: TData;
|
||
}
|
||
|
||
/**
|
||
* A JSON-RPC 2.0 error response.
|
||
*/
|
||
export interface JsonRpcErrorResponse<TData = unknown> {
|
||
readonly jsonrpc: typeof JSONRPC_VERSION;
|
||
/** Echoed request identifier. */
|
||
readonly id: JsonRpcId | null;
|
||
readonly result?: never;
|
||
/** The error object. */
|
||
readonly error: JsonRpcError<TData>;
|
||
}
|
||
|
||
/**
|
||
* A JSON-RPC 2.0 response — either success or error.
|
||
*/
|
||
export type JsonRpcResponse<TResult = unknown, TData = unknown> =
|
||
| JsonRpcSuccessResponse<TResult>
|
||
| JsonRpcErrorResponse<TData>;
|
||
|
||
/**
|
||
* A JSON-RPC 2.0 notification (a request with no `id`).
|
||
*
|
||
* Notifications are fire-and-forget — no response is expected.
|
||
*/
|
||
export interface JsonRpcNotification<
|
||
TMethod extends string = string,
|
||
TParams = unknown,
|
||
> {
|
||
readonly jsonrpc: typeof JSONRPC_VERSION;
|
||
readonly id?: never;
|
||
/** The notification method name. */
|
||
readonly method: TMethod;
|
||
/** Structured parameters for the notification. */
|
||
readonly params: TParams;
|
||
/**
|
||
* Host-issued metadata for host→worker push notifications such as events.
|
||
* Worker→host notifications echo only `paperclipInvocationId`.
|
||
*/
|
||
readonly paperclipInvocation?: PluginInvocationContext;
|
||
/** Opaque top-level invocation id echoed by worker→host notifications. */
|
||
readonly paperclipInvocationId?: string;
|
||
}
|
||
|
||
/**
|
||
* Any well-formed JSON-RPC 2.0 message (request, response, or notification).
|
||
*/
|
||
export type JsonRpcMessage =
|
||
| JsonRpcRequest
|
||
| JsonRpcResponse
|
||
| JsonRpcNotification;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Error Codes
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Standard JSON-RPC 2.0 error codes.
|
||
*
|
||
* @see https://www.jsonrpc.org/specification#error_object
|
||
*/
|
||
export const JSONRPC_ERROR_CODES = {
|
||
/** Invalid JSON was received by the server. */
|
||
PARSE_ERROR: -32700,
|
||
/** The JSON sent is not a valid Request object. */
|
||
INVALID_REQUEST: -32600,
|
||
/** The method does not exist or is not available. */
|
||
METHOD_NOT_FOUND: -32601,
|
||
/** Invalid method parameter(s). */
|
||
INVALID_PARAMS: -32602,
|
||
/** Internal JSON-RPC error. */
|
||
INTERNAL_ERROR: -32603,
|
||
} as const;
|
||
|
||
export type JsonRpcErrorCode =
|
||
(typeof JSONRPC_ERROR_CODES)[keyof typeof JSONRPC_ERROR_CODES];
|
||
|
||
/**
|
||
* Paperclip plugin-specific error codes.
|
||
*
|
||
* These live in the JSON-RPC "server error" reserved range (-32000 to -32099)
|
||
* as specified by JSON-RPC 2.0 for implementation-defined server errors.
|
||
*
|
||
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
|
||
*/
|
||
export const PLUGIN_RPC_ERROR_CODES = {
|
||
/** The worker process is not running or not reachable. */
|
||
WORKER_UNAVAILABLE: -32000,
|
||
/** The plugin does not have the required capability for this operation. */
|
||
CAPABILITY_DENIED: -32001,
|
||
/** The worker reported an unhandled error during method execution. */
|
||
WORKER_ERROR: -32002,
|
||
/** The method call timed out waiting for the worker response. */
|
||
TIMEOUT: -32003,
|
||
/** The worker does not implement the requested optional method. */
|
||
METHOD_NOT_IMPLEMENTED: -32004,
|
||
/** The worker→host call attempted to escape the current invocation company scope. */
|
||
INVOCATION_SCOPE_DENIED: -32005,
|
||
/**
|
||
* A `configChanged` delivery would have collapsed a single-tenant worker onto
|
||
* a second, distinct company's configuration. The worker fails closed instead
|
||
* of silently overwriting the already-applied tenant's config. A plugin that
|
||
* genuinely serves multiple companies from one worker must opt in via
|
||
* `multiCompanyConfig: true` on its definition.
|
||
*/
|
||
CROSS_TENANT_CONFIG: -32006,
|
||
/** A catch-all for errors that do not fit other categories. */
|
||
UNKNOWN: -32099,
|
||
} as const;
|
||
|
||
export type PluginRpcErrorCode =
|
||
(typeof PLUGIN_RPC_ERROR_CODES)[keyof typeof PLUGIN_RPC_ERROR_CODES];
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Invocation scope metadata
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Company scope attached by the host to one top-level plugin invocation.
|
||
* Absence of this metadata means the invocation is instance/global scoped.
|
||
*/
|
||
export interface PluginInvocationScope {
|
||
companyId: string;
|
||
}
|
||
|
||
/**
|
||
* Opaque invocation metadata generated by the host. Workers must not derive or
|
||
* mutate this. They only echo the id on nested worker→host RPC calls.
|
||
*/
|
||
export interface PluginInvocationContext {
|
||
id: string;
|
||
scope: PluginInvocationScope;
|
||
}
|
||
|
||
/**
|
||
* Context provided to host-side worker→host handlers after the worker echoes a
|
||
* host-issued invocation id.
|
||
*/
|
||
export interface WorkerHostCallContext {
|
||
invocationScope?: PluginInvocationScope | null;
|
||
invalidInvocationScope?: boolean;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Host → Worker Method Signatures (§13 Host-Worker Protocol)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Input for the `initialize` RPC method.
|
||
*
|
||
* @see PLUGIN_SPEC.md §13.1 — `initialize`
|
||
*/
|
||
export interface InitializeParams {
|
||
/** Full plugin manifest snapshot. */
|
||
manifest: PaperclipPluginManifestV1;
|
||
/** Bootstrap configuration. Company-scoped config is read via `ctx.config.get(companyId)`. */
|
||
config: Record<string, unknown>;
|
||
/** Instance-level metadata. */
|
||
instanceInfo: {
|
||
/** UUID of this Paperclip instance. */
|
||
instanceId: string;
|
||
/** Semver version of the running Paperclip host. */
|
||
hostVersion: string;
|
||
};
|
||
/** Host API version. */
|
||
apiVersion: number;
|
||
/** Host-derived plugin database namespace, when the manifest declares database access. */
|
||
databaseNamespace?: string | null;
|
||
}
|
||
|
||
/**
|
||
* Result returned by the `initialize` RPC method.
|
||
*/
|
||
export interface InitializeResult {
|
||
/** Whether initialization succeeded. */
|
||
ok: boolean;
|
||
/** Optional methods the worker has implemented (e.g. "validateConfig", "onEvent"). */
|
||
supportedMethods?: string[];
|
||
}
|
||
|
||
/**
|
||
* Input for the `configChanged` RPC method.
|
||
*
|
||
* @see PLUGIN_SPEC.md §13.4 — `configChanged`
|
||
*/
|
||
export interface ConfigChangedParams {
|
||
/** The newly resolved company-scoped configuration. */
|
||
config: Record<string, unknown>;
|
||
/** Company whose plugin config changed. */
|
||
companyId?: string | null;
|
||
}
|
||
|
||
/**
|
||
* Input for the `validateConfig` RPC method.
|
||
*
|
||
* @see PLUGIN_SPEC.md §13.3 — `validateConfig`
|
||
*/
|
||
export interface ValidateConfigParams {
|
||
/** The configuration to validate. */
|
||
config: Record<string, unknown>;
|
||
}
|
||
|
||
/**
|
||
* Input for the `onEvent` RPC method.
|
||
*
|
||
* @see PLUGIN_SPEC.md §13.5 — `onEvent`
|
||
*/
|
||
export interface OnEventParams {
|
||
/** The domain event to deliver. */
|
||
event: PluginEvent;
|
||
}
|
||
|
||
/**
|
||
* Input for the `runJob` RPC method.
|
||
*
|
||
* @see PLUGIN_SPEC.md §13.6 — `runJob`
|
||
*/
|
||
export interface RunJobParams {
|
||
/** Job execution context. */
|
||
job: PluginJobContext;
|
||
}
|
||
|
||
/**
|
||
* Input for the `getData` RPC method.
|
||
*
|
||
* @see PLUGIN_SPEC.md §13.8 — `getData`
|
||
*/
|
||
export interface GetDataParams {
|
||
/** Plugin-defined data key (e.g. `"sync-health"`). */
|
||
key: string;
|
||
/** Host-authorized active company scope, when this bridge call is company-scoped. */
|
||
companyId?: string | null;
|
||
/** Context and query parameters from the UI. */
|
||
params: Record<string, unknown>;
|
||
/** Optional launcher/container metadata from the host render environment. */
|
||
renderEnvironment?: PluginLauncherRenderContextSnapshot | null;
|
||
}
|
||
|
||
/**
|
||
* Input for the `performAction` RPC method.
|
||
*
|
||
* @see PLUGIN_SPEC.md §13.9 — `performAction`
|
||
*/
|
||
export type PluginPerformActionActorType = "user" | "agent" | "system";
|
||
|
||
export interface PluginPerformActionActorContext {
|
||
/** Authenticated principal type resolved by the Paperclip host. */
|
||
type: PluginPerformActionActorType;
|
||
/** Authenticated board user id when `type === "user"`, otherwise null. */
|
||
userId: string | null;
|
||
/** Authenticated agent id when `type === "agent"`, otherwise null. */
|
||
agentId: string | null;
|
||
/** Authenticated heartbeat/run id when available. */
|
||
runId: string | null;
|
||
/** Company id authorized by the host bridge for this action, when applicable. */
|
||
companyId: string | null;
|
||
}
|
||
|
||
export interface PluginPerformActionContext {
|
||
/** Immutable authenticated actor context supplied by the host. */
|
||
actor: Readonly<PluginPerformActionActorContext>;
|
||
/** Convenience alias for `actor.companyId`. */
|
||
companyId: string | null;
|
||
}
|
||
|
||
export interface PerformActionParams {
|
||
/** Plugin-defined action key (e.g. `"resync"`). */
|
||
key: string;
|
||
/** Host-authorized active company scope, when this bridge call is company-scoped. */
|
||
companyId?: string | null;
|
||
/** Action parameters from the UI. */
|
||
params: Record<string, unknown>;
|
||
/** Authenticated actor context resolved by the host, never by caller params. */
|
||
actorContext?: PluginPerformActionActorContext | null;
|
||
/** Optional launcher/container metadata from the host render environment. */
|
||
renderEnvironment?: PluginLauncherRenderContextSnapshot | null;
|
||
}
|
||
|
||
/**
|
||
* Input for the `executeTool` RPC method.
|
||
*
|
||
* @see PLUGIN_SPEC.md §13.10 — `executeTool`
|
||
*/
|
||
export interface ExecuteToolParams {
|
||
/** Tool name (without plugin namespace prefix). */
|
||
toolName: string;
|
||
/** Parsed parameters matching the tool's declared schema. */
|
||
parameters: unknown;
|
||
/** Agent run context. */
|
||
runContext: ToolRunContext;
|
||
}
|
||
|
||
export interface PluginExternalObjectUrlCandidate {
|
||
sanitizedCanonicalUrl: string;
|
||
sanitizedDisplayUrl: string;
|
||
canonicalIdentityHash: string;
|
||
canonicalIdentity: Record<string, unknown>;
|
||
redactedMatchedText: string;
|
||
}
|
||
|
||
export interface PluginExternalObjectSourceContext {
|
||
companyId: string;
|
||
sourceIssueId: string;
|
||
sourceKind: ExternalObjectMentionSourceKind;
|
||
sourceRecordId: string | null;
|
||
documentKey: string | null;
|
||
propertyKey: string | null;
|
||
}
|
||
|
||
export interface DetectExternalObjectsParams {
|
||
companyId: string;
|
||
urls: PluginExternalObjectUrlCandidate[];
|
||
sourceContext: PluginExternalObjectSourceContext;
|
||
}
|
||
|
||
export interface PluginExternalObjectDetection {
|
||
urlIdentityHash: string;
|
||
providerKey: string;
|
||
objectType: string;
|
||
externalId: string;
|
||
displayKey?: string | null;
|
||
iconKey?: string | null;
|
||
displayTitle?: string | null;
|
||
confidence?: ExternalObjectMentionConfidence;
|
||
}
|
||
|
||
export interface DetectExternalObjectsResult {
|
||
detections: PluginExternalObjectDetection[];
|
||
}
|
||
|
||
export interface PluginExternalObjectRecordSnapshot {
|
||
id: string;
|
||
companyId: string;
|
||
providerKey: string;
|
||
objectType: string;
|
||
externalId: string;
|
||
sanitizedCanonicalUrl: string | null;
|
||
canonicalIdentityHash: string | null;
|
||
displayKey: string | null;
|
||
iconKey: string | null;
|
||
displayTitle: string | null;
|
||
statusKey: string | null;
|
||
statusLabel: string | null;
|
||
statusIconKey: string | null;
|
||
statusCategory: ExternalObjectStatusCategory;
|
||
statusTone: ExternalObjectStatusTone;
|
||
liveness: ExternalObjectLivenessState;
|
||
isTerminal: boolean;
|
||
data: Record<string, unknown>;
|
||
remoteVersion: string | null;
|
||
etag: string | null;
|
||
}
|
||
|
||
export interface ResolveExternalObjectParams {
|
||
companyId: string;
|
||
providerKey: string;
|
||
objectType: string;
|
||
externalId: string;
|
||
object: PluginExternalObjectRecordSnapshot;
|
||
}
|
||
|
||
export interface PluginExternalObjectResolvedSnapshot {
|
||
displayKey?: string | null;
|
||
iconKey?: string | null;
|
||
displayTitle?: string | null;
|
||
statusKey?: string | null;
|
||
statusLabel?: string | null;
|
||
statusIconKey?: string | null;
|
||
statusCategory: ExternalObjectStatusCategory;
|
||
statusTone: ExternalObjectStatusTone;
|
||
isTerminal?: boolean;
|
||
data?: Record<string, unknown>;
|
||
remoteVersion?: string | null;
|
||
etag?: string | null;
|
||
ttlSeconds?: number;
|
||
}
|
||
|
||
export type PluginExternalObjectResolveResult =
|
||
| { ok: true; snapshot: PluginExternalObjectResolvedSnapshot }
|
||
| {
|
||
ok: false;
|
||
liveness: Extract<ExternalObjectLivenessState, "auth_required" | "unreachable">;
|
||
errorCode: string;
|
||
errorMessage?: string | null;
|
||
retryAfterSeconds?: number;
|
||
};
|
||
|
||
export interface RefreshExternalObjectsParams {
|
||
companyId: string;
|
||
objects: PluginExternalObjectRecordSnapshot[];
|
||
}
|
||
|
||
export interface RefreshExternalObjectsResult {
|
||
results: Array<{
|
||
objectId: string;
|
||
result: PluginExternalObjectResolveResult;
|
||
}>;
|
||
}
|
||
|
||
export interface PluginEnvironmentDiagnostic {
|
||
severity: "info" | "warning" | "error";
|
||
message: string;
|
||
code?: string;
|
||
details?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentDriverBaseParams {
|
||
driverKey: string;
|
||
companyId: string;
|
||
environmentId: string;
|
||
issueId?: string | null;
|
||
config: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentValidateConfigParams {
|
||
driverKey: string;
|
||
config: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentValidationResult {
|
||
ok: boolean;
|
||
warnings?: string[];
|
||
errors?: string[];
|
||
normalizedConfig?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentProbeParams extends PluginEnvironmentDriverBaseParams {}
|
||
|
||
export interface PluginEnvironmentProbeResult {
|
||
ok: boolean;
|
||
summary?: string;
|
||
diagnostics?: PluginEnvironmentDiagnostic[];
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentLease {
|
||
providerLeaseId: string | null;
|
||
metadata?: Record<string, unknown>;
|
||
expiresAt?: string | null;
|
||
}
|
||
|
||
export interface PluginEnvironmentAcquireLeaseParams extends PluginEnvironmentDriverBaseParams {
|
||
runId: string;
|
||
workspaceMode?: string;
|
||
requestedCwd?: string;
|
||
agentId?: string;
|
||
executionWorkspaceId?: string | null;
|
||
/**
|
||
* The harness/adapter type for THIS run (the agent's adapter), so a single
|
||
* environment can serve mixed harnesses. When omitted, the driver falls back to
|
||
* the environment's configured default adapter. A provider that materializes a
|
||
* per-run sandbox should use this to select the runtime image and per-run env.
|
||
*/
|
||
adapterType?: string;
|
||
executionWorkspaceSettings?: Record<string, unknown> | null;
|
||
}
|
||
|
||
export interface PluginEnvironmentResumeLeaseParams extends PluginEnvironmentDriverBaseParams {
|
||
providerLeaseId: string;
|
||
leaseMetadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentReleaseLeaseParams extends PluginEnvironmentDriverBaseParams {
|
||
providerLeaseId: string | null;
|
||
leaseMetadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentDestroyLeaseParams extends PluginEnvironmentReleaseLeaseParams {}
|
||
|
||
export interface PluginEnvironmentRealizeWorkspaceParams extends PluginEnvironmentDriverBaseParams {
|
||
lease: PluginEnvironmentLease;
|
||
workspace: {
|
||
localPath?: string;
|
||
remotePath?: string;
|
||
mode?: string;
|
||
metadata?: Record<string, unknown>;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* A plugin `environmentRealizeWorkspace` handler returns only the realized cwd and provider
|
||
* metadata. The server, not the plugin, builds the full workspace-realization record from the run
|
||
* request and merges this cwd and metadata into it. Do not return a `workspaceRealization` record
|
||
* here; the server owns that record, so the referenced (mentioned) project sources reach the adapter.
|
||
*/
|
||
export interface PluginEnvironmentRealizeWorkspaceResult {
|
||
cwd: string;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentExecuteParams extends PluginEnvironmentDriverBaseParams {
|
||
lease: PluginEnvironmentLease;
|
||
command: string;
|
||
args?: string[];
|
||
cwd?: string;
|
||
env?: Record<string, string>;
|
||
stdin?: string;
|
||
timeoutMs?: number;
|
||
}
|
||
|
||
export interface PluginEnvironmentExecuteResult {
|
||
exitCode: number | null;
|
||
signal?: string | null;
|
||
timedOut: boolean;
|
||
stdout: string;
|
||
stderr: string;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
/**
|
||
* A single source→target file or directory transfer within a sync operation.
|
||
*
|
||
* For `environmentSyncIn`, `sourcePath` is a host path and `targetPath` is a
|
||
* sandbox path; for `environmentSyncOut` the direction is reversed. All sandbox
|
||
* paths are POSIX. The contract is provider-agnostic: a provider may transfer a
|
||
* directory by whatever native mechanism it prefers (bulk upload, internal tar,
|
||
* per-file enumeration) as long as the observable result matches this mapping.
|
||
*/
|
||
export interface PluginSyncFileMapping {
|
||
/** Absolute path of the transfer source (host for syncIn, sandbox for syncOut). */
|
||
sourcePath: string;
|
||
/** Absolute path of the transfer target (sandbox for syncIn, host for syncOut). */
|
||
targetPath: string;
|
||
/** Whether the mapping transfers a single regular file or a directory tree. */
|
||
kind: "file" | "directory";
|
||
/**
|
||
* POSIX file mode to apply at the target (e.g. `0o600` for secret material).
|
||
* When set, providers MUST create the target with this mode with no
|
||
* world-readable window (create-with-mode or chmod-before-bytes, never after).
|
||
*/
|
||
mode?: number;
|
||
/** Glob patterns to exclude when `kind` is `"directory"`. */
|
||
exclude?: string[];
|
||
/**
|
||
* Symlink handling for `kind: "directory"` transfers. Falsy preserves symlinks
|
||
* as links; `true` dereferences them to their target bytes. Mirrors tar's `-h`.
|
||
*/
|
||
followSymlinks?: boolean;
|
||
/**
|
||
* Advisory read-write intent for the sandbox target. `"rw"` means the author
|
||
* expects the agent to change the bytes at the target and keep the change.
|
||
* `"ro"` means the target is a read-only tree. An absent value defaults to
|
||
* `"ro"` (read-only is the safe default for an advisory signal).
|
||
*
|
||
* This field is advisory metadata for an optional sandbox feedback wrapper. It
|
||
* does not change the transfer and adds no security. A provider may read it to
|
||
* bind the read-write targets read-write under the wrapper, but the ephemeral
|
||
* sandbox stays the only security boundary.
|
||
*/
|
||
access?: "rw" | "ro";
|
||
/**
|
||
* The sandbox directory that becomes read-write when `access` is `"rw"` and a
|
||
* post-upload command extracts `targetPath` into a different directory. A
|
||
* workspace, git-history, or asset mapping uploads a tar archive, so its
|
||
* `targetPath` is the staging archive under the runtime root, not the directory
|
||
* that the extract command fills. This field names that final destination
|
||
* directory, so a consumer records the real read-write destination, not the
|
||
* staging parent. When absent, the read-write destination is the parent
|
||
* directory of `targetPath`. This field is advisory and ignored when `access`
|
||
* is not `"rw"`.
|
||
*/
|
||
writablePath?: string;
|
||
}
|
||
|
||
/**
|
||
* A single control command run against the sandbox after a sync operation's
|
||
* files have landed. Ordered within {@link PluginSyncOperation.postUploadCommands}
|
||
* and executed in array order, fail-fast (the first non-zero exit or timeout
|
||
* aborts the operation).
|
||
*
|
||
* SECURITY — command origin (Stage-1 design review, condition C1). `command` is
|
||
* a **Paperclip/adapter-authored control operation**: it may be supplied ONLY by
|
||
* core/adapter code. No server route, issue/comment content, project/workspace
|
||
* file content, provider-plugin callback, or arbitrary adapter config may supply
|
||
* a raw `command` string, and any path embedded in it MUST be built by
|
||
* adapter/core helpers from already-confined paths and shell-quoted (C3). A
|
||
* provider MUST treat the command as **opaque**: it may execute or reject it, but
|
||
* MUST NOT rewrite, concatenate, or append provider-decided shell fragments to
|
||
* it.
|
||
*/
|
||
export interface PluginPostUploadCommand {
|
||
/**
|
||
* The opaque, adapter-authored shell command to run after upload. Executed
|
||
* verbatim by the provider (never rewritten/concatenated). See the security
|
||
* note above.
|
||
*/
|
||
command: string;
|
||
/**
|
||
* Working directory for the command. When present, MUST be an absolute POSIX
|
||
* path confined under the operation's allowed sandbox target root (condition
|
||
* C2); providers re-validate it before exec. When absent, the provider
|
||
* defaults to the resolved sync remote/runtime root — never a process default
|
||
* cwd.
|
||
*/
|
||
cwd?: string;
|
||
/** Optional per-command timeout in milliseconds. */
|
||
timeoutMs?: number;
|
||
}
|
||
|
||
/**
|
||
* An ordered, opaque unit of work handed to a sync hook. The `operationId` is an
|
||
* opaque, non-sensitive token authored by the orchestrator; a provider MUST NOT
|
||
* interpret it. Operations are applied in array order.
|
||
*/
|
||
export interface PluginSyncOperation {
|
||
operationId: string;
|
||
files: PluginSyncFileMapping[];
|
||
/**
|
||
* Optional ordered control commands run after this operation's files land, in
|
||
* array order, fail-fast. Absent means "no commands" — byte-identical to a
|
||
* pre-contract operation. See {@link PluginPostUploadCommand} for the command
|
||
* origin/confinement security contract (C1–C4).
|
||
*/
|
||
postUploadCommands?: PluginPostUploadCommand[];
|
||
}
|
||
|
||
export interface PluginEnvironmentSyncInParams extends PluginEnvironmentDriverBaseParams {
|
||
lease: PluginEnvironmentLease;
|
||
operations: PluginSyncOperation[];
|
||
}
|
||
|
||
export interface PluginEnvironmentSyncOutParams extends PluginEnvironmentDriverBaseParams {
|
||
lease: PluginEnvironmentLease;
|
||
operations: PluginSyncOperation[];
|
||
}
|
||
|
||
/** Per-operation transfer accounting returned by a sync hook, for observability. */
|
||
export interface PluginEnvironmentSyncResult {
|
||
operations: {
|
||
operationId: string;
|
||
filesTransferred: number;
|
||
bytesTransferred: number;
|
||
}[];
|
||
}
|
||
|
||
export type PluginEnvironmentInteractiveSetupStatus =
|
||
| "starting"
|
||
| "waiting_for_user"
|
||
| "capturing"
|
||
| "promoted"
|
||
| "cancelled"
|
||
| "timed_out"
|
||
| "failed"
|
||
| "missing";
|
||
|
||
export type PluginEnvironmentInteractiveSetupConnectionType =
|
||
| "ssh"
|
||
| (string & {});
|
||
|
||
export type PluginEnvironmentTemplateRefKind =
|
||
| "snapshot"
|
||
| "image"
|
||
| "provider_template"
|
||
| "unknown"
|
||
| (string & {});
|
||
|
||
export interface PluginEnvironmentInteractiveSetupConnectionSummary {
|
||
type: PluginEnvironmentInteractiveSetupConnectionType;
|
||
username?: string | null;
|
||
hostRedacted: boolean;
|
||
portRedacted: boolean;
|
||
commandRedacted?: boolean;
|
||
expiresAt?: string | null;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentInteractiveSetupConnectionPayload {
|
||
type: PluginEnvironmentInteractiveSetupConnectionType;
|
||
command?: string | null;
|
||
token?: string | null;
|
||
expiresAt?: string | null;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentInteractiveSetupSession {
|
||
providerLeaseId: string | null;
|
||
status: PluginEnvironmentInteractiveSetupStatus;
|
||
connectionSummary: PluginEnvironmentInteractiveSetupConnectionSummary | null;
|
||
connectionPayload?: PluginEnvironmentInteractiveSetupConnectionPayload | null;
|
||
expiresAt?: string | null;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentStartInteractiveSetupParams extends PluginEnvironmentDriverBaseParams {
|
||
sessionId: string;
|
||
sourceTemplateRef?: string | null;
|
||
sourceTemplateKind?: PluginEnvironmentTemplateRefKind | null;
|
||
connectionExpiresInMinutes?: number | null;
|
||
expiresAt?: string | null;
|
||
}
|
||
|
||
export interface PluginEnvironmentGetInteractiveSetupParams extends PluginEnvironmentDriverBaseParams {
|
||
providerLeaseId: string | null;
|
||
setupMetadata?: Record<string, unknown>;
|
||
includeConnectionPayload?: boolean;
|
||
connectionExpiresInMinutes?: number | null;
|
||
}
|
||
|
||
export interface PluginEnvironmentCaptureTemplateParams extends PluginEnvironmentDriverBaseParams {
|
||
providerLeaseId: string | null;
|
||
setupMetadata?: Record<string, unknown>;
|
||
sourceTemplateRef?: string | null;
|
||
previousTemplateRef?: string | null;
|
||
templateLabel?: string | null;
|
||
timeoutMs?: number | null;
|
||
}
|
||
|
||
export interface PluginEnvironmentCaptureTemplateResult {
|
||
templateRef: string;
|
||
templateKind: PluginEnvironmentTemplateRefKind;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentCancelInteractiveSetupParams extends PluginEnvironmentDriverBaseParams {
|
||
providerLeaseId: string | null;
|
||
setupMetadata?: Record<string, unknown>;
|
||
reason?: string | null;
|
||
}
|
||
|
||
export interface PluginEnvironmentCancelInteractiveSetupResult {
|
||
status: Extract<PluginEnvironmentInteractiveSetupStatus, "cancelled" | "timed_out" | "failed" | "missing">;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface PluginEnvironmentDeleteTemplateParams extends PluginEnvironmentDriverBaseParams {
|
||
templateRef: string;
|
||
templateKind?: PluginEnvironmentTemplateRefKind;
|
||
metadata?: Record<string, unknown>;
|
||
reason?: string | null;
|
||
}
|
||
|
||
export interface PluginEnvironmentDeleteTemplateResult {
|
||
deleted: boolean;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// UI launcher / modal host interaction payloads
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Bounds request issued by a plugin UI running inside a host-managed launcher
|
||
* container such as a modal, drawer, or popover.
|
||
*/
|
||
export interface PluginModalBoundsRequest {
|
||
/** High-level size preset requested from the host. */
|
||
bounds: PluginLauncherBounds;
|
||
/** Optional explicit width override in CSS pixels. */
|
||
width?: number;
|
||
/** Optional explicit height override in CSS pixels. */
|
||
height?: number;
|
||
/** Optional lower bounds for host resizing decisions. */
|
||
minWidth?: number;
|
||
minHeight?: number;
|
||
/** Optional upper bounds for host resizing decisions. */
|
||
maxWidth?: number;
|
||
maxHeight?: number;
|
||
}
|
||
|
||
/**
|
||
* Reason metadata supplied by host-managed close lifecycle callbacks.
|
||
*/
|
||
export interface PluginRenderCloseEvent {
|
||
reason:
|
||
| "escapeKey"
|
||
| "backdrop"
|
||
| "hostNavigation"
|
||
| "programmatic"
|
||
| "submit"
|
||
| "unknown";
|
||
nativeEvent?: unknown;
|
||
}
|
||
|
||
/**
|
||
* Map of host→worker RPC method names to their `[params, result]` types.
|
||
*
|
||
* This type is the single source of truth for all methods the host can call
|
||
* on a worker. Used by both the host dispatcher and the worker handler to
|
||
* ensure type safety across the IPC boundary.
|
||
*/
|
||
export interface HostToWorkerMethods {
|
||
/** @see PLUGIN_SPEC.md §13.1 */
|
||
initialize: [params: InitializeParams, result: InitializeResult];
|
||
/** @see PLUGIN_SPEC.md §13.2 */
|
||
health: [params: Record<string, never>, result: PluginHealthDiagnostics];
|
||
/** @see PLUGIN_SPEC.md §12.5 */
|
||
shutdown: [params: Record<string, never>, result: void];
|
||
/** @see PLUGIN_SPEC.md §13.3 */
|
||
validateConfig: [params: ValidateConfigParams, result: PluginConfigValidationResult];
|
||
/** @see PLUGIN_SPEC.md §13.4 */
|
||
configChanged: [params: ConfigChangedParams, result: void];
|
||
/** @see PLUGIN_SPEC.md §13.5 */
|
||
onEvent: [params: OnEventParams, result: void];
|
||
/** @see PLUGIN_SPEC.md §13.6 */
|
||
runJob: [params: RunJobParams, result: void];
|
||
/** @see PLUGIN_SPEC.md §13.7 */
|
||
handleWebhook: [params: PluginWebhookInput, result: void];
|
||
/** Scoped plugin API route dispatch. */
|
||
handleApiRequest: [params: PluginApiRequestInput, result: PluginApiResponse];
|
||
/** @see PLUGIN_SPEC.md §13.8 */
|
||
getData: [params: GetDataParams, result: unknown];
|
||
/** @see PLUGIN_SPEC.md §13.9 */
|
||
performAction: [params: PerformActionParams, result: unknown];
|
||
/** @see PLUGIN_SPEC.md §13.10 */
|
||
executeTool: [params: ExecuteToolParams, result: ToolResult];
|
||
detectExternalObjects: [
|
||
params: DetectExternalObjectsParams,
|
||
result: DetectExternalObjectsResult,
|
||
];
|
||
resolveExternalObject: [
|
||
params: ResolveExternalObjectParams,
|
||
result: PluginExternalObjectResolveResult,
|
||
];
|
||
refreshExternalObjects: [
|
||
params: RefreshExternalObjectsParams,
|
||
result: RefreshExternalObjectsResult,
|
||
];
|
||
environmentValidateConfig: [
|
||
params: PluginEnvironmentValidateConfigParams,
|
||
result: PluginEnvironmentValidationResult,
|
||
];
|
||
environmentProbe: [
|
||
params: PluginEnvironmentProbeParams,
|
||
result: PluginEnvironmentProbeResult,
|
||
];
|
||
environmentAcquireLease: [
|
||
params: PluginEnvironmentAcquireLeaseParams,
|
||
result: PluginEnvironmentLease,
|
||
];
|
||
environmentResumeLease: [
|
||
params: PluginEnvironmentResumeLeaseParams,
|
||
result: PluginEnvironmentLease,
|
||
];
|
||
environmentReleaseLease: [
|
||
params: PluginEnvironmentReleaseLeaseParams,
|
||
result: void,
|
||
];
|
||
environmentDestroyLease: [
|
||
params: PluginEnvironmentDestroyLeaseParams,
|
||
result: void,
|
||
];
|
||
environmentRealizeWorkspace: [
|
||
params: PluginEnvironmentRealizeWorkspaceParams,
|
||
result: PluginEnvironmentRealizeWorkspaceResult,
|
||
];
|
||
environmentExecute: [
|
||
params: PluginEnvironmentExecuteParams,
|
||
result: PluginEnvironmentExecuteResult,
|
||
];
|
||
environmentSyncIn: [
|
||
params: PluginEnvironmentSyncInParams,
|
||
result: PluginEnvironmentSyncResult,
|
||
];
|
||
environmentSyncOut: [
|
||
params: PluginEnvironmentSyncOutParams,
|
||
result: PluginEnvironmentSyncResult,
|
||
];
|
||
environmentStartInteractiveSetup: [
|
||
params: PluginEnvironmentStartInteractiveSetupParams,
|
||
result: PluginEnvironmentInteractiveSetupSession,
|
||
];
|
||
environmentGetInteractiveSetup: [
|
||
params: PluginEnvironmentGetInteractiveSetupParams,
|
||
result: PluginEnvironmentInteractiveSetupSession,
|
||
];
|
||
environmentCaptureTemplate: [
|
||
params: PluginEnvironmentCaptureTemplateParams,
|
||
result: PluginEnvironmentCaptureTemplateResult,
|
||
];
|
||
environmentCancelInteractiveSetup: [
|
||
params: PluginEnvironmentCancelInteractiveSetupParams,
|
||
result: PluginEnvironmentCancelInteractiveSetupResult,
|
||
];
|
||
environmentDeleteTemplate: [
|
||
params: PluginEnvironmentDeleteTemplateParams,
|
||
result: PluginEnvironmentDeleteTemplateResult,
|
||
];
|
||
}
|
||
|
||
/** Union of all host→worker method names. */
|
||
export type HostToWorkerMethodName = keyof HostToWorkerMethods;
|
||
|
||
/** Required methods the worker MUST implement. */
|
||
export const HOST_TO_WORKER_REQUIRED_METHODS: readonly HostToWorkerMethodName[] = [
|
||
"initialize",
|
||
"health",
|
||
"shutdown",
|
||
] as const;
|
||
|
||
/** Optional methods the worker MAY implement. */
|
||
export const HOST_TO_WORKER_OPTIONAL_METHODS: readonly HostToWorkerMethodName[] = [
|
||
"validateConfig",
|
||
"configChanged",
|
||
"onEvent",
|
||
"runJob",
|
||
"handleWebhook",
|
||
"handleApiRequest",
|
||
"getData",
|
||
"performAction",
|
||
"executeTool",
|
||
"detectExternalObjects",
|
||
"resolveExternalObject",
|
||
"refreshExternalObjects",
|
||
"environmentValidateConfig",
|
||
"environmentProbe",
|
||
"environmentAcquireLease",
|
||
"environmentResumeLease",
|
||
"environmentReleaseLease",
|
||
"environmentDestroyLease",
|
||
"environmentRealizeWorkspace",
|
||
"environmentExecute",
|
||
"environmentSyncIn",
|
||
"environmentSyncOut",
|
||
"environmentStartInteractiveSetup",
|
||
"environmentGetInteractiveSetup",
|
||
"environmentCaptureTemplate",
|
||
"environmentCancelInteractiveSetup",
|
||
"environmentDeleteTemplate",
|
||
] as const;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Worker → Host Method Signatures (SDK client calls)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Map of worker→host RPC method names to their `[params, result]` types.
|
||
*
|
||
* These represent the SDK client calls that the worker makes back to the
|
||
* host to access platform services (state, entities, config, etc.).
|
||
*/
|
||
export interface WorkerToHostMethods {
|
||
// Config
|
||
"config.get": [params: { companyId?: string }, result: Record<string, unknown>];
|
||
|
||
// Trusted local folders
|
||
"localFolders.declarations": [
|
||
params: Record<string, never>,
|
||
result: PluginLocalFolderDeclaration[],
|
||
];
|
||
"localFolders.configure": [
|
||
params: {
|
||
companyId: string;
|
||
folderKey: string;
|
||
path: string;
|
||
access?: "read" | "readWrite";
|
||
requiredDirectories?: string[];
|
||
requiredFiles?: string[];
|
||
},
|
||
result: PluginLocalFolderStatus,
|
||
];
|
||
"localFolders.status": [
|
||
params: { companyId: string; folderKey: string },
|
||
result: PluginLocalFolderStatus,
|
||
];
|
||
"localFolders.list": [
|
||
params: { companyId: string; folderKey: string; relativePath?: string | null; recursive?: boolean; maxEntries?: number },
|
||
result: PluginLocalFolderListing,
|
||
];
|
||
"localFolders.readText": [
|
||
params: { companyId: string; folderKey: string; relativePath: string },
|
||
result: string,
|
||
];
|
||
"localFolders.writeTextAtomic": [
|
||
params: {
|
||
companyId: string;
|
||
folderKey: string;
|
||
relativePath: string;
|
||
contents: string;
|
||
},
|
||
result: PluginLocalFolderStatus,
|
||
];
|
||
"localFolders.deleteFile": [
|
||
params: { companyId: string; folderKey: string; relativePath: string },
|
||
result: PluginLocalFolderStatus,
|
||
];
|
||
|
||
// State
|
||
"state.get": [
|
||
params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string },
|
||
result: unknown,
|
||
];
|
||
"state.set": [
|
||
params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string; value: unknown },
|
||
result: void,
|
||
];
|
||
"state.delete": [
|
||
params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string },
|
||
result: void,
|
||
];
|
||
|
||
// Restricted plugin database namespace
|
||
"db.namespace": [
|
||
params: Record<string, never>,
|
||
result: string,
|
||
];
|
||
"db.query": [
|
||
params: { sql: string; params?: unknown[] },
|
||
result: unknown[],
|
||
];
|
||
"db.execute": [
|
||
params: { sql: string; params?: unknown[] },
|
||
result: { rowCount: number },
|
||
];
|
||
|
||
// Entities
|
||
"entities.upsert": [
|
||
params: {
|
||
entityType: string;
|
||
scopeKind: PluginStateScopeKind;
|
||
scopeId?: string;
|
||
externalId?: string;
|
||
title?: string;
|
||
status?: string;
|
||
data: Record<string, unknown>;
|
||
},
|
||
result: {
|
||
id: string;
|
||
entityType: string;
|
||
scopeKind: PluginStateScopeKind;
|
||
scopeId: string | null;
|
||
externalId: string | null;
|
||
title: string | null;
|
||
status: string | null;
|
||
data: Record<string, unknown>;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
},
|
||
];
|
||
"entities.list": [
|
||
params: {
|
||
entityType?: string;
|
||
scopeKind?: PluginStateScopeKind;
|
||
scopeId?: string;
|
||
externalId?: string;
|
||
limit?: number;
|
||
offset?: number;
|
||
},
|
||
result: Array<{
|
||
id: string;
|
||
entityType: string;
|
||
scopeKind: PluginStateScopeKind;
|
||
scopeId: string | null;
|
||
externalId: string | null;
|
||
title: string | null;
|
||
status: string | null;
|
||
data: Record<string, unknown>;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}>,
|
||
];
|
||
|
||
// Events
|
||
"events.emit": [
|
||
params: { name: string; companyId: string; payload: unknown },
|
||
result: void,
|
||
];
|
||
"events.subscribe": [
|
||
params: { eventPattern: string; filter?: Record<string, unknown> | null },
|
||
result: void,
|
||
];
|
||
|
||
// HTTP
|
||
"http.fetch": [
|
||
params: { url: string; init?: Record<string, unknown> },
|
||
result: { status: number; statusText: string; headers: Record<string, string>; body: string },
|
||
];
|
||
|
||
// Secrets
|
||
"secrets.resolve": [
|
||
params: { secretRef: string | EnvSecretRefBinding; companyId?: string; configPath?: string },
|
||
result: string,
|
||
];
|
||
|
||
// Activity
|
||
"activity.log": [
|
||
params: {
|
||
companyId: string;
|
||
message: string;
|
||
entityType?: string;
|
||
entityId?: string;
|
||
metadata?: Record<string, unknown>;
|
||
},
|
||
result: void,
|
||
];
|
||
|
||
// Metrics
|
||
"metrics.write": [
|
||
params: {
|
||
name: string;
|
||
value: number;
|
||
tags?: Record<string, string>;
|
||
/** Owning tenant for `plugin_logs.company_id` (cascade-delete scope). `null`/omitted = instance-scope. */
|
||
companyId?: string | null;
|
||
},
|
||
result: void,
|
||
];
|
||
|
||
// Telemetry
|
||
"telemetry.track": [
|
||
params: { eventName: string; dimensions?: Record<string, string | number | boolean> },
|
||
result: void,
|
||
];
|
||
|
||
// Logger
|
||
"log": [
|
||
params: {
|
||
level: "info" | "warn" | "error" | "debug";
|
||
message: string;
|
||
meta?: Record<string, unknown>;
|
||
/** Owning tenant for `plugin_logs.company_id` (cascade-delete scope). `null`/omitted = instance-scope. */
|
||
companyId?: string | null;
|
||
},
|
||
result: void,
|
||
];
|
||
|
||
// Companies (read)
|
||
"companies.list": [
|
||
params: { limit?: number; offset?: number },
|
||
result: Company[],
|
||
];
|
||
"companies.get": [
|
||
params: { companyId: string },
|
||
result: Company | null,
|
||
];
|
||
|
||
// Projects (read)
|
||
"projects.list": [
|
||
params: { companyId: string; limit?: number; offset?: number },
|
||
result: Project[],
|
||
];
|
||
"projects.get": [
|
||
params: { projectId: string; companyId: string },
|
||
result: Project | null,
|
||
];
|
||
"projects.listWorkspaces": [
|
||
params: { projectId: string; companyId: string },
|
||
result: PluginWorkspace[],
|
||
];
|
||
"projects.getPrimaryWorkspace": [
|
||
params: { projectId: string; companyId: string },
|
||
result: PluginWorkspace | null,
|
||
];
|
||
"projects.getWorkspaceForIssue": [
|
||
params: { issueId: string; companyId: string },
|
||
result: PluginWorkspace | null,
|
||
];
|
||
"executionWorkspaces.get": [
|
||
params: {
|
||
workspaceId: string;
|
||
companyId: string;
|
||
},
|
||
result: PluginExecutionWorkspaceMetadata | null,
|
||
];
|
||
"projects.managed.get": [
|
||
params: { projectKey: string; companyId: string },
|
||
result: PluginManagedProjectResolution,
|
||
];
|
||
"projects.managed.reconcile": [
|
||
params: { projectKey: string; companyId: string },
|
||
result: PluginManagedProjectResolution,
|
||
];
|
||
"projects.managed.reset": [
|
||
params: { projectKey: string; companyId: string },
|
||
result: PluginManagedProjectResolution,
|
||
];
|
||
"routines.managed.get": [
|
||
params: { routineKey: string; companyId: string },
|
||
result: PluginManagedRoutineResolution,
|
||
];
|
||
"routines.managed.reconcile": [
|
||
params: {
|
||
routineKey: string;
|
||
companyId: string;
|
||
assigneeAgentId?: string | null;
|
||
projectId?: string | null;
|
||
},
|
||
result: PluginManagedRoutineResolution,
|
||
];
|
||
"routines.managed.reset": [
|
||
params: {
|
||
routineKey: string;
|
||
companyId: string;
|
||
assigneeAgentId?: string | null;
|
||
projectId?: string | null;
|
||
},
|
||
result: PluginManagedRoutineResolution,
|
||
];
|
||
"routines.managed.update": [
|
||
params: {
|
||
routineKey: string;
|
||
companyId: string;
|
||
status?: string;
|
||
},
|
||
result: Routine,
|
||
];
|
||
"routines.managed.run": [
|
||
params: {
|
||
routineKey: string;
|
||
companyId: string;
|
||
assigneeAgentId?: string | null;
|
||
projectId?: string | null;
|
||
},
|
||
result: RoutineRun,
|
||
];
|
||
"skills.managed.get": [
|
||
params: { skillKey: string; companyId: string },
|
||
result: PluginManagedSkillResolution,
|
||
];
|
||
"skills.managed.reconcile": [
|
||
params: { skillKey: string; companyId: string },
|
||
result: PluginManagedSkillResolution,
|
||
];
|
||
"skills.managed.reset": [
|
||
params: { skillKey: string; companyId: string },
|
||
result: PluginManagedSkillResolution,
|
||
];
|
||
|
||
// Issues
|
||
"issues.list": [
|
||
params: {
|
||
companyId: string;
|
||
projectId?: string;
|
||
assigneeAgentId?: string;
|
||
originKind?: string;
|
||
originKindPrefix?: string;
|
||
originId?: string;
|
||
status?: string;
|
||
includePluginOperations?: boolean;
|
||
limit?: number;
|
||
offset?: number;
|
||
},
|
||
result: Issue[],
|
||
];
|
||
"issues.get": [
|
||
params: { issueId: string; companyId: string },
|
||
result: Issue | null,
|
||
];
|
||
"issues.create": [
|
||
params: {
|
||
companyId: string;
|
||
projectId?: string;
|
||
goalId?: string;
|
||
parentId?: string;
|
||
inheritExecutionWorkspaceFromIssueId?: string;
|
||
title: string;
|
||
description?: string;
|
||
status?: string;
|
||
priority?: string;
|
||
assigneeAgentId?: string;
|
||
assigneeUserId?: string | null;
|
||
requestDepth?: number;
|
||
billingCode?: string | null;
|
||
assigneeAdapterOverrides?: IssueAssigneeAdapterOverrides | null;
|
||
surfaceVisibility?: string | null;
|
||
originKind?: string | null;
|
||
originId?: string | null;
|
||
originRunId?: string | null;
|
||
blockedByIssueIds?: string[];
|
||
labelIds?: string[];
|
||
executionWorkspaceId?: string | null;
|
||
executionWorkspacePreference?: string | null;
|
||
executionWorkspaceSettings?: Record<string, unknown> | null;
|
||
actorAgentId?: string | null;
|
||
actorUserId?: string | null;
|
||
actorRunId?: string | null;
|
||
},
|
||
result: Issue,
|
||
];
|
||
"issues.update": [
|
||
params: {
|
||
issueId: string;
|
||
patch: Record<string, unknown>;
|
||
companyId: string;
|
||
},
|
||
result: Issue,
|
||
];
|
||
"issues.relations.get": [
|
||
params: { issueId: string; companyId: string },
|
||
result: PluginIssueRelationSummary,
|
||
];
|
||
"issues.relations.setBlockedBy": [
|
||
params: {
|
||
issueId: string;
|
||
companyId: string;
|
||
blockedByIssueIds: string[];
|
||
actorAgentId?: string | null;
|
||
actorUserId?: string | null;
|
||
actorRunId?: string | null;
|
||
},
|
||
result: PluginIssueRelationSummary,
|
||
];
|
||
"issues.relations.addBlockers": [
|
||
params: {
|
||
issueId: string;
|
||
companyId: string;
|
||
blockerIssueIds: string[];
|
||
actorAgentId?: string | null;
|
||
actorUserId?: string | null;
|
||
actorRunId?: string | null;
|
||
},
|
||
result: PluginIssueRelationSummary,
|
||
];
|
||
"issues.relations.removeBlockers": [
|
||
params: {
|
||
issueId: string;
|
||
companyId: string;
|
||
blockerIssueIds: string[];
|
||
actorAgentId?: string | null;
|
||
actorUserId?: string | null;
|
||
actorRunId?: string | null;
|
||
},
|
||
result: PluginIssueRelationSummary,
|
||
];
|
||
"issues.assertCheckoutOwner": [
|
||
params: {
|
||
issueId: string;
|
||
companyId: string;
|
||
actorAgentId: string;
|
||
actorRunId: string;
|
||
},
|
||
result: PluginIssueCheckoutOwnership,
|
||
];
|
||
"issues.getSubtree": [
|
||
params: {
|
||
issueId: string;
|
||
companyId: string;
|
||
includeRoot?: boolean;
|
||
includeRelations?: boolean;
|
||
includeDocuments?: boolean;
|
||
includeActiveRuns?: boolean;
|
||
includeAssignees?: boolean;
|
||
},
|
||
result: PluginIssueSubtree,
|
||
];
|
||
"issues.requestWakeup": [
|
||
params: {
|
||
issueId: string;
|
||
companyId: string;
|
||
reason?: string;
|
||
contextSource?: string;
|
||
idempotencyKey?: string | null;
|
||
actorAgentId?: string | null;
|
||
actorUserId?: string | null;
|
||
actorRunId?: string | null;
|
||
},
|
||
result: PluginIssueWakeupResult,
|
||
];
|
||
"issues.requestWakeups": [
|
||
params: {
|
||
issueIds: string[];
|
||
companyId: string;
|
||
reason?: string;
|
||
contextSource?: string;
|
||
idempotencyKeyPrefix?: string | null;
|
||
actorAgentId?: string | null;
|
||
actorUserId?: string | null;
|
||
actorRunId?: string | null;
|
||
},
|
||
result: PluginIssueWakeupBatchResult[],
|
||
];
|
||
"issues.summaries.getOrchestration": [
|
||
params: {
|
||
issueId: string;
|
||
companyId: string;
|
||
includeSubtree?: boolean;
|
||
billingCode?: string | null;
|
||
},
|
||
result: PluginIssueOrchestrationSummary,
|
||
];
|
||
"issues.listComments": [
|
||
params: { issueId: string; companyId: string },
|
||
result: IssueComment[],
|
||
];
|
||
"issues.createComment": [
|
||
params: {
|
||
issueId: string;
|
||
body: string;
|
||
companyId: string;
|
||
authorAgentId?: string;
|
||
/** Active human company member the comment is attributed to. Requires `issue.comments.create_human_attributed`. */
|
||
actorUserId?: string;
|
||
},
|
||
result: IssueComment,
|
||
];
|
||
"issues.createInteraction": [
|
||
params: {
|
||
issueId: string;
|
||
companyId: string;
|
||
interaction: CreateIssueThreadInteraction;
|
||
authorAgentId?: string | null;
|
||
},
|
||
result: IssueThreadInteraction,
|
||
];
|
||
"issues.listInteractions": [
|
||
params: { issueId: string; companyId: string },
|
||
result: IssueThreadInteraction[],
|
||
];
|
||
"issues.respondInteraction": [
|
||
params: {
|
||
issueId: string;
|
||
interactionId: string;
|
||
companyId: string;
|
||
action: "accept" | "reject";
|
||
/**
|
||
* Active human company member the decision is attributed to. Required —
|
||
* resolving an interaction is a board-user action; the host re-verifies
|
||
* active membership at apply time and never trusts this value blindly.
|
||
*/
|
||
actorUserId?: string;
|
||
reason?: string | null;
|
||
},
|
||
result: { interaction: IssueThreadInteraction; applied: boolean },
|
||
];
|
||
"issues.listAttachments": [
|
||
params: { issueId: string; companyId: string },
|
||
result: IssueAttachment[],
|
||
];
|
||
"issues.getAttachmentContent": [
|
||
params: { attachmentId: string; companyId: string; maxBytes?: number | null },
|
||
result: PluginIssueAttachmentContent | null,
|
||
];
|
||
|
||
// Issue Documents
|
||
"issues.documents.list": [
|
||
params: { issueId: string; companyId: string },
|
||
result: IssueDocumentSummary[],
|
||
];
|
||
"issues.documents.get": [
|
||
params: { issueId: string; key: string; companyId: string },
|
||
result: IssueDocument | null,
|
||
];
|
||
"issues.documents.upsert": [
|
||
params: {
|
||
issueId: string;
|
||
key: string;
|
||
body: string;
|
||
companyId: string;
|
||
title?: string;
|
||
format?: string;
|
||
changeSummary?: string;
|
||
},
|
||
result: IssueDocument,
|
||
];
|
||
"issues.documents.delete": [
|
||
params: { issueId: string; key: string; companyId: string },
|
||
result: void,
|
||
];
|
||
|
||
// Approvals
|
||
"approvals.list": [
|
||
params: { companyId: string; status?: string | null },
|
||
result: Approval[],
|
||
];
|
||
"approvals.get": [
|
||
params: { approvalId: string; companyId: string },
|
||
result: Approval | null,
|
||
];
|
||
"approvals.decide": [
|
||
params: {
|
||
approvalId: string;
|
||
companyId: string;
|
||
action: "approve" | "reject";
|
||
/**
|
||
* Active human company member the decision is attributed to. Required —
|
||
* deciding an approval is a board-user action; the host re-verifies
|
||
* active membership at apply time and never trusts this value blindly.
|
||
*/
|
||
actorUserId?: string;
|
||
decisionNote?: string | null;
|
||
},
|
||
result: { approval: Approval; applied: boolean },
|
||
];
|
||
|
||
// Agents (read)
|
||
"agents.list": [
|
||
params: { companyId: string; status?: string; limit?: number; offset?: number },
|
||
result: Agent[],
|
||
];
|
||
"agents.get": [
|
||
params: { agentId: string; companyId: string },
|
||
result: Agent | null,
|
||
];
|
||
|
||
// Agents (write)
|
||
"agents.pause": [
|
||
params: { agentId: string; companyId: string },
|
||
result: Agent,
|
||
];
|
||
"agents.resume": [
|
||
params: { agentId: string; companyId: string },
|
||
result: Agent,
|
||
];
|
||
"agents.invoke": [
|
||
params: { agentId: string; companyId: string; prompt: string; reason?: string },
|
||
result: { runId: string },
|
||
];
|
||
"agents.managed.get": [
|
||
params: { agentKey: string; companyId: string },
|
||
result: PluginManagedAgentResolution,
|
||
];
|
||
"agents.managed.reconcile": [
|
||
params: { agentKey: string; companyId: string },
|
||
result: PluginManagedAgentResolution,
|
||
];
|
||
"agents.managed.reset": [
|
||
params: { agentKey: string; companyId: string },
|
||
result: PluginManagedAgentResolution,
|
||
];
|
||
|
||
// Agent Sessions
|
||
"agents.sessions.create": [
|
||
params: { agentId: string; companyId: string; taskKey?: string; reason?: string },
|
||
result: { sessionId: string; agentId: string; companyId: string; status: "active" | "closed"; createdAt: string },
|
||
];
|
||
"agents.sessions.list": [
|
||
params: { agentId: string; companyId: string },
|
||
result: Array<{ sessionId: string; agentId: string; companyId: string; status: "active" | "closed"; createdAt: string }>,
|
||
];
|
||
"agents.sessions.sendMessage": [
|
||
params: { sessionId: string; companyId: string; prompt: string; reason?: string },
|
||
result: { runId: string },
|
||
];
|
||
"agents.sessions.close": [
|
||
params: { sessionId: string; companyId: string },
|
||
result: void,
|
||
];
|
||
|
||
// Goals
|
||
"goals.list": [
|
||
params: { companyId: string; level?: string; status?: string; limit?: number; offset?: number },
|
||
result: Goal[],
|
||
];
|
||
"goals.get": [
|
||
params: { goalId: string; companyId: string },
|
||
result: Goal | null,
|
||
];
|
||
"goals.create": [
|
||
params: {
|
||
companyId: string;
|
||
title: string;
|
||
description?: string;
|
||
level?: string;
|
||
status?: string;
|
||
parentId?: string;
|
||
ownerAgentId?: string;
|
||
},
|
||
result: Goal,
|
||
];
|
||
"goals.update": [
|
||
params: {
|
||
goalId: string;
|
||
patch: Record<string, unknown>;
|
||
companyId: string;
|
||
},
|
||
result: Goal,
|
||
];
|
||
|
||
// Access
|
||
"access.members.list": [
|
||
params: { companyId: string; includeArchived?: boolean },
|
||
result: PluginAccessMember[],
|
||
];
|
||
"access.members.get": [
|
||
params: { memberId: string; companyId: string },
|
||
result: PluginAccessMember | null,
|
||
];
|
||
"access.members.update": [
|
||
params: {
|
||
memberId: string;
|
||
companyId: string;
|
||
patch: {
|
||
membershipRole?: string | null;
|
||
status?: "pending" | "active" | "suspended";
|
||
};
|
||
},
|
||
result: PluginAccessMember,
|
||
];
|
||
"access.invites.list": [
|
||
params: {
|
||
companyId: string;
|
||
state?: "active" | "revoked" | "accepted" | "expired";
|
||
limit?: number;
|
||
offset?: number;
|
||
},
|
||
result: { invites: PluginAccessInvite[]; nextOffset: number | null },
|
||
];
|
||
"access.invites.create": [
|
||
params: {
|
||
companyId: string;
|
||
allowedJoinTypes?: "human" | "agent" | "both";
|
||
humanRole?: string | null;
|
||
defaultsPayload?: Record<string, unknown> | null;
|
||
agentMessage?: string | null;
|
||
},
|
||
result: PluginAccessInvite & { token: string },
|
||
];
|
||
"access.invites.revoke": [
|
||
params: { inviteId: string; companyId: string },
|
||
result: PluginAccessInvite,
|
||
];
|
||
|
||
// Authorization
|
||
"authorization.grants.list": [
|
||
params: { companyId: string; principalType?: string; principalId?: string },
|
||
result: PrincipalPermissionGrant[],
|
||
];
|
||
"authorization.grants.set": [
|
||
params: {
|
||
companyId: string;
|
||
principalType: string;
|
||
principalId: string;
|
||
grants: Array<{ permissionKey: string; scope?: Record<string, unknown> | null }>;
|
||
grantedByUserId?: string | null;
|
||
},
|
||
result: PrincipalPermissionGrant[],
|
||
];
|
||
"authorization.policies.summary": [
|
||
params: { companyId: string },
|
||
result: PluginAuthorizationPolicySummary,
|
||
];
|
||
"authorization.policies.get": [
|
||
params: { companyId: string; resourceType: "company" | "agent" | "project" | "issue"; resourceId: string },
|
||
result: PluginAuthorizationPolicyRecord | null,
|
||
];
|
||
"authorization.policies.update": [
|
||
params: {
|
||
companyId: string;
|
||
resourceType: "company" | "agent" | "project" | "issue";
|
||
resourceId: string;
|
||
policy: Record<string, unknown> | null;
|
||
},
|
||
result: PluginAuthorizationPolicyRecord,
|
||
];
|
||
"authorization.policies.previewAssignment": [
|
||
params: PluginAssignmentPreviewInput,
|
||
result: PluginAuthorizationDecisionResult,
|
||
];
|
||
"authorization.policies.explainAssignment": [
|
||
params: PluginAssignmentPreviewInput,
|
||
result: PluginAuthorizationDecisionResult,
|
||
];
|
||
"authorization.audit.search": [
|
||
params: {
|
||
companyId: string;
|
||
action?: string;
|
||
actorType?: string;
|
||
actorId?: string;
|
||
entityType?: string;
|
||
entityId?: string;
|
||
decision?: string;
|
||
limit?: number;
|
||
offset?: number;
|
||
},
|
||
result: PluginAuthorizationAuditEntry[],
|
||
];
|
||
}
|
||
|
||
/** Union of all worker→host method names. */
|
||
export type WorkerToHostMethodName = keyof WorkerToHostMethods;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Worker→Host Notification Types (fire-and-forget, no response)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Typed parameter shapes for worker→host JSON-RPC notifications.
|
||
*
|
||
* Notifications are fire-and-forget — the worker does not wait for a response.
|
||
* These are used for streaming events and logging, not for request-response RPCs.
|
||
*/
|
||
export interface WorkerToHostNotifications {
|
||
/**
|
||
* Forward a stream event to connected SSE clients.
|
||
*
|
||
* Emitted by the worker for each event on a stream channel. The host
|
||
* publishes to the PluginStreamBus, which fans out to all SSE clients
|
||
* subscribed to the (pluginId, channel, companyId) tuple.
|
||
*
|
||
* The `event` payload is JSON-serializable and sent as SSE `data:`.
|
||
* The default SSE event type is `"message"`.
|
||
*/
|
||
"streams.emit": {
|
||
channel: string;
|
||
companyId: string;
|
||
event: unknown;
|
||
};
|
||
|
||
/**
|
||
* Signal that a stream channel has been opened.
|
||
*
|
||
* Emitted when the worker calls `ctx.streams.open(channel, companyId)`.
|
||
* UI clients may use this to display a "connected" indicator or begin
|
||
* buffering input. The host tracks open channels so it can emit synthetic
|
||
* close events if the worker crashes.
|
||
*/
|
||
"streams.open": {
|
||
channel: string;
|
||
companyId: string;
|
||
};
|
||
|
||
/**
|
||
* Signal that a stream channel has been closed.
|
||
*
|
||
* Emitted when the worker calls `ctx.streams.close(channel)`, or
|
||
* synthetically by the host when a worker process exits with channels
|
||
* still open. UI clients should treat this as terminal and disconnect
|
||
* the SSE connection.
|
||
*/
|
||
"streams.close": {
|
||
channel: string;
|
||
companyId: string;
|
||
};
|
||
}
|
||
|
||
/** Union of all worker→host notification method names. */
|
||
export type WorkerToHostNotificationName = keyof WorkerToHostNotifications;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Typed Request / Response Helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* A typed JSON-RPC request for a specific host→worker method.
|
||
*/
|
||
export type HostToWorkerRequest<M extends HostToWorkerMethodName> =
|
||
JsonRpcRequest<M, HostToWorkerMethods[M][0]>;
|
||
|
||
/**
|
||
* A typed JSON-RPC success response for a specific host→worker method.
|
||
*/
|
||
export type HostToWorkerResponse<M extends HostToWorkerMethodName> =
|
||
JsonRpcSuccessResponse<HostToWorkerMethods[M][1]>;
|
||
|
||
/**
|
||
* A typed JSON-RPC request for a specific worker→host method.
|
||
*/
|
||
export type WorkerToHostRequest<M extends WorkerToHostMethodName> =
|
||
JsonRpcRequest<M, WorkerToHostMethods[M][0]>;
|
||
|
||
/**
|
||
* A typed JSON-RPC success response for a specific worker→host method.
|
||
*/
|
||
export type WorkerToHostResponse<M extends WorkerToHostMethodName> =
|
||
JsonRpcSuccessResponse<WorkerToHostMethods[M][1]>;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Message Factory Functions
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** Counter for generating unique request IDs when no explicit ID is provided. */
|
||
let _nextId = 1;
|
||
|
||
/** Wrap around before reaching Number.MAX_SAFE_INTEGER to prevent precision loss. */
|
||
const MAX_SAFE_RPC_ID = Number.MAX_SAFE_INTEGER - 1;
|
||
|
||
/**
|
||
* Create a JSON-RPC 2.0 request message.
|
||
*
|
||
* @param method - The RPC method name
|
||
* @param params - Structured parameters
|
||
* @param id - Optional explicit request ID (auto-generated if omitted)
|
||
*/
|
||
export function createRequest<TMethod extends string>(
|
||
method: TMethod,
|
||
params: unknown,
|
||
id?: JsonRpcId,
|
||
): JsonRpcRequest<TMethod> {
|
||
if (_nextId >= MAX_SAFE_RPC_ID) {
|
||
_nextId = 1;
|
||
}
|
||
return {
|
||
jsonrpc: JSONRPC_VERSION,
|
||
id: id ?? _nextId++,
|
||
method,
|
||
params,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Create a JSON-RPC 2.0 success response.
|
||
*
|
||
* @param id - The request ID being responded to
|
||
* @param result - The result value
|
||
*/
|
||
export function createSuccessResponse<TResult>(
|
||
id: JsonRpcId,
|
||
result: TResult,
|
||
): JsonRpcSuccessResponse<TResult> {
|
||
return {
|
||
jsonrpc: JSONRPC_VERSION,
|
||
id,
|
||
result,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Create a JSON-RPC 2.0 error response.
|
||
*
|
||
* @param id - The request ID being responded to (null if the request ID could not be determined)
|
||
* @param code - Machine-readable error code
|
||
* @param message - Human-readable error message
|
||
* @param data - Optional structured error data
|
||
*/
|
||
export function createErrorResponse<TData = unknown>(
|
||
id: JsonRpcId | null,
|
||
code: number,
|
||
message: string,
|
||
data?: TData,
|
||
): JsonRpcErrorResponse<TData> {
|
||
const response: JsonRpcErrorResponse<TData> = {
|
||
jsonrpc: JSONRPC_VERSION,
|
||
id,
|
||
error: data !== undefined
|
||
? { code, message, data }
|
||
: { code, message } as JsonRpcError<TData>,
|
||
};
|
||
return response;
|
||
}
|
||
|
||
/**
|
||
* Create a JSON-RPC 2.0 notification (fire-and-forget, no response expected).
|
||
*
|
||
* @param method - The notification method name
|
||
* @param params - Structured parameters
|
||
*/
|
||
export function createNotification<TMethod extends string>(
|
||
method: TMethod,
|
||
params: unknown,
|
||
): JsonRpcNotification<TMethod> {
|
||
return {
|
||
jsonrpc: JSONRPC_VERSION,
|
||
method,
|
||
params,
|
||
};
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Type Guards
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Check whether a value is a well-formed JSON-RPC 2.0 request.
|
||
*
|
||
* A request has `jsonrpc: "2.0"`, a string `method`, and an `id`.
|
||
*/
|
||
export function isJsonRpcRequest(value: unknown): value is JsonRpcRequest {
|
||
if (typeof value !== "object" || value === null) return false;
|
||
const obj = value as Record<string, unknown>;
|
||
return (
|
||
obj.jsonrpc === JSONRPC_VERSION &&
|
||
typeof obj.method === "string" &&
|
||
"id" in obj &&
|
||
obj.id !== undefined &&
|
||
obj.id !== null
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Check whether a value is a well-formed JSON-RPC 2.0 notification.
|
||
*
|
||
* A notification has `jsonrpc: "2.0"`, a string `method`, but no `id`.
|
||
*/
|
||
export function isJsonRpcNotification(
|
||
value: unknown,
|
||
): value is JsonRpcNotification {
|
||
if (typeof value !== "object" || value === null) return false;
|
||
const obj = value as Record<string, unknown>;
|
||
return (
|
||
obj.jsonrpc === JSONRPC_VERSION &&
|
||
typeof obj.method === "string" &&
|
||
!("id" in obj)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Check whether a value is a well-formed JSON-RPC 2.0 response (success or error).
|
||
*/
|
||
export function isJsonRpcResponse(value: unknown): value is JsonRpcResponse {
|
||
if (typeof value !== "object" || value === null) return false;
|
||
const obj = value as Record<string, unknown>;
|
||
return (
|
||
obj.jsonrpc === JSONRPC_VERSION &&
|
||
"id" in obj &&
|
||
("result" in obj || "error" in obj)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Check whether a JSON-RPC response is a success response.
|
||
*/
|
||
export function isJsonRpcSuccessResponse(
|
||
response: JsonRpcResponse,
|
||
): response is JsonRpcSuccessResponse {
|
||
return "result" in response && !("error" in response && response.error !== undefined);
|
||
}
|
||
|
||
/**
|
||
* Check whether a JSON-RPC response is an error response.
|
||
*/
|
||
export function isJsonRpcErrorResponse(
|
||
response: JsonRpcResponse,
|
||
): response is JsonRpcErrorResponse {
|
||
return "error" in response && response.error !== undefined;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Serialization Helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Line delimiter for JSON-RPC messages over stdio.
|
||
*
|
||
* Each message is a single line of JSON terminated by a newline character.
|
||
* This follows the newline-delimited JSON (NDJSON) convention.
|
||
*/
|
||
export const MESSAGE_DELIMITER = "\n" as const;
|
||
|
||
/**
|
||
* Serialize a JSON-RPC message to a newline-delimited string for transmission
|
||
* over stdio.
|
||
*
|
||
* @param message - Any JSON-RPC message (request, response, or notification)
|
||
* @returns The JSON string terminated with a newline
|
||
*/
|
||
export function serializeMessage(message: JsonRpcMessage): string {
|
||
return JSON.stringify(message) + MESSAGE_DELIMITER;
|
||
}
|
||
|
||
/**
|
||
* Parse a JSON string into a JSON-RPC message.
|
||
*
|
||
* Returns the parsed message or throws a `JsonRpcParseError` if the input
|
||
* is not valid JSON or does not conform to the JSON-RPC 2.0 structure.
|
||
*
|
||
* @param line - A single line of JSON text (with or without trailing newline)
|
||
* @returns The parsed JSON-RPC message
|
||
* @throws {JsonRpcParseError} If parsing fails
|
||
*/
|
||
export function parseMessage(line: string): JsonRpcMessage {
|
||
const trimmed = line.trim();
|
||
if (trimmed.length === 0) {
|
||
throw new JsonRpcParseError("Empty message");
|
||
}
|
||
|
||
let parsed: unknown;
|
||
try {
|
||
parsed = JSON.parse(trimmed);
|
||
} catch {
|
||
throw new JsonRpcParseError(`Invalid JSON: ${trimmed.slice(0, 200)}`);
|
||
}
|
||
|
||
if (typeof parsed !== "object" || parsed === null) {
|
||
throw new JsonRpcParseError("Message must be a JSON object");
|
||
}
|
||
|
||
const obj = parsed as Record<string, unknown>;
|
||
|
||
if (obj.jsonrpc !== JSONRPC_VERSION) {
|
||
throw new JsonRpcParseError(
|
||
`Invalid or missing jsonrpc version (expected "${JSONRPC_VERSION}", got ${JSON.stringify(obj.jsonrpc)})`,
|
||
);
|
||
}
|
||
|
||
// It's a valid JSON-RPC 2.0 envelope — return as-is and let the caller
|
||
// use the type guards for more specific classification.
|
||
return parsed as JsonRpcMessage;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Error Classes
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Error thrown when a JSON-RPC message cannot be parsed.
|
||
*/
|
||
export class JsonRpcParseError extends Error {
|
||
override readonly name = "JsonRpcParseError";
|
||
constructor(message: string) {
|
||
super(message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Error thrown when a JSON-RPC call fails with a structured error response.
|
||
*
|
||
* Captures the full `JsonRpcError` so callers can inspect the code and data.
|
||
*/
|
||
export class JsonRpcCallError extends Error {
|
||
override readonly name = "JsonRpcCallError";
|
||
/** The JSON-RPC error code. */
|
||
readonly code: number;
|
||
/** Optional structured error data from the response. */
|
||
readonly data: unknown;
|
||
|
||
constructor(error: JsonRpcError) {
|
||
super(error.message);
|
||
this.code = error.code;
|
||
this.data = error.data;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Reset helper (testing only)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Reset the internal request ID counter. **For testing only.**
|
||
*
|
||
* @internal
|
||
*/
|
||
export function _resetIdCounter(): void {
|
||
_nextId = 1;
|
||
}
|