Skip to main content

OpenEve Adapters

OpenEve adapters are deliberately small. gateway.ts chooses where the runtime runs and which durable services it uses; channel files choose how external events become OpenEve turns; sandbox files choose where isolated code and shell work runs. These choices are independent.

Adapters are substitutable substrate: each role (channel, sandbox, blob, state, scheduler, gateway/deploy) has a generic contract with interchangeable providers, so the same agent runs unchanged on different infrastructure. Capabilities are a separate tier. A capability is a single-vendor feature that gives an agent something new to do rather than somewhere new to run — it exposes that vendor's own surface and has no generic contract to swap behind. Capability packages live under capabilities/ (see Capabilities), still ride the same open provider seam, and describe themselves through provider metadata so tools like the no-code builder can scaffold them automatically.

Adapter support levels:

  • Supported means OpenEve has a real runtime instantiation path, docs, and regression coverage in this repo.
  • Preview means the public helper, compiler metadata, and local regression coverage exist, but the adapter still needs live provider smoke coverage or more provider hardening before it should be announced as fully supported.
  • Planned means the docs may name the direction, but the adapter is not a production claim yet.

The current matrix is:

RoleSupportedPreviewPlanned
ChannelsSlack, Discord, Telegram, Microsoft Teams, Photon/SpectrumGitHub repo events-
Sandboxeslocal dev/test, Docker, Daytona, E2B, Modal--
Blob storagelocal dev/test, R2, generic S3-compatible storage--
Durable statelocal dev/test files, Postgres for production, with Neon, Supabase, local, and custom presets-other database families
Schedulerlocal in-process loop, gateway-triggered cloud scheduler, Postgres-backed multi-worker loop--
Subagent harnessesPi model loopCodex, Claude Code-
Gateway/deploylocal, RailwayDocker, Flyother gateway families

Other database families, gateway families, and sandbox providers are not part of this initial adapter set.

Capabilities are tracked separately from the adapter roles above because they are not substitutable substrate:

CapabilitySupportedPreviewPlanned
Voice / telephony-LiveKit voice dispatch and SIP calls-

Provider Docs Used

The adapter shapes follow current provider docs for:

Gateway

Gateway adapters answer one question: where does the compiled OpenEve runtime service run?

They do not choose your state, blob, sandbox, channels, or connections. Those are separate settings in the same gateway.ts file.

import { adapter, defineGateway } from "@openeve/core";
import { neonPostgres } from "@openeve/postgres";
import { r2Blob } from "@openeve/s3";
import { dockerSandbox } from "@openeve/docker";

export default defineGateway({
deploy: adapter("railway"),
runtime: adapter("node"),
state: neonPostgres(),
blob: r2Blob(),
sandbox: dockerSandbox()
});

Deploy target status:

  • Supported: adapter("railway") or railwayDeploy() runs the hosted Node runtime through the @openeve/railway deploy publisher.
  • Preview: adapter("docker") or dockerDeploy() builds the compiled artifact as a Docker image through the @openeve/docker deploy publisher and can optionally run it locally.
  • Preview: adapter("fly") or flyDeploy() generates fly.toml and publishes the artifact through the @openeve/fly deploy publisher and flyctl deploy. Fly is not a full support claim until live smoke coverage exists.

Useful CLI flags:

openeve deploy --target docker --docker-image openeve/my-agent --serve
openeve deploy --target fly --fly-app my-agent --fly-region iad
openeve deploy --target railway --railway-project prj_x --railway-service svc_y

Third-Party Provider Packages

Provider registration is open: any npm package can supply a state, blob, sandbox, deploy, or subagent provider with zero OpenEve core or host edits. Agents opt in through the third adapter() argument in gateway.ts:

export default defineGateway({
runtime: adapter("node"),
state: adapter("neon-state", { pool: 4 }, { package: "@acme/openeve-neon" })
});

The compiler stamps the package's requiredEnv and setup metadata into preflight at build time, and the Node host resolves the package's openeveProvider export at boot. The compiled .openeve/package.json also declares every third-party provider package as a dependency — pinned to the version installed in the agent root when present, else the agent's declared range, else * — so npm install --omit=dev in the deployed artifact pulls it without manual edits. To implement such a package — or any new channel, sandbox, blob, deploy, or state adapter — see Authoring Adapters.

Scheduler

Schedules compile to registration metadata, but the scheduler adapter chooses where the clock lives.

import { adapter, defineGateway } from "@openeve/core";

export default defineGateway({
scheduler: adapter("gateway")
});

Supported scheduler adapters:

  • adapter("local"): starts an in-process polling loop with the Node host. Use this for local development, tests, and simple single-process hosts.
  • adapter("gateway"): does not start a local loop. Use a platform cron, cloud scheduler, Durable Object alarm, queue worker, or gateway route to call GET or POST /openeve/scheduler/tick, or call runtime.runDueSchedules() from host code.
  • adapter("postgres"): starts the polling loop and coordinates duplicate workers through the Postgres state adapter's idempotency and dynamic schedule leases. Pair it with state: adapter("postgres").

For gateway-triggered production schedulers, set OPENEVE_SCHEDULER_SECRET and send it as Authorization: Bearer <secret> or x-openeve-scheduler-secret.

Capabilities

Capabilities live under capabilities/ and are single-vendor features, not substitutable adapters. They plug in through the same openeveProvider seam as any provider package and carry their own provider metadata — required env, declared capabilities, and stability. A no-code builder or catalog can render a plug-in card and credential form directly from that metadata without bespoke per-capability UI, so adding a capability package is enough to surface it.

Voice is delivered through the subagent-harness seam, which is a mechanism, not an engine claim: LiveKit is a telephony capability, not an alternative reasoning loop like Pi, Codex, or Claude Code.

LiveKit Voice

LiveKit support is preview. OpenEve does not run realtime media inside the text-model runtime. Instead, @openeve/livekit signs LiveKit server tokens, calls the LiveKit Agent Dispatch and SIP Twirp APIs, and lets an OpenEve turn start or route a LiveKit voice session.

Use a tool when the parent agent should decide to dial:

// tools/start_call.ts
import { defineLiveKitOutboundCallTool } from "@openeve/livekit";

export default defineLiveKitOutboundCallTool({
agentName: "support-voice",
outboundTrunkId: "ST_outbound"
});

Use a subagent when delegation should look like any other OpenEve subagent:

// subagents/caller/agent.ts
import { defineSubagent } from "@openeve/core";
import { liveKitVoiceHarness } from "@openeve/livekit";

export default defineSubagent({
description: "Use this subagent for phone-call voice sessions.",
harness: liveKitVoiceHarness({
agentName: "support-voice",
outboundTrunkId: "ST_outbound"
})
});

The LiveKit agent named by agentName must be running in your LiveKit Agents worker. For outbound phone calls, OpenEve dispatches that worker into a room and calls CreateSIPParticipant to dial the callee through your outbound SIP trunk. Incoming phone calls should be routed in LiveKit with SIP dispatch rules to the LiveKit agent worker; OpenEve can still be used by that worker as an app/runtime layer, but the phone media path remains LiveKit.

Declare an optional connection file when you want preflight to describe the LiveKit dependency even for tool-only agents:

// connections/livekit.ts
import { defineLiveKitConnection } from "@openeve/livekit";

export default defineLiveKitConnection();

Required environment:

PurposeEnv
LiveKit server APILIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET
Default outbound dialingoptional LIVEKIT_OUTBOUND_TRUNK_ID
Default voice worker dispatchoptional LIVEKIT_VOICE_AGENT_NAME

Channels

Channel packages export one-line helpers for normal use. They verify provider auth, normalize the provider event into ChannelTurn, preserve provider IDs in metadata/delivery, and send the final reply through provider APIs.

After a deploy, point each channel's provider at the deployed ingress URL with:

openeve channels wire <agentRoot> --url https://your-service.example.com

It computes each channel's ingress URL from the compiled route table and, for Telegram, calls setWebhook directly (needs TELEGRAM_BOT_TOKEN, and uses TELEGRAM_WEBHOOK_SECRET when set). For providers that configure their endpoint in a console (Slack Request URL, Discord Interactions Endpoint, Teams messaging endpoint), it prints the exact URL to paste. Output is a structured ChannelWireResult[] (action: "set" | "manual").

// channels/slack.ts
import { defineSlackChannel } from "@openeve/slack";

export default defineSlackChannel();
// channels/discord.ts
import { defineDiscordChannel } from "@openeve/discord";

export default defineDiscordChannel({
gateway: true,
requireMention: true
});
// channels/telegram.ts
import { defineTelegramChannel } from "@openeve/telegram";

export default defineTelegramChannel();
// channels/teams.ts
import { defineTeamsChannel } from "@openeve/teams";

export default defineTeamsChannel();
// channels/github.ts
import { defineGitHubChannel } from "@openeve/github";

export default defineGitHubChannel();

Required channel environment:

ChannelRequired env
SlackSLACK_SIGNING_SECRET, SLACK_BOT_TOKEN, optional SLACK_BOT_USER_ID, optional SLACK_ASSISTANT_ENABLED
DiscordDISCORD_PUBLIC_KEY, DISCORD_APPLICATION_ID, DISCORD_BOT_TOKEN, optional DISCORD_GATEWAY_ENABLED, optional DISCORD_GATEWAY_INTENTS, optional DISCORD_BOT_USER_ID
TelegramTELEGRAM_BOT_TOKEN; TELEGRAM_WEBHOOK_SECRET is required outside devMode
TeamsMICROSOFT_APP_ID, MICROSOFT_APP_PASSWORD
GitHubGITHUB_WEBHOOK_SECRET; replies need GITHUB_APP_ID + GITHUB_PRIVATE_KEY (installation tokens) or a GITHUB_TOKEN PAT fallback; optional GITHUB_API_URL

Channel-owned ingress auth and attachment resolution

Channels declare their own production ingress-auth requirements and resolve their own provider attachments; the runtime stays a generic dispatcher.

  • ChannelDefinition.ingress.requiredSecretEnv is a list of any-of groups of env var names: production boot succeeds when every var in at least one group is set (for example Photon declares [["PHOTON_WEBHOOK_SIGNING_SECRET"], ["PHOTON_INGRESS_TOKEN"]]). The compiler stamps the declaration into CompiledChannel.metadata.ingress; in production the runtime refuses to boot when no group is satisfied, and in devMode it logs a warning instead. The built-in helpers (defineSlackChannel, defineTelegramChannel, defineDiscordChannel, defineTeamsChannel, definePhotonChannel, defineGitHubChannel) declare this automatically; custom channels can set ingress on their channel config and re-export the same shape as ingressAuth on the module.
  • ChannelModule.resolveAttachment(attachment, ctx) turns a turn attachment into a download request ({ url, headers, filename? }). The runtime performs the download, applies size/type limits and timeouts, and stores the blob; the channel owns auth lookups (Slack files.info, Telegram getFile, the Teams Bot Framework token flow, Photon bridge bearer headers) and host allowlists. Return undefined to fall back to a generic unauthenticated URL download.
  • Trust boundary: the runtime only calls resolveAttachment on the module of the channel that produced the turn, and only when the turn's declared provider matches that channel — a hostile attachment claiming another provider can never route to that provider's credentials. Inside a resolver, use attachmentTrustedForProvider(attachment, "<provider>") and attachmentRemoteUrl(attachment) from @openeve/runtime to honor the per-attachment remote.trusted markers before attaching credentials.

Slack uses the Events API route /slack/events. It verifies the raw request body with Slack's signing secret, handles url_verification inline, returns a 2xx ACK for accepted events before model work, and uses Slack event_id as the delivery idempotency key. It starts turns only for intentional agent entry points: app mentions, user DM messages, and assistant-thread user messages when assistant mode is enabled. Delivery always uses the preserved Slack channel and thread target from the normalized turn.

Slack context augmentation runs after ACK and before default context bundle construction. It supplies the current conversation history plus up to three recent OpenEve Slack conversations for the same user from the last 24 hours as short summaries and structured metadata. It does not fetch workspace-wide Slack history during ingress.

Agent communication channel status:

  • Slack and Photon/Spectrum are reference agent channels. Slack maps app mentions, DMs, and assistant-thread user messages into durable OpenEve turns; Photon/Spectrum maps iMessage bridge events into the same lifecycle and adds native actions such as reactions, polls, app cards, and backgrounds.
  • Discord is an agent-oriented communication channel when Gateway ingress is enabled. Interactions still cover slash commands, components, modals, and autocomplete; Gateway ingress covers DMs, mentions, and thread/channel messages. Discord starts typing indicators for Gateway turns and delivers through either interaction responses or bot-token channel messages.
  • Telegram is an agent-oriented communication channel over Bot API webhooks. It supports messages, edited messages, callback queries, channel posts, business messages, forum topics, typing actions, inline keyboards, callback answers, media sends, and getFile attachment materialization.
  • Microsoft Teams is an agent-oriented communication channel over Bot Framework activities. It supports message activities, Adaptive Card invoke submissions, mention stripping, tenant/service URL constraints, typing activities, Adaptive Card replies, suggested actions, and protected attachment materialization.
  • GitHub is a repo-event channel (preview: local regression coverage exists; live GitHub App smoke evidence is still pending). defineGitHubChannel() serves /github/events, verifies X-Hub-Signature-256 HMACs against GITHUB_WEBHOOK_SECRET, answers ping inline, and maps repository events (issues, issue_comment, pull_request, pull_request_review, pull_request_review_comment, push, check_run, check_suite, workflow_run, installation) into durable turns. The delivery GUID (X-GitHub-Delivery) is the idempotency key; issue and PR events converse as owner/repo#number, pushes as owner/repo@ref. Bot senders are ignored so the agent's own comments never re-trigger runs. Replies post issue/PR comments using a GitHub App installation token when the event carried an installation id and GITHUB_APP_ID/GITHUB_PRIVATE_KEY are set, falling back to a GITHUB_TOKEN PAT; events without an unambiguous comment target (such as pushes) run without a reply delivery.

Connection helpers remain available when agents need provider capabilities beyond receiving messages:

Inbound files follow the same normalized ChannelTurn.attachments contract on every channel. Adapters should preserve provider file identity plus one of:

  • inline content (content, text, body, or base64 data)
  • a safe remote reference (url, downloadUrl, contentUrl, or remote: { provider, auth?, url })
  • provider-specific lookup metadata such as Telegram file_id

The runtime materializes remote attachments after the webhook ACK, stores the original bytes through the configured blob adapter, records them as read-only /files/original/... resources, and lists them in /files/manifest.json. Private download details and auth hints are stripped from model-visible attachment context after storage. Text and Markdown attachments read back as UTF-8; binary files remain byte-accurate for resource projection. ZIP uploads are also expanded when possible: the original archive remains available under /files/original/..., and safe entries are exposed as read-only resources under /files/extracted/<archive-name>/... for the core file tools. Unsupported, encrypted, oversized, or path-escaping ZIP entries are skipped or reported without dropping the original uploaded archive.

// connections/github.ts
import { defineGitHubConnection } from "@openeve/github";

export default defineGitHubConnection();
// connections/teams.ts
import { defineTeamsConnection } from "@openeve/teams";

export default defineTeamsConnection();

GitHub App connections require GITHUB_APP_ID and GITHUB_PRIVATE_KEY. Teams uses Bot Framework credentials.

Live smoke evidence for Discord, Telegram, and Teams lives under docs/internal/evidence/channels/. The root smoke scripts skip without credentials and point to those provider checklists when credentials are present.

Sandboxes

The sandbox adapter is acquired lazily when a tool or capability asks for a sandbox. Normal channel receipt, model turns without sandbox-backed tools, skill activation, memory reads/writes, and final delivery do not need to pay sandbox startup cost.

// sandbox/default.ts
import { dockerSandbox } from "@openeve/docker";

export default dockerSandbox({
image: "node:22-slim",
network: "none"
});

Sandbox adapter status:

  • Supported: adapter("local") for trusted dev/test only.
  • Supported: adapter("docker") or dockerSandbox() for local container isolation, one container per acquired session, cleanup on dispose, and relative file paths rooted under the sandbox workspace.
  • Supported: adapter("daytona") for hosted Daytona sandboxes.
  • Supported: adapter("e2b") or e2bSandbox() for hosted E2B sandboxes.
  • Supported: adapter("modal") or modalSandbox() for hosted Modal sandboxes.

All built-in sandbox adapters normalize OpenEve logical paths under the adapter workspace. /workspace/report.txt resolves under the adapter's sandbox root inside the provider sandbox, not the host or container root. .. traversal is rejected. Recursive file listings return OpenEve virtual absolute paths such as /workspace/report.txt and include file contents so runtime sync can persist memory and generated artifacts without provider-specific follow-up reads.

Dirty sandbox sessions are retained when async sandbox sync is pending: Docker stops the container, Daytona pauses/stops, E2B pauses with configurable memory retention, and Modal detaches. Clean sessions are removed, deleted, killed, or terminated through the provider lifecycle API. The sync worker reconnects first and then calls wake()/start when a retained sandbox is warm or paused.

Hosted adapters do not silently fall back to local execution. Daytona local fallback exists only for explicit development/test opt-in with OPENEVE_ALLOW_LOCAL_SANDBOX_FALLBACK=true.

The local sandbox is for trusted dev/test execution. Production Node runtime construction rejects adapter("local") for sandboxes unless OPENEVE_ALLOW_LOCAL_SANDBOX_IN_PRODUCTION=true is set to acknowledge that the host process, filesystem, and network are not isolated. Docker defaults OPENEVE_DOCKER_NETWORK to none; Daytona supports OPENEVE_DAYTONA_NETWORK_BLOCK_ALL and allow/domain lists for explicit egress policy.

Provider env:

SandboxRequired envLifecycle and policy env
DockerDocker CLI/daemon availableOptional DOCKER_HOST, OPENEVE_DOCKER_NETWORK (defaults to none), OPENEVE_DOCKER_CPUS, OPENEVE_DOCKER_MEMORY, OPENEVE_DOCKER_PULL_POLICY, OPENEVE_DOCKER_COMMAND_TIMEOUT_MS
DaytonaDAYTONA_API_KEYOptional DAYTONA_API_URL, DAYTONA_TARGET, OPENEVE_DAYTONA_CREATE_TIMEOUT_SECONDS, OPENEVE_DAYTONA_LIFECYCLE_TIMEOUT_SECONDS, OPENEVE_DAYTONA_AUTO_STOP_MINUTES, OPENEVE_DAYTONA_AUTO_ARCHIVE_MINUTES, OPENEVE_DAYTONA_AUTO_DELETE_MINUTES, OPENEVE_DAYTONA_EPHEMERAL, OPENEVE_DAYTONA_NETWORK_BLOCK_ALL, OPENEVE_DAYTONA_NETWORK_ALLOW_LIST, OPENEVE_DAYTONA_DOMAIN_ALLOW_LIST
E2BE2B_API_KEYOptional E2B_TEMPLATE, OPENEVE_E2B_TIMEOUT_MS, OPENEVE_E2B_RETAIN_TIMEOUT_MS, OPENEVE_E2B_REQUEST_TIMEOUT_MS, OPENEVE_E2B_PAUSE_KEEP_MEMORY, OPENEVE_E2B_ALLOW_INTERNET_ACCESS
ModalMODAL_TOKEN_ID, MODAL_TOKEN_SECRETOptional MODAL_APP_NAME, OPENEVE_MODAL_TIMEOUT_MS, OPENEVE_MODAL_WAIT_READY

Provider snapshots are opt-in through the OpenEve sandbox snapshot policy. Docker reports snapshots as unsupported. Daytona, E2B, and Modal expose snapshot creation only when the installed provider SDK exposes it. Normal production persistence remains OpenEve state/blob sync.

Live sandbox smoke is opt-in:

pnpm smoke:sandbox -- --provider=docker
pnpm smoke:sandbox -- --provider=daytona
pnpm smoke:sandbox -- --provider=e2b
pnpm smoke:sandbox -- --provider=modal

Docker smoke runs when Docker is available. Hosted smoke skips cleanly when disposable provider credentials are missing. Smoke evidence is written under docs/internal/smoke/ and records provider, lifecycle result, safe file hashes, and snapshot status without secrets.

Blob Storage

Production blob storage is S3-compatible. R2 remains first-class through the R2 wrapper, but the runtime contract is the same for R2, AWS S3, and MinIO-style endpoints.

import { defineGateway } from "@openeve/core";
import { s3Blob, r2Blob, minioBlob } from "@openeve/s3";

export default defineGateway({
blob: r2Blob()
// or blob: s3Blob()
// or blob: minioBlob()
});

Generic S3 env:

  • S3_BUCKET
  • S3_REGION
  • S3_ACCESS_KEY_ID
  • S3_SECRET_ACCESS_KEY
  • optional S3_ENDPOINT
  • optional S3_FORCE_PATH_STYLE
  • optional S3_PREFIX
  • optional S3_PUBLIC_BASE_URL

R2 env:

  • R2_ACCOUNT_ID
  • R2_BUCKET
  • R2_ACCESS_KEY_ID
  • R2_SECRET_ACCESS_KEY
  • optional R2_PREFIX
  • optional R2_PUBLIC_BASE_URL

@openeve/r2 still exports r2Adapter(), R2BlobAdapter, and InMemoryR2Bucket for existing imports.

S3_PUBLIC_BASE_URL and R2_PUBLIC_BASE_URL do not make every blob public. Blob writes are private by default; adapters return public HTTP URLs only when the write explicitly uses { visibility: "public" }.

Database

OpenEve stays opinionated here: Postgres is the only production durable state plane in this phase. That keeps runs, messages, tool traces, schedules, dynamic connections, durable skills, idempotency, and migrations on one auditable database contract.

SQLite is not included because it would create a second production state shape just as deploy targets and channels are expanding. File-backed state remains for local dev/demo/test, not hosted production: it is single-process by design (its idempotency reservations live in process memory, so two hosts sharing one state file cannot coordinate leases or claims). Its writes are crash-safe — temp file, fsync, then atomic rename — and an unreadable state file is backed up to <path>.corrupt-<timestamp> and replaced with a fresh state instead of crashing the host, but multi-replica guarantees always require Postgres.

Connection grant and authorization-session stores follow the same production rule. Postgres implements those stores directly. File-backed connection credential stores are for local development or deliberately small deployments; production Node hosts using them must set OPENEVE_CONNECTION_STORE_SECRET or OPENEVE_SECRET to a stable secret of at least 32 characters. The known local development fallback is rejected outside devMode: true.

import { defineGateway } from "@openeve/core";
import { neonPostgres, supabasePostgres, localPostgres, postgresAdapter } from "@openeve/postgres";

export default defineGateway({
state: neonPostgres()
// or state: supabasePostgres()
// or state: localPostgres()
// or state: postgresAdapter({ provider: "custom" })
});

Default env:

  • DATABASE_URL
  • optional OPENEVE_POSTGRES_CONNECTION_ENV
  • optional OPENEVE_POSTGRES_SSL_REJECT_UNAUTHORIZED

Neon, Supabase, local Postgres, and custom Postgres all run the same OpenEve migrations and schema.

Observability

Telemetry is not a gateway adapter — it lives in agent/instrumentation.ts. @openeve/otlp provides an OTLP/HTTP GenAI telemetry sink you wire in the setup callback; see Customizing Agents → Observability for the setup, capture-detail (captureContent) options, and the Langfuse recipe.