Conversation
This package was in preview but won't be maintained to GA. Added README warning, runtime FutureWarning on import, and "Development Status :: 7 - Inactive" classifier. Recommends testing with Teams directly or the Agents Playground. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Restores the previously-removed microsoft-teams-devtools package into the workspace and marks it as deprecated (README warning + import-time warning + inactive classifier), while wiring it into type-checking and the publish pipeline.
Changes:
- Re-add
microsoft-teams-devtoolsto the workspace (pyproject + pyright config + publish pipeline). - Implement the DevTools FastAPI/uvicorn server plugin + v3 route handlers and ship prebuilt web UI assets.
- Add deprecation messaging (README +
FutureWarningon import) and basic tests for the production-environment guard.
Reviewed changes
Copilot reviewed 23 out of 38 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pyrightconfig.json | Includes packages/devtools/src in strict type checking. |
| pyproject.toml | Adds microsoft-teams-devtools as a workspace dependency. |
| packages/devtools/pyproject.toml | Defines the devtools package metadata, deps, and “Inactive” classifier. |
| packages/devtools/README.md | Adds deprecation warning + basic usage snippet. |
| packages/devtools/tests/test_devtools_plugin.py | Adds tests for on_init env guard and basic plugin state. |
| packages/devtools/src/microsoft_teams/devtools/init.py | Emits FutureWarning on import and exports plugin types. |
| packages/devtools/src/microsoft/teams/devtools/init.py | Back-compat import shim with DeprecationWarning. |
| packages/devtools/src/microsoft_teams/devtools/devtools_plugin.py | Core DevTools plugin: HTTP server, websocket fanout, activity plumbing. |
| packages/devtools/src/microsoft_teams/devtools/event.py | Pydantic models for devtools activity events. |
| packages/devtools/src/microsoft_teams/devtools/page.py | Dataclass for registering custom pages. |
| packages/devtools/src/microsoft_teams/devtools/routes/init.py | Exposes route entrypoints and exports. |
| packages/devtools/src/microsoft_teams/devtools/routes/context.py | Defines RouteContext passed through routers. |
| packages/devtools/src/microsoft_teams/devtools/routes/router.py | Top-level router (prefixes /v3). |
| packages/devtools/src/microsoft_teams/devtools/routes/v3/init.py | v3 route module exports. |
| packages/devtools/src/microsoft_teams/devtools/routes/v3/router.py | v3 router composition. |
| packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/init.py | Conversations route exports. |
| packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/router.py | Conversations router composition. |
| packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/init.py | Activities route exports. |
| packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/router.py | Activities router + POST wiring. |
| packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/create.py | POST handler to create/process activities via the app pipeline. |
| packages/devtools/src/microsoft_teams/devtools/web/index.html | Web UI entrypoint referencing built assets. |
| packages/devtools/src/microsoft_teams/devtools/web/icon.png | UI icon asset. |
| packages/devtools/src/microsoft_teams/devtools/web/assets/index-DUmBwYhV.css | Bundled UI CSS asset. |
| packages/devtools/src/microsoft_teams/devtools/web/assets/FluentSystemIcons-Light-eu0dDZrh.woff2 | Bundled font asset. |
| packages/devtools/src/microsoft_teams/devtools/web/assets/FluentSystemIcons-Light-DzHdd4FE.woff | Bundled font asset. |
| .azdo/publish.yml | Installs devtools in the publish pipeline’s editable installs list. |
| logger.debug(f"WebSocket {socket_id} disconnected") | ||
| break | ||
| finally: | ||
| del self.sockets[socket_id] |
There was a problem hiding this comment.
on_socket_connection unconditionally does del self.sockets[socket_id] in finally. If the socket was already removed elsewhere (e.g., on send failure / disconnect handling), this will raise KeyError and mask the original disconnect. Use self.sockets.pop(socket_id, None) to make cleanup idempotent.
| del self.sockets[socket_id] | |
| self.sockets.pop(socket_id, None) |
| async def process(token: TokenProtocol, activity: Activity): | ||
| response_future = asyncio.get_event_loop().create_future() | ||
| if activity.id: | ||
| self.pending[activity.id] = response_future | ||
| try: | ||
| # Convert Activity to CoreActivity | ||
| activity_dict = activity.model_dump(by_alias=True, exclude_none=True) | ||
| core_activity = CoreActivity.model_validate(activity_dict) | ||
| result = self.on_activity_event(ActivityEvent(body=core_activity, token=token)) | ||
| # If the handler is a coroutine, schedule it | ||
| if asyncio.iscoroutine(result): | ||
| asyncio.create_task(result) | ||
| except Exception as error: | ||
| response_future.set_exception(error) | ||
| finally: | ||
| result = await response_future |
There was a problem hiding this comment.
In process(), if self.on_activity_event(...) returns a coroutine and that task raises, the exception is never observed and response_future is never completed (so the HTTP request will hang forever at await response_future). Consider awaiting the handler directly, or attach a done-callback to the task to set_exception on response_future (and/or add a timeout) to guarantee the future is resolved.
| config = uvicorn.Config(app=self.app, host="0.0.0.0", port=self._port, log_level="info") | ||
| self._server = uvicorn.Server(config) | ||
|
|
||
| logger.info(f"available at http://localhost:{self._port}/devtools") |
There was a problem hiding this comment.
The devtools server is bound to 0.0.0.0, which exposes this explicitly-insecure debugging UI/API to the entire network by default. Bind to 127.0.0.1 (or make the host configurable with a safe default) to reduce accidental exposure.
| config = uvicorn.Config(app=self.app, host="0.0.0.0", port=self._port, log_level="info") | |
| self._server = uvicorn.Server(config) | |
| logger.info(f"available at http://localhost:{self._port}/devtools") | |
| host = os.getenv("MICROSOFT_TEAMS_DEVTOOLS_HOST", "127.0.0.1") | |
| config = uvicorn.Config(app=self.app, host=host, port=self._port, log_level="info") | |
| self._server = uvicorn.Server(config) | |
| display_host = "localhost" if host == "127.0.0.1" else host | |
| logger.info(f"available at http://{display_host}:{self._port}/devtools") |
| logger.error(err) | ||
| raise HTTPException(status_code=500, detail=str(err)) from err |
There was a problem hiding this comment.
The error handling here logs with logger.error(err) (no stack trace) and then returns the raw exception string to the client via HTTPException(detail=str(err)). Prefer logger.exception(...) (or logger.error(..., exc_info=True)) and return a generic message to avoid leaking internal details over HTTP (even if this is a dev-only tool).
| logger.error(err) | |
| raise HTTPException(status_code=500, detail=str(err)) from err | |
| logger.exception("Failed to create activity") | |
| raise HTTPException(status_code=500, detail="Internal server error") from err |
| token = JsonWebToken( | ||
| jwt.encode({"serviceurl": f"http://localhost:{context.port}"}, "secret", algorithm="HS256") | ||
| ) |
There was a problem hiding this comment.
This JWT is being created with a hard-coded signing key ("secret"). Even if the SDK later decodes without verifying signatures, hard-coded secrets tend to get flagged by security tooling and can be copied into real usage. Consider generating an ephemeral random key per run, or avoid JWT creation entirely by providing a minimal TokenProtocol implementation that only supplies the needed service_url/caller fields.
| name: str | ||
| "The unique name of the view (must be URL safe)" | ||
|
|
||
| display_name: str | ||
| "The display name of the view to be shown in the view header" | ||
|
|
||
| url: str | ||
| "The URL of the view" |
There was a problem hiding this comment.
The field descriptions are currently bare string literals following the annotations (e.g., name: str then a quoted string). These strings are no-ops at runtime and won’t show up in generated docs or type help. Use comments/docstrings, or dataclasses.field(metadata={...}) (or switch to a Pydantic model if you want schema/doc generation).
| async def emit_activity_to_sockets(self, event: DevToolsActivityEvent): | ||
| data = event.model_dump(mode="json", exclude_none=True) | ||
| for socket_id, websocket in self.sockets.items(): | ||
| try: | ||
| await websocket.send_json(data) | ||
| except WebSocketDisconnect: | ||
| logger.debug(f"WebSocket {socket_id} disconnected") | ||
| del self.sockets[socket_id] | ||
|
|
There was a problem hiding this comment.
emit_activity_to_sockets deletes entries from self.sockets while iterating over self.sockets.items(). Mutating a dict during iteration can raise RuntimeError and can also race with on_socket_connection's cleanup. Collect disconnected socket IDs and remove them after the loop (and prefer pop(..., None) to avoid KeyError).
## Summary - `uv.lock` was missing the devtools workspace member after it was restored in #410 - Ran `uv lock` to regenerate ## Test plan - [ ] `uv sync` works without errors Co-authored-by: Claude <noreply@anthropic.com>
Summary
FutureWarningon import, andDevelopment Status :: 7 - InactiveclassifierTest plan
poe checkpassespython -c "import microsoft_teams.devtools"shows FutureWarning