Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 41 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export OPENAI_API_KEY=sk-... # Create a key at https://platform.ope

# -- Runtime Overrides (Optional) --
# export LOG_LEVEL=debug # Defaults to "error"; Channel lifecycle breadcrumbs log at "warn".
# export AGENT_AUTH_HEADER="Bearer ..." # Forwarded to an agent that requires authentication.
# export INTELLIGENCE_API_URL=https://api.intelligence.copilotkit.ai
# export INTELLIGENCE_GATEWAY_WS_URL=wss://realtime.intelligence.copilotkit.ai
# export INTELLIGENCE_LEARNING_CONTAINER_ID=support-quality # Existing container in the API key's project.
Expand Down Expand Up @@ -61,3 +60,44 @@ export OPENAI_API_KEY=sk-... # Create a key at https://platform.ope
# -- Notion (Optional) --
# export NOTION_MCP_URL=https://your-notion-mcp.example.com/mcp
# export NOTION_MCP_AUTH_TOKEN=your-remote-mcp-bearer-token

# -- Composio (Optional) --
# Connect any Composio toolkit without writing an MCP block. Three steps per
# app: add the toolkit at https://app.composio.dev, name its slug below, and
# restart the agent. A shared toolkit is also connected once, see setup.md.
# Slugs are Composio's own — lowercase, unspaced: `googlecalendar`, not `gcal`.
# COMPOSIO_API_KEY is the master switch; without it nothing is constructed.
# export COMPOSIO_API_KEY=ak_...
#
# One shared identity everyone in Slack reaches. Connect each of these once with
# cd agent && uv run python -m composio_tools.connect_cli <toolkit>
# Do not also configure the same app over MCP: `linear` here plus LINEAR_API_KEY
# above gives the agent two sets of Linear tools and startup says so.
# export COMPOSIO_TOOLKITS=jira,salesforce
#
# Each person's own account. They connect it themselves from a Slack thread:
# the agent posts a Connect card, and whoever clicks gets their own private link.
# Two requirements: a Slack-backed Channel (Teams has no private message, so the
# link cannot be delivered there) and a non-empty AGENT_AUTH_HEADER below on both
# services — empty reads as unconfigured and every mint is refused.
# export COMPOSIO_USER_TOOLKITS=gmail,googlecalendar
#
# on (default) | off — whether a call that is not a read waits for a person.
# `destructive` and `writes` are the old spellings; both still parse as `on`.
# export COMPOSIO_APPROVALS=on
#
# The Composio user_id shared toolkits act as. Defaults to the agent's own
# INTELLIGENCE_CHANNEL_NAME, and to "open-tag" when that is unset there.
# export COMPOSIO_WORKSPACE_USER_ID=open-tag
#
# Pins which auth config a toolkit connects against when it has several.
# Ids are case-sensitive. Unset, Composio picks one from the project.
# export COMPOSIO_AUTH_CONFIGS=jira:ac_ExAmPle1

# -- Agent authentication (Optional; required to connect personal accounts) --
# One shared secret, the same non-empty value on both services: the runtime sends
# it and the agent checks it. Unset or empty, the connect endpoint refuses to mint
# a link, since that link is a bearer capability. Ordinary agent traffic is only
# checked once this holds a value, so leaving it out changes nothing else.
# Quote it — the value contains a space.
# export AGENT_AUTH_HEADER="Bearer generate-a-long-random-string"
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
- run: pnpm test
- name: Install AWS deployment dependencies
run: pnpm --dir deployment/aws install --frozen-lockfile
- name: Typecheck AWS deployment
run: pnpm --dir deployment/aws build
- name: Test AWS deployment
run: pnpm --dir deployment/aws test
- name: Validate Railway graph
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ state.db-wal
# AWS CDK synthesis artifacts
deployment/aws/cdk.out
deployment/aws/cdk.context.json

# Composio working documents — design, plan, and the agent-port plan. Kept
# local for the same reason 0577c63 removed docs/superpowers specs and plans.
docs/composio-tools-design.md
docs/composio-tools-plan.md
docs/composio-agent-port-plan.md
18 changes: 18 additions & 0 deletions .railway/railway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ export default defineRailway(() => {
LINEAR_API_KEY: preserve(),
NOTION_MCP_URL: preserve(),
NOTION_MCP_AUTH_TOKEN: preserve(),
// Composio is read by the agent, which is where the toolkits live. The
// runtime carries only the shared secret it presents when asking for a
// connect link.
COMPOSIO_API_KEY: preserve(),
COMPOSIO_TOOLKITS: preserve(),
COMPOSIO_USER_TOOLKITS: preserve(),
COMPOSIO_APPROVALS: preserve(),
COMPOSIO_WORKSPACE_USER_ID: preserve(),
COMPOSIO_AUTH_CONFIGS: preserve(),
// The agent side of the shared secret the runtime presents; see the
// runtime's copy below. Both services have to hold the same value or
// every request the runtime makes comes back 401.
AGENT_AUTH_HEADER: preserve(),
// Read by the agent as the default Composio workspace user id, and by the
// runtime as the Channel to attach to. Both, and the same value.
INTELLIGENCE_CHANNEL_NAME: "open-tag",
},
});

Expand Down Expand Up @@ -71,6 +87,8 @@ export default defineRailway(() => {
"wss://realtime.intelligence.copilotkit.ai",
INTELLIGENCE_LEARNING_CONTAINER_ID: preserve(),
INTELLIGENCE_CHANNEL_NAME: "open-tag",
// The runtime side of the pair the agent declares above.
AGENT_AUTH_HEADER: preserve(),
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",
Expand Down
38 changes: 29 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ files.
| Agent | `agent/agent.py` | LangGraph deep agent served over AG-UI |
| AG-UI adapter | `agent/agui.py` | Slack recursion limit and user-facing graph-stop handling |
| Persona | `agent/prompts/` | `system.py` is the base system prompt |
| Approval gate | `agent/write_confirmation.py` | Emits `confirm_write` before Linear or Notion writes |
| Approval gate | `agent/write_confirmation.py` | Emits `confirm_write` before a Linear, Notion, or Composio write |
| Composio | `agent/composio_tools/` | Toolkit sessions, per-person identity, effect classification, connect links |
| Coder | `agent/coding/` | GitHub credentials, Daytona sandbox, repository publish tools, coder prompt |
| Coder skills | `agent/coding/skills/` | Committed skills. Do not put them in `agent/skills/` |
| Deployment | `.railway/railway.ts` | Two services, declared as code |
Expand Down Expand Up @@ -80,10 +81,21 @@ you actually ran; do not claim a check that did not run.
- **Channel names claim deliveries.** Two runtimes declaring the same name in one
Intelligence project race per delivery and the loser is silently starved. Give a
local runtime its own project, key, and Channel name — never reuse `open-tag`.
- **Socket Mode stays off on the Slack app.** With it on, Slack delivers events
over the socket and stops posting them to the Request URL, so Intelligence —
and therefore your runtime — receives nothing while the app still reads as
installed. Do not turn it on to "test locally"; there is nothing here that
needs it.
- **Personal Composio toolkits are Slack-only, and need a non-empty
`AGENT_AUTH_HEADER` on both services.** The speaker arrives as
`forwardedProps.channelActor`, and `agent/composio_tools/state.py` is the one
place that decides what counts as an identity — `agent/agui.py` applies it to
every run. `agent/agent_auth.py` treats an empty secret as unconfigured and
refuses to mint a connect link on that basis.
- **Slash commands and modals are registered but unverified on the managed
path.** Delivery depends on the generated Slack manifest declaring
`slash_commands`; as of the 0.7.0 verification it declared none. Do not describe
them as working without sending a real command.
path.** Delivery depends on the generated Slack manifest, which Intelligence
produces server-side — nothing in this repository decides it. Do not describe
them as working without sending a real command against your own Channel.
- **Trigger routing is not symmetric.** A mentioned turn goes to `onMention` if
registered and falls back to `onMessage`; an unmentioned turn reaches
`onMessage` only. `onMention` subscribes the thread. Always verify with a
Expand All @@ -94,12 +106,20 @@ you actually ran; do not claim a check that did not run.

- **Pinned SDK versions live in `package.json` and nowhere else.** Do not restate
`@copilotkit/channels` or `@copilotkit/runtime` versions in prose or in a test
assertion. Three copies of `0.7.0` drifted at once when the deps were bumped,
and one of them broke the build. `app/cleanup.test.ts` asserts the pin *shape*
assertion. Three copies of an earlier pin drifted at once when the deps were
bumped, and one broke the build. `app/cleanup.test.ts` asserts the pin *shape*
for this reason.
- **No Slack or Teams credential belongs in this repository.** Intelligence owns
the adapters. One root `.env` configures both services; the Python agent loads
it explicitly for local development.
- **No platform credential belongs here.** Intelligence owns the adapters, so no
Slack or Teams token, signing secret, or app token goes in this repository and
this app attaches no adapter of its own. It briefly held `SLACK_BOT_TOKEN` and
`SLACK_APP_TOKEN` so a Composio connect link could reach one person privately;
the managed adapter in the pair pinned in `package.json` does that itself, and
the pair was worse than useless — a second Slack ingress answered every message
twice, and its Socket Mode connection stopped Slack delivering events to
Intelligence from 24 August until it was turned off. Both tokens and the
adapter are gone from this repository; `app/server.test.ts` pins the removal.
One root `.env` configures both services; the Python agent loads it explicitly
for local development.
- **`@copilotkit/channels` and `@copilotkit/runtime` upgrade together.** They ship
as a tested pair.
- Commit messages follow the conventional prefixes already in the log (`feat:`,
Expand Down
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,16 @@ 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
#### Four 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:

- **Leave Socket Mode off.** Managed delivery never uses it, and a Slack app with
Socket Mode on installs green and delivers nothing to the Request URL — so
nothing reaches Intelligence and nothing reaches your runtime. It is the one
failure here that looks exactly like success. See
[`setup.md`](./setup.md#leave-socket-mode-off).
- 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.
Expand Down Expand Up @@ -320,7 +325,8 @@ agent (Python + LangGraph deepagents)
├── GitHub MCP (optional, read-only)
├── PostHog MCP (optional, read-only)
├── Linear MCP (optional)
└── Notion MCP (optional remote server)
├── Notion MCP (optional remote server)
└── Composio toolkits (optional; shared or per-person accounts)
```

| You run | CopilotKit Intelligence manages |
Expand All @@ -329,16 +335,27 @@ agent (Python + LangGraph deepagents)
| The long-running Node Channels runtime | Platform ingress and credentialed delivery |
| Deployment, state, and logs | Runtime registration, health, and reconnects |

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`.
Neither of those legs 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`.

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.
`CopilotRuntime`, and one adapter-free managed Channel.
Intelligence owns the Slack and Microsoft Teams adapters, their credentials, and
attachments.

No platform credential belongs here at all. Composio's per-person toolkits need
a connect link to reach one person privately, and the managed adapter delivers
that itself with the SDK pair pinned in [`package.json`](./package.json) —
proven end to end on Slack.
Teams has no private message, so a Teams-backed Channel cannot deliver a connect
link and says so rather than posting one in the thread. What per-person toolkits
do need is `AGENT_AUTH_HEADER`, a shared secret between the two services, set to
a **non-empty** value: the agent treats an empty string as unconfigured and
refuses to mint. Leave it unset and nothing else changes.
See [`setup.md`](./setup.md#composio).

`@copilotkit/channels` and `@copilotkit/runtime` are pinned for reproducible
deploys. [`package.json`](./package.json) is the source of truth for both
Expand All @@ -361,6 +378,7 @@ knowledge work, and renders UI from model knowledge.
| `GITHUB_PERSONAL_ACCESS_TOKEN` | Read-only repository, code, PR, and CI search |
| `POSTHOG_PERSONAL_API_KEY` | PostHog analytics, read-only (use the **MCP Server** key preset) |
| `LINEAR_API_KEY` | Hosted Linear MCP |
| `COMPOSIO_API_KEY` | Composio toolkits, under one shared team account or under each person's own (per-person accounts are Slack-only and need `AGENT_AUTH_HEADER`; see setup.md) |
| `NOTION_MCP_URL` + `NOTION_MCP_AUTH_TOKEN` | Remote Notion MCP; setting only one disables it |
| `DAYTONA_API_KEY` + a PAT or GitHub App | Coding subagent: edit in Daytona, then push and publish a draft PR after `confirm_write` |

Expand Down
2 changes: 2 additions & 0 deletions agent/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ __pycache__/
*.pyc
.env
/reports/
*.egg-info/
dist/
Loading