Add Microsoft 365 community app (plugins/omi-ms365-app)#6879
Add Microsoft 365 community app (plugins/omi-ms365-app)#6879snyfer wants to merge 2 commits intoBasedHardware:mainfrom
Conversation
Full-featured Microsoft 365 integration for Omi — Outlook Mail, Outlook Calendar, Microsoft Teams (chats + meetings), SharePoint and OneDrive — exposed as Omi chat tools. Self-contained FastAPI app following the convention of sibling apps in plugins/ (e.g. omi-google-calendar-app, omi-notion-app). Supersedes closed PR BasedHardware#6865, which used the deprecated community-plugins.json entry format. Layout: - main.py FastAPI + tool dispatcher (16 tools) - config.py Settings + Graph scopes - services/ auth, mail, calendar, teams, sharepoint, profile, graph_client, storage - omi-tools.json Tool manifest served at /.well-known/omi-tools.json - Procfile, railway.toml Railway/Render/Heroku deploy - requirements.txt, .env.example, .gitignore
Greptile SummaryThis PR adds a new self-contained Microsoft 365 community plugin under Two issues need attention before merge:
Confidence Score: 3/5Not safe to merge until OData injection in search_files and KeyError masking in tool dispatch are fixed. Two P1 bugs are present: a path-level OData injection in sharepoint search that breaks on any query containing a single quote and is exploitable for OData operator injection, and incorrect KeyError swallowing in the tool dispatcher that masks all internal errors as 404s. These affect the primary user paths for file search and all tool invocations. plugins/omi-ms365-app/services/sharepoint.py (OData injection), plugins/omi-ms365-app/main.py (KeyError masking)
|
| Filename | Overview |
|---|---|
| plugins/omi-ms365-app/services/sharepoint.py | OData injection via unescaped query in search_files; in-function imports and second httpx client in read_file_text bypass GraphClient retry logic. |
| plugins/omi-ms365-app/main.py | FastAPI entrypoint; KeyError from tool internals incorrectly caught as Unknown tool 404; in-function import json. |
| plugins/omi-ms365-app/config.py | pydantic-settings config; session_secret has weak insecure default that should be required. |
| plugins/omi-ms365-app/services/auth.py | MSAL OAuth flow with per-user token cache via TokenStore; clean silent refresh and error handling. |
| plugins/omi-ms365-app/services/graph_client.py | Async Graph API client with 429/5xx retry and exponential backoff; well-structured context manager. |
| plugins/omi-ms365-app/services/storage.py | Redis-backed token store with in-memory fallback; clean protocol abstraction and 90-day TTL. |
| plugins/omi-ms365-app/services/calendar.py | Calendar list/create/free-slots; hardcoded Europe/Vienna timezone default will affect non-Austrian users. |
| plugins/omi-ms365-app/services/mail.py | Outlook mail list/search/read/send via GraphClient; clean implementation. |
| plugins/omi-ms365-app/services/teams.py | Teams chats, messages, joined teams, online meetings; straightforward Graph API calls. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Omi App] -->|POST tools/tool_name| B[FastAPI Dispatcher]
B -->|get_access_token uid| C[MSAL Auth]
C -->|load cache| D[(TokenStore)]
C -->|silent refresh| E[Microsoft Entra ID]
E -->|updated cache| D
C -->|valid header| B
B -->|Graph API call| F[Microsoft Graph]
F -->|JSON response| B
B -->|tool result| A
G[Browser] -->|OAuth flow| H[setup + auth endpoints]
H -->|acquire tokens| E
E -->|cache blob| D
Reviews (1): Last reviewed commit: "Add Microsoft 365 community app (plugins..." | Re-trigger Greptile
| async def search_files(user_id: str, query: str, limit: int = 15) -> list[dict[str, Any]]: | ||
| async with GraphClient(user_id) as g: | ||
| data = await g.get( | ||
| f"/me/drive/root/search(q='{query}')", |
There was a problem hiding this comment.
OData path injection via unescaped
query string
query is interpolated directly into the URL path segment as f"/me/drive/root/search(q='{query}')". Because this is a path — not a query param — httpx will not percent-encode it. A single ' in the search term terminates the OData string literal (e.g. it's → /me/drive/root/search(q='it's')), producing a malformed request. Malicious input can further inject OData operators.
Move the search term to a $search query parameter instead.
| try: | ||
| return await _TOOLS[tool_name](uid, **args) | ||
| except KeyError: | ||
| raise HTTPException(404, f"Unknown tool: {tool_name}") |
There was a problem hiding this comment.
KeyError from tool internals masked as "Unknown tool" 404
The except KeyError block wraps the entire await _TOOLS[tool_name](uid, **args) call. Any KeyError raised inside a tool (e.g. a missing key in a Graph response) is caught here and surfaced as 404 Unknown tool: <name> rather than a 500, permanently hiding real errors. Separate the dict lookup from the invocation so only the lookup can produce the KeyError.
| import httpx | ||
| from services.auth import get_access_token |
There was a problem hiding this comment.
In-function imports should be moved to module level
import httpx and from services.auth import get_access_token are defined inside read_file_text. Move them to the top of the file. Additionally, this function bypasses GraphClient's throttling/retry logic by creating a second bare httpx.AsyncClient.
Context Used: Backend Python import rules - no in-function impor... (source)
| import json | ||
| with open(MANIFEST_PATH) as f: |
There was a problem hiding this comment.
import json is a standard-library import that should live at the top of the module, not inside _load_manifest. The in-function form is inconsistent with the rest of the file and is flagged by linters.
Context Used: Backend Python import rules - no in-function impor... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| body: str = "", | ||
| online: bool = True, | ||
| timezone_str: str = "Europe/Vienna", | ||
| ) -> dict[str, Any]: |
There was a problem hiding this comment.
Hardcoded locale-specific timezone default
timezone_str: str = "Europe/Vienna" is the plugin author's local timezone and will silently create events in the wrong timezone for all other users. A neutral default ("UTC") or one derived from the user's mailbox settings (MailboxSettings.Read is already in scope) would be more appropriate.
| microsoft_tenant_id: str = "common" | ||
| microsoft_redirect_uri: str = "http://localhost:8080/auth/microsoft/callback" | ||
|
|
||
| app_base_url: str = "http://localhost:8080" |
There was a problem hiding this comment.
Insecure default for
session_secret
The field has a weak placeholder default. If SESSION_SECRET is not set in the environment it is used silently — an attacker who knows the default can forge signed OAuth states. Making the field required (removing the default) causes the app to fail at startup rather than running insecurely.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5432523d4c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| attendees: list[str] | None = None, | ||
| body: str = "", | ||
| online: bool = True, | ||
| timezone_str: str = "Europe/Vienna", |
There was a problem hiding this comment.
Default event timezone to user context, not Europe/Vienna
create_event hard-codes timezone_str to Europe/Vienna, so any call that omits the timezone (which is common for tool invocations relying on defaults) will schedule events in CET/CEST rather than the user’s actual locale. This silently creates meetings at the wrong wall-clock time for most users outside that region; the default should come from the mailbox/user profile (or require an explicit timezone) instead of a fixed city.
Useful? React with 👍 / 👎.
| if attempt == MAX_RETRIES - 1: | ||
| raise GraphError(resp.status_code, resp.text) | ||
| retry_after = int(resp.headers.get("Retry-After", "2")) | ||
| backoff = min(retry_after, 2 ** attempt + 1) |
There was a problem hiding this comment.
Respect Retry-After instead of capping below server request
The retry delay uses min(retry_after, 2 ** attempt + 1), which can sleep less than the Retry-After value (for example retrying after 2s when Graph asked for 30s), causing repeated throttling and premature failure under load. Backoff should honor the server minimum (typically max(...) or direct retry_after) so 429 recovery is reliable.
Useful? React with 👍 / 👎.
| async def search_files(user_id: str, query: str, limit: int = 15) -> list[dict[str, Any]]: | ||
| async with GraphClient(user_id) as g: | ||
| data = await g.get( | ||
| f"/me/drive/root/search(q='{query}')", |
There was a problem hiding this comment.
Escape OneDrive search text before embedding in OData path
The search endpoint interpolates raw user text into search(q='...'); queries containing a single quote (for example Bob's notes) produce an invalid OData expression and the request fails with 4xx. User input should be escaped/encoded for OData string literals before constructing the path.
Useful? React with 👍 / 👎.
- graph_client: add get_bytes() helper that reuses session + throttling - sharepoint.search_files: escape single quotes in OData query (avoid injection) - sharepoint.read_file_text: use GraphClient.get_bytes() instead of bare httpx so throttling/retry apply; move imports to top - calendar.create_event: default timezone_str to 'UTC' (was 'Europe/Vienna') - config: make session_secret required (no default) so app fails fast if unset - main: move 'import json' to module top, split _TOOLS lookup from handler call so KeyError from unknown tool doesn't mask bugs inside handlers
Add Microsoft 365 community app (
plugins/omi-ms365-app)This PR adds a self-contained Microsoft 365 integration as a new app under
plugins/, following the same convention as sibling apps in this folder (omi-google-calendar-app,omi-notion-app,omi-slack-app, etc.).What it does
A FastAPI app that brings the Microsoft 365 surface into Omi:
Who am I?//meOAuth 2.0 with Microsoft Entra ID via MSAL. Token caching in Redis with in-memory fallback. Throttling-aware Graph client with exponential backoff. The
omi-tools.jsonmanifest is auto-served at/.well-known/omi-tools.json.Layout
Mirrors the structure of
omi-google-calendar-app:main.py— FastAPI + tool dispatcher (16 tools)config.py— Settings + Graph scopesomi-tools.json— Tool manifest Omi readsservices/— auth, storage, graph_client, profile, mail, calendar, teams, sharepointrequirements.txt,Procfile,railway.toml,.env.exampleVerified
/tools/{name}return valid responses/setup_checkand/webhook/memoryendpoints align with Omi's setup-completion contract.env.exampleonly contains placeholder values; real credentials live in the deploy environmentLocal test
Full setup walkthrough (Azure app registration, Graph permissions, deploy, Omi app config) is in the README.