Hybrid Gene Development Guide
Hybrid genes extend the Rotifer safety model to support controlled network access. Unlike Wrapped genes (pure functions) or Native genes (WASM-only), Hybrid genes can call external APIs — but only through a Network Gateway that enforces domain whitelisting, rate limiting, timeouts, and response size caps.
This guide walks you through creating, testing, and publishing a Hybrid gene.
When to Use Hybrid
Section titled “When to Use Hybrid”| Fidelity | Network | Use Case |
|---|---|---|
| Wrapped | None | Pure data transformation, text processing |
| Hybrid | Gateway-controlled | LLM APIs, database queries, embeddings |
| Native | None | CPU-intensive WASM computation |
Use Hybrid when your gene needs to call an external service (OpenAI, Anthropic, Supabase, etc.) but you still want the protocol’s safety guarantees.
Step 1: Initialize a Hybrid Project
Section titled “Step 1: Initialize a Hybrid Project”rotifer init my-hybrid-gene --fidelity HybridThis creates a project with a pre-configured phenotype.json that includes the network block:
{ "name": "my-hybrid-gene", "domain": "general", "fidelity": "Hybrid", "version": "0.1.0", "inputSchema": { "type": "object" }, "outputSchema": { "type": "object" }, "network": { "allowedDomains": ["api.example.com"], "maxTimeoutMs": 30000, "maxResponseBytes": 1048576, "maxRequestsPerMin": 10 }}You can also convert an existing gene to Hybrid:
rotifer wrap my-gene --fidelity Hybrid --domain text.analyzeStep 2: Configure Network Access
Section titled “Step 2: Configure Network Access”Edit phenotype.json to declare the exact domains your gene needs:
{ "network": { "allowedDomains": ["api.openai.com", "*.supabase.co"], "maxTimeoutMs": 30000, "maxResponseBytes": 1048576, "maxRequestsPerMin": 10 }}Network Configuration Reference
Section titled “Network Configuration Reference”| Parameter | Type | Default | Description |
|---|---|---|---|
allowedDomains | string[] | [] | Domains the gene may call. Supports exact match and *.domain.com wildcards |
maxTimeoutMs | number | 30000 | Per-request timeout in milliseconds |
maxResponseBytes | number | 1048576 | Maximum response body size (1 MiB default) |
maxRequestsPerMin | number | 10 | Rate limit — requests per 60-second sliding window |
Domain Rules
Section titled “Domain Rules”- Exact match:
api.openai.comallows onlyapi.openai.com - Wildcard:
*.supabase.coallowsabc.supabase.coandsupabase.co - Forbidden domains (rejected at publish):
localhost,127.x.x.x,0.0.0.0,::1- Private IPs:
10.x.x.x,172.16–31.x.x,192.168.x.x
Step 3: Write the Express Function
Section titled “Step 3: Write the Express Function”Hybrid genes receive a gatewayFetch function via the second argument. Use it instead of the global fetch:
export async function express( input: { query: string }, ctx?: { gatewayFetch?: typeof fetch }): Promise<{ answer: string }> { if (!ctx?.gatewayFetch) { throw new Error("gatewayFetch is required for Hybrid genes"); }
const response = await ctx.gatewayFetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: input.query }], }), });
const data = await response.json(); return { answer: data.choices[0].message.content };}Gateway Behavior
Section titled “Gateway Behavior”When your gene calls gatewayFetch:
- Domain check — the URL’s hostname is validated against
allowedDomains. If blocked, aDOMAIN_BLOCKEDerror is thrown immediately. - Rate check — if the gene has exceeded
maxRequestsPerMinin the current 60-second window, aRATE_LIMITEDerror is thrown. - Timeout — the request is aborted after
maxTimeoutMswith aTIMEOUTerror. - Response cap — if the response body exceeds
maxResponseBytes, it is truncated and atruncated: trueflag is set.
Gateway Error Codes
Section titled “Gateway Error Codes”| Code | Meaning |
|---|---|
DOMAIN_BLOCKED | URL hostname not in allowedDomains |
RATE_LIMITED | Exceeded maxRequestsPerMin |
TIMEOUT | Request exceeded maxTimeoutMs |
RESPONSE_TOO_LARGE | Response body exceeded maxResponseBytes |
NETWORK_ERROR | Other fetch failure |
Step 4: Compile
Section titled “Step 4: Compile”rotifer compile my-hybrid-geneFor Hybrid genes, the compiler preserves the fidelity: "Hybrid" flag and embeds the network configuration into the IR custom sections. The output shows the allowed domains:
✓ Compiled to Rotifer IR Fidelity: Hybrid Network: api.openai.com, *.supabase.co IR Hash: sha256-abc123...Step 5: Publish
Section titled “Step 5: Publish”rotifer publish my-hybrid-geneThe publish command runs two Hybrid-specific validations before authenticating:
E0055 — Missing Allowed Domains
Section titled “E0055 — Missing Allowed Domains”If network.allowedDomains is missing or empty:
✗ [E0055] Hybrid gene must declare at least one allowed domain in network.allowedDomains
Fix: Add "network": { "allowedDomains": ["api.example.com"] } to phenotype.jsonE0056 — Forbidden Domains
Section titled “E0056 — Forbidden Domains”If any domain resolves to localhost or a private IP:
✗ [E0056] Forbidden domain in network.allowedDomains: "localhost" (localhost/private IP not allowed)
Fix: Remove private/localhost domains before publishingStep 6: Run in an Agent
Section titled “Step 6: Run in an Agent”Create an agent that includes your Hybrid gene:
rotifer agent create my-agent --genes my-hybrid-generotifer agent run my-agent --input '{"query": "What is Rotifer?"}'The agent runtime automatically:
- Detects
fidelity: "Hybrid"in the gene’s phenotype - Creates a
NetworkGatewaywith the gene’snetworkconfiguration - Injects
gatewayFetchinto theexpress()call - Logs gateway stats with
--verbose
Real-World Example: RAG Pipeline
Section titled “Real-World Example: RAG Pipeline”The Rotifer docs-assistant pipeline demonstrates a 4-gene Hybrid composition:
query-parser (Wrapped) │ ▼doc-retrieval (Hybrid) → OpenAI Embeddings + Supabase pgvector │ ▼answer-synthesizer (Hybrid) → Claude / OpenAI LLM │ ▼source-linker (Wrapped)Each Hybrid gene declares only the domains it needs:
| Gene | Allowed Domains |
|---|---|
doc-retrieval | api.openai.com, *.supabase.co |
answer-synthesizer | api.anthropic.com, api.openai.com |
Best Practices
Section titled “Best Practices”Minimal Domain Surface
Section titled “Minimal Domain Surface”Only declare the domains your gene actually calls. Each domain in allowedDomains is a trust surface — keep it small.
"allowedDomains": ["api.openai.com"]Graceful Degradation
Section titled “Graceful Degradation”Handle gateway errors explicitly — don’t let network failures crash your gene:
try { const res = await ctx.gatewayFetch(url, options); // ...} catch (err) { if (err.code === "RATE_LIMITED") { return { answer: "Rate limited — please retry.", error: true }; } throw err;}Environment Variables for API Keys
Section titled “Environment Variables for API Keys”Never hardcode API keys in gene source. Use environment variables:
const apiKey = process.env.OPENAI_API_KEY;if (!apiKey) throw new Error("OPENAI_API_KEY not set");LLM Provider Agnosticism
Section titled “LLM Provider Agnosticism”Design genes to support multiple LLM providers via environment variables:
const provider = process.env.ROTIFER_LLM_PROVIDER || "openai";const endpoint = provider === "claude" ? "https://api.anthropic.com/v1/messages" : "https://api.openai.com/v1/chat/completions";Reasonable Limits
Section titled “Reasonable Limits”Tune maxTimeoutMs and maxResponseBytes for your use case:
| Use Case | Timeout | Response Size |
|---|---|---|
| LLM chat completion | 30s | 1 MiB |
| Embedding API | 10s | 256 KiB |
| Database query | 5s | 512 KiB |