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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export AGENT_URL=http://localhost:8123/ # AG-UI endpoint; the bundled Deep Age
# export OPENAI_VERBOSITY=low

# -- Internal Sources (Optional) --
# A fine-grained PAT with read access enables GitHub code, repo, issue, and PR search.
# export GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_...
# export GITHUB_MCP_URL=https://api.githubcopilot.com/mcp/readonly
# Create a personal API key with PostHog's "MCP Server" preset.
# The bundled connection uses CLI mode and is read-only.
# export POSTHOG_PERSONAL_API_KEY=phx_...
Expand Down
2 changes: 2 additions & 0 deletions .railway/railway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export default defineRailway(() => {
PORT: "8123",
OPENAI_API_KEY: preserve(),
TAVILY_API_KEY: preserve(),
GITHUB_PERSONAL_ACCESS_TOKEN: preserve(),
GITHUB_MCP_URL: preserve(),
POSTHOG_PERSONAL_API_KEY: preserve(),
POSTHOG_MCP_URL: preserve(),
LINEAR_API_KEY: preserve(),
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ runtime (Node + CopilotRuntime with embedded Channels)
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)
Expand Down Expand Up @@ -67,8 +68,9 @@ Prerequisites: Node.js 22+, pnpm, Python 3.12, and
INTELLIGENCE_API_KEY=cpk-...
```

Tavily, PostHog, Linear, and Notion are optional. Both Node and Python load
this root `.env`; Railway supplies the same values as service variables.
Tavily, GitHub, PostHog, Linear, and Notion are optional. Both Node and
Python load this root `.env`; Railway supplies the same values as service
variables.

`INTELLIGENCE_API_URL` and `INTELLIGENCE_GATEWAY_WS_URL` default to the
production Intelligence endpoints. `INTELLIGENCE_CHANNEL_NAME` defaults to
Expand Down Expand Up @@ -140,6 +142,9 @@ Slack app**. Reusing it preserves the bot user, workspace installation, and

- `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.
Expand All @@ -165,8 +170,9 @@ 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, PostHog, Linear, and remote Notion settings are optional. Connecting both
services to `main` enables GitHub-triggered deployments after merges.
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.
Expand Down
17 changes: 15 additions & 2 deletions agent/internal_sources.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Optional PostHog, Linear, and Notion MCP integrations."""
"""Optional GitHub, PostHog, Linear, and Notion MCP integrations."""

import asyncio
import logging
Expand All @@ -13,6 +13,15 @@


MCP_SERVERS = {
"github": {
"token_env": "GITHUB_PERSONAL_ACCESS_TOKEN",
"url_env": "GITHUB_MCP_URL",
"default_url": "https://api.githubcopilot.com/mcp/readonly",
"headers": {
"X-MCP-Readonly": "true",
"X-MCP-Toolsets": "repos,issues,pull_requests",
},
},
"posthog": {
"token_env": "POSTHOG_PERSONAL_API_KEY",
"url_env": "POSTHOG_MCP_URL",
Expand Down Expand Up @@ -82,10 +91,14 @@ def _configured_connections(
)
continue

headers = {
"Authorization": f"Bearer {token}",
**config.get("headers", {}),
}
connections[name] = {
"transport": "streamable_http",
"url": url,
"headers": {"Authorization": f"Bearer {token}"},
"headers": headers,
}
return connections

Expand Down
2 changes: 1 addition & 1 deletion agent/prompts/system.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Core OpenTag persona and workflow."""

SYSTEM_PROMPT = """You are OpenTag, your team's on-call triage assistant for fast
incident support, research, and Linear/Notion workflows.
incident support, research, GitHub search, and Linear/Notion workflows.

Hard rules (ALWAYS follow):
- NEVER output raw JSON, data structures, or code blocks in your messages
Expand Down
4 changes: 3 additions & 1 deletion agent/prompts/tools.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Guidance for internal sources and write tools."""

TOOLS_PROMPT = """- For internal or company-specific questions, prefer the team's Notion/Linear
(internal sources) first; use the web for external questions
and GitHub sources first; use the web for external questions
- Use GitHub tools to search repositories, code, issues, and pull requests. The
GitHub integration is read-only
- CRITICAL: Every Linear or Notion mutation tool automatically pauses with its
exact action and draft details. Call the mutation once; it runs only after
the user grants approval, and otherwise no write occurs
Expand Down
3 changes: 3 additions & 0 deletions agent/tests/test_agent_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def build_with_captured_configuration(monkeypatch):

monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)
monkeypatch.delenv("LINEAR_API_KEY", raising=False)
monkeypatch.delenv("NOTION_MCP_AUTH_TOKEN", raising=False)
Expand Down Expand Up @@ -146,6 +147,7 @@ def _generate(
model = RecordingOpenAIModel()
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)
monkeypatch.delenv("LINEAR_API_KEY", raising=False)
monkeypatch.delenv("NOTION_MCP_AUTH_TOKEN", raising=False)
Expand All @@ -170,6 +172,7 @@ def _configure_minimal_environment(monkeypatch):
monkeypatch.delenv("OPENAI_REASONING_EFFORT", raising=False)
monkeypatch.delenv("OPENAI_VERBOSITY", raising=False)
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)
monkeypatch.delenv("LINEAR_API_KEY", raising=False)
monkeypatch.delenv("NOTION_MCP_AUTH_TOKEN", raising=False)
Expand Down
4 changes: 4 additions & 0 deletions agent/tests/test_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
def test_health_ok(monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)
monkeypatch.delenv("LINEAR_API_KEY", raising=False)
monkeypatch.delenv("NOTION_MCP_AUTH_TOKEN", raising=False)
Expand All @@ -24,6 +25,7 @@ def test_health_ok(monkeypatch):
def test_server_exposes_opentag_metadata(monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)
monkeypatch.delenv("LINEAR_API_KEY", raising=False)
monkeypatch.delenv("NOTION_MCP_AUTH_TOKEN", raising=False)
Expand Down Expand Up @@ -52,6 +54,7 @@ def test_local_agent_port_rejects_invalid_server_port():
def test_build_agent_without_tavily(monkeypatch, capsys):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)
monkeypatch.delenv("LINEAR_API_KEY", raising=False)
monkeypatch.delenv("NOTION_MCP_AUTH_TOKEN", raising=False)
Expand All @@ -64,6 +67,7 @@ def test_build_agent_without_tavily(monkeypatch, capsys):
def test_build_agent_with_tavily(monkeypatch, capsys):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.setenv("TAVILY_API_KEY", "tvly-test")
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)
monkeypatch.delenv("LINEAR_API_KEY", raising=False)
monkeypatch.delenv("NOTION_MCP_AUTH_TOKEN", raising=False)
Expand Down
46 changes: 45 additions & 1 deletion agent/tests/test_internal_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@


@pytest.fixture(autouse=True)
def clear_posthog_credentials(monkeypatch):
def clear_ambient_credentials(monkeypatch):
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)


def test_mcp_servers_are_configured_in_one_place():
assert internal_sources.MCP_SERVERS == {
"github": {
"token_env": "GITHUB_PERSONAL_ACCESS_TOKEN",
"url_env": "GITHUB_MCP_URL",
"default_url": "https://api.githubcopilot.com/mcp/readonly",
"headers": {
"X-MCP-Readonly": "true",
"X-MCP-Toolsets": "repos,issues,pull_requests",
},
},
"posthog": {
"token_env": "POSTHOG_PERSONAL_API_KEY",
"url_env": "POSTHOG_MCP_URL",
Expand All @@ -36,12 +46,46 @@ def test_mcp_servers_are_configured_in_one_place():


def test_internal_source_tools_empty_without_env(monkeypatch):
monkeypatch.delenv("GITHUB_PERSONAL_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("POSTHOG_PERSONAL_API_KEY", raising=False)
monkeypatch.delenv("LINEAR_API_KEY", raising=False)
monkeypatch.delenv("NOTION_MCP_AUTH_TOKEN", raising=False)
assert internal_sources.internal_source_tools() == []


def test_github_uses_hosted_read_only_search_mcp_with_pat():
assert internal_sources._configured_connections(
{"GITHUB_PERSONAL_ACCESS_TOKEN": "github_pat_test"}
) == {
"github": {
"transport": "streamable_http",
"url": "https://api.githubcopilot.com/mcp/readonly",
"headers": {
"Authorization": "Bearer github_pat_test",
"X-MCP-Readonly": "true",
"X-MCP-Toolsets": "repos,issues,pull_requests",
},
}
}


def test_github_url_can_be_overridden_without_disabling_read_only_mode():
assert internal_sources._configured_connections(
{
"GITHUB_PERSONAL_ACCESS_TOKEN": "github_pat_test",
"GITHUB_MCP_URL": "https://github.example.test/mcp",
}
)["github"] == {
"transport": "streamable_http",
"url": "https://github.example.test/mcp",
"headers": {
"Authorization": "Bearer github_pat_test",
"X-MCP-Readonly": "true",
"X-MCP-Toolsets": "repos,issues,pull_requests",
},
}


def test_posthog_uses_hosted_read_only_mcp_with_personal_api_key():
assert internal_sources._configured_connections(
{"POSTHOG_PERSONAL_API_KEY": "phx_test"}
Expand Down
5 changes: 5 additions & 0 deletions agent/tests/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ def test_prompt_uses_triage_first_behavior():
assert "/reports/final_report.md" not in BASE_SYSTEM_PROMPT


def test_prompt_describes_read_only_github_search():
assert "GitHub tools to search repositories" in BASE_SYSTEM_PROMPT
assert "GitHub integration is read-only" in BASE_SYSTEM_PROMPT


def test_prompt_describes_direct_optional_web_search():
assert "web_search(query, max_results=5)" in WEB_SEARCH_TOOL_ADDENDUM
assert "source snippets" in WEB_SEARCH_TOOL_ADDENDUM
Expand Down
2 changes: 2 additions & 0 deletions app/railway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ describe("Railway deployment graph", () => {
expect(agent?.variables).toMatchObject({
OPENAI_API_KEY: { type: "preserve" },
TAVILY_API_KEY: { type: "preserve" },
GITHUB_PERSONAL_ACCESS_TOKEN: { type: "preserve" },
GITHUB_MCP_URL: { type: "preserve" },
POSTHOG_PERSONAL_API_KEY: { type: "preserve" },
POSTHOG_MCP_URL: { type: "preserve" },
LINEAR_API_KEY: { type: "preserve" },
Expand Down
15 changes: 13 additions & 2 deletions setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ cp .env.example .env
| `OPENAI_REASONING_EFFORT` | No | Defaults to `low` |
| `OPENAI_VERBOSITY` | No | Defaults to `low` |
| `TAVILY_API_KEY` | No | Enables live web research |
| `GITHUB_PERSONAL_ACCESS_TOKEN` | No | Enables read-only GitHub repository, code, issue, and PR search |
| `GITHUB_MCP_URL` | No | Overrides the hosted GitHub MCP URL; OpenTag still sends read-only headers |
| `POSTHOG_PERSONAL_API_KEY` | No | Enables the hosted PostHog MCP in read-only CLI mode |
| `POSTHOG_MCP_URL` | No | Overrides the hosted PostHog MCP URL |
| `LINEAR_API_KEY` | No | Enables the hosted Linear MCP |
Expand Down Expand Up @@ -179,6 +181,15 @@ Reads and UI rendering are never gated.
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.

### 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.

### PostHog

Create a PostHog personal API key using the **MCP Server** preset, then set
Expand Down Expand Up @@ -212,8 +223,8 @@ The IaC file declares exactly:
`runtime.AGENT_URL` references the agent's Railway private domain and port.
Production Intelligence URLs are literal configuration, the API key is
preserved, and the Channel name is `open-tag`. `OPENAI_API_KEY` is required on
`agent`; Tavily, PostHog, Linear, and the paired remote Notion variables are
optional preserved settings.
`agent`; Tavily, GitHub, PostHog, Linear, and the paired remote Notion variables
are optional preserved settings.

Evaluate the configuration locally without applying it:

Expand Down