Skip to content

Commit abc940f

Browse files
committed
feat: fallback on bot token when oauth creds are missing
1 parent 657d88e commit abc940f

3 files changed

Lines changed: 96 additions & 15 deletions

File tree

pydantic-ai/app_oauth.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33

44
from dotenv import load_dotenv
55
from slack_bolt import App
6-
from slack_bolt.oauth.oauth_settings import OAuthSettings
76
from slack_sdk import WebClient
87

98
from agent import get_model
109
from listeners import register_listeners
11-
from oauth import BOT_SCOPES, USER_SCOPES, installation_store, state_store
10+
from oauth import oauth_settings
1211

1312
load_dotenv(dotenv_path=".env", override=False)
1413
get_model() # Fail fast if no AI provider key is configured
@@ -25,14 +24,7 @@
2524
# Allow bot-posted messages (e.g. issue modal submissions with metadata)
2625
# to reach the message handler instead of being silently dropped
2726
ignoring_self_events_enabled=False,
28-
oauth_settings=OAuthSettings(
29-
client_id=os.environ.get("SLACK_CLIENT_ID"),
30-
client_secret=os.environ.get("SLACK_CLIENT_SECRET"),
31-
scopes=BOT_SCOPES,
32-
user_scopes=USER_SCOPES,
33-
installation_store=installation_store,
34-
state_store=state_store,
35-
),
27+
oauth_settings=oauth_settings,
3628
)
3729

3830
register_listeners(app)

pydantic-ai/listeners/views/app_home_builder.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,23 +94,42 @@ def build_app_home_view(
9494
"type": "section",
9595
"text": {
9696
"type": "mrkdwn",
97-
"text": (
98-
"🟢 *Slack MCP Server is connected.*\n"
99-
"Casey has access to search messages, read channels, and more."
100-
),
97+
"text": "🟢 *Slack MCP Server is connected.*",
10198
},
10299
}
103100
)
101+
blocks.append(
102+
{
103+
"type": "context",
104+
"elements": [
105+
{
106+
"type": "mrkdwn",
107+
"text": "Casey has access to search messages, read channels, and more.",
108+
}
109+
],
110+
}
111+
)
104112
elif authorize_url:
105113
blocks.append(
106114
{
107115
"type": "section",
108116
"text": {
109117
"type": "mrkdwn",
110-
"text": f"🔴 *Slack MCP Server is disconnected.* <{authorize_url}|Connect now.>",
118+
"text": f"🔴 *Slack MCP Server is disconnected.* <{authorize_url}|Connect the Slack MCP Server.>",
111119
},
112120
}
113121
)
122+
blocks.append(
123+
{
124+
"type": "context",
125+
"elements": [
126+
{
127+
"type": "mrkdwn",
128+
"text": "The Slack MCP Server enables Casey to search messages, read channels, and more.",
129+
}
130+
],
131+
}
132+
)
114133

115134
return {
116135
"type": "home",

pydantic-ai/oauth.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import json
2+
import logging
23
import os
34
from pathlib import Path
45

6+
from slack_bolt.authorization.authorize_result import AuthorizeResult
7+
from slack_bolt.oauth.oauth_settings import OAuthSettings
8+
from slack_sdk import WebClient
59
from slack_sdk.oauth import AuthorizeUrlGenerator
610
from slack_sdk.oauth.installation_store import FileInstallationStore
711
from slack_sdk.oauth.state_store import FileOAuthStateStore
812

13+
logger = logging.getLogger(__name__)
14+
915
_manifest = json.loads(Path("manifest.json").read_text())
1016
BOT_SCOPES = _manifest["oauth_config"]["scopes"]["bot"]
1117

@@ -36,3 +42,67 @@
3642
scopes=BOT_SCOPES,
3743
user_scopes=USER_SCOPES,
3844
)
45+
46+
oauth_settings = OAuthSettings(
47+
client_id=os.environ.get("SLACK_CLIENT_ID"),
48+
client_secret=os.environ.get("SLACK_CLIENT_SECRET"),
49+
scopes=BOT_SCOPES,
50+
user_scopes=USER_SCOPES,
51+
installation_store=installation_store,
52+
state_store=state_store,
53+
)
54+
55+
# ---------------------------------------------------------------------------
56+
# Bot-token fallback for pre-OAuth first-run experience
57+
# ---------------------------------------------------------------------------
58+
# When installed via Slack CLI, SLACK_BOT_TOKEN is available but Bolt clears
59+
# it when oauth_settings is present. This wrapper lets the bot token serve as
60+
# a fallback so App Home (with the OAuth install URL) and basic bot operations
61+
# work before anyone has completed the OAuth flow.
62+
63+
_fallback_bot_token = os.environ.get("SLACK_BOT_TOKEN")
64+
65+
if _fallback_bot_token:
66+
_original_authorize = oauth_settings.authorize
67+
68+
def _authorize_with_fallback_bot_token(
69+
*,
70+
context,
71+
enterprise_id,
72+
team_id,
73+
user_id,
74+
actor_enterprise_id=None,
75+
actor_team_id=None,
76+
actor_user_id=None,
77+
):
78+
result = _original_authorize(
79+
context=context,
80+
enterprise_id=enterprise_id,
81+
team_id=team_id,
82+
user_id=user_id,
83+
actor_enterprise_id=actor_enterprise_id,
84+
actor_team_id=actor_team_id,
85+
actor_user_id=actor_user_id,
86+
)
87+
if result is not None:
88+
return result
89+
90+
logger.info(
91+
"No OAuth installation found (team=%s); falling back to SLACK_BOT_TOKEN",
92+
team_id,
93+
)
94+
try:
95+
fallback_client = WebClient(
96+
base_url=os.environ.get("SLACK_API_URL", "https://slack.com/api"),
97+
token=_fallback_bot_token,
98+
)
99+
auth_test = fallback_client.auth_test()
100+
return AuthorizeResult.from_auth_test_response(
101+
auth_test_response=auth_test,
102+
bot_token=_fallback_bot_token,
103+
)
104+
except Exception:
105+
logger.exception("Fallback bot token auth.test failed")
106+
return None
107+
108+
oauth_settings.authorize = _authorize_with_fallback_bot_token

0 commit comments

Comments
 (0)