Developer Preview: This page documents the planned plugin contract. It is not a stable SDK reference yet.
Manifest
export interface PluginManifest {
id: string;
name: string;
version: string;
runtimeRange: string;
description?: string;
capabilities?: PluginCapabilities;
email?: PluginEmailConfig;
setup?: { required?: boolean };
recover?: { enabled?: boolean };
skills?: PluginSkillDefinition[];
}| Field | Type | Description |
|---|---|---|
id | string | Stable plugin identifier, such as sample-market. |
name | string | Human-readable provider or plugin name. |
version | string | Plugin package version. |
runtimeRange | string | Runtime compatibility range, such as ^0.1. |
capabilities | object | Runtime services the plugin needs from Atomical. |
email | object | Sender domain routing config. |
setup | object | Setup behavior metadata. |
recover | object | Recovery behavior metadata. |
skills | array | Named skill definitions exported by the plugin. |
Capabilities
export interface PluginCapabilities {
browser?: boolean;
otp?: {
email?: boolean;
sms?: boolean;
};
http?: {
egressDomains: string[];
};
resources?: Array<'payment_method'>;
state?: boolean;
logs?: boolean;
}Declare only the capabilities your provider flow needs. HTTP calls are limited to http.egressDomains. Plugin state is scoped to the agent, provider, and plugin.
Email config
export interface PluginEmailConfig {
allowedSenderDomains: string[];
}allowedSenderDomains contains provider sender domains. Matching is exact-domain and subdomain aware: sample.example matches verify@sample.example and no-reply@mail.sample.example, but not sample.example.attacker.net.
Lifecycle handlers
export type SetupHandler = (context: SetupContext) => Promise<SetupResult>;
export type RecoverHandler = (context: RecoverContext) => Promise<RecoverResult>;
export interface SetupContext extends BaseContext {}
export interface RecoverContext extends BaseContext {
reason?: 'login_required' | 'session_expired' | 'interrupted' | string;
}
export type SetupResult =
| { status: 'active'; displayName?: string; data?: unknown }
| { status: 'awaiting_input'; message: string; data?: unknown }
| { status: 'failed'; errorCode: string; message: string; retryable?: boolean };
export type RecoverResult = SetupResult;Setup creates, signs in to, or verifies the provider account for the agent. Recovery restores an existing account after logout, expired verification, or interrupted work.
Base context
export interface BaseContext {
agent: AgentIdentity;
browser: BrowserProfile;
otp: OtpInbox;
http: PluginHttp;
log: PluginLogger;
state: PluginState;
resources: ManagedResources;
}browser is an opaque managed browser-profile handle. The concrete automation provider is a runtime detail.
Browser profile
The preview browser surface is deliberately small.
export interface BrowserProfile {
navigate(url: string, options?: {
waitUntil?: 'load' | 'network_idle';
timeoutMs?: number;
}): Promise<void>;
act(instruction: string, options?: {
timeoutMs?: number;
}): Promise<BrowserActionResult>;
snapshot(options?: {
includeText?: boolean;
}): Promise<BrowserSnapshot>;
sessionFetch(
input: string,
init?: PluginFetchInit
): Promise<PluginFetchResponse>;
}
export interface BrowserActionResult {
ok: boolean;
summary?: string;
currentUrl?: string;
}
export interface BrowserSnapshot {
url: string;
title?: string;
text?: string;
}Use navigate for page transitions, act for visible UI work, snapshot for inspection, and sessionFetch when a provider call must share the agent’s authenticated browser session. act takes natural-language instructions; the runtime resolves them against the current page. The runtime does not expose raw browser objects or vendor-specific session handles.
HTTP
export interface PluginHttp {
fetch(input: string, init?: PluginFetchInit): Promise<PluginFetchResponse>;
}http.fetch enforces declared egress domains. Use session-bound browser helpers when a provider call must share the authenticated browser session.
OTP inbox
export interface OtpInbox {
waitForEmailCode(options: {
service: string;
timeoutMs?: number;
}): Promise<string>;
waitForSmsCode(options?: {
service?: string;
timeoutMs?: number;
}): Promise<string>;
clear?(options?: {
service?: string;
channel?: 'email' | 'sms';
}): Promise<void>;
}Use email OTP when the provider sends mail from a declared sender domain. Use SMS OTP when the provider verifies the agent phone number. If one plugin can wait on SMS for more than one provider flow, pass service; otherwise the runtime scopes SMS to the current agent and plugin run.
Plugin state
export interface VersionedState<T = unknown> {
value: T | null;
version: string | null;
}
export interface CompareAndSetResult {
ok: boolean;
version: string;
}
export interface PluginState {
get<T = unknown>(key: string): Promise<T | null>;
getWithVersion<T = unknown>(key: string): Promise<VersionedState<T>>;
set<T = unknown>(key: string, value: T): Promise<void>;
merge<T extends Record<string, unknown>>(key: string, patch: T): Promise<T>;
compareAndSet<T = unknown>(
key: string,
expectedVersion: string | null,
value: T
): Promise<CompareAndSetResult>;
delete(key: string): Promise<void>;
}State is for checkpoints the provider would recognize, such as signup_started, profile_saved, active, provider account IDs, and timestamps. Use compareAndSet when maxConcurrency is greater than 1 or when duplicate skill calls may update the same key. Do not store OTPs, raw credentials, or full provider API responses.
Managed resources
export interface ManagedResources {
paymentMethod?: {
last4: string;
label?: string;
};
}Managed resources are capability-gated. If a required resource is unavailable, return missing_resource.
Logger
export interface PluginLogger {
info(message: string, fields?: Record<string, unknown>): void;
warn(message: string, fields?: Record<string, unknown>): void;
error(message: string, fields?: Record<string, unknown>): void;
}Use short human-readable messages and structured fields. Put provider IDs, request IDs, and raw response identifiers in fields, not in message text.
Skill definition
export interface PluginSkillDefinition {
name: string;
description: string;
inputSchema?: JsonSchema;
mutating?: boolean;
requiresConfirmation?: boolean;
timeoutMs?: number;
maxConcurrency?: number;
}Set mutating: true for actions that change provider state. Set requiresConfirmation: true for actions with cost or user-visible effects. timeoutMs is a strict runtime budget for the handler. On timeout, the runtime stops the invocation and returns provider_unavailable with retryable: true; put provider cleanup in resumable state rather than relying on local finally blocks to finish.
Skill handler
export type SkillHandler<TInput = unknown> = (
context: SkillContext<TInput>
) => Promise<SkillResult>;
export interface SkillContext<TInput> extends BaseContext {
input: TInput;
idempotencyKey: string;
}
export type SkillResult =
| { ok: true; data?: unknown; message?: string }
| { ok: false; errorCode: string; message: string; retryable?: boolean };Handlers return structured data for machines and short, safe messages for humans. Use idempotencyKey for duplicate protection on actions that change provider state.
Error codes
| Code | Use when |
|---|---|
setup_required | A skill was called before setup completed. |
login_required | The provider profile exists but the session is logged out. |
otp_timeout | A required OTP did not arrive in time. |
provider_rejected | The provider rejected the request with a non-retryable response. |
provider_unavailable | The provider is down or temporarily blocked. |
rate_limited | The provider accepted the identity or session but told the plugin to slow down. |
missing_resource | A capability-gated resource is required but unavailable. |
confirmation_required | The action needs caller confirmation before execution. |
invalid_input | The skill input failed validation. |
Set retryable: true only when another attempt may succeed without changing the plugin or user input.
Not available in preview
- Raw filesystem access.
- Raw browser-driver, protocol, or vendor-session handles.
- Undeclared network egress.
- Background daemons or long-lived local processes.
- Marketplace upload and sandbox distribution.
- Stable sandbox or version negotiation beyond
runtimeRange. - Plugin-specific secret storage, such as OAuth client secrets or provider API keys.
Local testing
The preview test harness should validate manifests, setup, recovery, skills, OTP input, and duplicate execution of skills that change provider state. Planned import path: @atomic/plugin-preview/testing.
export interface PluginTestHarness {
validateManifest(manifest: PluginManifest): Promise<void>;
runSetup(input: TestSetupInput): Promise<SetupResult>;
runRecover(input: TestRecoverInput): Promise<RecoverResult>;
runSkill<TInput>(name: string, input: TInput): Promise<SkillResult>;
feedEmail(message: TestEmailMessage): Promise<void>;
feedSms(message: TestSmsMessage): Promise<void>;
}A plugin is not ready if it only works on the happy path.