Made Open

Plugin System

Plugins are the mechanism by which infinite capabilities are added without touching the core platform. Every external integration — data source, communication channel, custom rule operator — is a plugin.

Three Plugin Types

1. Connector Plugins

Bridge external data sources to the platform. Pull data, transform it, store it.

interface Connector {
  id: string;          // e.g. "com.microsoft.graph"
  name: string;
  version: string;

  // Called once on init — receives config and PluginContext
  init(context: PluginContext): Promise<void>;

  // Pull data since a given date
  sync(options: SyncOptions): AsyncGenerator<RawItem>;

  // Map raw items to unified model entities
  mapToUnifiedModel(item: RawItem): Entity[];

  // Optional: register for real-time push (webhooks)
  registerRealtimeListener?(webhookUrl: string): Promise<void>;

  // Optional: bidirectional — take actions on the external service
  executeAction?(action: ConnectorAction): Promise<ActionResult>;

  stop(): Promise<void>;
}

2. Channel Plugins

Handle bidirectional communication. Send and receive messages, calls, faxes.

interface Channel {
  id: string;          // e.g. "io.twilio.sms"
  name: string;
  version: string;
  type: 'sms' | 'voice' | 'video' | 'email' | 'whatsapp' | 'fax' | string;

  init(context: PluginContext): Promise<void>;

  // Handle incoming event from external service (called by webhook handler)
  handleIncomingEvent(payload: unknown): Promise<void>;

  // Send outbound message
  sendMessage(message: OutboundMessage): Promise<void>;

  // Optional capabilities
  placeCall?(call: OutboundCall): Promise<CallSession>;
  sendFax?(fax: OutboundFax): Promise<void>;
  generateAccessToken?(userId: string): Promise<string>; // For WebRTC clients

  stop(): Promise<void>;
}

3. Rule Operator Plugins

Extend the Rules Engine with custom conditions and actions.

interface ConditionOperator {
  id: string;          // e.g. "location.IsUserAtVenue"
  evaluate(params: Record<string, unknown>, context: RuleContext): Promise<boolean>;
}

interface ActionOperator {
  id: string;          // e.g. "smart-home.SetLightingScene"
  execute(params: Record<string, unknown>, context: RuleContext): Promise<void>;
}

Plugin Manifest (plugin.json)

Every plugin must have a manifest declaring its identity and required permissions. The Plugin Manager validates this before loading. The shape below matches the real plugin.json files under apps/hub/plugins/ (flat permissions array + JSON-Schema configSchema). Note that packages/plugin-sdk/src/types.ts currently exports a stricter PluginManifest interface with a structured permissions: { network, data } object and a config: Array<...> field — the SDK types are ahead of the on-disk manifests and will converge in a future release.

{
  "id": "com.microsoft.graph",
  "name": "Microsoft Graph Connector",
  "version": "1.0.0",
  "type": "connector",
  "entrypoint": "./index.ts",
  "permissions": [
    "network:outbound:graph.microsoft.com",
    "event:publish:connector.*",
    "event:publish:data.*"
  ],
  "configSchema": {
    "type": "object",
    "properties": {
      "clientId": { "type": "string" },
      "tenantId": { "type": "string" }
    },
    "required": ["clientId", "tenantId"]
  }
}

Permission Namespaces

PermissionGrants
network:outbound:<domain>HTTPS requests to specific domain only
network:inboundReceive webhook calls
filesystem:read:<path>Read files under a path (Windows agent only)
event:publish:<domain>.*Publish events under a domain
event:subscribe:<domain>.*Subscribe to events under a domain
data:read:<entityType>Query entities via Data Service
data:write:<entityType>Write entities via Data Service
storage:readRead files from Supabase Storage
storage:writeWrite files to Supabase Storage

V8 Sandboxing (isolated-vm)

Each plugin runs in a completely isolated V8 isolate via the isolated-vm package. This is meaningfully different from Node.js worker_threads:

isolated-vmworker_threads
Memory isolationComplete — separate heapShared process heap
CPU limitsEnforced (wall time + CPU time)Not enforced
Memory limitsEnforced (max heap size)Not enforced
SecurityNo access to host globalsCan access shared Buffer, etc.

PluginContext API

The PluginContext is the only interface a plugin has to the outside world. It contains exactly the functions the plugin's declared permissions grant.

// Source: packages/plugin-sdk/src/types.ts
interface PluginContext {
  // Resolved config values (secrets pre-fetched from Supabase Vault)
  config: Record<string, string | boolean | number>;

  // Structured logger — writes to the hub log stream
  logger: {
    info(msg: string, data?: unknown): void;
    warn(msg: string, data?: unknown): void;
    error(msg: string, data?: unknown): void;
  };

  // Publish an immutable event to the NATS JetStream event bus
  emitEvent(subject: string, data: unknown): Promise<void>;

  // Read-only access to the unified data model
  data: {
    query<T>(table: string, filter: Record<string, unknown>): Promise<T[]>;
    getById<T>(table: string, id: string): Promise<T | null>;
  };

  // Sandboxed HTTP client — only outbound hosts declared in
  // manifest.permissions.network are allowed
  http: {
    get(url: string, headers?: Record<string, string>): Promise<unknown>;
    post(url: string, body: unknown, headers?: Record<string, string>): Promise<unknown>;
  };
}

Note: the SDK does not currently expose a storage client on PluginContext. Plugins that need storage must emit events and let a core service perform the write.

A plugin cannot do anything not exposed through PluginContext. It cannot import Node.js modules. It cannot call fetch directly. It cannot read process.env. The sandbox enforces this at the V8 level.

Plugin Lifecycle

Discovery → Validation → Installation → Loading → Sandboxing → Configuration → Running → Health Monitoring
     │           │             │            │           │              │             │             │
  scan       validate       install     load code    create        inject         call          monitor
 plugins/   manifest       deps in     into VM      isolate       PluginContext   init()        + restart
 directory  against       isolated                              with validated   start()       on crash
            schema        env                                   config

Communications Rule Operators

The Deep Communications integration ships a comms rule-operator plugin that exposes condition and action operators for all communication channels. These operators are used by the Rules Engine to build communication-aware automations.

Condition Operators

Operator IDParametersDescription
comms.callCountcontactId, direction (inbound|outbound|any), windowDaysTrue when the call count with a contact exceeds a threshold within a rolling time window
comms.lastContactcontactId, channelType (voice|sms|email|any), withinDaysTrue when the last interaction with a contact was within N days
comms.hasVoicemailcontactId?True when there is at least one unlistened voicemail (optionally from a specific contact)
comms.missedCallscontactId?, countTrue when there are N or more missed calls (optionally from a specific contact)

Example rule using comms conditions:

# "If a VIP misses 3+ calls, send them a follow-up email"
name: VIP Missed Call Follow-up
trigger:
  event: comms.CallMissed
conditions:
  - operator: comms.missedCalls
    params:
      contactId: "{{ event.fromContactId }}"
      count: 3
  - operator: contact.HasTag
    params:
      tag: "vip"
actions:
  - operator: comms.sendEmail
    params:
      to: "{{ event.fromContactId }}"
      subject: "Sorry we missed you"
      body: "We noticed we've missed your last few calls. Please reply to this email or call us back at your convenience."

Action Operators

Operator IDParametersDescription
comms.sendSmsto (contactId or E.164 number), bodySends an SMS via the active Twilio channel plugin
comms.sendEmailto (contactId or email address), subject, body, replyToMessageId?Sends an email via MS Graph on behalf of the authenticated user
comms.createFollowUpcontactId, dueAt, note?Creates a task in the unified model flagged as a communication follow-up

Example rule using comms actions:

# "When I arrive at the office, send my pending follow-ups"
name: Office Arrival Follow-up Flush
trigger:
  event: location.PlaceArrived
conditions:
  - operator: location.IsUserAtPlace
    params:
      placeType: work
actions:
  - operator: comms.sendSms
    params:
      to: "{{ followUp.contactId }}"
      body: "Hey! I'm now at the office — happy to connect. Give me a call or reply here."

Registering the Comms Rule-Operator Plugin

// apps/hub/plugins/rule-operators/comms/plugin.json
{
  "id": "io.madeopen.comms-rules",
  "name": "Communications Rule Operators",
  "version": "1.0.0",
  "type": "operator",
  "entrypoint": "./index.ts",
  "permissions": [
    "data:read:conversations",
    "data:read:messages",
    "data:read:tasks",
    "data:write:tasks",
    "event:publish:comms.*"
  ]
}

Plugin Locations

apps/hub/plugins/
├── connectors/            8 connector plugins
│   ├── ms-graph/          Microsoft Graph (contacts, calendar, email)
│   ├── twilio/            Twilio (logs historical calls/SMS)
│   ├── fitbit/            Fitbit health + activity
│   ├── google-fit/        Google Fit health + activity
│   ├── plaid/             Plaid financial accounts + transactions
│   ├── nutritionix/       Nutritionix food database
│   ├── usda-food/         USDA FoodData Central
│   └── device/            Device connector (Android/Windows sensor events)
└── channels/              2 channel plugins
    ├── twilio/            Twilio Channel (real-time voice/SMS)
    └── whatsapp/          WhatsApp Cloud API (Meta Graph)

Each directory contains a plugin.json manifest plus an index.ts entry file.

Total: 10 plugins (8 connectors + 2 channels).

Note: Twilio appears twice — once as a connector (syncing historical call/SMS data) and once as a channel (handling real-time inbound/outbound communication). This is the correct pattern per the architecture.

The communications rule-operators described above are not currently shipped as a standalone plugin directory under apps/hub/plugins/rule-operators/; they are documented here as the intended surface once rule-operator plugins land.