diff --git a/.env.example b/.env.example index e1ffe93d..97f9620c 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,16 @@ -# -- Intelligence Variables -- -export INTELLIGENCE_API_KEY=cpk-... # Create a runtime key at https://intelligence.copilotkit.ai. -export INTELLIGENCE_CHANNEL_NAME=open-tag # Must match the Channel name; defaults to "open-tag". +# -- Runtime Variables -- +export INTELLIGENCE_API_KEY=cpk-... # Runtime key from `copilotkit project select`, or https://intelligence.copilotkit.ai. +export INTELLIGENCE_CHANNEL_NAME=open-tag # Must match .copilotkit/channels.json exactly. Forks: use your own name. +export AGENT_URL=http://localhost:8123/ # AG-UI endpoint the runtime calls; the bundled Python agent serves this locally. + +# Slack and Teams credentials are NOT set here. Intelligence owns the adapters; +# `copilotkit channels add` tells you where its credentials go. # -- Agent Variables -- export OPENAI_API_KEY=sk-... # Create a key at https://platform.openai.com/api-keys. -export AGENT_URL=http://localhost:8123/ # AG-UI endpoint; the bundled Deep Agent uses this locally. + +# -- Runtime Overrides (Optional) -- +# export LOG_LEVEL=debug # Defaults to "error"; Channel lifecycle breadcrumbs log at "warn". # -- Agent Overrides (Optional) -- # export TAVILY_API_KEY=tvly-... diff --git a/.gitignore b/.gitignore index 76fabf87..196416bc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,16 @@ node_modules .env .env.local +# CopilotKit CLI channel artifacts. +# .copilotkit/channels.json is the tracked Channel declaration; artifacts are not. +.copilotkit/artifacts + +# CopilotKit CLI skill installs (`copilotkit channels setup`, `copilotkit skills install`) +.agents +.claude/skills +agent/skills +skills-lock.json + # E2E harness artifacts e2e/.chrome-profile e2e/results diff --git a/README.md b/README.md index ba07d2d2..dbcaf6c5 100644 --- a/README.md +++ b/README.md @@ -5,97 +5,198 @@ Teams, with research available on demand. It runs on CopilotKit Channels, connects to CopilotKit Intelligence, and uses a Python LangGraph agent over AG-UI. -The launch supports: +Slack and Microsoft Teams are supported today. Discord, Telegram, and WhatsApp +are coming soon. -- Slack through Intelligence, including the existing production `@kite` app. -- Microsoft Teams through Intelligence. +## Quick start -Discord, Telegram, and WhatsApp are coming soon. +Prerequisites: Node.js 22+, pnpm, Python 3.12, +[`uv`](https://docs.astral.sh/uv/), a CopilotKit account, an OpenAI API key, +and a Slack workspace you can install an app into. -## Architecture +### 1. Install dependencies -```text -Slack / Microsoft Teams - │ - ▼ -CopilotKit Intelligence - │ realtime - ▼ -runtime (Node + CopilotRuntime with embedded Channels) - │ AG-UI - ▼ -agent (Python + LangGraph deepagents) - ├── OpenAI - ├── Tavily (optional) - ├── GitHub MCP (optional, read-only) - ├── PostHog MCP (optional, read-only) - ├── Linear MCP (optional) - └── Notion MCP (optional remote server) +```bash +pnpm install ``` -There is one canonical runtime host: [`server.ts`](./server.ts). -[`app/index.ts`](./app/index.ts) composes one `CopilotKitIntelligence`, one -`CopilotRuntime`, and one adapter-free managed Channel. Intelligence owns its -Slack and Microsoft Teams adapters, credentials, and attachments. +### 2. Create the managed Channel -The SDK versions are pinned for reproducible deploys: +Do this **before** touching Slack. The Channel is what generates a Slack app +manifest already pointed at the right Request URL, so creating the Slack app +first means creating the wrong one. -- `@copilotkit/channels@0.7.0` -- `@copilotkit/runtime@1.66.0` +Two ways to do it. Both end in the same place. -## Quick start +#### Let your coding agent drive it + +```bash +npx --yes copilotkit@latest channels setup +``` + +That installs a `channels-setup` skill and copies a one-line prompt to your +clipboard (`--no-clipboard` prints it instead). Paste it into your agent: + +> Use the `channels-setup` skill to set up a Channel for this project. + +The skill is deliberately a pointer rather than a copy of the steps: it fetches +its workflow from at run time, so it +cannot go stale against the CLI. It covers the whole path — selecting the +project, creating and reconciling the Channel, walking you through the Slack +console handoff, and proving a real mention gets a reply. It hands every secret +back to you; it never asks you to paste one into chat. + +It writes `.agents/skills/channels-setup` and a `skills-lock.json`, both already +gitignored here. Note that it installs to every coding agent it detects with no +way to narrow the list. To install for one agent only, use the equivalent +command, which takes the same prompt: + +```bash +npx --yes copilotkit@latest skills onboard --channels --agent claude-code +``` + +The skill will also carry you through the rest of this quick start. One thing to +watch: its workflow is written for a project starting from nothing, so it has +phases for building the agent and writing the Channel runtime. **OpenTag already +has both** — [`agent/`](./agent) and [`server.ts`](./server.ts). Point your agent +at the existing code to verify and run, not to rewrite. + +#### Or run it yourself + +```bash +npx --yes copilotkit@latest project select +``` + +```bash +npx --yes copilotkit@latest channels add --name open-tag --display-name "OpenTag" --adapter slack --json +``` + +`channels add` declares the Channel in `.copilotkit/channels.json`, creates it +on the server, and returns a JSON envelope with one of three states: + +- `completed` — the adapter is attached. Continue to step 3. +- `blocked` — a normal pause waiting on you in the Slack console. Read + `nextAction`: it carries the prefilled manifest link, the exact environment + variable names to set, and the `resumeCommand` to run afterward. This exits 0. +- `failed` — stop and read the error code. Do not continue. + +`--name` is a slug and must match `INTELLIGENCE_CHANNEL_NAME` character for +character. If you are running a fork against your own project, pick your own +name — see [Channel names claim deliveries](#channel-names-claim-deliveries). + +For Microsoft Teams, use `--adapter teams`. Two Teams steps stay yours because +nothing can work around them: granting tenant admin consent, and uploading the +app package through **Apps → Manage your apps → Upload an app**. + +#### Three Slack details that cost the most time + +These apply on either path. Follow the CLI's emitted `nextAction` rather than +remembered Slack steps, and watch for: + +- After creating the app from the link, open **OAuth & Permissions** and choose + **Reinstall to Workspace**. Slack applies the manifest's real scopes only on + reinstall. +- Take the **Bot User OAuth Token** (`xoxb-`) from **OAuth & Permissions**, not + the token shown in the app-creation modal, and the **Signing Secret** from + **Basic Information → App Credentials**. Those two values are all the Slack + adapter wants — there is no app-level `xapp-` token anywhere on this path. +- The signing secret is reissued on reinstall. If auth fails right after a + reinstall, suspect a stale stored secret before a missing scope. -Prerequisites: Node.js 22+, pnpm, Python 3.12, and -[`uv`](https://docs.astral.sh/uv/). +### 3. Configure the environment -1. Install the Node dependencies. +```bash +cp .env.example .env +``` + +Set: + +```dotenv +OPENAI_API_KEY=sk-... +AGENT_URL=http://localhost:8123/ +INTELLIGENCE_API_KEY=cpk-... +INTELLIGENCE_CHANNEL_NAME=open-tag +``` + +Both the Node runtime and the Python agent load this one root `.env`; Railway +supplies the same values as service variables. Tavily, GitHub, PostHog, Linear, +and Notion are optional — see [Optional research +sources](#optional-research-sources). - ```bash - pnpm install - ``` +`INTELLIGENCE_API_URL` and `INTELLIGENCE_GATEWAY_WS_URL` already default to the +production Intelligence endpoints in +[`app/env.ts`](./app/env.ts), so leaving them unset is correct. -2. Configure both services from the shared root environment. +### 4. Run the stack - ```bash - cp .env.example .env - ``` +```bash +pnpm dev +``` - Set: +The `predev` hook syncs the locked Python environment and installs Playwright's +Chromium. `pnpm dev` then runs the Python agent with reload enabled and the Node +runtime in watch mode. The runtime waits for its Intelligence connection to +become ready before its HTTP listener accepts traffic. - ```dotenv - OPENAI_API_KEY=sk-... - AGENT_URL=http://localhost:8123/ - INTELLIGENCE_API_KEY=cpk-... - ``` +### 5. Invite the bot - Tavily, GitHub, PostHog, Linear, and Notion are optional. Both Node and - Python load this root `.env`; Railway supplies the same values as service - variables. +```text +/invite @OpenTag +``` - `INTELLIGENCE_API_URL` and `INTELLIGENCE_GATEWAY_WS_URL` default to the - production Intelligence endpoints. `INTELLIGENCE_CHANNEL_NAME` defaults to - `open-tag`. +Installed in the workspace is not the same as present in a conversation. Slack +emits no `app_mention` at all for a channel the app is not a member of, so +without this OpenTag looks broken while behaving correctly. - The Intelligence API key selects a project, and the Channel name selects a - Channel inside that project. Use a non-production project and API key - locally; Railway keeps its existing production key and `open-tag` Channel. +## Prove it works -3. In the project selected by your local Intelligence API key, create one - managed Channel named `open-tag` and configure its Slack and Microsoft - Teams adapters. +A Channel that installs cleanly and answers nothing is the most expensive +failure available here, because it looks finished. Three checks separate the +two. Send them from a real human account: -4. Run the complete local stack. +1. **Mention it.** `@OpenTag what changed in the last deploy?` — expect a + useful, model-backed reply. +2. **Follow up without mentioning it,** in that same thread — expect a reply. + A mention subscribes the thread; unmentioned messages run the agent only in + already-subscribed threads. +3. **Send an unmentioned message in a fresh conversation** — expect + **silence**. A reply here means the trigger rules are wrong. - ```bash - pnpm dev - ``` +If any of those fail, in this order: - The `predev` hook syncs the locked Python environment and installs - Playwright's Chromium browser. `pnpm dev` then runs the Python agent with - reload enabled and the Node runtime in watch mode. +```bash +npx --yes copilotkit@latest channels status --json +``` -The runtime waits for its Intelligence connection to become ready before its -HTTP listener accepts traffic. +It reports declaration, source, server, adapter, environment, and lifecycle +diagnostics. Resolve every one. Two of its warnings are expected for OpenTag +and are not faults: it flags `INTELLIGENCE_API_URL` and +`INTELLIGENCE_GATEWAY_WS_URL` as unset because +[`app/env.ts`](./app/env.ts) defaults them in code rather than in `.env`. + +```bash +LOG_LEVEL=debug pnpm runtime +``` + +The runtime logger defaults to `error`, while every Channel lifecycle +breadcrumb is emitted at `warn` — including `channel "" requires setup`, +the single highest-value diagnostic here. At the default level it is written and +discarded. + +Note that the runtime does not hot-reload Channel wiring. After editing a +handler, the agent, or the Channel, restart the process and confirm `online` +again before retesting. A stale process answering with the old behavior is +indistinguishable from a change that did not work. + +### Channel names claim deliveries + +Managed delivery is claim-based: two runtimes declaring the same Channel name in +the same Intelligence project race per delivery, and the loser silently receives +nothing. The tell is a Slack reply your terminal knows nothing about. + +`INTELLIGENCE_CHANNEL_NAME` defaults to `open-tag`, which is the name the +production deployment uses. Give a local or forked runtime its own Intelligence +project, its own API key, and its own Channel name. ## What OpenTag includes @@ -113,54 +214,72 @@ The Python agent is the only supported backend. Its identity and behavior live in [`agent/agent.py`](./agent/agent.py); the Channel UI and behavior live under [`app/`](./app/). -## Platform setup - -### Intelligence - -Use the normal Intelligence flow for both launch platforms: +## Architecture -1. Create one OpenTag project. -2. Create one Channel named `open-tag`. -3. Issue a runtime API key. -4. Configure the Slack and Microsoft Teams adapters on that Channel. -5. Run one `pnpm runtime` process with that Channel name and key. +```text +Slack / Microsoft Teams + │ HTTPS to an Intelligence-hosted Request URL + ▼ +CopilotKit Intelligence + │ outbound websocket from your runtime + ▼ +runtime (Node + CopilotRuntime with embedded Channels) + │ AG-UI + ▼ +agent (Python + LangGraph deepagents) + ├── OpenAI + ├── Tavily (optional) + ├── GitHub MCP (optional, read-only) + ├── PostHog MCP (optional, read-only) + ├── Linear MCP (optional) + └── Notion MCP (optional remote server) +``` -No organization, project, Channel ID, or runtime-instance ID environment -variables—or Slack/Teams credentials—are required by the runtime. +Neither leg is Socket Mode, and neither needs a tunnel or a public URL of your +own. Slack reaches Intelligence over HTTPS, authenticated by the signing secret +Intelligence holds. Intelligence reaches your runtime over a websocket your +process opens outbound, authenticated by `INTELLIGENCE_API_KEY`. -### Slack manifests +There is one canonical runtime host: [`server.ts`](./server.ts). +[`app/index.ts`](./app/index.ts) composes one `CopilotKitIntelligence`, one +`CopilotRuntime`, and one adapter-free managed Channel. Intelligence owns the +Slack and Microsoft Teams adapters, their credentials, and attachments — no +platform credential belongs in this repository's environment. -[`slack-app-manifest.yaml`](./slack-app-manifest.yaml) and -[`slack-app-manifest.json`](./slack-app-manifest.json) describe OpenTag for new -installations. +`@copilotkit/channels` and `@copilotkit/runtime` are pinned for reproducible +deploys. [`package.json`](./package.json) is the source of truth for both +versions. -For the production migration, **do not recreate or reinstall the existing -Slack app**. Reusing it preserves the bot user, workspace installation, and -`@kite` handle. Stop the old Socket Mode consumer before attaching its existing -`xapp` and `xoxb` tokens in Intelligence so only one consumer is active. +One naming collision is worth internalizing: the CopilotKit CLI's `channels` +commands configure **managed Intelligence Channels**. They do not configure the +open-source `@copilotkit/channels` adapter packages, which are a separate +product that shares the words "channels" and "Slack". OpenTag uses the package +to define its Channel and Intelligence to deliver to it. ## Optional research sources -- `TAVILY_API_KEY` enables live web research. Without it, OpenTag still chats, - triages requests and renders UI from model knowledge. -- `GITHUB_PERSONAL_ACCESS_TOKEN` enables read-only repository, code, issue, and - pull-request search through GitHub's hosted MCP. `GITHUB_MCP_URL` can override - the endpoint; OpenTag still sends GitHub's read-only configuration header. -- `POSTHOG_PERSONAL_API_KEY` enables PostHog analytics through its hosted MCP. - Create the key with PostHog's **MCP Server** preset. The default connection is - token-efficient and read-only; `POSTHOG_MCP_URL` can override the endpoint. -- `LINEAR_API_KEY` enables the hosted Linear MCP. -- Notion is optional and remote-only. Set both `NOTION_MCP_URL` and - `NOTION_MCP_AUTH_TOKEN`; setting only one disables the integration. +Every one of these is optional. Without them OpenTag still chats, triages +requests, and renders UI from model knowledge. + +| Variable | Enables | +| --- | --- | +| `TAVILY_API_KEY` | Live web research | +| `GITHUB_PERSONAL_ACCESS_TOKEN` | Read-only repository, code, issue, and PR search | +| `POSTHOG_PERSONAL_API_KEY` | PostHog analytics, read-only (use the **MCP Server** key preset) | +| `LINEAR_API_KEY` | Hosted Linear MCP | +| `NOTION_MCP_URL` + `NOTION_MCP_AUTH_TOKEN` | Remote Notion MCP; setting only one disables it | Every Linear and Notion mutation is intercepted in code before the MCP request runs. The interceptor emits `confirm_write` and proceeds only after approval; reads and rendering do not pause. -## Railway +[`setup.md`](./setup.md) documents each source, its overrides, and the full +environment contract. + +## Deploying [`.railway/railway.ts`](./.railway/railway.ts) defines exactly two services, -all sourced from `CopilotKit/OpenTag` on `main`: +both sourced from `CopilotKit/OpenTag` on `main`: | Service | Root | Start | Health | | --- | --- | --- | --- | @@ -168,15 +287,9 @@ all sourced from `CopilotKit/OpenTag` on `main`: | `runtime` | repository root | `pnpm runtime` | `/api/copilotkit/info` | The runtime reaches the agent over Railway private networking and embeds the -managed `open-tag` Channel. Railway sets -`INTELLIGENCE_CHANNEL_NAME=open-tag`; Intelligence owns both platform -adapters. The runtime API key is preserved. OpenAI is required on `agent`; -Tavily, GitHub search, PostHog, Linear, and remote Notion settings are optional. -Connecting both services to `main` enables GitHub-triggered deployments after -merges. - -The repository configuration does not mutate the existing production Railway -project. Inventory and cutover should happen after Railway authentication. +managed Channel. Connecting both services to `main` enables GitHub-triggered +deployments after merges. See [`setup.md`](./setup.md#railway) for the variable +contract. ## Verification @@ -190,8 +303,14 @@ node node_modules/railway/dist/iac/bin.js ``` The Slack live harness is documented in [`e2e/README.md`](./e2e/README.md). -Production acceptance is one end-to-end `@kite` mention that returns the -OpenTag persona through the Python agent. + +## Docs + +- [`setup.md`](./setup.md) — full reference: components, environment contract, + commands, optional sources, Railway. +- [`docs/migration-kite.md`](./docs/migration-kite.md) — CopilotKit-internal + cutover of the existing production `@kite` app. +- [`e2e/README.md`](./e2e/README.md) — Slack live harness. ## License diff --git a/app/cleanup.test.ts b/app/cleanup.test.ts index c51a5fc0..7e35a6cc 100644 --- a/app/cleanup.test.ts +++ b/app/cleanup.test.ts @@ -10,13 +10,18 @@ const packageJson = JSON.parse( dependencies: Record; }; +// A bare semver with no range operator, so deploys resolve reproducibly. +const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/; + describe("launch dependency and cleanup contract", () => { - it("keeps the exact Channels and Runtime versions", () => { - expect(packageJson.dependencies["@copilotkit/channels"]).toBe( - "0.7.0", + // Asserts the pin *shape*, not a literal version: the docs and this test both + // used to restate "0.7.0", and both drifted the moment the deps were bumped. + it("pins the Channels and Runtime versions exactly", () => { + expect(packageJson.dependencies["@copilotkit/channels"]).toMatch( + EXACT_VERSION, ); - expect(packageJson.dependencies["@copilotkit/runtime"]).toBe( - "1.66.0", + expect(packageJson.dependencies["@copilotkit/runtime"]).toMatch( + EXACT_VERSION, ); }); diff --git a/docs/migration-kite.md b/docs/migration-kite.md new file mode 100644 index 00000000..1e518545 --- /dev/null +++ b/docs/migration-kite.md @@ -0,0 +1,125 @@ +# Migrating production `@kite` to OpenTag + +This is a CopilotKit-internal runbook for cutting the existing production +`@kite` Slack app over to OpenTag on managed Intelligence Channels. It is not +part of getting OpenTag running — for that, use the +[README quick start](../README.md#quick-start), which creates a fresh Slack app +from a CLI-generated manifest and never touches `@kite`. + +## Constraint + +**Do not create, reinstall, or replace the production Slack app.** It owns the +bot user, the workspace installation, and the `@kite` handle. Losing any of the +three is user-visible and not cheaply reversible. + +That constraint is in tension with one fact about the managed path, and the +tension is the whole difficulty of this cutover: Slack must POST events to an +Intelligence-hosted Request URL, and `@kite` was built for Socket Mode. Socket +Mode and a Request URL are mutually exclusive delivery modes. So the app's event +delivery has to change even though the app itself must not be recreated. + +Two consequences follow, and both need confirming against the live app before +anyone touches it: + +- Slack applies manifest scope changes only on **Reinstall to Workspace**. If the + cutover needs no scope change, no reinstall is needed. If it does, reinstalling + is unavoidable and its blast radius on the existing installation must be + understood first. +- The **signing secret is reissued on reinstall**. Any reinstall invalidates the + secret stored in Intelligence, so treat a post-reinstall auth failure as a + stale stored secret before suspecting a missing scope. + +## Credentials + +The managed Slack adapter takes exactly two values: + +| Field | Where | +| --- | --- | +| `channelToken` | **Bot User OAuth Token** (`xoxb-`), from **OAuth & Permissions** | +| `signingSecret` | **Signing Secret**, from **Basic Information → App Credentials** | + +There is no app-level `xapp-` token anywhere on this path. If you are looking for +a field to paste one into, you are on the Socket Mode path, which is the thing +being migrated away from. Earlier revisions of this runbook asked for `xapp-`; +that was wrong. + +Never put either value in source control or in chat. + +## Sequence + +Drive the Slack-side changes from the CLI's emitted next actions rather than from +remembered steps. Run `channels add` against the existing app's Channel and read +`nextAction.instructions`, `nextAction.caveats`, `nextAction.requiredEnvVars`, +and `nextAction.resumeCommand`: + +```bash +npx --yes copilotkit@latest channels add --name open-tag --display-name "OpenTag" --adapter slack --json +``` + +Then: + +1. **Stop the old Kite Socket Mode runtime**, so there is exactly one consumer. + Managed delivery is claim-based: two runtimes on the same Channel name in the + same project race per delivery and the loser silently receives nothing. The + tell is a Slack reply nobody's terminal knows anything about. +2. **Reconfigure the existing app's event delivery** per the CLI's emitted + instructions — Socket Mode off, the Intelligence Request URL set, and + interactivity enabled so human-in-the-loop buttons fire. +3. **Attach the existing `xoxb-` token and signing secret** in Intelligence, + entered directly into the Slack attachment. +4. **Start the OpenTag `runtime` service** with `INTELLIGENCE_CHANNEL_NAME` set + to the Channel name in `.copilotkit/channels.json`. +5. **Verify.** See below. + +## Acceptance + +Production acceptance is one end-to-end `@kite` mention that returns the OpenTag +persona through the Python agent. Add the two trigger checks that catch the +regressions this repo has actually shipped — an unmentioned follow-up in that +subscribed thread should reply, and an unmentioned message in a fresh +conversation should stay silent. + +Run diagnostics before concluding anything: + +```bash +npx --yes copilotkit@latest channels status --json +``` + +```bash +LOG_LEVEL=debug pnpm runtime +``` + +The runtime logger defaults to `error` while every Channel lifecycle breadcrumb +is emitted at `warn`. The line `channel "" requires setup` is the single +highest-value diagnostic here, and at the default level it is written and +discarded. + +### Signals that are not health + +Several things look like success and are not: + +- **`ready()` resolving.** It resolves on `setup_required` too, which is a valid + degraded state, not a failure. Only `controls.status()` distinguishes them. +- **`/api/copilotkit/info` returning 200.** It reports license and runtime state + and says nothing about Slack. +- **The dashboard's Agent run column reading `—`**, Overview showing + `AGENT: Not declared`, and an `…:activation` pseudo-thread. None of those + indicate a failed turn; the activation thread only means the runtime activated. + +The tab that proves a real round trip is **Usage**: `Completed turns` plus a +non-zero `Outbound`. + +## Rollback + +If the cutover fails, stop the new Channel **before** restoring the prior Railway +deployment, so the old Socket Mode consumer does not come back up alongside a +live managed Channel. + +## Railway + +The repository configuration does not mutate the existing production Railway +project. The live migration reuses the existing Kite `runtime` service, adds the +`agent` service, connects both to `CopilotKit/OpenTag` on `main`, and enables +GitHub autodeploys. Inventory and cutover happen after Railway authentication. + +See [`setup.md`](../setup.md#railway) for the service and variable contract. diff --git a/setup.md b/setup.md index d9c5116d..353203b9 100644 --- a/setup.md +++ b/setup.md @@ -1,10 +1,13 @@ -# OpenTag setup +# OpenTag reference -This guide covers the canonical OpenTag deployment: one Python agent service -and one Node CopilotRuntime service with Channels embedded. +The five-minute path to a working OpenTag is in the +[README quick start](./README.md#quick-start). This file is the reference behind +it: components, the full environment contract, Channel commands, optional +sources, Railway, and tests. -Slack and Microsoft Teams are supported for this launch. Discord, Telegram, -and WhatsApp are coming soon. +The canonical deployment is one Python agent service and one Node +CopilotRuntime service with Channels embedded. Slack and Microsoft Teams are +supported; Discord, Telegram, and WhatsApp are coming soon. ## Components @@ -14,12 +17,14 @@ and WhatsApp are coming soon. | Application composition | [`app/index.ts`](./app/index.ts) | SDK agent factory, managed Channel, and runtime | | Channel definition | [`app/channel.tsx`](./app/channel.tsx) | Mentions, commands, components, modals, and interrupts | | Intelligence runtime | [`app/runtime-host.ts`](./app/runtime-host.ts) | One `CopilotKitIntelligence` and one `CopilotRuntime` | +| Environment contract | [`app/env.ts`](./app/env.ts) | Required variables and in-code defaults | | Python agent | [`agent/`](./agent) | LangGraph deep agent served over AG-UI | | Railway topology | [`.railway/railway.ts`](./.railway/railway.ts) | Two services sourced from OpenTag `main` | The host always uses the Intelligence-owned runtime. It declares one -adapter-free Channel using the configured name. Its Slack and Microsoft Teams -adapters, credentials, and attachments are configured only in Intelligence. +adapter-free Channel using the configured name. The Slack and Microsoft Teams +adapters, their credentials, and attachments are configured only in +Intelligence — never here. ## Install @@ -32,8 +37,6 @@ Prerequisites: - A CopilotKit Intelligence project, Channel, and runtime API key - An OpenAI API key for the Python agent -Install both dependency sets: - ```bash pnpm install --frozen-lockfile cd agent @@ -41,19 +44,23 @@ uv sync cd .. ``` -The Channels and Runtime packages are intentionally pinned: - -```text -@copilotkit/channels 0.7.0 -@copilotkit/runtime 1.66.0 -``` +`@copilotkit/channels` and `@copilotkit/runtime` are intentionally pinned. +[`package.json`](./package.json) is the single source of truth for both +versions; this file does not restate them, because a hand-copied pin drifts on +the next bump. -## Configure the environment +## Environment contract ```bash cp .env.example .env ``` +One root `.env` configures both services. The Python agent loads it explicitly +for local development; Railway supplies the same values as service variables +without a checked-in file. + +### Agent + | Variable | Required | Purpose | | --- | --- | --- | | `OPENAI_API_KEY` | Yes | Model access | @@ -73,13 +80,11 @@ cp .env.example .env | `SERVER_PORT` / `PORT` | No | Local port; defaults to `8123` | Only `OPENAI_API_KEY` is required. Without Tavily or internal-source -credentials, the agent still chats, triages, and renders supported UI -components. Planning and virtual files remain available for explicitly -substantial work. The Python agent explicitly loads this root `.env` for local -development; Railway service variables work normally without a checked-in -environment file. +credentials the agent still chats, triages, and renders supported UI +components; planning and virtual files remain available for explicitly +substantial work. -Run it: +Run it alone: ```bash pnpm agent @@ -88,28 +93,29 @@ pnpm agent The AG-UI endpoint is `http://localhost:8123/`; `/health` reports the `opentag-agent` service. -## Configure Intelligence - -In [CopilotKit Intelligence](https://intelligence.copilotkit.ai): - -1. Create or select the OpenTag project. -2. Create one Channel named `open-tag`. -3. Issue a runtime API key. -4. Configure the Slack and Microsoft Teams adapters on that Channel. +### Runtime | Variable | Required | Purpose | | --- | --- | --- | | `AGENT_URL` | Yes | Python AG-UI endpoint, locally `http://localhost:8123/` | -| `INTELLIGENCE_API_KEY` | Yes | Runtime authentication | +| `INTELLIGENCE_API_KEY` | Yes | Runtime authentication; also selects the project | +| `INTELLIGENCE_CHANNEL_NAME` | No | Defaults to `open-tag`; must match the Channel name exactly | | `INTELLIGENCE_API_URL` | No | Defaults to `https://api.intelligence.copilotkit.ai` | | `INTELLIGENCE_GATEWAY_WS_URL` | No | Defaults to `wss://realtime.intelligence.copilotkit.ai` | -| `INTELLIGENCE_CHANNEL_NAME` | No | Defaults to `open-tag`; Railway uses `open-tag` | | `AGENT_AUTH_HEADER` | No | Authorization header forwarded to the agent | | `PORT` | No | Channel HTTP port; defaults to `3000` | +| `LOG_LEVEL` | No | Defaults to `error`; use `debug` to see Channel lifecycle breadcrumbs | +The API key selects a project; the Channel name selects a Channel inside it. Legacy organization, project, Channel ID, and runtime-instance ID variables are -not used. Slack and Teams credentials also do not belong in this environment; -Intelligence owns them. +not used. Slack and Teams credentials do not belong here — Intelligence owns +them. + +Both Intelligence URLs are defaulted in [`app/env.ts`](./app/env.ts) rather than +in `.env`. That is deliberate, and it is why `copilotkit channels status` +reports them as unset. A genuinely missing `INTELLIGENCE_GATEWAY_WS_URL` does +not error: the realtime plane is a different host from the API plane and is not +derived from it, so `channels.ready()` simply hangs until it times out. Start the runtime: @@ -117,44 +123,79 @@ Start the runtime: pnpm runtime ``` -Use `pnpm dev` for watch mode. `pnpm start` and `pnpm runtime` both run the -same canonical entrypoint. Startup waits for `listener.channels.ready()` before -opening HTTP. SIGINT and SIGTERM stop Channels, HTTP, and the rendering browser -once, even if shutdown is requested more than once. - -## Slack - -### Existing production `@kite` - -Do not create, reinstall, or replace the current production Slack app. The -existing app owns the bot user, workspace installation, and `@kite` handle. - -For cutover: - -1. Stop the old Kite Socket Mode runtime so there is only one consumer. -2. Enter the existing `xapp` and `xoxb` tokens directly into the Slack - attachment in Intelligence. Do not put them in source control or chat. -3. Start the OpenTag `runtime` service with - `INTELLIGENCE_CHANNEL_NAME=open-tag`. -4. Send one `@kite` mention and verify the reply uses the OpenTag persona and - Python deep agent. - -If the cutover fails, stop the new Channel before restoring the prior Railway -deployment. - -### New Slack installations - -The JSON and YAML Slack manifests in this repository describe OpenTag for -future installations. Create a new Slack app from either manifest, install it -to the workspace, and attach its app-level and bot tokens in Intelligence. - -Those manifests are not a production migration step for `@kite`. - -## Microsoft Teams - -Managed Microsoft Teams is supported. Configure the Teams adapter on the same -`open-tag` Channel in Intelligence. The same Node process and runtime host both -platforms; there is no direct adapter or Railway platform credential. +`pnpm start` and `pnpm runtime` run the same canonical entrypoint; `pnpm dev` +adds watch mode for both services. Startup waits for +`listener.channels.ready()` before opening HTTP. SIGINT and SIGTERM stop +Channels, HTTP, and the rendering browser exactly once, even if shutdown is +requested more than once. + +Note that `ready()` resolving is not proof of health. It also resolves on +`setup_required`, which is a valid degraded state rather than a failure. Only +`controls.status()` → `{ overall, channels }` distinguishes them, and +`/api/copilotkit/info` returning 200 reports license and runtime state while +saying nothing at all about Slack. + +## Channel reference + +The Channel is created and reconciled with the public CopilotKit CLI. These +commands configure **managed Intelligence Channels**; they do not configure the +open-source `@copilotkit/channels` adapter packages, which are a separate +product sharing the words "channels" and "Slack". + +| Command | Purpose | +| --- | --- | +| `copilotkit project select` | Select or create the hosted Intelligence project | +| `copilotkit channels add [name]` | Declare a Channel, reconcile it, and report the next step | +| `copilotkit channels status` | Compare your configuration, your code, and the server | +| `copilotkit channels list` | List Channels and their attachment state | +| `copilotkit channels rotate ` | Replace stored provider credentials | +| `copilotkit channels providers` | List providers and the credentials each asks for | +| `copilotkit channels setup` | Install the `channels-setup` skill and hand the flow to your coding agent | +| `copilotkit skills onboard --channels` | The same prompt, but `--agent` narrows which agents it installs to | + +No flag accepts a credential value. Credentials are read from `.env`, from a +named variable via `--credential-env =`, or from a JSON document on +stdin via `--credentials-stdin` for CI and secret managers. `--json` implies +non-interactive: it never prompts and never opens a browser. + +`channels add` writes `.copilotkit/channels.json`. Keep that file tracked; keep +`.env` and `.copilotkit/artifacts/` ignored. + +The `channels-setup` skill installed by `channels setup` is a pointer, not a copy +of the steps: it fetches its workflow from + at run time so it cannot go stale +against the CLI. That workflow assumes a project starting from nothing, so it +includes phases for building the agent and writing the Channel runtime — OpenTag +has both already. Its Slack handoff never asks anyone to paste a secret into chat. + +### Credentials each provider asks for + +| Provider | Fields | +| --- | --- | +| `slack` | `channelToken` — Bot User OAuth Token (`xoxb-`), from **OAuth & Permissions**; `signingSecret`, from **Basic Information → App Credentials** | +| `teams` | `clientId` and `tenantId`, from the Entra app registration **Overview**; `clientSecret` — the secret **Value**, not the Secret ID | + +There is no app-level `xapp-` token on the managed path. Slack reaches +Intelligence over HTTPS at an Intelligence-hosted Request URL, authenticated by +the signing secret Intelligence holds, and Intelligence reaches your runtime +over a websocket your process opens outbound. Nothing here uses Socket Mode, and +a Slack app configured for Socket Mode installs green and delivers nothing. + +`copilotkit channels add --adapter teams --provision` can create the +provider-side Teams app for you. Two Teams gates stay user-owned regardless: +granting tenant admin consent, and uploading the app package through **Apps → +Manage your apps → Upload an app**. + +### Channel names claim deliveries + +Managed delivery is claim-based. Two runtimes declaring the same Channel name in +the same project race per delivery, and the loser silently receives nothing — +the tell is a Slack reply your terminal knows nothing about. Give a local or +forked runtime its own project, key, and Channel name rather than reusing +`open-tag`. + +The name is a slug: lowercase, digits, single hyphens. It must match +`INTELLIGENCE_CHANNEL_NAME` character for character. ## Tools, commands, and UI @@ -169,47 +210,62 @@ The Channel also forwards sender context, Slack-specific tools on Slack turns, file content, and rich issue/page/table/native-Slack-chart/diagram/status/ incident/link components. +Trigger routing is not symmetric. A mentioned turn goes to `onMention` if +registered and falls back to `onMessage` otherwise; an unmentioned turn reaches +`onMessage` only. `onMention` subscribes the thread, which is what lets +unmentioned follow-ups in that thread run the agent. Always verify with a +channel mention first. + +Mentions, messages, and button and select clicks are the proven managed-path +triggers — interactivity is enabled deliberately, which is what makes +human-in-the-loop fire. **Slash commands and modals are registered in code but +their managed-path delivery depends on the Channel's generated Slack manifest +declaring them.** As of the last verification against `@copilotkit/channels` +0.7.0 the generated manifest declared no `slash_commands` and `view_submission` +was not handled, so those handlers compiled, started, reported online, and never +fired. Send a real command and submit a real modal before relying on either. + Before a Linear or Notion mutation reaches MCP, a Python interceptor emits `confirm_write`. The Channel posts an approval card, and the button resumes the -graph with the user's decision. The MCP handler runs only after approval. -Reads and UI rendering are never gated. +graph with the user's decision. The MCP handler runs only after approval. Reads +and UI rendering are never gated. ## Optional sources ### Tavily -Set `TAVILY_API_KEY` in the root `.env` to enable live web research. The -`web_search` tool is not registered when the key is absent. +Set `TAVILY_API_KEY` to enable live web research. The `web_search` tool is not +registered when the key is absent. ### GitHub -Set `GITHUB_PERSONAL_ACCESS_TOKEN` in the root `.env` to enable GitHub search. -Use a fine-grained personal access token limited to the repositories and read -permissions the agent needs. OpenTag connects to GitHub's hosted MCP with only -the repository, issue, and pull-request toolsets and requests read-only mode. -Set `GITHUB_MCP_URL` only to override the hosted endpoint, then restart -`pnpm agent` so it discovers the tools. +Set `GITHUB_PERSONAL_ACCESS_TOKEN` to enable GitHub search. Use a fine-grained +personal access token limited to the repositories and read permissions the agent +needs. OpenTag connects to GitHub's hosted MCP with only the repository, issue, +and pull-request toolsets and requests read-only mode. Set `GITHUB_MCP_URL` only +to override the hosted endpoint, then restart `pnpm agent` so it rediscovers the +tools. ### PostHog Create a PostHog personal API key using the **MCP Server** preset, then set -`POSTHOG_PERSONAL_API_KEY` in the root `.env`. OpenTag connects to -`https://mcp.posthog.com/mcp` in token-efficient CLI mode with server-enforced -read-only access. Set `POSTHOG_MCP_URL` only to override the complete endpoint, -including its `mode=cli&readonly=true` safety parameters. Restart `pnpm agent` -after changing either variable. +`POSTHOG_PERSONAL_API_KEY`. OpenTag connects to `https://mcp.posthog.com/mcp` in +token-efficient CLI mode with server-enforced read-only access. Set +`POSTHOG_MCP_URL` only to override the complete endpoint, including its +`mode=cli&readonly=true` safety parameters. Restart `pnpm agent` after changing +either variable. ### Linear -Set `LINEAR_API_KEY` in the root `.env`. OpenTag connects to the hosted Linear -MCP by default. Railway preserves this optional secret on the `agent` service. +Set `LINEAR_API_KEY`. OpenTag connects to the hosted Linear MCP by default. +Railway preserves this optional secret on the `agent` service. ### Notion -Notion is optional and is not a separate Railway service. Configure an existing -remote MCP endpoint by setting both `NOTION_MCP_URL` and -`NOTION_MCP_AUTH_TOKEN`, then restart `pnpm agent` so it discovers the tools. -If either value is absent, OpenTag skips Notion without blocking startup. +Notion is optional and remote-only, not a separate Railway service. Set both +`NOTION_MCP_URL` and `NOTION_MCP_AUTH_TOKEN`, then restart `pnpm agent` so it +discovers the tools. If either value is absent OpenTag skips Notion without +blocking startup. ## Railway @@ -232,15 +288,6 @@ Evaluate the configuration locally without applying it: node node_modules/railway/dist/iac/bin.js ``` -The live Railway migration reuses the existing Kite `runtime` service, adds the -`agent`, connects both to OpenTag, and enables GitHub autodeploys. - -## Coming soon - -Discord, Telegram, and WhatsApp are intentionally not configured for this -launch. Their adapters and setup instructions will be added after launch -support is ready. - ## Tests ```bash @@ -257,5 +304,9 @@ pnpm e2e ``` See [`e2e/README.md`](./e2e/README.md) for its required workspace credentials. -There is no launch-blocking Teams E2E harness; production acceptance is the -single `@kite` round trip described above. +There is no launch-blocking Teams E2E harness. + +## Coming soon + +Discord, Telegram, and WhatsApp are intentionally not configured. Their adapters +and setup instructions will be added once launch support is ready. diff --git a/slack-app-manifest.json b/slack-app-manifest.json deleted file mode 100644 index bebd6088..00000000 --- a/slack-app-manifest.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "display_information": { - "name": "OpenTag", - "description": "OpenTag research and triage assistant for Slack.", - "background_color": "#1a1a1a" - }, - "features": { - "app_home": { - "home_tab_enabled": false, - "messages_tab_enabled": true, - "messages_tab_read_only_enabled": false - }, - "bot_user": { - "display_name": "OpenTag", - "always_online": true - }, - "assistant_view": { - "assistant_description": "OpenTag research and triage assistant.", - "suggested_prompts": [ - { - "title": "Triage my open issues", - "message": "Triage my open issues" - }, - { - "title": "What shipped this week?", - "message": "Summarize what shipped this week" - } - ] - }, - "slash_commands": [ - { - "command": "/agent", - "description": "Talk to the AG-UI agent from any channel", - "usage_hint": "", - "should_escape": false - }, - { - "command": "/triage", - "description": "Triage this thread \u2014 summarize and propose Linear issues", - "usage_hint": "", - "should_escape": false - }, - { - "command": "/preview", - "description": "Privately preview the issue I'd file", - "usage_hint": "", - "should_escape": false - }, - { - "command": "/file-issue", - "description": "Open a form to file a Linear issue", - "usage_hint": "", - "should_escape": false - } - ] - }, - "oauth_config": { - "scopes": { - "user": ["chat:write"], - "bot": [ - "app_mentions:read", - "assistant:write", - "channels:history", - "groups:history", - "im:history", - "mpim:history", - "channels:read", - "groups:read", - "im:read", - "mpim:read", - "users:read", - "users:read.email", - "team:read", - "chat:write", - "chat:write.public", - "chat:write.customize", - "im:write", - "mpim:write", - "files:read", - "files:write", - "channels:join", - "commands" - ] - } - }, - "settings": { - "event_subscriptions": { - "bot_events": [ - "app_mention", - "assistant_thread_started", - "assistant_thread_context_changed", - "message.im", - "message.mpim" - ] - }, - "interactivity": { - "is_enabled": true - }, - "socket_mode_enabled": true, - "org_deploy_enabled": false, - "token_rotation_enabled": false - } -} diff --git a/slack-app-manifest.yaml b/slack-app-manifest.yaml deleted file mode 100644 index 94b38c0a..00000000 --- a/slack-app-manifest.yaml +++ /dev/null @@ -1,88 +0,0 @@ -display_information: - name: OpenTag - description: OpenTag research and triage assistant for Slack. - background_color: "#1a1a1a" -features: - bot_user: - display_name: OpenTag - always_online: true - # App Home: enable the Messages tab so users can DM the bot (and so the tab - # isn't read-only — otherwise Slack shows "Sending messages to this app has - # been turned off"). - app_home: - home_tab_enabled: false - messages_tab_enabled: true - messages_tab_read_only_enabled: false - # Enables the assistant pane ("Agents & AI Apps"). The greeting + chips here - # mirror the suggestions registered when an OpenTag thread starts. - assistant_view: - assistant_description: OpenTag research and triage assistant. - suggested_prompts: - - title: Triage my open issues - message: Triage my open issues - - title: What shipped this week? - message: Summarize what shipped this week - slash_commands: - - command: /agent - description: Talk to the AG-UI agent from any channel - usage_hint: "" - should_escape: false - - command: /triage - description: Triage this thread — summarize and propose Linear issues - usage_hint: "" - should_escape: false - - command: /preview - description: Privately preview the issue I'd file - usage_hint: "" - should_escape: false - - command: /file-issue - description: Open a form to file a Linear issue - usage_hint: "" - should_escape: false -oauth_config: - scopes: - user: - - chat:write # user-token write access for the optional live E2E harness - bot: - # --- Read what the user said --- - - app_mentions:read # see @mentions of the bot - - assistant:write # assistant pane: status, title, suggested prompts - - channels:history # read messages in public channels bot is in - - groups:history # read messages in private channels bot is in - - im:history # read DMs - - mpim:history # read group DMs - - channels:read # list/lookup public channels (names, members) - - groups:read # list/lookup private channels - - im:read # list DMs - - mpim:read # list group DMs - - users:read # resolve user IDs to names for context - - users:read.email # resolve user emails to match Slack users to Linear/Notion - - team:read # workspace info - # --- Write replies and generative UI --- - - chat:write # post and edit messages in channels bot is in - - chat:write.public # post in channels bot isn't a member of - - chat:write.customize # per-message username/icon (handy for branding) - - im:write # open DMs - - mpim:write # open group DMs - # --- Files (for future image/file passing) --- - - files:read - - files:write - # --- Bot can join channels on its own --- - - channels:join - # --- Slash commands --- - - commands -settings: - event_subscriptions: - bot_events: - - app_mention - - assistant_thread_started # user opened the assistant pane - - assistant_thread_context_changed # the channel/context the user is viewing - - message.im # DMs to the bot + assistant-pane messages - - message.mpim # messages in group DMs the bot is in - # Add message.channels/message.groups only when using - # respondTo.threadReplies: "afterBotReply" for legacy plain thread continuation. - interactivity: - is_enabled: true - socket_mode_enabled: true - org_deploy_enabled: false - token_rotation_enabled: false