Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
97c5124
Migrate Channels runtime to canonical server
tylerslaton Jul 27, 2026
9236c33
Fix platform scoping and interrupt confirmation
tylerslaton Jul 27, 2026
81ecb61
Finish OpenTag launch configuration
tylerslaton Jul 27, 2026
925ceed
Fix launch blockers from review
tylerslaton Jul 27, 2026
42f778a
Enforce write approvals and isolate direct channels
tylerslaton Jul 27, 2026
6664d62
Surface recoverable integration failures
tylerslaton Jul 27, 2026
b130b0e
Address OpenTag migration review comments
tylerslaton Jul 27, 2026
ac0a333
Fix packaging and terminal error reporting
tylerslaton Jul 27, 2026
8d0682e
Simplify OpenTag example configuration
tylerslaton Jul 27, 2026
83a8956
fix(deploy): restore Notion MCP sidecar
tylerslaton Jul 27, 2026
3d2e109
fix(deploy): make Railway agent deployable
tylerslaton Jul 28, 2026
5a3b711
refactor(deploy): embed channels in runtime
tylerslaton Jul 28, 2026
885cef4
fix(channels): use one Intelligence channel
tylerslaton Jul 28, 2026
c6877e7
fix(deploy): watch all runtime source changes
tylerslaton Jul 28, 2026
0e4b4a4
fix(deploy): clear runtime watch filters
tylerslaton Jul 28, 2026
bafeb22
fix(channels): surface action failures
tylerslaton Jul 28, 2026
4ac1a7a
feat(agent): make OpenTag triage-first
tylerslaton Jul 28, 2026
303b6bb
feat(channels): restore rich UI and remove dead paths
tylerslaton Jul 28, 2026
08ac67c
Merge pull request #12 from CopilotKit/tyler/oss-613-fast-rich-ui
tylerslaton Jul 28, 2026
cd1e0c7
fix: use Responses API for gpt-5.5 tools
tylerslaton Jul 28, 2026
735c00a
Merge pull request #13 from CopilotKit/tyler/oss-631-responses-api-demo
tylerslaton Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 18 additions & 115 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,121 +1,24 @@
# Set SLACK_*, DISCORD_*, TELEGRAM_BOT_TOKEN, and/or WHATSAPP_* — the bot starts
# an adapter for each platform whose secrets are present (any one, or several at once).
# -- Intelligence Variables --
# Get an API key at https://intelligence.copilotkit.ai.
# Both variables are required. The included agent listens on port 8123.
AGENT_URL=http://localhost:8123/
INTELLIGENCE_API_KEY=cpk-...

# ── Slack credentials (from api.slack.com/apps → your app) ──────────────
# Set both to run on Slack; leave blank to skip Slack.
# Bot Token: OAuth & Permissions page, "Bot User OAuth Token" (xoxb-...)
SLACK_BOT_TOKEN=xoxb-...
# App-Level Token: Basic Information → App-Level Tokens → generate with the
# "connections:write" scope (xapp-...)
SLACK_APP_TOKEN=xapp-...
# Optional. Defaults to "open-tag"; set this to the Channel name in Intelligence.
# INTELLIGENCE_CHANNEL_NAME=open-tag

# ── Discord credentials (from discord.com/developers/applications → your app) ──
# Set both to run on Discord; leave blank to skip Discord. Bot page → "Reset
# Token"; under "Privileged Gateway Intents" enable BOTH Message Content and
# Server Members (both required). Application ID is on General Information.
# DISCORD_BOT_TOKEN=
# DISCORD_APP_ID=
# Optional: a guild (server) id registers slash commands instantly during dev
# (global commands can take up to ~1h). Omit in production.
# DISCORD_GUILD_ID=

# ── Telegram credentials (from @BotFather on Telegram) ──────────────────
# Set to run on Telegram; leave blank to skip Telegram. Message @BotFather →
# /newbot → copy the token it gives you. Long-polling is the default ingress
# (no public URL or webhook setup needed).
# TELEGRAM_BOT_TOKEN=

# ── Agent backend (runtime.ts) ──────────────────────────────────────────
# The AG-UI endpoint the bridge POSTs to. REQUIRED — the bridge exits at
# startup if this is unset; there is no code-level fallback. The value below
# is a template pointing at the local CopilotKit runtime started by
# `pnpm runtime`; change it if you deploy the runtime elsewhere.
AGENT_URL=http://localhost:8200/api/copilotkit/agent/triage/run
# Optional auth header forwarded to the agent (for a deployed runtime).
# AGENT_AUTH_HEADER=Bearer ...

# ── Intelligence channel mode — app/managed.ts (`pnpm channel`) ──────────
# Runs the SAME bot over the CopilotKit Intelligence Realtime Gateway instead
# of a native adapter — this process holds NO Slack tokens (Intelligence owns
# the Slack edge). The agent brain reuses AGENT_URL / AGENT_AUTH_HEADER above.
# All values come from your CopilotKit Intelligence project + channel setup.
# Gateway runner WebSocket URL (the /runner endpoint).
# INTELLIGENCE_GATEWAY_WS_URL=wss://dev.intelligence.copilotkit.ai/runner
# Project runtime API key (cpk-{projectId}_...), presented as the socket auth token.
# INTELLIGENCE_API_KEY=cpk-...
# Authoritative org/project/channel scope (from the Intelligence dashboard):
# INTELLIGENCE_ORG_ID=org_...
# INTELLIGENCE_PROJECT_ID=123
# INTELLIGENCE_CHANNEL_ID=channel_...
# The registered channel name (lowercase kebab). Defaults to "kitebot".
# INTELLIGENCE_CHANNEL_NAME=kitebot
# Optional stable runtime instance id; auto-generated (rti_...) if unset.
# INTELLIGENCE_RUNTIME_INSTANCE_ID=rti_...

# Model for the BuiltInAgent. The runtime is OpenAI-only (it uses OpenAI's
# Responses API via TanStack AI's `openaiText` adapter, needed for the
# `web_search` provider tool) — there is no multi-provider resolution.
# AGENT_MODEL only accepts an OpenAI model id, optionally prefixed with
# "openai/" (the prefix is stripped); defaults to openai/gpt-5.5.
# AGENT_MODEL=openai/gpt-5.5
# -- Agent Variables --
# Get an API key at https://platform.openai.com/api-keys.
OPENAI_API_KEY=sk-...
# ANTHROPIC_API_KEY / GOOGLE_API_KEY are NOT supported by this runtime today
# (runtime.ts never reads them) — do not set AGENT_MODEL to an anthropic/... or
# google/... value.

# ── Deep-research agent (agent/) — alternative brain to runtime.ts ──────
# A standalone Python (deepagents/LangGraph) service served over AG-UI on
# :8123. To use it instead of the TS runtime above, point the bridge at it:
# AGENT_URL=http://localhost:8123/
# Only OPENAI_API_KEY (above) is required to run it — it still chats and
# generates UI components without Tavily.
# OPTIONAL — web research for the agent/ service. Without it the `research`
# web tool is simply not loaded (chat + UI-component generation still work,
# answering from the model's own knowledge); with it, live web research
# turns on. Get a key at https://tavily.com. This is scoped to agent/, not
# to runtime.ts, which has no research tool.
# Optional web research and model behavior overrides.
# TAVILY_API_KEY=tvly-...
# Model used by the agent/ service (its own env, separate from AGENT_MODEL
# above which is read by runtime.ts). Defaults to gpt-5.5.
# OPENAI_MODEL=gpt-5.5

# ── Linear MCP ──────────────────────────────────────────────────────────
# The hosted Linear MCP accepts a raw API key as a bearer token (no OAuth
# dance). Create one at linear.app → Settings → API → Personal API keys.
# Leave LINEAR_API_KEY blank to run without Linear.
LINEAR_API_KEY=lin_api_...
# Override only if you front the MCP yourself; default is the hosted server.
# LINEAR_MCP_URL=https://mcp.linear.app/mcp
# Default Linear team the bot files/queries against.
LINEAR_TEAM_KEY=CPK

# ── Notion MCP ──────────────────────────────────────────────────────────
# Run the official Notion MCP as a Streamable-HTTP sidecar: `pnpm notion-mcp`.
# NOTION_TOKEN is the Notion integration secret the sidecar uses to call the
# Notion API (notion.so → Settings → Connections → develop integrations).
# NOTION_MCP_AUTH_TOKEN is the bearer the sidecar requires on its HTTP
# transport; the agent sends it. Pick any strong string and use it in both
# `pnpm notion-mcp` and here. Leave NOTION_MCP_AUTH_TOKEN blank to run
# without Notion.
NOTION_TOKEN=ntn_...
NOTION_MCP_AUTH_TOKEN=choose-a-strong-shared-secret
# Override only if the sidecar runs elsewhere; default is the local sidecar.
# NOTION_MCP_URL=http://127.0.0.1:3001/mcp
# Port the `pnpm notion-mcp` sidecar listens on (scripts/start-notion-mcp.ts).
# Must agree with the port in NOTION_MCP_URL above. Defaults to 3001.
# NOTION_MCP_PORT=3001
# Notion-Version header the sidecar sends to the Notion API. Defaults to
# 2022-06-28; override only if you need a newer Notion API version.
# NOTION_VERSION=2022-06-28

# ── WhatsApp Cloud API (optional — omit to run Slack-only) ──────────────
# From your Meta App → WhatsApp → API Setup. Leave WHATSAPP_ACCESS_TOKEN blank
# to disable the WhatsApp adapter entirely. Use a System User token in prod
# (the temporary token expires in 24h).
WHATSAPP_ACCESS_TOKEN=
WHATSAPP_PHONE_NUMBER_ID=
WHATSAPP_APP_SECRET=
WHATSAPP_VERIFY_TOKEN=choose-any-string-and-paste-it-in-the-webhook-config
# Webhook path (default /webhook). The server listens on $PORT — Railway injects
# it and routes the service's public domain there; locally it defaults to 3000.
# WHATSAPP_PATH=/webhook
# OPENAI_REASONING_EFFORT=low
# OPENAI_VERBOSITY=low

# -- Internal Sources (Optional) --
# LINEAR_API_KEY=lin_api_...
# Remote Notion requires both values; no local sidecar is included.
# NOTION_MCP_URL=https://your-notion-mcp.example.com/mcp
# NOTION_MCP_AUTH_TOKEN=your-remote-mcp-bearer-token
7 changes: 0 additions & 7 deletions .npmrc

This file was deleted.

128 changes: 37 additions & 91 deletions .railway/railway.ts
Original file line number Diff line number Diff line change
@@ -1,121 +1,67 @@
/**
* Reconciles OpenTag's production Railway topology: a Python agent and the
* CopilotRuntime process that embeds Channels. Platform credentials and
* attachments stay in Intelligence.
*/
import { defineRailway, github, preserve, project, service } from "railway/iac";

// KiteBot on CopilotKit Intelligence — one-click Railway topology.
// Three services build from this repo; the Python agent uses rootDirectory "agent".
// Inter-service URLs use Railway reference variables (${{svc.VAR}}), resolved at
// deploy over private networking. SECRETS are declared with preserve() so applying
// never clobbers deployer-set values — set their actual values in the Railway UI
// (see README "Deploy to Railway").
//
// Two topology invariants this file encodes:
// 1. Ports are pinned as explicit service variables (NOT left to Railway's
// auto-injected $PORT) so each service's listen port and the ${{svc.PORT}}
// its peers dial always agree.
// 2. Services reached over Railway private networking must bind :: (all
// interfaces) — private DNS (RAILWAY_PRIVATE_DOMAIN) resolves to IPv6, and
// legacy environments are IPv6-only. A service bound to 127.0.0.1/0.0.0.0
// is unreachable by its peers.
const REPO = "CopilotKit/OpenTag";
const BRANCH = "main";

export default defineRailway(() => {
// Notion MCP sidecar — streamable-HTTP MCP server. Its launcher
// (scripts/start-notion-mcp.ts) binds NOTION_MCP_PORT (default 3001) and does
// NOT read Railway's injected $PORT, so we pin NOTION_MCP_PORT here and have
// the agent dial that same variable. We also set NOTION_MCP_HOST=:: so the
// sidecar binds all interfaces (its upstream default is 127.0.0.1, which is
// unreachable across containers on the private network).
//
// Notion is an OPTIONAL research source: the agent runs fine (chat + UI + web
// research) without it and drops the Notion tools if this sidecar is
// unreachable. But if you deploy this service it REQUIRES NOTION_TOKEN and
// NOTION_MCP_AUTH_TOKEN — its launcher exits non-zero without them. To skip
// Notion entirely, remove this service from `resources` below and delete the
// agent's NOTION_MCP_URL / NOTION_MCP_AUTH_TOKEN wiring.
const notionMcp = service("notion-mcp", {
source: github(REPO),
start: "pnpm notion-mcp",
deploy: {
restartPolicyType: "ON_FAILURE",
restartPolicyMaxRetries: 5,
},
env: {
NOTION_MCP_PORT: "3001",
NOTION_MCP_HOST: "::",
// secrets (set in Railway UI):
NOTION_TOKEN: preserve(),
NOTION_MCP_AUTH_TOKEN: preserve(),
},
});

// Python deep-research agent — deepagents over AG-UI (uvicorn, /health).
// This service owns its build + deploy config here (single source of truth):
// there is deliberately NO agent/railway.toml, because Railway forbids a
// service being managed by both IaC and config-as-code at once.
const agent = service("agent", {
source: github(REPO, { rootDirectory: "agent" }),
build: { builder: "NIXPACKS" },
source: github(REPO, {
branch: BRANCH,
rootDirectory: "agent",
}),
build: { builder: "RAILPACK" },
deploy: {
// Bind :: so the agent is reachable over Railway private networking (see
// invariant 2 above). The Railway startCommand runs `uvicorn main:app`
// directly, so agent/main.py's own __main__ port logic does NOT run here —
// the port comes solely from this --port arg. The ${PORT:-8123} fallback
// keeps the bind valid even if PORT were unset; PORT is pinned in env below
// so this and ${{agent.PORT}} (dialed by channel) always agree.
startCommand: "uvicorn main:app --host :: --port ${PORT:-8123}",
startCommand: 'uvicorn main:app --host "" --port ${PORT:-8123}',
healthcheckPath: "/health",
healthcheckTimeout: 300,
restartPolicyType: "ON_FAILURE",
restartPolicyMaxRetries: 5,
},
env: {
// Pin PORT so uvicorn's bind and ${{agent.PORT}} (dialed by channel) agree.
PORT: "8123",
// internal research source (optional Notion), wired to the sidecar on its
// pinned NOTION_MCP_PORT over private networking. The shared bearer is
// referenced from notion-mcp so it has a single source of truth — set
// NOTION_MCP_AUTH_TOKEN once, on the notion-mcp service.
NOTION_MCP_URL:
"http://${{notion-mcp.RAILWAY_PRIVATE_DOMAIN}}:${{notion-mcp.NOTION_MCP_PORT}}/mcp",
NOTION_MCP_AUTH_TOKEN: "${{notion-mcp.NOTION_MCP_AUTH_TOKEN}}",
// OPENAI_MODEL is intentionally NOT set here: the agent defaults to
// gpt-5.5 (agent/agent.py), and leaving it unmanaged means a deployer can
// override it in the Railway UI without a later `config apply` clobbering
// the change. Set OPENAI_MODEL in the agent service's Variables to change it.
//
// secrets (set in Railway UI): OPENAI_API_KEY required; others optional:
OPENAI_API_KEY: preserve(),
TAVILY_API_KEY: preserve(),
LINEAR_API_KEY: preserve(),
NOTION_MCP_URL: preserve(),
NOTION_MCP_AUTH_TOKEN: preserve(),
},
});

// KiteBot channel host — runs the bot over the Intelligence Realtime Gateway.
const channel = service("channel", {
source: github(REPO),
start: "pnpm channel",
const runtime = service("runtime", {
source: github(REPO, { branch: BRANCH }),
start: "pnpm runtime",
// Rich rendering needs Chromium and its runtime libraries.
build: {
builder: "RAILPACK",
buildCommand: "pnpm exec playwright install chromium",
// No watch-path filter: deploy every push to the connected branch.
watchPatterns: [],
},
deploy: {
// Parity with agent/notion-mcp: restart the long-running channel host on
// crash instead of leaving KiteBot silently offline.
healthcheckPath: "/api/copilotkit/info",
healthcheckTimeout: 300,
restartPolicyType: "ON_FAILURE",
restartPolicyMaxRetries: 5,
},
env: {
// brain: points at the agent service over private networking. The agent
// pins PORT=8123, so ${{agent.PORT}} resolves to the port uvicorn binds.
AGENT_URL: "http://${{agent.RAILWAY_PRIVATE_DOMAIN}}:${{agent.PORT}}/",
// INTELLIGENCE_CHANNEL_NAME is intentionally NOT set here: app/managed.ts
// already defaults it to "kitebot", and leaving it unmanaged means a
// deployer can set their own channel name in the Railway UI without a
// later `config apply` clobbering it. Set it in the channel service's
// Variables if your Intelligence channel is named something else.
// secrets (set in Railway UI):
INTELLIGENCE_GATEWAY_WS_URL: preserve(),
PORT: "3000",
AGENT_URL:
"http://${{agent.RAILWAY_PRIVATE_DOMAIN}}:${{agent.PORT}}/",
INTELLIGENCE_API_KEY: preserve(),
INTELLIGENCE_ORG_ID: preserve(),
INTELLIGENCE_PROJECT_ID: preserve(),
INTELLIGENCE_CHANNEL_ID: preserve(),
INTELLIGENCE_API_URL: "https://api.intelligence.copilotkit.ai",
INTELLIGENCE_GATEWAY_WS_URL:
"wss://realtime.intelligence.copilotkit.ai",
INTELLIGENCE_CHANNEL_NAME: "open-tag",
PLAYWRIGHT_BROWSERS_PATH: "0",
RAILPACK_DEPLOY_APT_PACKAGES:
"fonts-liberation fonts-noto-color-emoji fonts-unifont libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libexpat1 libfontconfig1 libfreetype6 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 libxrender1 libxshmfence1",
},
});

return project("kitebot", { resources: [notionMcp, agent, channel] });
return project("opentag", { resources: [agent, runtime] });
});
Loading