@patimweb/pi-teams 1.0.0 → 1.2.0
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.
- package/README.md +232 -40
- package/docs/entra-app-registration.md +218 -0
- package/package.json +3 -2
- package/prompts/teams-doctor.md +9 -2
- package/skills/teams-collaboration/SKILL.md +91 -3
- package/src/auth/cache-plugin.ts +215 -0
- package/src/auth/index.ts +301 -54
- package/src/auth/msal.ts +168 -0
- package/src/config/index.ts +197 -19
- package/src/extension/index.ts +176 -2
- package/src/graph/messages.ts +14 -4
- package/src/safety/index.ts +4 -4
- package/src/status.ts +43 -8
- package/src/tools/teams-accounts.ts +22 -17
- package/src/tools/teams-doctor.ts +36 -24
- package/src/tools/teams-login.ts +111 -60
- package/src/tools/teams-send-channel-message.ts +7 -2
- package/src/tools/teams-send-chat-message.ts +7 -2
- package/src/tools/teams-setup.ts +12 -2
- package/src/tools/teams-status.ts +8 -8
- package/src/tools/teams-watch.ts +229 -0
- package/src/tools/tool-names.ts +1 -0
- package/src/utils/environment.ts +41 -0
- package/src/utils/formatting.ts +0 -12
- package/src/utils/richtext.ts +150 -0
- package/src/watch/index.ts +224 -0
- package/src/watch/loop.ts +304 -0
- package/src/watch/prompt.ts +59 -0
- package/src/auth/client-credentials.ts +0 -72
- package/src/auth/device-code.ts +0 -276
- package/src/auth/token-store.ts +0 -156
package/README.md
CHANGED
|
@@ -19,7 +19,9 @@ pi install /path/to/pi-teams
|
|
|
19
19
|
|
|
20
20
|
## Quick start
|
|
21
21
|
|
|
22
|
-
1. **Register an app in Entra ID** (once per organization —
|
|
22
|
+
1. **Register an app in Entra ID** (once per organization) — the step-by-step
|
|
23
|
+
walkthrough, including the redirect URI and public-client settings that sign-in
|
|
24
|
+
needs, is in [docs/entra-app-registration.md](docs/entra-app-registration.md).
|
|
23
25
|
2. **Configure the account:**
|
|
24
26
|
```
|
|
25
27
|
teams_setup:
|
|
@@ -28,16 +30,84 @@ pi install /path/to/pi-teams
|
|
|
28
30
|
clientId: <application (client) ID>
|
|
29
31
|
setDefault: true
|
|
30
32
|
```
|
|
31
|
-
3. **Sign in:** run `/teams-login`.
|
|
32
|
-
|
|
33
|
+
3. **Sign in:** run `/teams-login`. Your browser opens at the Microsoft
|
|
34
|
+
sign-in page and closes itself when you are done — nothing to type.
|
|
33
35
|
4. **Check it works:** `/teams-status`, then `/teams-inbox`.
|
|
34
36
|
|
|
37
|
+
Full walkthrough with the exact portal paths: **[docs/entra-app-registration.md](docs/entra-app-registration.md)**.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Listen mode
|
|
42
|
+
|
|
43
|
+
Off by default. When it is on, pi polls your chats and turns an incoming message
|
|
44
|
+
into a prompt it answers — as you, in the chat it arrived in.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
/teams-listen on # switch it on for the session's account
|
|
48
|
+
/teams-listen status # what is configured, and what is running right now
|
|
49
|
+
/teams-listen off
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
"watch": {
|
|
54
|
+
"enabled": true,
|
|
55
|
+
"intervalSeconds": 60,
|
|
56
|
+
"chats": ["Anna*", "Vertrieb*"],
|
|
57
|
+
"from": ["anna.schmidt@contoso.com"],
|
|
58
|
+
"mentionOnly": false,
|
|
59
|
+
"cooldownSeconds": 300,
|
|
60
|
+
"maxTriggersPerHour": 10
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Key | Default | Meaning |
|
|
65
|
+
|-----|---------|---------|
|
|
66
|
+
| `enabled` | `false` | whether the watcher runs |
|
|
67
|
+
| `intervalSeconds` | `60` | seconds between polls (minimum 15) |
|
|
68
|
+
| `chats` | `[]` | **where** pi listens: glob patterns matched against topic, label, chat ID and participant names. Empty means every recent chat. |
|
|
69
|
+
| `from` | `[]` | **who** pi listens to: glob patterns matched against display name, UPN and e-mail. Empty means any sender. |
|
|
70
|
+
| `mentionOnly` | `false` | only wake where you are mentioned |
|
|
71
|
+
| `cooldownSeconds` | `300` | stay quiet in a chat after waking pi for it |
|
|
72
|
+
| `maxTriggersPerHour` | `10` | hard cap on wakes per hour |
|
|
73
|
+
|
|
74
|
+
Set it globally, or per account (the account level wins field by field).
|
|
75
|
+
`teams_watch` writes the same keys; `/teams-listen` writes to the session's
|
|
76
|
+
account.
|
|
77
|
+
|
|
78
|
+
What it does, and what it deliberately does not:
|
|
79
|
+
|
|
80
|
+
- **Costs a model turn per wake.** That is what the filters and the hourly cap
|
|
81
|
+
are for — narrow `chats` and `from`, and enable it for a handful of
|
|
82
|
+
conversations rather than the whole company.
|
|
83
|
+
- **Never answers the backlog.** The first poll records what is there; only what
|
|
84
|
+
arrives afterwards wakes pi.
|
|
85
|
+
- **Never wakes for its own messages.** pi posts as you, so its own reply comes
|
|
86
|
+
back as "my own message" and stops the loop.
|
|
87
|
+
- **Respects the read rules.** A chat excluded by `permissions.read.chats` is
|
|
88
|
+
not even polled.
|
|
89
|
+
- **Does not bypass the safety model.** A reply it decides to send goes through
|
|
90
|
+
the same safety level and scope rules as one you asked for. At `confirm` you
|
|
91
|
+
approve each one; for hands-free answers, narrow `permissions.write.chats` and
|
|
92
|
+
set `safetyLevel: open` — deliberately, and on an account where you mean it.
|
|
93
|
+
- **Chats only, not channels.** Channel polling needs `ChannelMessage.Read.All`
|
|
94
|
+
(admin consent) and multiplies the calls; answering in channels stays a
|
|
95
|
+
requested action.
|
|
96
|
+
|
|
97
|
+
Why polling and not a webhook: Graph change notifications for chats need a
|
|
98
|
+
publicly reachable HTTPS endpoint plus subscription renewal, which a laptop on a
|
|
99
|
+
home network cannot offer. Polling costs one `/me/chats` call per interval and
|
|
100
|
+
works anywhere.
|
|
101
|
+
|
|
35
102
|
---
|
|
36
103
|
|
|
37
104
|
## Entra ID app registration
|
|
38
105
|
|
|
39
|
-
|
|
40
|
-
|
|
106
|
+
Full walkthrough: **[docs/entra-app-registration.md](docs/entra-app-registration.md)**.
|
|
107
|
+
The short version, with the two settings that sign-in actually depends on:
|
|
108
|
+
|
|
109
|
+
pi signs in as you with a delegated OAuth 2.0 flow — the browser opens, you sign
|
|
110
|
+
in with your own credentials and MFA, and pi never sees your password.
|
|
41
111
|
|
|
42
112
|
### 1. Create the registration
|
|
43
113
|
|
|
@@ -76,41 +146,122 @@ Without this, sign-in fails with `AADSTS7000218`.
|
|
|
76
146
|
| `Calendars.ReadWrite` | reading and creating calendar events |
|
|
77
147
|
| `OnlineMeetings.ReadWrite` | Teams meeting links |
|
|
78
148
|
| `Files.Read.All`, `Sites.Read.All` | channel files |
|
|
79
|
-
| `offline_access` | staying signed in |
|
|
80
149
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
150
|
+
`openid`, `profile` and `offline_access` are not listed: MSAL always requests
|
|
151
|
+
them and rejects them in an explicit scope list, so the lasting sign-in they
|
|
152
|
+
buy is automatic.
|
|
153
|
+
|
|
154
|
+
### 4. Add the redirect URI for the browser sign-in
|
|
155
|
+
|
|
156
|
+
**Authentication** → **Add a platform** → **Mobile and desktop applications** →
|
|
157
|
+
tick `http://localhost`.
|
|
158
|
+
|
|
159
|
+
MSAL picks a free loopback port at sign-in time, and Entra accepts any port on
|
|
160
|
+
localhost for a public client, so this one entry covers every run. If your
|
|
161
|
+
tenant insists on an exact URI, register e.g. `http://localhost:3000` and set
|
|
162
|
+
`loopbackPort: 3000` on the account.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Who has to approve what
|
|
167
|
+
|
|
168
|
+
Two separate hurdles, and only one of them normally needs an administrator.
|
|
84
169
|
|
|
85
|
-
|
|
170
|
+
### Creating the registration
|
|
86
171
|
|
|
87
|
-
|
|
172
|
+
By default any member of the tenant may register an application (Entra ID →
|
|
173
|
+
**User settings** → *Users can register applications*). Where that is switched
|
|
174
|
+
off, the least-privileged role that can do it is **Application Developer**;
|
|
175
|
+
Cloud Application Administrator and Application Administrator also work.
|
|
176
|
+
Changing *Allow public client flows* and adding the redirect URI need no extra
|
|
177
|
+
role once the registration is yours.
|
|
178
|
+
|
|
179
|
+
### Consenting to the permissions
|
|
180
|
+
|
|
181
|
+
Only two of the default scopes require **admin consent**:
|
|
182
|
+
|
|
183
|
+
| Scope | Consent | What you lose without it |
|
|
184
|
+
|-------|---------|--------------------------|
|
|
185
|
+
| `ChannelMessage.Read.All` | **admin** | reading channel messages and threads |
|
|
186
|
+
| `ChannelMember.Read.All` | **admin** | channel member lists |
|
|
187
|
+
| `ChannelMessage.Send` | user | — |
|
|
188
|
+
| `Chat.ReadWrite`, `ChatMessage.Send` | user | — |
|
|
189
|
+
| `Team.ReadBasic.All`, `Channel.ReadBasic.All` | user | — |
|
|
190
|
+
| `User.Read`, `User.ReadBasic.All` | user | — |
|
|
191
|
+
| `Presence.ReadWrite`, `Presence.Read.All` | user | — |
|
|
192
|
+
| `Calendars.ReadWrite`, `OnlineMeetings.ReadWrite` | user | — |
|
|
193
|
+
| `Files.Read.All`, `Sites.Read.All` | user | — |
|
|
194
|
+
|
|
195
|
+
Posting in a channel needs no admin; *reading* the channel does. Microsoft
|
|
196
|
+
treats bulk reading of channel content as the more sensitive right.
|
|
197
|
+
|
|
198
|
+
**Without an administrator**, drop the two admin-consent scopes and everything
|
|
199
|
+
else still works — chats in full, posting and replying in channels, presence,
|
|
200
|
+
calendar, meetings and directory lookup:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
"scopes": [
|
|
204
|
+
"User.Read", "User.ReadBasic.All",
|
|
205
|
+
"Team.ReadBasic.All", "Channel.ReadBasic.All", "ChannelMessage.Send",
|
|
206
|
+
"Chat.ReadWrite", "ChatMessage.Send",
|
|
207
|
+
"Presence.ReadWrite", "Presence.Read.All",
|
|
208
|
+
"Calendars.ReadWrite", "OnlineMeetings.ReadWrite",
|
|
209
|
+
"Files.Read.All", "Sites.Read.All"
|
|
210
|
+
]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
One caveat: if the tenant has *Users can consent to applications* switched off,
|
|
214
|
+
even the user-consentable scopes need an administrator — usually through the
|
|
215
|
+
admin consent request workflow.
|
|
216
|
+
|
|
217
|
+
`teams_doctor` reports exactly which scopes were actually granted, so a missing
|
|
218
|
+
consent shows up as a named permission rather than a 403.
|
|
219
|
+
|
|
220
|
+
### Guest and customer tenants
|
|
88
221
|
|
|
89
222
|
For a tenant that is not your own:
|
|
90
223
|
|
|
91
224
|
- the registration must be **multitenant**,
|
|
92
225
|
- your account must be a guest in that tenant,
|
|
93
|
-
- an administrator
|
|
226
|
+
- an administrator **of that tenant** consents to the app once.
|
|
94
227
|
|
|
95
|
-
|
|
96
|
-
|
|
228
|
+
In practice this is the bigger hurdle, not your own tenant. Once it is done,
|
|
229
|
+
add the tenant beneath your account (`parentAccount` in `teams_setup`) and sign
|
|
230
|
+
in against it.
|
|
97
231
|
|
|
98
232
|
---
|
|
99
233
|
|
|
100
|
-
##
|
|
234
|
+
## How the sign-in works
|
|
235
|
+
|
|
236
|
+
`teams_login` uses MSAL (`@azure/msal-node`) and picks the flow that fits the
|
|
237
|
+
machine:
|
|
101
238
|
|
|
102
|
-
| | `
|
|
103
|
-
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
239
|
+
| | `interactive` (default) | `device-code` | `client-credentials` |
|
|
240
|
+
|---|---|---|---|
|
|
241
|
+
| What you do | browser opens, you sign in | type a short code elsewhere | nothing |
|
|
242
|
+
| Identity | you | you | the application |
|
|
243
|
+
| Needs a browser | yes | no | no |
|
|
244
|
+
| Reading chats/channels | yes | yes | only via protected APIs |
|
|
245
|
+
| **Sending messages** | **yes** | **yes** | **no** — refused with an explanation |
|
|
246
|
+
| Presence, calendar | yes | yes | partially |
|
|
247
|
+
|
|
248
|
+
**Interactive** is chosen automatically wherever a browser can actually be
|
|
249
|
+
reached. MSAL opens the system browser, listens on a loopback port for the
|
|
250
|
+
redirect, and handles PKCE — nothing to copy or type.
|
|
251
|
+
|
|
252
|
+
**Device code** takes over automatically over SSH, in containers, and on any
|
|
253
|
+
machine without a display server, because a browser opened there would appear
|
|
254
|
+
where nobody is looking. Force it anywhere with `PI_TEAMS_NO_BROWSER=1`, per
|
|
255
|
+
account with `"authMode": "device-code"`, or per call with
|
|
256
|
+
`teams_login mode: device-code`.
|
|
257
|
+
|
|
258
|
+
Either way the session is cached on disk and renewed silently, so signing in is
|
|
259
|
+
a once-per-account affair.
|
|
109
260
|
|
|
110
261
|
Microsoft Graph only permits app-only writes to chats and channels for
|
|
111
262
|
migration scenarios (`Teamwork.Migrate.All`), so pi-teams refuses those calls
|
|
112
263
|
up front instead of letting them fail deep inside a Graph request. Use
|
|
113
|
-
`client-credentials` for unattended read-only jobs,
|
|
264
|
+
`client-credentials` for unattended read-only jobs, and a delegated flow for
|
|
114
265
|
everything that speaks in your name.
|
|
115
266
|
|
|
116
267
|
---
|
|
@@ -126,10 +277,10 @@ prefer.
|
|
|
126
277
|
"accounts": [
|
|
127
278
|
{
|
|
128
279
|
"name": "work",
|
|
129
|
-
"displayName": "
|
|
130
|
-
"tenantId": "
|
|
280
|
+
"displayName": "Contoso",
|
|
281
|
+
"tenantId": "contoso.onmicrosoft.com",
|
|
131
282
|
"clientId": "00000000-0000-0000-0000-000000000000",
|
|
132
|
-
"authMode": "
|
|
283
|
+
"authMode": "interactive",
|
|
133
284
|
"safetyLevel": "confirm",
|
|
134
285
|
"permissions": {
|
|
135
286
|
"read": {
|
|
@@ -142,7 +293,7 @@ prefer.
|
|
|
142
293
|
"deny": ["*/Announcements"]
|
|
143
294
|
},
|
|
144
295
|
"chats": { "allow": ["*"] },
|
|
145
|
-
"people": { "deny": ["ceo@
|
|
296
|
+
"people": { "deny": ["ceo@contoso.com"] }
|
|
146
297
|
}
|
|
147
298
|
},
|
|
148
299
|
"tenants": [
|
|
@@ -250,20 +401,20 @@ teams_permissions:
|
|
|
250
401
|
| Tool | Description |
|
|
251
402
|
|------|-------------|
|
|
252
403
|
| `teams_setup` | Write an account or tenant to the config file |
|
|
253
|
-
| `teams_login` | Sign in as yourself (device code
|
|
404
|
+
| `teams_login` | Sign in as yourself (browser, or device code where there is none) |
|
|
254
405
|
| `teams_logout` | Remove a saved session |
|
|
255
406
|
| `teams_accounts` | List accounts, switch the default, change a safety level, delete |
|
|
256
407
|
| `teams_status` | Which account is active, who pi acts as, what it may do |
|
|
257
408
|
| `teams_permissions` | Show, test or change the allow/deny rules |
|
|
258
409
|
| `teams_doctor` | Diagnose config, sign-in, consent and connectivity |
|
|
410
|
+
| `teams_watch` | Listen mode: status, enable, disable, and the filters for who and where |
|
|
259
411
|
|
|
260
412
|
### Reading
|
|
261
413
|
|
|
262
414
|
| Tool | Description |
|
|
263
415
|
|------|-------------|
|
|
264
416
|
| `teams_inbox` | Chats with new messages plus recent mentions of you |
|
|
265
|
-
| `teams_list_chats` | Recent chats with a preview of the last message |
|
|
266
|
-
| `teams_read_chat` | Messages of one chat |
|
|
417
|
+
| `teams_list_chats` | Recent chats with a preview of the last message || `teams_read_chat` | Messages of one chat |
|
|
267
418
|
| `teams_list_teams` | Teams you belong to |
|
|
268
419
|
| `teams_list_channels` | Channels of a team |
|
|
269
420
|
| `teams_read_channel` | Posts in a channel |
|
|
@@ -298,8 +449,9 @@ teams_permissions:
|
|
|
298
449
|
| Command | Description |
|
|
299
450
|
|---------|-------------|
|
|
300
451
|
| `/teams-status` | Show the connection and permissions |
|
|
301
|
-
| `/teams-login` |
|
|
452
|
+
| `/teams-login` | Sign in to Teams as yourself |
|
|
302
453
|
| `/teams-inbox` | What needs your attention right now |
|
|
454
|
+
| `/teams-listen` | Listen mode: `on`, `off`, or `status` |
|
|
303
455
|
| `/teams-permissions` | What pi may and may not do |
|
|
304
456
|
| `/teams-doctor` | Diagnose the setup |
|
|
305
457
|
|
|
@@ -316,6 +468,32 @@ teams_permissions:
|
|
|
316
468
|
|
|
317
469
|
---
|
|
318
470
|
|
|
471
|
+
## How messages are formatted
|
|
472
|
+
|
|
473
|
+
A message body is treated as **lightweight markdown** and converted to the HTML
|
|
474
|
+
subset Teams renders in a chat bubble:
|
|
475
|
+
|
|
476
|
+
| You write | Teams shows |
|
|
477
|
+
|-----------|-------------|
|
|
478
|
+
| `**bold**`, `*italic*`, `~~struck~~` | bold, italic, struck through |
|
|
479
|
+
| `` `code` ``, fenced blocks | inline code, code block |
|
|
480
|
+
| `- item`, `1. item` | bullet list, numbered list |
|
|
481
|
+
| `[label](https://…)` | link |
|
|
482
|
+
| `# Heading` | a bold line — Teams has no headings in a chat |
|
|
483
|
+
|
|
484
|
+
Tables and images are deliberately **not** translated: Teams renders neither in
|
|
485
|
+
a chat, so passing them through would look worse than plain text. Put a table in
|
|
486
|
+
a file and link it instead.
|
|
487
|
+
|
|
488
|
+
Pass `html: true` on a send tool to bypass the conversion and supply raw HTML
|
|
489
|
+
instead. Everything else is escaped, so a message body cannot inject markup.
|
|
490
|
+
|
|
491
|
+
This is also why pi writes the way it does: see *How to format a Teams message*
|
|
492
|
+
in the bundled skill. Short, answer-first, bullets for anything enumerable — the
|
|
493
|
+
medium is a chat bubble, not a document.
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
319
497
|
## Addressing things
|
|
320
498
|
|
|
321
499
|
**Channels** — a `Team/Channel` path, or `team` and `channel` separately.
|
|
@@ -354,7 +532,7 @@ teams_send_channel_message:
|
|
|
354
532
|
| File | Contents | Mode |
|
|
355
533
|
|------|----------|------|
|
|
356
534
|
| `~/.pi/agent/pi-teams.json` | accounts, tenants, safety levels, scope rules | `0600` |
|
|
357
|
-
| `~/.pi/agent/pi-teams-tokens
|
|
535
|
+
| `~/.pi/agent/pi-teams-tokens/` | one MSAL token cache per account+tenant | `0600` |
|
|
358
536
|
| `~/.pi/agent/pi-teams-audit.jsonl` | one line per write: when, who, where, what | `0600` |
|
|
359
537
|
|
|
360
538
|
Both the config and the token cache are written through a private temp file and
|
|
@@ -368,17 +546,22 @@ Data-oriented: plain immutable data, I/O at the edges, pure functions in the
|
|
|
368
546
|
middle.
|
|
369
547
|
|
|
370
548
|
- **`src/types.ts`** — domain data as plain interfaces. No behavior.
|
|
371
|
-
- **`src/config/index.ts`** — accounts, tenants, the safety cascade, persistence.
|
|
549
|
+
- **`src/config/index.ts`** — accounts, tenants, the safety cascade, listen-mode settings, persistence.
|
|
372
550
|
- **`src/config/scope.ts`** — allow/deny matching. Pure, and the most heavily
|
|
373
551
|
tested module in the package.
|
|
374
|
-
- **`src/auth/`** —
|
|
552
|
+
- **`src/auth/`** — MSAL applications, the file-backed token cache, JWT claims.
|
|
375
553
|
- **`src/graph/client.ts`** — fetch wrapper: bearer token, paging, throttling.
|
|
376
554
|
- **`src/graph/*.ts`** — one module per resource area, returning domain types.
|
|
377
555
|
- **`src/graph/mappers.ts`** — Graph JSON → domain types. Pure.
|
|
556
|
+
- **`src/watch/`** — listen mode: the decision rules (`index.ts`, pure), the
|
|
557
|
+
polling loop (`loop.ts`), and the prompt an incoming message turns into
|
|
558
|
+
(`prompt.ts`).
|
|
378
559
|
- **`src/utils/formatting.ts`** — domain types → display strings. Pure.
|
|
560
|
+
- **`src/utils/richtext.ts`** — markdown → the HTML subset Teams renders. Pure.
|
|
379
561
|
- **`src/safety/`** — the three gates and the audit log.
|
|
380
562
|
- **`src/tools/`** — one module per tool.
|
|
381
|
-
- **`src/extension/index.ts`** — registration, commands, the `tool_call`
|
|
563
|
+
- **`src/extension/index.ts`** — registration, commands, the `tool_call`
|
|
564
|
+
interceptor, and the listen-mode lifecycle.
|
|
382
565
|
|
|
383
566
|
## Development
|
|
384
567
|
|
|
@@ -389,20 +572,29 @@ npm test
|
|
|
389
572
|
```
|
|
390
573
|
|
|
391
574
|
Tests cover the pure logic — scope matching, the config cascade, message body
|
|
392
|
-
construction, HTML flattening, the
|
|
393
|
-
a network connection.
|
|
575
|
+
construction, markdown rendering, HTML flattening, the listen-mode decision
|
|
576
|
+
rules, the safety gates — and run without a tenant or a network connection.
|
|
394
577
|
|
|
395
578
|
## Troubleshooting
|
|
396
579
|
|
|
397
580
|
| Symptom | Cause and fix |
|
|
398
581
|
|---------|---------------|
|
|
582
|
+
| `AADSTS500113` (no reply address) | The registration has no redirect URI. Add `http://localhost` under *Mobile and desktop applications* — see [docs/entra-app-registration.md](docs/entra-app-registration.md) |
|
|
399
583
|
| `AADSTS7000218` | "Allow public client flows" is off in the app registration |
|
|
400
584
|
| `AADSTS65001` | Consent missing — sign in again, or have an admin consent |
|
|
585
|
+
| `AADSTS50011` (redirect URI mismatch) | Add `http://localhost` under *Mobile and desktop applications*, or pin `loopbackPort` |
|
|
401
586
|
| `AADSTS50020` | The account is not a member/guest of that tenant, or the app is single-tenant |
|
|
402
|
-
|
|
|
587
|
+
| Signed out after about an hour | The tenant grants no `offline_access`, so there is no refresh token. `teams_doctor` names the missing scope; otherwise sign in again when it expires |
|
|
588
|
+
| Browser never opens | No browser is reachable here; `teams_doctor` says why. `teams_login mode: device-code` always works |
|
|
589
|
+
| Browser opens, page never returns | A firewall is blocking the loopback port. Pin `loopbackPort` and allow it, or use the device code flow |
|
|
590
|
+
| "Not signed in" | Run `teams_login`; the saved session may have been revoked |
|
|
591
|
+
| Status line says "not signed in" although sign-in worked | The line is written at session start and after every auth tool; if it lags, `/teams-status` repaints it |
|
|
403
592
|
| "Blocked by configuration" | Your own scope rules. `teams_permissions action: test` shows which rule |
|
|
404
|
-
| "cannot run on an app-only token" | Set `authMode: "
|
|
405
|
-
| 403
|
|
593
|
+
| "cannot run on an app-only token" | Set `authMode: "interactive"` and run `teams_login` |
|
|
594
|
+
| 403 reading channel messages | `ChannelMessage.Read.All` needs **admin** consent — see "Who has to approve what" |
|
|
595
|
+
| Listen mode does nothing | `teams_watch action: status` says whether it is enabled and running. Only messages arriving *after* it was switched on wake pi |
|
|
596
|
+
| Listen mode answers too much | Narrow `watch.chats` and `watch.from`, raise `cooldownSeconds`, lower `maxTriggersPerHour` |
|
|
597
|
+
| Listen mode stopped by itself | `teams_watch action: status` shows the last polling error; a revoked session or a sleeping laptop is the usual cause, and it resumes on its own |
|
|
406
598
|
| Throttled | Graph rate limit; the client retries with back-off, then reports it |
|
|
407
599
|
|
|
408
600
|
Run `teams_doctor` first — it checks all of the above at once.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Entra ID app registration for pi-teams
|
|
2
|
+
|
|
3
|
+
The one-time setup that lets pi sign in as you. Fifteen minutes, once per
|
|
4
|
+
organization, and nothing in it needs a build or a server.
|
|
5
|
+
|
|
6
|
+
Everything below uses placeholders. Wherever you see
|
|
7
|
+
|
|
8
|
+
| Placeholder | Where it comes from |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `<APPLICATION_CLIENT_ID>` | *Overview* → **Application (client) ID** |
|
|
11
|
+
| `<DIRECTORY_TENANT_ID>` | *Overview* → **Directory (tenant) ID** |
|
|
12
|
+
| `<TENANT_DOMAIN>` | e.g. `contoso.onmicrosoft.com`, or your verified domain |
|
|
13
|
+
|
|
14
|
+
…substitute your own values locally. Do not paste real tenant or client IDs into
|
|
15
|
+
a README, a ticket, a screenshot or a chat — they identify your organization,
|
|
16
|
+
and the client ID is half of what an attacker needs to start a sign-in flow
|
|
17
|
+
against your tenant.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Create the registration
|
|
22
|
+
|
|
23
|
+
**Entra admin center** → [entra.microsoft.com](https://entra.microsoft.com) →
|
|
24
|
+
**App registrations** → **New registration**
|
|
25
|
+
|
|
26
|
+
| Field | Value |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Name | `pi-teams` (anything recognizable) |
|
|
29
|
+
| Supported account types | **Accounts in any organizational directory (multitenant)** if pi should also reach guest or customer tenants; **single tenant** is fine for one company |
|
|
30
|
+
| Redirect URI | leave empty here — it goes in step 2, on the right platform |
|
|
31
|
+
|
|
32
|
+
Two things to copy out of **Overview** before moving on:
|
|
33
|
+
|
|
34
|
+
- **Application (client) ID** → `<APPLICATION_CLIENT_ID>`
|
|
35
|
+
- **Directory (tenant) ID** → `<DIRECTORY_TENANT_ID>`
|
|
36
|
+
|
|
37
|
+
## 2. Authentication — the part that actually decides whether sign-in works
|
|
38
|
+
|
|
39
|
+
**App registrations** → your app → **Authentication**. Two settings matter, and
|
|
40
|
+
they are needed for two different reasons. Both are in this one blade.
|
|
41
|
+
|
|
42
|
+
Direct link, with your own application ID substituted:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Authentication/appId/<APPLICATION_CLIENT_ID>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2a. A reply address, for the browser sign-in
|
|
49
|
+
|
|
50
|
+
**Authentication** → **Add a platform** → **Mobile and desktop applications** →
|
|
51
|
+
tick **`http://localhost`** → **Configure**.
|
|
52
|
+
|
|
53
|
+
MSAL picks a free loopback port when the browser sign-in starts, and Entra
|
|
54
|
+
treats the port-less `http://localhost` entry as matching *any* port for a
|
|
55
|
+
public client. One entry therefore covers every run — no per-port registration
|
|
56
|
+
and no fixed port needed.
|
|
57
|
+
|
|
58
|
+
Skipping this is the single most common setup mistake, and it fails *after* the
|
|
59
|
+
user has typed their password, with:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
AADSTS500113: No reply address is registered for the application.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If your tenant insists on an exact URI, register e.g. `http://localhost:3000`
|
|
66
|
+
instead and pin the same number in pi (`loopbackPort` under the account, or
|
|
67
|
+
`teams_login port: 3000`).
|
|
68
|
+
|
|
69
|
+
### 2b. Public client flows, for the device code sign-in
|
|
70
|
+
|
|
71
|
+
Same blade → **Advanced settings** → **Allow public client flows** → **Yes** →
|
|
72
|
+
**Save**.
|
|
73
|
+
|
|
74
|
+
pi uses delegated OAuth flows and never handles a client secret for sign-in, so
|
|
75
|
+
Entra must treat the app as a public client. Without this, sign-in fails with:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
AADSTS7000218: The request body must contain the following parameter:
|
|
79
|
+
'client_assertion' or 'client_secret'.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 3. API permissions
|
|
83
|
+
|
|
84
|
+
**App registrations** → your app → **API permissions** → **Add a permission** →
|
|
85
|
+
**Microsoft Graph** → **Delegated permissions**:
|
|
86
|
+
|
|
87
|
+
| Permission | Needed for |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `User.Read`, `User.ReadBasic.All` | identity, looking colleagues up |
|
|
90
|
+
| `Team.ReadBasic.All`, `Channel.ReadBasic.All` | listing teams and channels |
|
|
91
|
+
| `ChannelMessage.Read.All` | reading channel messages and threads |
|
|
92
|
+
| `ChannelMessage.Send` | posting and replying in channels |
|
|
93
|
+
| `ChannelMember.Read.All` | channel member lists |
|
|
94
|
+
| `Chat.ReadWrite` | reading chats, creating chats |
|
|
95
|
+
| `ChatMessage.Send` | sending chat messages |
|
|
96
|
+
| `Presence.ReadWrite`, `Presence.Read.All` | your status, colleagues' availability |
|
|
97
|
+
| `Calendars.ReadWrite` | reading and creating calendar events |
|
|
98
|
+
| `OnlineMeetings.ReadWrite` | Teams meeting links |
|
|
99
|
+
| `Files.Read.All`, `Sites.Read.All` | channel files |
|
|
100
|
+
|
|
101
|
+
`openid`, `profile` and `offline_access` are deliberately absent: MSAL always
|
|
102
|
+
requests them and rejects them in an explicit scope list.
|
|
103
|
+
|
|
104
|
+
**What is not needed:** no client secret, no certificate, no redirect URI beyond
|
|
105
|
+
step 2, no application (as opposed to delegated) permissions.
|
|
106
|
+
|
|
107
|
+
## 4. Consent
|
|
108
|
+
|
|
109
|
+
**API permissions** → **Grant admin consent** — only for the two scopes that
|
|
110
|
+
require it:
|
|
111
|
+
|
|
112
|
+
| Scope | Consent | Without it |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `ChannelMessage.Read.All` | **admin** | cannot read channel messages or threads |
|
|
115
|
+
| `ChannelMember.Read.All` | **admin** | cannot list channel members |
|
|
116
|
+
| everything else above | user | a normal sign-in consent prompt covers it |
|
|
117
|
+
|
|
118
|
+
Without an administrator, drop those two scopes from the configuration and
|
|
119
|
+
everything else still works: chats in full, posting and replying in channels,
|
|
120
|
+
presence, calendar, meetings, directory lookup. See the README, *Who has to
|
|
121
|
+
approve what*, for the reduced scope list.
|
|
122
|
+
|
|
123
|
+
If the tenant has *Users can consent to applications* switched off, even the
|
|
124
|
+
user-consentable scopes need an administrator — usually through the admin
|
|
125
|
+
consent request workflow.
|
|
126
|
+
|
|
127
|
+
## 5. Give it to pi
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
teams_setup:
|
|
131
|
+
name: work
|
|
132
|
+
displayName: Contoso
|
|
133
|
+
tenantId: "<DIRECTORY_TENANT_ID or <TENANT_DOMAIN>>"
|
|
134
|
+
clientId: "<APPLICATION_CLIENT_ID>"
|
|
135
|
+
setDefault: true
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Or edit `~/.pi/agent/pi-teams.json` directly — the same file, mode `0600`:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"accounts": [
|
|
143
|
+
{
|
|
144
|
+
"name": "work",
|
|
145
|
+
"tenantId": "<TENANT_DOMAIN>",
|
|
146
|
+
"clientId": "<APPLICATION_CLIENT_ID>",
|
|
147
|
+
"authMode": "interactive",
|
|
148
|
+
"safetyLevel": "confirm"
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"defaultAccount": "work"
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 6. Sign in and verify
|
|
156
|
+
|
|
157
|
+
1. `teams_login` — the browser opens at the Microsoft sign-in page, you sign in
|
|
158
|
+
with your own credentials and MFA, and the page closes itself. Nothing to
|
|
159
|
+
copy or type. Over SSH or in a container, pi falls back to the device code
|
|
160
|
+
flow and shows a short code to enter on any device.
|
|
161
|
+
2. `teams_status` — who pi acts as, which token is cached, what it may do.
|
|
162
|
+
3. `teams_doctor` — checks configuration, sign-in, granted scopes and
|
|
163
|
+
connectivity in one go. It reports a missing consent as a **named
|
|
164
|
+
permission**, which is the quickest way to see whether the admin consent of
|
|
165
|
+
step 4 actually landed.
|
|
166
|
+
|
|
167
|
+
A successful sign-in is cached on disk and renewed silently, so this is a
|
|
168
|
+
once-per-account affair — as long as the tenant grants the session a refresh
|
|
169
|
+
token (see below).
|
|
170
|
+
|
|
171
|
+
## 7. Guest and customer tenants
|
|
172
|
+
|
|
173
|
+
For a tenant that is not your own:
|
|
174
|
+
|
|
175
|
+
1. the registration must be **multitenant** (step 1),
|
|
176
|
+
2. your account must be a **guest in that tenant**,
|
|
177
|
+
3. an administrator **of that tenant** consents to the app once.
|
|
178
|
+
|
|
179
|
+
In practice the third step is the hurdle, not your own tenant. Once it is done:
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
teams_setup:
|
|
183
|
+
parentAccount: work
|
|
184
|
+
name: customer-alpha
|
|
185
|
+
tenantId: "customeralpha.onmicrosoft.com"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
then `teams_login` with `tenant: customer-alpha`, and `teams_doctor` to confirm
|
|
189
|
+
that tenant on its own.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Troubleshooting the registration
|
|
194
|
+
|
|
195
|
+
| Error | Cause | Fix |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `AADSTS500113` — *No reply address is registered* | no redirect URI on the registration | step 2a: add `http://localhost` under **Mobile and desktop applications** |
|
|
198
|
+
| `AADSTS50011` — *redirect URI mismatch* | an exact URI is registered that does not match the chosen port | register the port-less `http://localhost`, or pin the port in the same file |
|
|
199
|
+
| `AADSTS7000218` — *client_assertion or client_secret* | **Allow public client flows** is off | step 2b |
|
|
200
|
+
| `AADSTS65001` — *consent required* | permissions added but never consented | sign in again; for the two admin scopes, step 4 |
|
|
201
|
+
| `AADSTS50020` — *not a member of the tenant* | wrong tenant, no guest account, or the app is single-tenant | steps 1 and 7 |
|
|
202
|
+
| `AADSTS700016` — *application not found in directory* | wrong tenant ID or client ID | recheck `<DIRECTORY_TENANT_ID>` and `<APPLICATION_CLIENT_ID>` |
|
|
203
|
+
| signed out after about an hour | the tenant grants no `offline_access` | the app needs a refresh token to renew silently; without it, run `teams_login` when the session expires. `teams_doctor` names the missing scope |
|
|
204
|
+
|
|
205
|
+
Run `teams_doctor` first for anything in this table: it validates the
|
|
206
|
+
configuration, the cached session, the granted scopes and the connection to
|
|
207
|
+
Graph, and it says which of them failed rather than only that something did.
|
|
208
|
+
|
|
209
|
+
## Rotating or revoking access
|
|
210
|
+
|
|
211
|
+
- **To sign out of pi:** `teams_logout` (per account), or `all: true` for every
|
|
212
|
+
account. This deletes the cached session, not the app.
|
|
213
|
+
- **To revoke pi's access for everyone:** Entra admin center → **Enterprise
|
|
214
|
+
applications** → your app → **Properties** → *User assignment required*, or
|
|
215
|
+
delete the registration. Deleting it invalidates every cached session at once.
|
|
216
|
+
- **To change the registration:** editing scopes or redirect URIs does not touch
|
|
217
|
+
the local configuration — but it does invalidate a *new* consent, so sign in
|
|
218
|
+
again afterwards.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patimweb/pi-teams",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Microsoft Teams integration for pi coding agent — acts fully as the signed-in user across multiple accounts and tenants: chats, channels, meetings, presence, with configurable guardrails",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"
|
|
32
|
+
"@azure/msal-node": "^6.0.0",
|
|
33
|
+
"typebox": ">=1.0.0"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
36
|
"@earendil-works/pi-coding-agent": ">=0.70.0",
|
package/prompts/teams-doctor.md
CHANGED
|
@@ -7,12 +7,19 @@ Something is wrong with my Teams connection.
|
|
|
7
7
|
|
|
8
8
|
2. Explain each problem in plain language, and say who can fix it — me or a
|
|
9
9
|
tenant administrator. In particular:
|
|
10
|
-
- missing consent → name the exact Graph scope and what it is needed for
|
|
10
|
+
- missing consent → name the exact Graph scope and what it is needed for,
|
|
11
|
+
and say whether it needs an admin (`ChannelMessage.Read.All` and
|
|
12
|
+
`ChannelMember.Read.All` do; the rest I can consent to myself)
|
|
11
13
|
- "Allow public client flows" disabled → the setting is in Entra ID →
|
|
12
14
|
App registrations → Authentication
|
|
15
|
+
- a redirect URI mismatch → `http://localhost` must be registered under
|
|
16
|
+
"Mobile and desktop applications"
|
|
17
|
+
- the browser sign-in unavailable → say why, and that
|
|
18
|
+
`teams_login mode: device-code` works anywhere
|
|
13
19
|
- expired or revoked sign-in → I need to run `teams_login` again
|
|
14
20
|
- a guest tenant that rejects the sign-in → the app registration must be
|
|
15
|
-
multi-tenant
|
|
21
|
+
multi-tenant, I must be invited there, and an admin of that tenant must
|
|
22
|
+
have consented
|
|
16
23
|
|
|
17
24
|
3. Run `teams_permissions` with `action: "show"` and tell me, in one short
|
|
18
25
|
paragraph, what you are currently allowed to do and what you are not.
|