Metadata-Version: 2.4
Name: piilot-pack-microsoft-365
Version: 0.1.3
Summary: Microsoft 365 connector for Piilot — Outlook, OneDrive, SharePoint, Teams via Microsoft Graph
Author-email: Piilot <support@piilot.ai>
License-Expression: Apache-2.0
Project-URL: Homepage, https://piilot.ai
Project-URL: Repository, https://github.com/Kinetics-Consulting-V2/piilot-pack-microsoft-365
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: piilot-sdk<1.0.0,>=0.6.0
Requires-Dist: httpx<1.0.0,>=0.27.0
Requires-Dist: langchain-core>=0.3
Requires-Dist: fastapi>=0.115
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: pytest-cov>=6.0; extra == "dev"
Requires-Dist: ruff>=0.9; extra == "dev"
Requires-Dist: black>=25.0; extra == "dev"
Dynamic: license-file

# piilot-pack-microsoft-365

Plugin Piilot pour **Microsoft 365** — accès Outlook, OneDrive,
SharePoint et Teams via l'API Microsoft Graph, avec gating granulaire
côté admin company et HITL systématique sur toutes les écritures.

> **Statut** : v0.1 — release initiale (Outlook + OneDrive + SharePoint + Teams).

## Surface v0.1

| Service | Lecture | Écriture (HITL) | Admin consent Microsoft ? |
|---------|---------|-----------------|----------------------------|
| Outlook | search / read / list | send_mail | Non (user delegated) |
| OneDrive | search / list / download | upload_file | Non |
| SharePoint | list_sites / files / download | upload_file | **Oui** (`Sites.*.All`) |
| Teams | list_chats / channels / read | send_message | **Oui** (`ChannelMessage.Read.All`) |

Soit 14 tools agents, dont 4 en HITL (pattern PLT-28). Excel + Teams
Bot (= chantier C14 distinct) reportés en v0.2.

## Architecture

```
piilot-pack-microsoft-365/
├── piilot_pack_microsoft_365/
│   ├── __init__.py        # Plugin entry point — register_*  + register_module
│   ├── _db.py             # connections + scope_grants helpers (encrypted)
│   ├── oauth.py           # OAuth2 flow, state token, exchange/refresh
│   ├── client.py          # GraphAPIClient (httpx wrapper)
│   ├── connector.py       # SCOPE_GROUPS + admin_consent_required
│   ├── routes/
│   │   ├── oauth.py       # /authorize-url /callback /admin-consent-url /status /disconnect
│   │   └── scopes.py      # GET / + PUT / (8 toggles)
│   └── tools/
│       ├── _auth.py       # resolve_access_token + ensure_permission (fail-closed)
│       ├── outlook.py     # 4 tools — search, read, list, send_mail (HITL)
│       ├── onedrive.py    # 4 tools — search, list, download, upload_file (HITL)
│       ├── sharepoint.py  # 4 tools — list_sites, list_files, download, upload (HITL)
│       └── teams.py       # 4 tools — list_chats, list_channels, read, send (HITL)
├── frontend/              # piilot-pack-microsoft-365-ui (npm package)
│   ├── package.json
│   └── src/
│       ├── index.ts                  # register(core) entry
│       ├── MicrosoftSettingsView.tsx # Settings page orchestrator
│       ├── components/               # ConnectionPanel, AdminConsentPanel, ScopeTogglesPanel
│       ├── hooks/                    # useMicrosoftStatus, useScopeGrants
│       ├── locales/{fr,en}.json      # ~25 keys under "microsoft365" namespace
│       └── services/microsoftService.ts
└── tests/
    ├── conftest.py        # autouse stubs for piilot.sdk.{db,crypto}
    ├── test_oauth.py      # 20 tests — pure OAuth helpers (no DB)
    ├── test_db.py         # 19 tests — _db.py with mocked cursor()
    ├── test_auth.py       # 12 tests — resolve_access_token + ensure_permission
    └── test_{outlook,onedrive,sharepoint,teams,client,connector,whoami}.py
```

**194/194 tests** sur la suite plugin (sans la stack du core).

## Gating côté Piilot (admin company)

8 toggles indépendants dans Settings → Microsoft 365, **fail-closed**
par défaut :

```
☐ Outlook — Lire les emails
☐ Outlook — Envoyer des emails (HITL)
☐ OneDrive — Lire les fichiers
☐ OneDrive — Uploader des fichiers (HITL)
☐ SharePoint — Lire les sites/fichiers
☐ SharePoint — Uploader des fichiers (HITL)
☐ Teams — Lire les messages
☐ Teams — Envoyer des messages (HITL)
```

Chaque toggle débloque les tools agents correspondants. Si un toggle
est OFF, le tool refuse l'appel avec `M365_PERMISSION_DISABLED`. Si
le toggle est ON mais que la connexion Microsoft est absente ou
expirée, le tool refuse avec `M365_NOT_CONNECTED` — l'agent est
formé pour relayer ces deux codes à l'utilisateur.

## Gating côté Microsoft (admin tenant)

Les scopes `.All` (`Sites.Read.All`, `ChannelMessage.Read.All`, …)
exigent un **admin consent** Microsoft Entra avant que les utilisateurs
puissent consentir individuellement.

Le plugin détecte si le tenant a validé l'app et, sinon, propose le
lien d'admin consent à l'admin Piilot pour qu'il le transmette à
l'admin tenant Microsoft. Le banner jaune dans Settings → Microsoft 365
guide la démarche.

## Variables d'environnement

```bash
MICROSOFT_CLIENT_ID=<azure_app_id>      # partagé avec SSO PLT-37
MICROSOFT_CLIENT_SECRET=<secret_value>  # à créer dans Azure App Registration
MICROSOFT_TENANT_ID=common              # ou un GUID tenant pour single-tenant
FRONTEND_URL=https://app.piilot.ai      # base de la redirection après callback OAuth
```

L'Azure App Registration est **la même** que pour l'auth SSO (PLT-37) —
pas d'app Microsoft séparée à créer. Il faut juste y générer un
**Client Secret** (l'auth SSO utilise PKCE, donc l'app n'avait pas
besoin de secret) et y ajouter les scopes Graph délégués listés
dans `connector.SCOPE_GROUPS`.

## Données stockées

Migration `110_microsoft_365_integrations.sql` (versionnée dans le
repo core `AICockpit`, même convention que Pennylane mig 056) :

- `integrations_microsoft_365.connections` — 1 row par company,
  tokens chiffrés Fernet via `piilot.sdk.crypto`, `expires_at`,
  `scopes_granted`, `admin_consent_granted`. RLS FORCEd, admin-only.
- `integrations_microsoft_365.scope_grants` — jusqu'à 8 rows par
  company (CHECK contraint sur `permission_id`). RLS FORCEd,
  admin-only.

Les access tokens sont rafraîchis proactivement à T-120 secondes via
`oauth.refresh_tokens` ; un échec de refresh fail-closed (l'agent
renvoie `M365_NOT_CONNECTED`).

## Tests

```bash
pip install -e .[dev]
pytest -v
```

194 tests, ~1.5 s d'exécution. Les tests utilisent un `conftest.py`
autouse qui rebind `piilot.sdk.db.run_in_thread` + `piilot.sdk.crypto.{encrypt,decrypt}`
en stubs déterministes — aucun PG / aucune Fernet réelle.

## Release

Backend + frontend publiés ensemble sur un tag unique `v<version>` via
`.github/workflows/release.yml` :

| Package | Registry |
|---|---|
| `piilot-pack-microsoft-365` (backend) | PyPI / TestPyPI |
| `piilot-pack-microsoft-365-ui` (frontend) | npm (tag `latest` ou `rc`) |

PyPI auth = OIDC Trusted Publisher (sans token stocké). npm auth =
secret `NPM_TOKEN` dans l'environnement GitHub `npm`. Le pipeline
refuse le tag si `pyproject.toml` et `frontend/package.json` divergent
sur la version — bump les deux ensemble.

Convention :
- `v0.1.0` → PyPI + npm `latest`
- `v0.1.0-rc1` → TestPyPI + npm dist-tag `rc` (dry run)

Côté core (`AICockpit`) :

```
backend/api/requirements.txt  → piilot-pack-microsoft-365==<version>
frontend/package.json         → "piilot-pack-microsoft-365-ui": "<version>"
```

Migration `110_microsoft_365_integrations.sql` reste dans `backend/migrations/`
côté core (convention usine).

## License

Apache-2.0.
