zjcode 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,199 @@
1
+ """UI-agnostic interaction interface for MCP OAuth login.
2
+
3
+ The OAuth login flow needs to ask the user a few things during the
4
+ handshake — open or display the authorize URL, accept a pasted callback
5
+ URL when the provider has no loopback redirect, show RFC 8628 device-code
6
+ instructions, and report success or failure. The CLI uses `print` and
7
+ `input`; a TUI surface needs in-app widgets instead. `OAuthInteraction` is
8
+ the small Protocol both implementations satisfy, and `CliOAuthInteraction`
9
+ is the existing CLI behavior preserved as one implementation of that
10
+ interface.
11
+
12
+ Important: implementations must never embed access or refresh tokens
13
+ in user-facing messages. The interaction surface only ever sees
14
+ authorize URLs, callback URLs, device codes, and short status strings,
15
+ so leaks come from misuse, not from this interface's shape.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Protocol
21
+
22
+
23
+ class OAuthInteraction(Protocol):
24
+ """User-facing OAuth interaction surface shared by CLI and TUI."""
25
+
26
+ async def show_authorize_url(self, url: str, *, opened_in_browser: bool) -> None:
27
+ """Tell the user about the authorize URL.
28
+
29
+ Args:
30
+ url: Final authorize URL with provider-specific extras applied.
31
+ opened_in_browser: `True` when the caller already launched the
32
+ URL via `webbrowser.open`; `False` when the caller needs the
33
+ user to open it manually.
34
+ """
35
+ ...
36
+
37
+ async def request_callback_url(self) -> str:
38
+ """Wait for the user to paste back the full provider callback URL.
39
+
40
+ Returns:
41
+ The raw pasted URL (the caller parses `code`/`state`/`error`).
42
+
43
+ Raises:
44
+ RuntimeError: When the user interaction cannot complete (for
45
+ example, the input surface is unavailable or was dismissed).
46
+ """
47
+ ...
48
+
49
+ async def show_device_code(
50
+ self,
51
+ *,
52
+ verification_uri: str,
53
+ user_code: str,
54
+ expires_in: int,
55
+ ) -> None:
56
+ """Show RFC 8628 device-code instructions to the user.
57
+
58
+ Args:
59
+ verification_uri: Provider URL the user visits in a browser.
60
+ user_code: Short code the user enters on `verification_uri`.
61
+ expires_in: Lifetime of the device code in seconds.
62
+ """
63
+ ...
64
+
65
+ async def show_success(self, message: str) -> None:
66
+ """Report a successful login step.
67
+
68
+ Implementations must not embed token material in `message`; the
69
+ login code only passes structural facts ("logged in", token file
70
+ path) here.
71
+
72
+ Args:
73
+ message: Plain-text status line.
74
+ """
75
+ ...
76
+
77
+ async def show_notice(self, message: str) -> None:
78
+ """Report a non-fatal progress notice (e.g. fallback path taken).
79
+
80
+ Args:
81
+ message: Plain-text notice.
82
+ """
83
+ ...
84
+
85
+ async def show_error(self, message: str) -> None:
86
+ """Report a fatal (flow-ending) error.
87
+
88
+ "Fatal" rather than "terminal" because this is a TUI codebase
89
+ where "terminal" reads ambiguously.
90
+
91
+ Args:
92
+ message: Plain-text error description.
93
+ """
94
+ ...
95
+
96
+
97
+ class CliOAuthInteraction:
98
+ """Default `OAuthInteraction` that drives the flow via stdin/stdout.
99
+
100
+ Preserves the previous `dcode mcp login` behavior — paste-back input,
101
+ plain-text prompts, success messages printed to stdout.
102
+ """
103
+
104
+ async def show_authorize_url( # noqa: PLR6301
105
+ self,
106
+ url: str,
107
+ *,
108
+ opened_in_browser: bool,
109
+ ) -> None:
110
+ """Print the full authorize instruction block to stdout.
111
+
112
+ Uses browser-opened wording when `opened_in_browser` is `True`;
113
+ otherwise instructs the user to open the URL and paste back the callback.
114
+ """
115
+ if opened_in_browser:
116
+ print( # noqa: T201
117
+ "\nOpened your browser to approve MCP access. "
118
+ "If it did not open, visit this URL:\n"
119
+ f"\n {url}\n",
120
+ )
121
+ else:
122
+ print( # noqa: T201
123
+ "\nOpen this URL in a browser, approve access, then paste the full "
124
+ "callback URL back here:\n"
125
+ f"\n {url}\n",
126
+ )
127
+
128
+ async def request_callback_url(self) -> str: # noqa: PLR6301
129
+ """Read a trimmed callback URL from stdin via a worker thread.
130
+
131
+ Returns:
132
+ The trimmed callback URL string.
133
+
134
+ Raises:
135
+ RuntimeError: If stdin is closed before the user replies.
136
+ """
137
+ import asyncio
138
+
139
+ try:
140
+ raw = await asyncio.to_thread(input, "Callback URL: ")
141
+ except EOFError as exc:
142
+ msg = (
143
+ "No callback URL received (stdin closed). "
144
+ "Re-run `dcode mcp login <server>` and paste the URL."
145
+ )
146
+ raise RuntimeError(msg) from exc
147
+ return raw.strip()
148
+
149
+ async def show_device_code( # noqa: PLR6301
150
+ self,
151
+ *,
152
+ verification_uri: str,
153
+ user_code: str,
154
+ expires_in: int,
155
+ ) -> None:
156
+ """Print RFC 8628 device-code instructions to stdout."""
157
+ print( # noqa: T201
158
+ f"\nVisit {verification_uri} and enter code: "
159
+ f"{user_code}\n(code expires in {expires_in}s)\n",
160
+ )
161
+
162
+ async def prompt_slack_team_id(self) -> str | None: # noqa: PLR6301
163
+ """Ask for a Slack team ID via `input()` on a worker thread.
164
+
165
+ Returns:
166
+ The entered Slack team ID, or `None` if the prompt was blank or
167
+ stdin was closed.
168
+ """
169
+ import asyncio
170
+
171
+ try:
172
+ raw = await asyncio.to_thread(
173
+ input,
174
+ "Slack team ID to install the app into "
175
+ "(e.g. T01234567 — leave blank to pick on Slack's page): ",
176
+ )
177
+ except EOFError:
178
+ return None
179
+ return raw.strip() or None
180
+
181
+ async def show_success(self, message: str) -> None: # noqa: PLR6301
182
+ """Print success message to stdout."""
183
+ print(message) # noqa: T201
184
+
185
+ async def show_notice(self, message: str) -> None: # noqa: PLR6301
186
+ """Print progress notice to stdout."""
187
+ print(message) # noqa: T201
188
+
189
+ async def show_error(self, message: str) -> None: # noqa: PLR6301
190
+ """Print error message to stderr."""
191
+ import sys
192
+
193
+ print(message, file=sys.stderr) # noqa: T201
194
+
195
+
196
+ __all__ = [
197
+ "CliOAuthInteraction",
198
+ "OAuthInteraction",
199
+ ]
@@ -0,0 +1,23 @@
1
+ """Provider-specific MCP OAuth dispatch.
2
+
3
+ `resolve_provider(url)` returns the registered policy whose `matches`
4
+ predicate fires for `url`, with `GenericProvider` as the fallback.
5
+ """
6
+
7
+ from deepagents_code.mcp_providers._registry import resolve_provider
8
+ from deepagents_code.mcp_providers.base import (
9
+ GenericProvider,
10
+ LoginResult,
11
+ OAuthProvider,
12
+ )
13
+ from deepagents_code.mcp_providers.github import GitHubProvider
14
+ from deepagents_code.mcp_providers.slack import SlackProvider
15
+
16
+ __all__ = [
17
+ "GenericProvider",
18
+ "GitHubProvider",
19
+ "LoginResult",
20
+ "OAuthProvider",
21
+ "SlackProvider",
22
+ "resolve_provider",
23
+ ]
@@ -0,0 +1,39 @@
1
+ """Ordered provider registry for MCP OAuth dispatch.
2
+
3
+ `resolve_provider` walks `_REGISTRY` in order and returns the first
4
+ provider whose `matches(url)` is `True`. `GenericProvider` sits last so
5
+ spec-compliant servers always resolve to a usable policy.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from deepagents_code.mcp_providers.base import GenericProvider, OAuthProvider
11
+ from deepagents_code.mcp_providers.github import GitHubProvider
12
+ from deepagents_code.mcp_providers.slack import SlackProvider
13
+
14
+ _REGISTRY: tuple[OAuthProvider, ...] = (
15
+ SlackProvider(),
16
+ GitHubProvider(),
17
+ GenericProvider(),
18
+ )
19
+ """Ordered provider list; `GenericProvider` must stay last as the fallback."""
20
+
21
+
22
+ def resolve_provider(server_url: str) -> OAuthProvider:
23
+ """Return the provider policy that owns `server_url`.
24
+
25
+ Args:
26
+ server_url: Remote MCP endpoint URL.
27
+
28
+ Returns:
29
+ The first matching `OAuthProvider`; falls back to `GenericProvider`.
30
+
31
+ Raises:
32
+ RuntimeError: If no provider matches (unreachable in practice since
33
+ `GenericProvider.matches` always returns `True`).
34
+ """
35
+ for provider in _REGISTRY:
36
+ if provider.matches(server_url):
37
+ return provider
38
+ msg = f"No MCP OAuth provider matched {server_url!r}"
39
+ raise RuntimeError(msg)
@@ -0,0 +1,133 @@
1
+ """Policy interface for provider-specific MCP OAuth quirks.
2
+
3
+ Each concrete provider module (e.g. `slack`, `github`) subclasses
4
+ `OAuthProvider` to encode its own URL match rule, client metadata, and
5
+ any pre-handshake login steps (preseeding client info, running a
6
+ device flow, prompting for workspace IDs). `mcp_auth` dispatches to
7
+ the first matching provider via `resolve_provider`, so adding a new
8
+ provider is one new module plus one registry entry — no edits to
9
+ `build_oauth_provider` or `login`.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from abc import ABC, abstractmethod
15
+ from dataclasses import dataclass, field
16
+ from typing import TYPE_CHECKING
17
+
18
+ from mcp.shared.auth import AnyUrl, OAuthClientMetadata
19
+
20
+ if TYPE_CHECKING:
21
+ from deepagents_code.mcp_auth import FileTokenStorage
22
+ from deepagents_code.mcp_oauth_ui import OAuthInteraction
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class LoginResult:
27
+ """Outcome of a provider's pre-handshake `run_login` step."""
28
+
29
+ completed: bool = False
30
+ """`True` means tokens are persisted and `login` should skip the handshake."""
31
+
32
+ extra_auth_params: dict[str, str] = field(default_factory=dict)
33
+ """Extra query params to thread into the interactive auth URL."""
34
+
35
+
36
+ class OAuthProvider(ABC):
37
+ """Base class for provider-specific OAuth dispatch.
38
+
39
+ Subclasses override `matches` plus whichever of `client_metadata`
40
+ and `run_login` they customize. The default implementations cover
41
+ the spec-compliant Authorization Code + PKCE + Dynamic Client
42
+ Registration path.
43
+ """
44
+
45
+ @abstractmethod
46
+ def matches(self, server_url: str) -> bool:
47
+ """Return `True` when this provider owns `server_url`."""
48
+
49
+ def supports_loopback_callback(self) -> bool: # noqa: PLR6301 # subclass hook
50
+ """Return whether this provider can use a runtime loopback redirect URI.
51
+
52
+ When `False`, `client_metadata()` ignores the `redirect_uri` argument
53
+ and uses the provider's own pre-registered static URI instead.
54
+
55
+ Returns:
56
+ `True` when the provider accepts dynamically registered redirect URIs.
57
+ """
58
+ return True
59
+
60
+ def loopback_port(self) -> int | None: # noqa: PLR6301 # subclass hook
61
+ """Return a fixed loopback port, or `None` for a random ephemeral port.
62
+
63
+ Override when the provider's app registration requires a specific
64
+ pre-registered port (e.g. Slack registers `http://localhost:3118/callback`).
65
+ Ignored when `supports_loopback_callback()` is `False`.
66
+
67
+ Returns:
68
+ A fixed TCP port, or `None` to pick a random ephemeral port.
69
+ """
70
+ return None
71
+
72
+ def client_metadata( # noqa: PLR6301 # subclass hook
73
+ self,
74
+ *,
75
+ redirect_uri: str | None = None,
76
+ ) -> OAuthClientMetadata:
77
+ """Return the `OAuthClientMetadata` used to build the auth provider.
78
+
79
+ Args:
80
+ redirect_uri: Optional runtime redirect URI for CLI loopback auth.
81
+
82
+ Returns:
83
+ Metadata for the spec-compliant Authorization Code + PKCE +
84
+ Dynamic Client Registration flow.
85
+ """
86
+ return OAuthClientMetadata(
87
+ client_name="deepagents-code",
88
+ redirect_uris=[AnyUrl(redirect_uri or "http://localhost/callback")],
89
+ grant_types=["authorization_code", "refresh_token"],
90
+ response_types=["code"],
91
+ )
92
+
93
+ async def run_login( # noqa: PLR6301 # subclass hook
94
+ self,
95
+ *,
96
+ server_name: str,
97
+ server_url: str,
98
+ storage: FileTokenStorage,
99
+ ui: OAuthInteraction,
100
+ ) -> LoginResult:
101
+ """Perform any provider-specific pre-handshake work.
102
+
103
+ Args:
104
+ server_name: MCP server name from `mcpServers`.
105
+ server_url: Remote MCP endpoint URL.
106
+ storage: File-backed token storage for this server identity.
107
+ ui: Interaction surface used for any provider-specific
108
+ prompts (e.g. device-code instructions, workspace IDs).
109
+
110
+ Returns:
111
+ `LoginResult.completed=True` if the provider finished the
112
+ login itself (e.g. device flow). Otherwise the caller drives
113
+ the standard Authorization Code handshake and passes any
114
+ returned `extra_auth_params` to the redirect URL.
115
+ """
116
+ del server_name, server_url, storage, ui
117
+ return LoginResult()
118
+
119
+
120
+ class GenericProvider(OAuthProvider):
121
+ """Fallback provider for spec-compliant MCP servers with no quirks."""
122
+
123
+ def matches(self, server_url: str) -> bool: # noqa: PLR6301 # subclass hook
124
+ """Match any URL — the registry places this provider last.
125
+
126
+ Args:
127
+ server_url: Remote MCP endpoint URL (unused).
128
+
129
+ Returns:
130
+ Always `True`.
131
+ """
132
+ del server_url
133
+ return True
@@ -0,0 +1,102 @@
1
+ """GitHub-hosted MCP OAuth provider.
2
+
3
+ GitHub's remote MCP at `api.githubcopilot.com` authenticates via RFC
4
+ 8628 Device Authorization Grant — the app runs the device flow,
5
+ persists the resulting token plus a stub client-info record, and skips
6
+ the standard Authorization Code handshake entirely.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING
12
+ from urllib.parse import urlparse
13
+
14
+ from mcp.shared.auth import AnyUrl, OAuthClientInformationFull
15
+
16
+ from deepagents_code.mcp_auth import _run_device_flow
17
+ from deepagents_code.mcp_providers.base import LoginResult, OAuthProvider
18
+
19
+ if TYPE_CHECKING:
20
+ from deepagents_code.mcp_auth import FileTokenStorage
21
+ from deepagents_code.mcp_oauth_ui import OAuthInteraction
22
+
23
+
24
+ _GITHUB_MCP_CLIENT_ID = "Iv23libxz8qOApH0WQL3"
25
+ """Public OAuth client ID for the GitHub App backing GitHub's remote MCP."""
26
+
27
+ _GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code"
28
+ """GitHub Device Authorization Grant endpoint that issues the user/device code pair."""
29
+
30
+ _GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token" # noqa: S105
31
+ """GitHub OAuth token endpoint polled while the user completes the device flow."""
32
+
33
+
34
+ def _is_github_mcp_url(url: str) -> bool:
35
+ """Return `True` when `url` points at GitHub's remote MCP endpoint."""
36
+ return (urlparse(url).hostname or "") == "api.githubcopilot.com"
37
+
38
+
39
+ async def _preseed_github_auth(
40
+ storage: FileTokenStorage, *, ui: OAuthInteraction
41
+ ) -> None:
42
+ """Run GitHub Device Flow and persist the token and stub client info.
43
+
44
+ Args:
45
+ storage: File-backed token storage for this server identity.
46
+ ui: Interaction surface that renders the device-code prompt.
47
+ """
48
+ token = await _run_device_flow(
49
+ device_code_url=_GITHUB_DEVICE_CODE_URL,
50
+ token_url=_GITHUB_TOKEN_URL,
51
+ client_id=_GITHUB_MCP_CLIENT_ID,
52
+ ui=ui,
53
+ )
54
+ await storage.set_tokens_and_client_info(
55
+ token,
56
+ OAuthClientInformationFull(
57
+ client_id=_GITHUB_MCP_CLIENT_ID,
58
+ redirect_uris=[AnyUrl("http://localhost/callback")],
59
+ grant_types=["urn:ietf:params:oauth:grant-type:device_code"],
60
+ response_types=["code"],
61
+ token_endpoint_auth_method="none", # noqa: S106
62
+ ),
63
+ )
64
+
65
+
66
+ class GitHubProvider(OAuthProvider):
67
+ """GitHub-hosted MCP: RFC 8628 Device Authorization Grant."""
68
+
69
+ def matches(self, server_url: str) -> bool: # noqa: PLR6301 # subclass hook
70
+ """Match `api.githubcopilot.com`.
71
+
72
+ Args:
73
+ server_url: Remote MCP endpoint URL.
74
+
75
+ Returns:
76
+ `True` when `server_url`'s host is GitHub's MCP endpoint.
77
+ """
78
+ return _is_github_mcp_url(server_url)
79
+
80
+ async def run_login( # noqa: PLR6301 # subclass hook
81
+ self,
82
+ *,
83
+ server_name: str,
84
+ server_url: str,
85
+ storage: FileTokenStorage,
86
+ ui: OAuthInteraction,
87
+ ) -> LoginResult:
88
+ """Run the device flow and short-circuit the Authorization Code handshake.
89
+
90
+ Args:
91
+ server_name: MCP server name (unused).
92
+ server_url: Remote MCP endpoint URL (unused).
93
+ storage: File-backed token storage for this server identity.
94
+ ui: Interaction surface that renders the device-code prompt.
95
+
96
+ Returns:
97
+ `LoginResult(completed=True)` — tokens are already persisted so
98
+ the caller must skip the handshake step.
99
+ """
100
+ del server_name, server_url
101
+ await _preseed_github_auth(storage, ui=ui)
102
+ return LoginResult(completed=True)
@@ -0,0 +1,175 @@
1
+ """Slack-hosted MCP OAuth provider.
2
+
3
+ Slack's hosted MCP endpoint uses the Authorization Code flow with a
4
+ hardcoded public client ID and a fixed pre-registered loopback redirect
5
+ URI (`http://localhost:3118/callback`). The local callback server listens
6
+ on that port so the browser redirect completes automatically. An optional
7
+ `team` query parameter selects the workspace to install the app into.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
13
+ from urllib.parse import urlparse
14
+
15
+ from mcp.shared.auth import (
16
+ AnyUrl,
17
+ OAuthClientInformationFull,
18
+ OAuthClientMetadata,
19
+ )
20
+
21
+ from deepagents_code.mcp_providers.base import LoginResult, OAuthProvider
22
+
23
+ if TYPE_CHECKING:
24
+ from deepagents_code.mcp_auth import FileTokenStorage
25
+ from deepagents_code.mcp_oauth_ui import OAuthInteraction
26
+
27
+
28
+ @runtime_checkable
29
+ class _SupportsSlackTeamPrompt(Protocol):
30
+ """Optional interaction-surface capability: prompt for a Slack team ID.
31
+
32
+ `OAuthInteraction` deliberately omits this method — only the CLI
33
+ surface implements it. The TUI lets Slack's browser page handle
34
+ workspace selection. Marked `runtime_checkable` so `isinstance`
35
+ can replace `getattr`-style structural probes at the call site.
36
+ """
37
+
38
+ async def prompt_slack_team_id(self) -> str | None: ...
39
+
40
+
41
+ # Public OAuth client ID — safe to check in. No secret is associated;
42
+ # Slack treats this as a browser-style public client where the security
43
+ # boundary is the redirect URI rather than client secrecy.
44
+ _SLACK_MCP_CLIENT_ID = "4518649543379.10944517634130"
45
+ """Public OAuth client ID registered with Slack for the hosted MCP endpoint."""
46
+
47
+ _SLACK_LOOPBACK_PORT = 3118
48
+ """Fixed TCP port the local callback server binds to for Slack OAuth.
49
+
50
+ Slack validates the redirect URI against the app's registered allowlist, so
51
+ the port must be pre-registered in the Slack app dashboard. Only one Slack
52
+ login can proceed at a time per machine (port conflicts are surfaced as an
53
+ OSError from the loopback server's `start()` call).
54
+ """
55
+
56
+ _SLACK_REDIRECT_URI = f"http://localhost:{_SLACK_LOOPBACK_PORT}/callback"
57
+ """Pre-registered loopback redirect URI for the Slack MCP OAuth app."""
58
+
59
+
60
+ def _is_slack_mcp_url(url: str) -> bool:
61
+ """Return `True` when `url` points at a Slack-hosted MCP endpoint."""
62
+ host = urlparse(url).hostname or ""
63
+ return host == "slack.com" or host.endswith(".slack.com")
64
+
65
+
66
+ async def _prompt_slack_team(ui: OAuthInteraction) -> str | None:
67
+ """Return a Slack team ID when the interaction surface supports prompting.
68
+
69
+ `prompt_slack_team_id` is **not** a member of the `OAuthInteraction`
70
+ Protocol — only the CLI surface implements it. The TUI omits it so
71
+ Slack's browser page handles workspace selection instead. Detection
72
+ uses `isinstance` against the `runtime_checkable`
73
+ `_SupportsSlackTeamPrompt` capability protocol.
74
+
75
+ Args:
76
+ ui: Interaction surface to use.
77
+
78
+ Returns:
79
+ The entered Slack team ID, or `None` when the surface lacks the
80
+ optional prompt method or the user declined to specify one.
81
+ """
82
+ if not isinstance(ui, _SupportsSlackTeamPrompt):
83
+ return None
84
+ return await ui.prompt_slack_team_id()
85
+
86
+
87
+ async def _preseed_slack_client_info(storage: FileTokenStorage) -> None:
88
+ """Write the hardcoded Slack `client_info` to `storage` if not current."""
89
+ existing = await storage.get_client_info()
90
+ redirect_uris = existing.redirect_uris if existing is not None else None
91
+ current_redirect = str(redirect_uris[0]) if redirect_uris else None
92
+ if (
93
+ existing is not None
94
+ and existing.client_id == _SLACK_MCP_CLIENT_ID
95
+ and current_redirect == _SLACK_REDIRECT_URI
96
+ ):
97
+ return
98
+ await storage.set_client_info(
99
+ OAuthClientInformationFull(
100
+ client_id=_SLACK_MCP_CLIENT_ID,
101
+ redirect_uris=[AnyUrl(_SLACK_REDIRECT_URI)],
102
+ grant_types=["authorization_code", "refresh_token"],
103
+ response_types=["code"],
104
+ token_endpoint_auth_method="none", # noqa: S106
105
+ )
106
+ )
107
+
108
+
109
+ class SlackProvider(OAuthProvider):
110
+ """Slack-hosted MCP: loopback Authorization Code with a public client."""
111
+
112
+ def matches(self, server_url: str) -> bool: # noqa: PLR6301 # subclass hook
113
+ """Match `slack.com` and any `*.slack.com` subdomain.
114
+
115
+ Args:
116
+ server_url: Remote MCP endpoint URL.
117
+
118
+ Returns:
119
+ `True` when `server_url`'s host is Slack.
120
+ """
121
+ return _is_slack_mcp_url(server_url)
122
+
123
+ def loopback_port(self) -> int: # noqa: PLR6301 # subclass hook
124
+ """Return the fixed loopback port registered in the Slack OAuth app.
125
+
126
+ Returns:
127
+ `_SLACK_LOOPBACK_PORT` (3118).
128
+ """
129
+ return _SLACK_LOOPBACK_PORT
130
+
131
+ def client_metadata( # noqa: PLR6301 # subclass hook
132
+ self, *, redirect_uri: str | None = None
133
+ ) -> OAuthClientMetadata:
134
+ """Return public-client metadata with Slack's pre-registered loopback URI.
135
+
136
+ Args:
137
+ redirect_uri: Ignored; Slack requires its pre-registered loopback URI.
138
+
139
+ Returns:
140
+ Metadata configured for Slack's public OAuth client (no token secret).
141
+ """
142
+ del redirect_uri
143
+ return OAuthClientMetadata(
144
+ client_name="deepagents-code",
145
+ redirect_uris=[AnyUrl(_SLACK_REDIRECT_URI)],
146
+ grant_types=["authorization_code", "refresh_token"],
147
+ response_types=["code"],
148
+ token_endpoint_auth_method="none", # noqa: S106
149
+ )
150
+
151
+ async def run_login( # noqa: PLR6301 # subclass hook
152
+ self,
153
+ *,
154
+ server_name: str,
155
+ server_url: str,
156
+ storage: FileTokenStorage,
157
+ ui: OAuthInteraction,
158
+ ) -> LoginResult:
159
+ """Preseed client info and optionally thread the team ID into auth URL.
160
+
161
+ Args:
162
+ server_name: MCP server name (unused).
163
+ server_url: Remote MCP endpoint URL (unused).
164
+ storage: File-backed token storage for this server identity.
165
+ ui: Interaction surface used to prompt for the Slack team ID.
166
+
167
+ Returns:
168
+ A `LoginResult` carrying the optional `team=<id>` extra param
169
+ so the Slack authorize URL installs into the chosen workspace.
170
+ """
171
+ del server_name, server_url
172
+ await _preseed_slack_client_info(storage)
173
+ team_id = await _prompt_slack_team(ui)
174
+ extras = {"team": team_id} if team_id else {}
175
+ return LoginResult(extra_auth_params=extras)