AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Integrate Openclaw

skill-patterai-skills-integrate-openclaw · by PatterAI

>

No reviews yet
0 installs
22 views
0.0% view→install

Install

$ agentstack add skill-patterai-skills-integrate-openclaw

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-patterai-skills-integrate-openclaw)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Integrate Openclaw? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Integrate Patter with OpenClaw (brain on the line)

OpenClaw is a self-hosted gateway of LLM-backed agents with scoped tool connections (calendars, customer DBs, …). Patter is the voice layer: it owns the carrier leg, talks with low latency, and reaches into OpenClaw only when a turn needs real data or an action. This is the brain-on-the-line architecture — the inverse of wiring OpenClaw in as a "Custom LLM" that sits on every turn (the topology that kills calls on slow tools).

CALLER (any number, no allowlist)
   │
   ▼
Twilio / Telnyx DID  ──►  PATTER voice agent (OpenAI Realtime, low-latency EN)
                              │  owns turn-taking + TTS locally
                              │  speaks "let me check, one moment"
                              ▼
                          consult ──► local adapter ──► OpenClaw
                              ▲          (loopback)        /v1/chat/completions
                              │                            model="openclaw/"
                          speaks the reply                 (ONE scoped agent)

The two directions — and which one to use

| Direction | Who initiates | Transport | Use when | |---|---|---|---| | A — OpenClaw drives Patter | OpenClaw agent | patter-mcp MCP server (tools) | OpenClaw should place outbound calls ("call the clinic, wait, tell me what they said"). Blocking until the call ends is the desired semantics here. | | B — Patter consults OpenClaw | In-call Patter agent | ConsultConfig → HTTP adapter → OpenClaw gateway | Patter answers an inbound call and needs the brain mid-conversation. This is the brain-on-the-line pattern the after-hours-receptionist use case needs. |

They compose, but for an inbound receptionist line Direction B is the one to build first. The rest of this skill is mostly Direction B; Direction A is at the end.

Why not OpenClaw's native voice plugin?

OpenClaw's built-in voice plugin only accepts inbound calls from an allowlist (inboundPolicy: "allowlist" + allowFrom: [...]); there is no "accept anyone" mode. That is useless for a public after-hours line where random people call. Patter owns the DID and answers everyone, then reaches the receptionist over OpenClaw's operator-side /v1/chat/completions gateway — the caller is never an OpenClaw "sender" subject to the allowlist. This is the single clearest reason to put Patter in front.


Direction B — Patter consults OpenClaw mid-call

Step 1 — Enable OpenClaw's chat-completions gateway

OpenClaw exposes an OpenAI-compatible POST /v1/chat/completions. It is disabled by default. Enable it in ~/.openclaw/openclaw.json (JSON5):

{
  gateway: {
    http: {
      endpoints: {
        chatCompletions: {
          enabled: true,
        },
      },
    },
  },
}

The gateway credential is operator-grade — the model field is NOT a security boundary. Selecting an agent in the model field picks the persona, but with shared-secret (token/password) auth the gateway credential carries full operator scope across the whole gateway. Real isolation comes from three things, none of which is the token:

  1. A dedicated least-privileged receptionist agent with a tight per-agent

tools.allow / tools.deny (see Step 5).

  1. One gateway per client — bind the gateway to loopback / tailnet only

and never expose /v1/chat/completions to the public internet. On a single always-on Mac Mini per client (one OS user = one gateway = one credential set), this isolation is free.

  1. An auth credential on the gateway (mirror OpenClaw's own gateway-auth

guidance — do not stand up an unauthenticated, network-reachable endpoint).

Confirm the exact gateway bind/auth keys and the model-routing alias forms against the live OpenClaw docs (docs.openclaw.ai) before you ship — they are OpenClaw-side config, not Patter's.

Step 2 — Route the consult to ONE specific receptionist agent

On /v1/chat/completions the model field is an agent target:

  • "openclaw" / "openclaw/default" → the default agent — which may be the

master / CEO / financial agent and can drift between environments. Never use this from the voice layer.

  • "openclaw/" → one explicit, named agent. **Always pin

this.** (Alias forms openclaw: / agent: are commonly accepted — verify against OpenClaw docs.)

Per client, set the agentId explicitly:

| Client | Receptionist agentId | |---|---| | Roofing contractor (CA) | openclaw/roofing-ca-receptionist | | Home-automation contractor (FL) | openclaw/home-fl-receptionist |

Step 3 — Point consult at the OpenClaw agent

Recommended: the native target — no adapter. Patter's consult speaks OpenClaw's /chat/completions endpoint directly; point it at one scoped agent in a single line:

from getpatter import ConsultConfig, OpenAIRealtime2

agent = phone.agent(
    engine=OpenAIRealtime2(),
    system_prompt="You are the after-hours receptionist...",
    consult=ConsultConfig.openclaw("receptionist"),
)
import { openclawConsult, OpenAIRealtime2 } from "getpatter";

const agent = phone.agent({
  engine: new OpenAIRealtime2(),
  systemPrompt: "You are the after-hours receptionist...",
  consult: openclawConsult("receptionist"),
});

It targets model="openclaw/receptionist", sends the call id as the OpenAI user field plus the x-openclaw-session-key header (one OpenClaw session per call), reads the operator-grade bearer from OPENCLAW_API_KEY (never logged), auto-enables allow_loopback for the co-located gateway, and attaches a default "let me check" reassurance filler. Notify OpenClaw at call end with openclaw_post_call_notifier("receptionist") / openclawPostCallNotifier("receptionist") on serve(on_call_end=...) to post the call record (caller, line, duration, transcript) to the same agent and session.

Escape hatch — a hand-written adapter, only when you need a custom request/response mapping (e.g. a non-OpenClaw back office). Patter POSTs { request, call_id, caller, callee }; the adapter must:

  1. Target one named agent (model="openclaw/").
  2. Carry call_id as the OpenClaw session key so a multi-turn call maps to

one OpenClaw session (continuity), and forward caller for caller-ID prefetch of the customer record.

  1. Return a concise, spoken-ready string (don't dump a raw API blob back

into the voice context).

# adapter.py — forwards Patter consult requests to ONE OpenClaw receptionist agent
import os
import httpx
from fastapi import FastAPI, Request

app = FastAPI()

OPENCLAW_URL = os.environ["OPENCLAW_URL"]          # e.g. http://127.0.0.1:18789/v1/chat/completions
OPENCLAW_TOKEN = os.environ["OPENCLAW_GATEWAY_TOKEN"]
RECEPTIONIST_AGENT = os.environ.get("OPENCLAW_AGENT", "openclaw/receptionist")

@app.post("/consult")
async def consult(req: Request):
    body = await req.json()
    request_text = body.get("request", "")
    call_id = body.get("call_id", "")
    caller = body.get("caller", "")

    payload = {
        "model": RECEPTIONIST_AGENT,                # ONE scoped agent, never "openclaw"
        "user": call_id,                            # one OpenClaw session per call
        "messages": [{
            "role": "user",
            # caller-id is forwarded so the brain can prefetch the customer record
            "content": f"[caller={caller}] {request_text}",
        }],
    }
    headers = {"Authorization": f"Bearer {OPENCLAW_TOKEN}"}

    # 75 s is sized to OpenClaw's worst-case tool time (see "timeout ladder").
    async with httpx.AsyncClient(timeout=75.0) as client:
        resp = await client.post(OPENCLAW_URL, json=payload, headers=headers)
        resp.raise_for_status()
        content = resp.json()["choices"][0]["message"]["content"]

    # Trim to a short, spoken-ready reply before it re-enters the voice context.
    return {"reply": content.strip()[:600]}
// adapter.ts — forwards Patter consult requests to ONE OpenClaw receptionist agent
import express from "express";

const app = express();
app.use(express.json());

const OPENCLAW_URL = process.env.OPENCLAW_URL!;            // http://127.0.0.1:18789/v1/chat/completions
const OPENCLAW_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN!;
const RECEPTIONIST_AGENT = process.env.OPENCLAW_AGENT ?? "openclaw/receptionist";

app.post("/consult", async (req, res) => {
  const { request = "", call_id = "", caller = "" } = req.body ?? {};

  const payload = {
    model: RECEPTIONIST_AGENT,                              // ONE scoped agent, never "openclaw"
    user: call_id,                                          // one OpenClaw session per call
    messages: [{ role: "user", content: `[caller=${caller}] ${request}` }],
  };

  const resp = await fetch(OPENCLAW_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${OPENCLAW_TOKEN}` },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(75_000),                    // see "timeout ladder"
  });
  const data = await resp.json();
  const content: string = data.choices[0].message.content;

  res.json({ reply: content.trim().slice(0, 600) });        // short, spoken-ready
});

app.listen(8000);

> The adapter is also where you can drop slow work onto an OpenClaw sub-agent > (e.g. spawn a worker, return a short "checking…" and let a later turn fetch the > result). Optional — the in-band reassurance below already covers dead air for > a single 60 s lookup.

Step 4 — Point the Patter agent at the adapter

ConsultConfig (Python) / consult (TS) auto-injects a consult_agent tool the in-call agent can call. On a single Mac Mini the adapter is on loopback, so opt in with allow_loopback / allowLoopback.

from getpatter import Patter, Twilio, OpenAIRealtime2, ConsultConfig

phone = Patter(carrier=Twilio(), phone_number="+15550001234")

agent = phone.agent(
    engine=OpenAIRealtime2(),                    # low-latency English, strongest tool flow
    system_prompt=(
        "You are the after-hours receptionist for Acme Roofing. "
        "When the caller asks about appointments, availability, or account "
        "details, call `consult_agent` with a clear request. "
        "Say a brief 'let me check' first, then read back the answer."
    ),
    first_message="Thanks for calling Acme Roofing, how can I help?",
    consult=ConsultConfig(
        url="http://127.0.0.1:8000/consult",
        timeout_s=75.0,                          # sized to the brain's worst-case tool time
        allow_loopback=True,                     # adapter is co-located on this box
        headers={"Authorization": "Bearer "},  # optional, never logged
        # tool_name / description default to "consult_agent" + a sensible prompt;
        # override description to steer WHEN the agent escalates.
    ),
)

phone.serve(agent)
import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";

const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });

const agent = phone.agent({
  engine: new OpenAIRealtime2(),                 // low-latency English, strongest tool flow
  systemPrompt:
    "You are the after-hours receptionist for Acme Roofing. " +
    "When the caller asks about appointments, availability, or account " +
    "details, call consult_agent with a clear request. " +
    "Say a brief 'let me check' first, then read back the answer.",
  firstMessage: "Thanks for calling Acme Roofing, how can I help?",
  consult: {
    url: "http://127.0.0.1:8000/consult",
    timeoutMs: 75_000,                           // sized to the brain's worst-case tool time
    allowLoopback: true,                         // adapter is co-located on this box
    headers: { Authorization: "Bearer " }, // optional, never logged
  },
});

await phone.serve({ agent });

Defaults to know: consult timeout defaults to 30 s (timeout_s=30.0 / timeoutMs: 30000), the tool is named consult_agent, and consult is injected in Realtime and Pipeline only (a warning is emitted if you set it with ElevenLabs ConvAI).


Long-tool-call survival (the literal blocker)

The prior failure with another platform was: the agent makes a 30-60 s tool call (sometimes browser automation) and the call dies. The fix is three layers, all of which must hold.

1. The timeout ladder — the call dies at the SHORTEST link

| Layer | Where | Set to | Why | |---|---|---|---| | Patter consult timeout | ConsultConfig.timeout_s / consult.timeoutMs | 75-90 s | Above the brain's worst-case tool time. Default 30 s is too short for 30-60 s automation. | | Adapter HTTP timeout | httpx.AsyncClient(timeout=…) / AbortSignal.timeout(…) | = consult timeout | The adapter must outlast the consult, not undercut it. | | OpenClaw gateway chatCompletions timeout | OpenClaw config | raised | The gateway must not give up before the agent's tool returns. | | Direction A: mcp.servers.patter.timeout | OpenClaw config | 600 s | A blocking make_call runs for the whole call. |

> Verify the per-server timeout is honoured. OpenClaw's docs don't publish a > default for the per-server MCP timeout, and an MCP client can impose its own > read-timeout ceiling that silently caps a long blocking call regardless of config. > After wiring, probe the server with openclaw mcp doctor patter --probe (a live > connection check). openclaw mcp status --verbose shows the resolved timeout but > does NOT open a connection.

2. Reassurance — speak a filler the instant the tool starts

Without a filler the caller hears dead air for the whole lookup and assumes the line froze. Attach reassurance to the consult tool (or any slow tool) so the agent speaks immediately and keeps the media stream alive while the brain works.

The consult_agent tool is auto-built, so to give it reassurance the cleanest path today is to add your own slow tool that wraps the consult, OR steer the agent via the system prompt to say "let me check, one moment" before it calls consult_agent. For a hand-built slow tool, set reassurance on the Tool (Python) / ToolDefinition (TS):

from getpatter import Tool

# reassurance is a field on the Tool dataclass (string shorthand → after_ms=1500,
# or a dict {"message": str, "after_ms": int}). Realtime mode only today.
check_schedule = Tool(
    name="check_schedule",
    description="Look up the caller's appointment in the calendar.",
    parameters={"type": "object", "properties": {"day": {"type": "string"}}, "required": ["day"]},
    handler=my_slow_handler,                         # may take 30-60 s
    reassurance="Let me check the calendar for you, one moment.",
)
// In TS, reassurance and a per-tool timeoutMs are fields on the raw
// ToolDefinition (defineTool does not expose them — build the object directly).
const checkSchedule = {
  name: "check_schedule",
  description: "Look up the caller's appointment in the calendar.",
  parameters: {
    type: "object",
    properties: { day: { type: "string" } },
    required: ["day"],
  },
  handler: mySlowHandler,                            // may take 30-60 s
  reassurance: "Let me check the calendar for you, one moment.",
  timeoutMs: 60_000,                                 // raise off the 10 s default
};

Reassurance is Realtime-only as of 0.7.0. In Pipeline mode the field is silently ignored (the LLM has to generate its own filler); ConvAI doesn't expose it. For the receptionist line, use Realtime — it is also the lowest-latency English path and has the strongest tool flow.

3. Per-tool timeout

  • TypeScript: ToolDefinition.timeoutMs (default 10 000 ms, clamped to

300 000 ms). Raise it to 60_000 for slow tools — shown above. A ti

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.