@open-everywhere/provider-teams 0.0.0-stage → 0.1.1

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 (146) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +46 -2
  4. package/agent/channels/teams-bot-name.ts +162 -0
  5. package/agent/channels/teams-catch-up.ts +244 -0
  6. package/agent/channels/teams-connector.ts +183 -0
  7. package/agent/channels/teams-conversation-ref.ts +398 -0
  8. package/agent/channels/teams-directory.ts +193 -0
  9. package/agent/channels/teams-engagement.ts +391 -0
  10. package/agent/channels/teams-fire-eligibility.ts +152 -0
  11. package/agent/channels/teams-front.ts +1404 -0
  12. package/agent/channels/teams-graph-boundary.ts +178 -0
  13. package/agent/channels/teams-graph-team-id.ts +106 -0
  14. package/agent/channels/teams-id-restore.ts +131 -0
  15. package/agent/channels/teams-identity.ts +395 -0
  16. package/agent/channels/teams-input-response.ts +129 -0
  17. package/agent/channels/teams-message-ops.ts +79 -0
  18. package/agent/channels/teams-origin-codec.ts +104 -0
  19. package/agent/channels/teams-post-arm.ts +323 -0
  20. package/agent/channels/teams-reaction-feedback.ts +127 -0
  21. package/agent/channels/teams-read.ts +510 -0
  22. package/agent/channels/teams-roster.ts +136 -0
  23. package/agent/channels/teams-stop.ts +319 -0
  24. package/agent/channels/teams-supervisor.ts +638 -0
  25. package/agent/channels/teams.ts +270 -0
  26. package/agent/contributions.ts +31 -0
  27. package/agent/docs-generated.ts +15 -0
  28. package/agent/ops.ts +129 -0
  29. package/agent/provider-events.ts +69 -0
  30. package/agent/register.ts +219 -0
  31. package/agent/surface-parity.ts +112 -0
  32. package/dist/agent/channels/teams-bot-name.d.ts +36 -0
  33. package/dist/agent/channels/teams-bot-name.d.ts.map +1 -0
  34. package/dist/agent/channels/teams-bot-name.js +141 -0
  35. package/dist/agent/channels/teams-bot-name.js.map +1 -0
  36. package/dist/agent/channels/teams-catch-up.d.ts +53 -0
  37. package/dist/agent/channels/teams-catch-up.d.ts.map +1 -0
  38. package/dist/agent/channels/teams-catch-up.js +221 -0
  39. package/dist/agent/channels/teams-catch-up.js.map +1 -0
  40. package/dist/agent/channels/teams-connector.d.ts +57 -0
  41. package/dist/agent/channels/teams-connector.d.ts.map +1 -0
  42. package/dist/agent/channels/teams-connector.js +132 -0
  43. package/dist/agent/channels/teams-connector.js.map +1 -0
  44. package/dist/agent/channels/teams-conversation-ref.d.ts +123 -0
  45. package/dist/agent/channels/teams-conversation-ref.d.ts.map +1 -0
  46. package/dist/agent/channels/teams-conversation-ref.js +368 -0
  47. package/dist/agent/channels/teams-conversation-ref.js.map +1 -0
  48. package/dist/agent/channels/teams-directory.d.ts +11 -0
  49. package/dist/agent/channels/teams-directory.d.ts.map +1 -0
  50. package/dist/agent/channels/teams-directory.js +179 -0
  51. package/dist/agent/channels/teams-directory.js.map +1 -0
  52. package/dist/agent/channels/teams-engagement.d.ts +174 -0
  53. package/dist/agent/channels/teams-engagement.d.ts.map +1 -0
  54. package/dist/agent/channels/teams-engagement.js +336 -0
  55. package/dist/agent/channels/teams-engagement.js.map +1 -0
  56. package/dist/agent/channels/teams-fire-eligibility.d.ts +26 -0
  57. package/dist/agent/channels/teams-fire-eligibility.d.ts.map +1 -0
  58. package/dist/agent/channels/teams-fire-eligibility.js +126 -0
  59. package/dist/agent/channels/teams-fire-eligibility.js.map +1 -0
  60. package/dist/agent/channels/teams-front.d.ts +249 -0
  61. package/dist/agent/channels/teams-front.d.ts.map +1 -0
  62. package/dist/agent/channels/teams-front.js +1143 -0
  63. package/dist/agent/channels/teams-front.js.map +1 -0
  64. package/dist/agent/channels/teams-graph-boundary.d.ts +45 -0
  65. package/dist/agent/channels/teams-graph-boundary.d.ts.map +1 -0
  66. package/dist/agent/channels/teams-graph-boundary.js +168 -0
  67. package/dist/agent/channels/teams-graph-boundary.js.map +1 -0
  68. package/dist/agent/channels/teams-graph-team-id.d.ts +30 -0
  69. package/dist/agent/channels/teams-graph-team-id.d.ts.map +1 -0
  70. package/dist/agent/channels/teams-graph-team-id.js +89 -0
  71. package/dist/agent/channels/teams-graph-team-id.js.map +1 -0
  72. package/dist/agent/channels/teams-id-restore.d.ts +34 -0
  73. package/dist/agent/channels/teams-id-restore.d.ts.map +1 -0
  74. package/dist/agent/channels/teams-id-restore.js +123 -0
  75. package/dist/agent/channels/teams-id-restore.js.map +1 -0
  76. package/dist/agent/channels/teams-identity.d.ts +148 -0
  77. package/dist/agent/channels/teams-identity.d.ts.map +1 -0
  78. package/dist/agent/channels/teams-identity.js +336 -0
  79. package/dist/agent/channels/teams-identity.js.map +1 -0
  80. package/dist/agent/channels/teams-input-response.d.ts +23 -0
  81. package/dist/agent/channels/teams-input-response.d.ts.map +1 -0
  82. package/dist/agent/channels/teams-input-response.js +102 -0
  83. package/dist/agent/channels/teams-input-response.js.map +1 -0
  84. package/dist/agent/channels/teams-message-ops.d.ts +3 -0
  85. package/dist/agent/channels/teams-message-ops.d.ts.map +1 -0
  86. package/dist/agent/channels/teams-message-ops.js +72 -0
  87. package/dist/agent/channels/teams-message-ops.js.map +1 -0
  88. package/dist/agent/channels/teams-origin-codec.d.ts +16 -0
  89. package/dist/agent/channels/teams-origin-codec.d.ts.map +1 -0
  90. package/dist/agent/channels/teams-origin-codec.js +99 -0
  91. package/dist/agent/channels/teams-origin-codec.js.map +1 -0
  92. package/dist/agent/channels/teams-post-arm.d.ts +37 -0
  93. package/dist/agent/channels/teams-post-arm.d.ts.map +1 -0
  94. package/dist/agent/channels/teams-post-arm.js +280 -0
  95. package/dist/agent/channels/teams-post-arm.js.map +1 -0
  96. package/dist/agent/channels/teams-reaction-feedback.d.ts +22 -0
  97. package/dist/agent/channels/teams-reaction-feedback.d.ts.map +1 -0
  98. package/dist/agent/channels/teams-reaction-feedback.js +108 -0
  99. package/dist/agent/channels/teams-reaction-feedback.js.map +1 -0
  100. package/dist/agent/channels/teams-read.d.ts +127 -0
  101. package/dist/agent/channels/teams-read.d.ts.map +1 -0
  102. package/dist/agent/channels/teams-read.js +424 -0
  103. package/dist/agent/channels/teams-read.js.map +1 -0
  104. package/dist/agent/channels/teams-roster.d.ts +54 -0
  105. package/dist/agent/channels/teams-roster.d.ts.map +1 -0
  106. package/dist/agent/channels/teams-roster.js +121 -0
  107. package/dist/agent/channels/teams-roster.js.map +1 -0
  108. package/dist/agent/channels/teams-stop.d.ts +74 -0
  109. package/dist/agent/channels/teams-stop.d.ts.map +1 -0
  110. package/dist/agent/channels/teams-stop.js +252 -0
  111. package/dist/agent/channels/teams-stop.js.map +1 -0
  112. package/dist/agent/channels/teams-supervisor.d.ts +351 -0
  113. package/dist/agent/channels/teams-supervisor.d.ts.map +1 -0
  114. package/dist/agent/channels/teams-supervisor.js +366 -0
  115. package/dist/agent/channels/teams-supervisor.js.map +1 -0
  116. package/dist/agent/channels/teams.d.ts +5 -0
  117. package/dist/agent/channels/teams.d.ts.map +1 -0
  118. package/dist/agent/channels/teams.js +246 -0
  119. package/dist/agent/channels/teams.js.map +1 -0
  120. package/dist/agent/contributions.d.ts +12 -0
  121. package/dist/agent/contributions.d.ts.map +1 -0
  122. package/dist/agent/contributions.js +31 -0
  123. package/dist/agent/contributions.js.map +1 -0
  124. package/dist/agent/docs-generated.d.ts +3 -0
  125. package/dist/agent/docs-generated.d.ts.map +1 -0
  126. package/dist/agent/docs-generated.js +13 -0
  127. package/dist/agent/docs-generated.js.map +1 -0
  128. package/dist/agent/ops.d.ts +14 -0
  129. package/dist/agent/ops.d.ts.map +1 -0
  130. package/dist/agent/ops.js +122 -0
  131. package/dist/agent/ops.js.map +1 -0
  132. package/dist/agent/provider-events.d.ts +3 -0
  133. package/dist/agent/provider-events.d.ts.map +1 -0
  134. package/dist/agent/provider-events.js +67 -0
  135. package/dist/agent/provider-events.js.map +1 -0
  136. package/dist/agent/register.d.ts +16 -0
  137. package/dist/agent/register.d.ts.map +1 -0
  138. package/dist/agent/register.js +198 -0
  139. package/dist/agent/register.js.map +1 -0
  140. package/dist/agent/surface-parity.d.ts +3 -0
  141. package/dist/agent/surface-parity.d.ts.map +1 -0
  142. package/dist/agent/surface-parity.js +106 -0
  143. package/dist/agent/surface-parity.js.map +1 -0
  144. package/package.json +183 -4
  145. package/tsconfig.build.json +22 -0
  146. package/tsconfig.json +15 -0
@@ -0,0 +1,174 @@
1
+ import type { TeamsConversationScope, TeamsMessageActivity } from "eve/channels/teams";
2
+ import { type EngagementRow, type Partition } from "@open-everywhere/core/agent/adapter-api.js";
3
+ /** The RSC permission Microsoft requires before it delivers NON-MENTION messages in this scope, or
4
+ * `undefined` for a scope the seam does not anchor (a 1:1 chat, which needs no grant — a personal
5
+ * message is delivered to its own bot unconditionally). */
6
+ export declare function teamsSeamGrant(scope: TeamsConversationScope): string | undefined;
7
+ /** The named platform reason the seam is UNSUPPORTED without the tenant's RSC grant. One sentence,
8
+ * naming the grant, so a log line or a doc row can carry it verbatim. */
9
+ export declare function teamsSeamUnsupportedReason(scope: TeamsConversationScope): string;
10
+ /**
11
+ * Is this activity MACHINE-AUTHORED — the agent itself, a Bot-Framework bot role, or any `28:`
12
+ * application id?
13
+ *
14
+ * 🔴 ONE PREDICATE, TWO CALLERS, AND THAT IS THE POINT. `teams-front.ts` computes this to decide
15
+ * whether an activity may wake anything, and the native gate below needs the SAME answer: a
16
+ * machine-authored CHANNEL activity is still FORWARDED to the native path (only the DM branch and
17
+ * the single-window branch drop one), so an engaged conversation would otherwise admit a bot's own
18
+ * post and self-trigger. A weaker second copy here — `from.role === "bot"` alone, say — would miss
19
+ * exactly the id-shaped cases the front catches.
20
+ */
21
+ export declare function teamsMachineAuthored(a: TeamsMessageActivity): boolean;
22
+ /**
23
+ * The engagement-registry anchor for one conversation, or `undefined` for a 1:1 chat.
24
+ *
25
+ * 🔴 IT IS THE ONE PREDICATE THE MAPPER AND THE FRONT SHARE. `bindTeams` publishes exactly this as
26
+ * `engagementKey`, so `stand_down`'s mark and this seam's read address the same row. Two spellings
27
+ * of "which conversations are anchored" is how a stop comes to mark a row no gate reads.
28
+ */
29
+ export declare function teamsEngagementAnchor(scope: TeamsConversationScope, conversationId: string | undefined): string | undefined;
30
+ /** Record that a NON-MENTION message arrived in this scope — positive proof the RSC grant is
31
+ * consented, because Microsoft would not have delivered it otherwise. ONE-DIRECTIONAL: silence
32
+ * here is not evidence of an absent grant (see the module header). */
33
+ export declare function noteTeamsSeamDelivery(scope: TeamsConversationScope): void;
34
+ /** Has a non-mention message ever arrived in this scope in this process? */
35
+ export declare function teamsSeamDeliveryProven(scope: TeamsConversationScope): boolean;
36
+ /**
37
+ * Declare the seam UNSUPPORTED-so-far for this scope, ONCE. Called when an @mention arms the seam
38
+ * and no non-mention message has yet proved the grant present. It logs and returns whether it
39
+ * logged, so a test can drive the once-ness; it never throws and never suppresses a wake.
40
+ */
41
+ export declare function noteTeamsSeamArmed(scope: TeamsConversationScope): boolean;
42
+ /** Test seam: clear both per-process flags. */
43
+ export declare function resetTeamsSeamSupport(): void;
44
+ /**
45
+ * What the seam says about one inbound activity. Four outcomes, and each names what the CALLER owes:
46
+ *
47
+ * • `unanchored` — a 1:1 chat (or an activity with no conversation id). The seam has nothing to say
48
+ * and the caller keeps today's path exactly.
49
+ * • `engage` — an @mention. The caller marks the conversation engaged ({@link markTeamsEngaged});
50
+ * the mention itself is answered by the caller's normal path, never by the seam. It carries the
51
+ * row's `org` so the caller can DEDUPE the mark: a mark re-invites, which clears a `stopped`
52
+ * flag, and a redelivered mention must never clear a stand-down nobody asked to lift.
53
+ * • `deliver` — a plain message in an engaged conversation. The caller wakes a turn.
54
+ * • `hold` — a plain message the seam does not admit. `stopped` says WHY, and it is the one field a
55
+ * caller must branch on: `stopped: true` means the agent was asked to stand down here, so the
56
+ * message must be SUPPRESSED outright — including on an ambient leg that never reads this
57
+ * registry. `stopped: false` means the conversation simply is not engaged, which is not a
58
+ * suppression: the caller keeps today's path.
59
+ */
60
+ export type TeamsEngagementVerdict = {
61
+ readonly kind: "unanchored";
62
+ readonly reason: string;
63
+ } | {
64
+ readonly kind: "engage";
65
+ readonly anchor: string;
66
+ readonly org: string;
67
+ } | {
68
+ readonly kind: "deliver";
69
+ readonly anchor: string;
70
+ readonly reason: string;
71
+ } | {
72
+ readonly kind: "hold";
73
+ readonly anchor: string;
74
+ readonly reason: string;
75
+ readonly stopped: boolean;
76
+ };
77
+ /** Injectable seams so the whole verdict is unit-tested without a store. */
78
+ export interface TeamsEngagementDeps {
79
+ readonly getEngagement?: (p: Partition, anchor: string) => Promise<EngagementRow | null>;
80
+ readonly markEngaged?: (p: Partition, anchor: string, atIso: string) => Promise<void>;
81
+ readonly now?: () => string;
82
+ }
83
+ /**
84
+ * Resolve the seam for one verified message activity — READ-ONLY. The mark is a separate call
85
+ * ({@link markTeamsEngaged}) so that exactly one caller writes the row: two writers is how a mention
86
+ * comes to engage a conversation twice per activity, once on each path.
87
+ *
88
+ * ORDERING. A mention is settled before any read: it is always answered by the caller's own path, so
89
+ * its verdict costs no store round-trip. Only a plain message pays for a read, and it pays for
90
+ * exactly one — the ROW, not a boolean, because the row carries both facts the decision needs (is
91
+ * this engaged, and was the agent asked to stand down here) and two reads could disagree.
92
+ *
93
+ * The DECISION ITSELF IS CORE'S. `shouldDeliverReply` owns the disqualifier order and the reasons;
94
+ * this function only translates Teams facts into its input. Nothing here re-states the policy.
95
+ *
96
+ * FAIL-CLOSED on a store error: `getThreadEngagement` reads a failure as `null`, so an unreadable
97
+ * registry holds rather than delivers — the agent does not barge into a conversation it cannot prove
98
+ * it was invited to.
99
+ */
100
+ export declare function resolveTeamsEngagement(a: TeamsMessageActivity, machineAuthored: boolean, deps?: TeamsEngagementDeps): Promise<TeamsEngagementVerdict>;
101
+ /**
102
+ * Mark one conversation ENGAGED on an @mention, and declare the RSC dependency once.
103
+ *
104
+ * `participating: true` is the re-invitation: it promotes the row and CLEARS a previous stand-down,
105
+ * which is what "an @mention brings the agent back" means on every surface that wires this seam.
106
+ * Best-effort throughout — `markThreadEngaged` swallows a store failure, because engagement
107
+ * bookkeeping must never fail a turn.
108
+ *
109
+ * 🔴 THE PROMOTION CARRIES THE ACTIVITY ID, SO REPLAYING ONE THE ROW STILL HOLDS LIFTS NOTHING
110
+ * (OPE-1527). The
111
+ * Bot Framework redelivers an activity whenever it does not get a prompt 200, and a redelivered
112
+ * @mention is a REPLAYED invitation: it used to clear a `stopped` flag nobody asked to lift. The
113
+ * front's dedupe caught the ordinary case and could not catch this one, because it expires — its
114
+ * store TTL is ten minutes and a stand-down has no expiry at all, so a redelivery on a fresh process
115
+ * an hour later re-marked a stood-down conversation as engaged. `engagedBy` closes that durably: the
116
+ * store records the promoting id and refuses to clear `stopped` when a promotion repeats it. A
117
+ * SECOND, GENUINE @mention carries a different activity id and still brings the agent back, which is
118
+ * the behaviour this must not break.
119
+ *
120
+ * BE EXACT ABOUT THE REACH. The store keeps a BOUNDED LIST of recent promoters (OPE-1562), so what
121
+ * cannot be replayed is every promotion the list still holds — see `EngagementRow.recentPromoters`.
122
+ * Mention A, mention B, a stand-down, then a redelivery of A leaves the stop SET: a superseded
123
+ * mention is guarded like the latest one. What remains open is the BOUND — a replay of a mention
124
+ * that `PROMOTER_LIST_MAX` distinct later promotions have pushed off the list clears again.
125
+ */
126
+ export declare function markTeamsEngaged(a: TeamsMessageActivity, deps?: TeamsEngagementDeps): Promise<string | null>;
127
+ /**
128
+ * Has a stand-down landed on this partition's ROOM row?
129
+ *
130
+ * 🔴 FAIL-CLOSED, and the direction is the opposite of core's `isThreadStopped` on purpose. That
131
+ * predicate fails OPEN because it decides whether to SILENCE an answer the model has already
132
+ * written, where "unknown" must mean deliver. This one is a WAKE GATE: it decides whether to barge
133
+ * into a room nobody invited the agent into, where "unknown" must mean stay out. It is the same
134
+ * posture the group-chat branch of `teams-front.ts` takes two screens above, in its own words — an
135
+ * unreadable registry cannot prove the agent is still welcome.
136
+ *
137
+ * The cost is real and worth stating: while the operational store is unreadable, EVERY room on this
138
+ * surface holds its ambient wakes, including rooms nobody ever stood down. An @mention is
139
+ * unaffected — the addressed arm never reads this — so a person can still reach the agent through
140
+ * the affordance the stop copy itself points them at. That is the right trade for a gate whose
141
+ * failure mode in the other direction is an agent that keeps talking in a room that asked it to
142
+ * stop, and cannot be re-silenced because the same outage fails the write.
143
+ *
144
+ * It reads the store DIRECTLY rather than through `getThreadEngagement`, because that wrapper
145
+ * swallows an error to `null` and a null is indistinguishable from "no row" — which is exactly the
146
+ * distinction this answer turns on.
147
+ */
148
+ export declare function teamsRoomWatchStopped(partition: Partition): Promise<boolean>;
149
+ /** Store the ROOM-grain wake row for an ambient (undirected) Leg A wake — the row a later stop
150
+ * marks. Best-effort: `markThreadEngaged` swallows a store failure, and a lost watch row costs a
151
+ * stop that could not land, never a turn. */
152
+ export declare function markTeamsRoomWatched(partition: Partition, atIso: string): Promise<void>;
153
+ /**
154
+ * Lift a ROOM-grain stand-down on an @mention — the way back in, and the reason the room stop is not
155
+ * a trap. Without it a room stop would outlive every invitation: Leg E's mention mark promotes the
156
+ * CONVERSATION row, which says nothing about the room, so the room would stay silent with no verb
157
+ * able to reach it.
158
+ *
159
+ * `engagedBy` carries the mention's activity id for the same reason the conversation mark does — a
160
+ * redelivered mention must not lift a stand-down twice over.
161
+ */
162
+ export declare function clearTeamsRoomStop(partition: Partition, atIso: string, engagedBy: string): Promise<void>;
163
+ /**
164
+ * The NATIVE path's one-line admission (channels/teams.ts's mention gate): may this plain,
165
+ * non-mention message run a turn?
166
+ *
167
+ * It exists because the native channel is where a Teams turn is actually minted — the front holds no
168
+ * continuation-token primitive it could wake an existing native session with, so admitting the
169
+ * activity through the mention gate IS the wake. The front owns the SUPPRESSION half above it (a
170
+ * stood-down conversation is never forwarded at all) and the MARK, so this call only ever answers
171
+ * the delivery question, and it never writes.
172
+ */
173
+ export declare function teamsEngagedAdmits(a: TeamsMessageActivity, deps?: TeamsEngagementDeps): Promise<boolean>;
174
+ //# sourceMappingURL=teams-engagement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-engagement.d.ts","sourceRoot":"","sources":["../../../agent/channels/teams-engagement.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,336 @@
1
+ // THE TEAMS AMBIENT ENGAGEMENT SEAM (chat-surface contract §2.5) — the Teams wiring over core's
2
+ // pure engagement policy (`packages/core/agent/channels/engagement.ts`). It gives Teams the middle
3
+ // posture Slack and Discord already have: an @mention marks the conversation ENGAGED, a later PLAIN
4
+ // message in that conversation wakes the agent, and `stand_down` stores the stop.
5
+ //
6
+ // THE ANCHOR is `conversation.id`, and it names a different container per scope — which is exactly
7
+ // what the seam wants:
8
+ // • a TEAM CHANNEL activity carries `<channelId>;messageid=<root>`, so the anchor is the REPLY
9
+ // CHAIN, not the channel. Engagement is per reply chain, matching Slack's per-thread grain.
10
+ // • a GROUP CHAT activity carries the CHAT id, so the anchor is the chat.
11
+ // • a 1:1 (personal) chat carries NO anchor at all. That omission is the honest half, and it is
12
+ // Discord's DM precedent (discord-identity.ts): a 1:1 is answered unconditionally, so no gate
13
+ // anywhere reads a mark on it and writing one would suppress nothing. `stand_down` refuses on a
14
+ // 1:1 for the same reason, in its own words.
15
+ //
16
+ // 🔴 THE PLATFORM CONSTRAINT, AND WHY IT IS NOT A DEFECT. Microsoft delivers a NON-MENTION message
17
+ // to a bot only where the tenant has consented to the matching resource-specific-consent (RSC)
18
+ // grant: `ChannelMessage.Read.Group` for a team channel, `ChatMessage.Read.Chat` for a chat (both
19
+ // are in `teams-app/manifest.json`). Without it, only @mention activities ever arrive, so the seam
20
+ // is armed but nothing can exercise it — the surface answers once per mention, exactly as before.
21
+ // There is NO API that reports whether a tenant granted an RSC permission, so this module states
22
+ // the dependency out loud instead of guessing, and logs the named reason ONCE per scope while no
23
+ // proof has arrived. Never a silent gap (contract §3), and never a refusal either — a declaration.
24
+ //
25
+ // 🔴 BE EXACT ABOUT WHAT THE PROOF COVERS, because the sentence here used to overstate it
26
+ // (OPE-1527). The probe sits on the seam's own read, which the front reaches for a HUMAN-authored
27
+ // activity with a RESOLVABLE partition and nothing else: a machine-authored activity skips the seam,
28
+ // and an activity whose identity will not resolve returns `unanchored` above the probe. So the proof
29
+ // is one-directional. A scope that HAS printed the proof is granted — Microsoft delivered a
30
+ // non-mention message, and nothing else could have. A scope that has NOT printed it says only that
31
+ // no such message has been SEEN in this process; the grant may be present and the traffic may simply
32
+ // have been mentions, bot posts, or activities this deployment could not anchor. A serverless
33
+ // invocation is a fresh process, so a granted tenant keeps printing "delivery UNPROVEN" per cold
34
+ // start. That is the honest reading of a signal no API can give us, not a defect to design around.
35
+ import { buildTeamsAuth, teamsMembershipType, teamsMode } from "./teams-identity.js";
36
+ import { getOperationalStore, getThreadEngagement, markThreadEngaged, resolvePartition, roomEngagementAddress, shouldDeliverReply, } from "@open-everywhere/core/agent/adapter-api.js";
37
+ /** The RSC permission Microsoft requires before it delivers NON-MENTION messages in this scope, or
38
+ * `undefined` for a scope the seam does not anchor (a 1:1 chat, which needs no grant — a personal
39
+ * message is delivered to its own bot unconditionally). */
40
+ export function teamsSeamGrant(scope) {
41
+ if (scope === "personal")
42
+ return undefined;
43
+ return scope === "groupChat" ? "ChatMessage.Read.Chat" : "ChannelMessage.Read.Group";
44
+ }
45
+ /** The named platform reason the seam is UNSUPPORTED without the tenant's RSC grant. One sentence,
46
+ * naming the grant, so a log line or a doc row can carry it verbatim. */
47
+ export function teamsSeamUnsupportedReason(scope) {
48
+ const grant = teamsSeamGrant(scope) ?? "no RSC grant";
49
+ const room = scope === "groupChat" ? "group-chat" : "channel";
50
+ return (`Microsoft delivers non-mention ${room} messages only with the ${grant} RSC grant consented in this tenant; ` +
51
+ `without it a plain follow-up never reaches this deployment, so the engaged seam can wake nothing here`);
52
+ }
53
+ /**
54
+ * Is this activity MACHINE-AUTHORED — the agent itself, a Bot-Framework bot role, or any `28:`
55
+ * application id?
56
+ *
57
+ * 🔴 ONE PREDICATE, TWO CALLERS, AND THAT IS THE POINT. `teams-front.ts` computes this to decide
58
+ * whether an activity may wake anything, and the native gate below needs the SAME answer: a
59
+ * machine-authored CHANNEL activity is still FORWARDED to the native path (only the DM branch and
60
+ * the single-window branch drop one), so an engaged conversation would otherwise admit a bot's own
61
+ * post and self-trigger. A weaker second copy here — `from.role === "bot"` alone, say — would miss
62
+ * exactly the id-shaped cases the front catches.
63
+ */
64
+ export function teamsMachineAuthored(a) {
65
+ // DEFENSIVE ON `recipient`, unlike the front's own copy. The front only ever sees an activity
66
+ // `parseTeamsActivity` produced, which always carries one; this is also called from the native
67
+ // handler, which a test or an ops script can drive with a hand-built activity. An absent recipient
68
+ // must not throw here — the author question still has two other answers.
69
+ const self = a.recipient?.id;
70
+ return (self !== undefined && a.from.id === self) || a.from.role === "bot" || a.from.id.startsWith("28:");
71
+ }
72
+ /**
73
+ * The engagement-registry anchor for one conversation, or `undefined` for a 1:1 chat.
74
+ *
75
+ * 🔴 IT IS THE ONE PREDICATE THE MAPPER AND THE FRONT SHARE. `bindTeams` publishes exactly this as
76
+ * `engagementKey`, so `stand_down`'s mark and this seam's read address the same row. Two spellings
77
+ * of "which conversations are anchored" is how a stop comes to mark a row no gate reads.
78
+ */
79
+ export function teamsEngagementAnchor(scope, conversationId) {
80
+ if (!conversationId || conversationId.length === 0)
81
+ return undefined;
82
+ return teamsMode(scope) === "dm" ? undefined : conversationId;
83
+ }
84
+ // --- the RSC support declaration (per-process, once per scope) ---------------------------------
85
+ //
86
+ // Module-scoped once-flags, the same shape `teams-front.ts`'s `warnedDedupeStore` uses: a serverless
87
+ // invocation is a fresh process, so "once" means once per cold start per scope rather than once
88
+ // forever. That is the right grain — the declaration is for the operator reading logs, and a
89
+ // deployment whose grant is missing keeps saying so on each cold start rather than saying it once
90
+ // and never again.
91
+ const seamDeliveryProven = new Set();
92
+ const seamUnsupportedLogged = new Set();
93
+ /** Record that a NON-MENTION message arrived in this scope — positive proof the RSC grant is
94
+ * consented, because Microsoft would not have delivered it otherwise. ONE-DIRECTIONAL: silence
95
+ * here is not evidence of an absent grant (see the module header). */
96
+ export function noteTeamsSeamDelivery(scope) {
97
+ if (teamsSeamGrant(scope) !== undefined)
98
+ seamDeliveryProven.add(scope);
99
+ }
100
+ /** Has a non-mention message ever arrived in this scope in this process? */
101
+ export function teamsSeamDeliveryProven(scope) {
102
+ return seamDeliveryProven.has(scope);
103
+ }
104
+ /**
105
+ * Declare the seam UNSUPPORTED-so-far for this scope, ONCE. Called when an @mention arms the seam
106
+ * and no non-mention message has yet proved the grant present. It logs and returns whether it
107
+ * logged, so a test can drive the once-ness; it never throws and never suppresses a wake.
108
+ */
109
+ export function noteTeamsSeamArmed(scope) {
110
+ if (teamsSeamGrant(scope) === undefined)
111
+ return false; // a 1:1 needs no grant
112
+ if (seamDeliveryProven.has(scope) || seamUnsupportedLogged.has(scope))
113
+ return false;
114
+ seamUnsupportedLogged.add(scope);
115
+ // WORDED AS A DEPENDENCY, NOT A VERDICT. On a cold start whose first activity is a mention, nothing
116
+ // has been observed either way — and a serverless invocation is a fresh process, so that is the
117
+ // COMMON case even on a fully granted tenant. Saying "unsupported" there would be a confident wrong
118
+ // answer in an operator's log. The line says what the seam needs and what would prove it instead.
119
+ console.warn(`[open-everywhere:teams-engagement] engaged seam armed, delivery UNPROVEN in this process — ${teamsSeamUnsupportedReason(scope)}. One non-mention message from a person, in a conversation this deployment can anchor, would prove the grant present; nothing else this process sees can.`);
120
+ return true;
121
+ }
122
+ /** Test seam: clear both per-process flags. */
123
+ export function resetTeamsSeamSupport() {
124
+ seamDeliveryProven.clear();
125
+ seamUnsupportedLogged.clear();
126
+ }
127
+ /**
128
+ * The partition one activity's engagement row lives in — the CONVERSATION grain
129
+ * (`teams/<tenant>/<conversationId>`), never `domainKeyOf`'s channel grain.
130
+ *
131
+ * 🔴 THE GRAIN IS THE CONTRACT WITH `stand_down`. The verb resolves its own partition through
132
+ * `bindFrom` → `resolvePartition`, which keys on the binding's `surface` (the conversation). A seam
133
+ * that read the widened channel key would look at a different row from the one the stop marked, and
134
+ * the stop would suppress nothing. Null on an unresolvable identity — the caller then keeps today's
135
+ * path, the fail-open posture every carriage leg in this file family takes.
136
+ */
137
+ function teamsSeamPartition(a) {
138
+ try {
139
+ const auth = buildTeamsAuth({
140
+ userId: a.from.id,
141
+ tenantId: a.tenantId ?? a.conversation.tenantId,
142
+ conversationId: a.conversation.id,
143
+ channelId: a.teamsChannelId,
144
+ teamId: a.teamId,
145
+ scope: a.scope,
146
+ membershipType: teamsMembershipType(a.channelData),
147
+ });
148
+ return resolvePartition({ channel: { kind: "teams" }, session: { auth: { current: auth } } });
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ /**
155
+ * Resolve the seam for one verified message activity — READ-ONLY. The mark is a separate call
156
+ * ({@link markTeamsEngaged}) so that exactly one caller writes the row: two writers is how a mention
157
+ * comes to engage a conversation twice per activity, once on each path.
158
+ *
159
+ * ORDERING. A mention is settled before any read: it is always answered by the caller's own path, so
160
+ * its verdict costs no store round-trip. Only a plain message pays for a read, and it pays for
161
+ * exactly one — the ROW, not a boolean, because the row carries both facts the decision needs (is
162
+ * this engaged, and was the agent asked to stand down here) and two reads could disagree.
163
+ *
164
+ * The DECISION ITSELF IS CORE'S. `shouldDeliverReply` owns the disqualifier order and the reasons;
165
+ * this function only translates Teams facts into its input. Nothing here re-states the policy.
166
+ *
167
+ * FAIL-CLOSED on a store error: `getThreadEngagement` reads a failure as `null`, so an unreadable
168
+ * registry holds rather than delivers — the agent does not barge into a conversation it cannot prove
169
+ * it was invited to.
170
+ */
171
+ export async function resolveTeamsEngagement(a, machineAuthored, deps = {}) {
172
+ const anchor = teamsEngagementAnchor(a.scope, a.conversation.id);
173
+ if (!anchor)
174
+ return { kind: "unanchored", reason: "a 1:1 chat carries no engagement anchor — it is answered unconditionally" };
175
+ const partition = teamsSeamPartition(a);
176
+ if (!partition)
177
+ return { kind: "unanchored", reason: "the activity has no resolvable partition — no row to address" };
178
+ if (a.isBotMentioned)
179
+ return { kind: "engage", anchor, org: partition.org };
180
+ // A non-mention message ARRIVED, which is positive proof Microsoft is delivering this scope's
181
+ // ambient traffic — the RSC grant is consented. Recorded before the decision, so a stood-down
182
+ // conversation still counts as proof.
183
+ noteTeamsSeamDelivery(a.scope);
184
+ const read = deps.getEngagement ?? ((p, key) => getThreadEngagement(p, key));
185
+ const row = await read(partition, anchor);
186
+ const decision = shouldDeliverReply({
187
+ isEngaged: row !== null,
188
+ stopped: row?.stopped ?? false,
189
+ authorIsBot: machineAuthored,
190
+ isAppMention: false, // settled above — a mention never reaches this branch
191
+ inThread: true, // an anchored Teams conversation IS the container engagement is kept per
192
+ });
193
+ if (decision.deliver)
194
+ return { kind: "deliver", anchor, reason: decision.reason };
195
+ return { kind: "hold", anchor, reason: decision.reason, stopped: row?.stopped === true };
196
+ }
197
+ /**
198
+ * Mark one conversation ENGAGED on an @mention, and declare the RSC dependency once.
199
+ *
200
+ * `participating: true` is the re-invitation: it promotes the row and CLEARS a previous stand-down,
201
+ * which is what "an @mention brings the agent back" means on every surface that wires this seam.
202
+ * Best-effort throughout — `markThreadEngaged` swallows a store failure, because engagement
203
+ * bookkeeping must never fail a turn.
204
+ *
205
+ * 🔴 THE PROMOTION CARRIES THE ACTIVITY ID, SO REPLAYING ONE THE ROW STILL HOLDS LIFTS NOTHING
206
+ * (OPE-1527). The
207
+ * Bot Framework redelivers an activity whenever it does not get a prompt 200, and a redelivered
208
+ * @mention is a REPLAYED invitation: it used to clear a `stopped` flag nobody asked to lift. The
209
+ * front's dedupe caught the ordinary case and could not catch this one, because it expires — its
210
+ * store TTL is ten minutes and a stand-down has no expiry at all, so a redelivery on a fresh process
211
+ * an hour later re-marked a stood-down conversation as engaged. `engagedBy` closes that durably: the
212
+ * store records the promoting id and refuses to clear `stopped` when a promotion repeats it. A
213
+ * SECOND, GENUINE @mention carries a different activity id and still brings the agent back, which is
214
+ * the behaviour this must not break.
215
+ *
216
+ * BE EXACT ABOUT THE REACH. The store keeps a BOUNDED LIST of recent promoters (OPE-1562), so what
217
+ * cannot be replayed is every promotion the list still holds — see `EngagementRow.recentPromoters`.
218
+ * Mention A, mention B, a stand-down, then a redelivery of A leaves the stop SET: a superseded
219
+ * mention is guarded like the latest one. What remains open is the BOUND — a replay of a mention
220
+ * that `PROMOTER_LIST_MAX` distinct later promotions have pushed off the list clears again.
221
+ */
222
+ export async function markTeamsEngaged(a, deps = {}) {
223
+ const anchor = teamsEngagementAnchor(a.scope, a.conversation.id);
224
+ if (!anchor)
225
+ return null;
226
+ const partition = teamsSeamPartition(a);
227
+ if (!partition)
228
+ return null;
229
+ const now = deps.now ?? (() => new Date().toISOString());
230
+ const mark = deps.markEngaged ?? ((p, key, atIso) => markThreadEngaged(p, key, atIso, { participating: true, engagedBy: a.id }));
231
+ await mark(partition, anchor, now());
232
+ // The seam is armed here. Whether Microsoft will DELIVER the follow-ups is the tenant's RSC grant,
233
+ // which nothing can probe — so say it once rather than leave the gap silent.
234
+ noteTeamsSeamArmed(a.scope);
235
+ return anchor;
236
+ }
237
+ // --- THE ROOM-GRAIN WATCH ROW (OPE-1511) -------------------------------------------------------
238
+ //
239
+ // 🔴 A SECOND GRAIN, FOR A SECOND WAKE. Everything above is keyed per CONVERSATION, because an
240
+ // @mention engages a reply chain. Leg A of the single-window front (teams-front.ts) is a different
241
+ // wake with a different grain: on a vouched channel it wakes on ANY message in the ROOM, including a
242
+ // reply chain nobody ever @mentioned the agent in. That wake read no registry of its own, so
243
+ // `stand_down` found no row, refused, and the only off switch left was an operator un-vouching the
244
+ // surface — a person in the room could not stop it and the agent could not stop it for them.
245
+ //
246
+ // THE RULE IS THAT A STORED STOP EXISTS EXACTLY WHERE A STORED WAKE EXISTS. So Leg A stores a
247
+ // room-grain row on each ambient wake, its own gate reads that row's `stopped` flag, and
248
+ // `tools/stand_down.ts` marks it when the conversation grain holds nothing. The address is core's
249
+ // (`roomEngagementAddress`), derived from the partition by BOTH sides, so the wake and the stop
250
+ // cannot drift onto two spellings of one room.
251
+ //
252
+ // WHAT THE ROOM ROW IS AND IS NOT. Leg E never reads it: the seam above keys on the CONVERSATION and
253
+ // is untouched by any of this. The only two fields that matter here are `stopped` and — through the
254
+ // promotion that clears it — `recentPromoters`.
255
+ //
256
+ // 🔴 `participating` ON A ROOM ROW MEANS NOTHING, AND THAT IS A HAZARD WORTH NAMING. An ambient
257
+ // WATCH writes `participating: false`, but the @mention clear below writes `true`, and the flag is
258
+ // monotonic in both store backends — so after the first @mention in the room it stays `true`
259
+ // forever. Nothing reads it today: no production caller reads `listEngagements`, and Teams never
260
+ // calls `bumpSilentStreak`, which is what the flag exists to gate. It is set as a side effect of
261
+ // being the only way to clear `stopped`, not as a claim that anybody invited the agent into the
262
+ // room. A future warm-shed rule must not read this row and conclude the agent was invited here.
263
+ /**
264
+ * Has a stand-down landed on this partition's ROOM row?
265
+ *
266
+ * 🔴 FAIL-CLOSED, and the direction is the opposite of core's `isThreadStopped` on purpose. That
267
+ * predicate fails OPEN because it decides whether to SILENCE an answer the model has already
268
+ * written, where "unknown" must mean deliver. This one is a WAKE GATE: it decides whether to barge
269
+ * into a room nobody invited the agent into, where "unknown" must mean stay out. It is the same
270
+ * posture the group-chat branch of `teams-front.ts` takes two screens above, in its own words — an
271
+ * unreadable registry cannot prove the agent is still welcome.
272
+ *
273
+ * The cost is real and worth stating: while the operational store is unreadable, EVERY room on this
274
+ * surface holds its ambient wakes, including rooms nobody ever stood down. An @mention is
275
+ * unaffected — the addressed arm never reads this — so a person can still reach the agent through
276
+ * the affordance the stop copy itself points them at. That is the right trade for a gate whose
277
+ * failure mode in the other direction is an agent that keeps talking in a room that asked it to
278
+ * stop, and cannot be re-silenced because the same outage fails the write.
279
+ *
280
+ * It reads the store DIRECTLY rather than through `getThreadEngagement`, because that wrapper
281
+ * swallows an error to `null` and a null is indistinguishable from "no row" — which is exactly the
282
+ * distinction this answer turns on.
283
+ */
284
+ export async function teamsRoomWatchStopped(partition) {
285
+ const room = roomEngagementAddress(partition);
286
+ try {
287
+ return (await getOperationalStore().getEngagement(room.partition.org, room.partition.surfaceKey, room.anchor))?.stopped === true;
288
+ }
289
+ catch (err) {
290
+ console.warn(`[open-everywhere:teams-engagement] room stop unreadable for ${room.partition.surfaceKey} — HOLDING the ambient wake (fail-closed): ${String(err)}`);
291
+ return true;
292
+ }
293
+ }
294
+ /** Store the ROOM-grain wake row for an ambient (undirected) Leg A wake — the row a later stop
295
+ * marks. Best-effort: `markThreadEngaged` swallows a store failure, and a lost watch row costs a
296
+ * stop that could not land, never a turn. */
297
+ export async function markTeamsRoomWatched(partition, atIso) {
298
+ const room = roomEngagementAddress(partition);
299
+ // `participating: false` — a WATCH, not an invitation. It also means this write never clears a
300
+ // `stopped` flag, which is what keeps an ambient wake from lifting the stand-down that should be
301
+ // suppressing it.
302
+ await markThreadEngaged(room.partition, room.anchor, atIso, { participating: false });
303
+ }
304
+ /**
305
+ * Lift a ROOM-grain stand-down on an @mention — the way back in, and the reason the room stop is not
306
+ * a trap. Without it a room stop would outlive every invitation: Leg E's mention mark promotes the
307
+ * CONVERSATION row, which says nothing about the room, so the room would stay silent with no verb
308
+ * able to reach it.
309
+ *
310
+ * `engagedBy` carries the mention's activity id for the same reason the conversation mark does — a
311
+ * redelivered mention must not lift a stand-down twice over.
312
+ */
313
+ export async function clearTeamsRoomStop(partition, atIso, engagedBy) {
314
+ const room = roomEngagementAddress(partition);
315
+ await markThreadEngaged(room.partition, room.anchor, atIso, { participating: true, engagedBy });
316
+ }
317
+ /**
318
+ * The NATIVE path's one-line admission (channels/teams.ts's mention gate): may this plain,
319
+ * non-mention message run a turn?
320
+ *
321
+ * It exists because the native channel is where a Teams turn is actually minted — the front holds no
322
+ * continuation-token primitive it could wake an existing native session with, so admitting the
323
+ * activity through the mention gate IS the wake. The front owns the SUPPRESSION half above it (a
324
+ * stood-down conversation is never forwarded at all) and the MARK, so this call only ever answers
325
+ * the delivery question, and it never writes.
326
+ */
327
+ export async function teamsEngagedAdmits(a, deps = {}) {
328
+ if (a.isBotMentioned)
329
+ return true; // the caller's own mention gate already admitted it
330
+ // 🔴 THE AUTHOR GATE RUNS HERE TOO, over the SAME predicate the front uses. A machine-authored
331
+ // CHANNEL activity IS forwarded to the native path, so this is the line that stops an engaged
332
+ // conversation from admitting the agent's own post and looping on itself.
333
+ const verdict = await resolveTeamsEngagement(a, teamsMachineAuthored(a), deps);
334
+ return verdict.kind === "deliver";
335
+ }
336
+ //# sourceMappingURL=teams-engagement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-engagement.js","sourceRoot":"","sources":["../../../agent/channels/teams-engagement.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { TeamsAccountActiveCheck, TeamsRoomMembership, TeamsRoomMembershipCheck } from "@open-everywhere/core/agent/adapter-api.js";
2
+ import { type TeamsGraphRequester } from "./teams-read.js";
3
+ /**
4
+ * Is `aadObjectId` a member of Teams channel `channelId` (inside team `botTeamId`) RIGHT NOW?
5
+ * Resolves `botTeamId` — the activity's own Bot Connector team id — into the Microsoft 365 group id
6
+ * Graph takes, then paginates `GET /teams/{team}/channels/{channel}/members`, bounded at
7
+ * {@link TEAMS_MEMBER_MAX_PAGES}.
8
+ *
9
+ * Pure but for the injected `req` — exported (not the `req`-defaulted live wrapper below) so the
10
+ * resolution, the pagination and every fail-open arm are unit-testable with a canned requester, no
11
+ * token mint and no network call. The team-id resolution is driven in a test through
12
+ * teams-graph-team-id.ts's own `_seedTeamsGraphTeamIdForTest` seam rather than a second injected
13
+ * dependency here: one team id maps to one group id for the life of the process, so the cache IS
14
+ * the seam.
15
+ */
16
+ export declare function teamsRoomMembershipViaGraph(botTeamId: string, channelId: string, aadObjectId: string, serviceUrl: string | undefined, req: TeamsGraphRequester): Promise<TeamsRoomMembership>;
17
+ /** Live default: {@link teamsRoomMembershipViaGraph} through the real Graph token + transport. */
18
+ export declare const liveTeamsRoomMembership: TeamsRoomMembershipCheck;
19
+ /**
20
+ * Is `aadObjectId`'s Entra account CURRENTLY enabled? `GET /users/{id}?$select=accountEnabled`.
21
+ * Pure but for the injected `req` — same DI shape as {@link teamsRoomMembershipViaGraph}.
22
+ */
23
+ export declare function teamsAccountActiveViaGraph(aadObjectId: string, req: TeamsGraphRequester): Promise<boolean | undefined>;
24
+ /** Live default: {@link teamsAccountActiveViaGraph} through the real Graph token + transport. */
25
+ export declare const liveTeamsAccountActive: TeamsAccountActiveCheck;
26
+ //# sourceMappingURL=teams-fire-eligibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-fire-eligibility.d.ts","sourceRoot":"","sources":["../../../agent/channels/teams-fire-eligibility.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EACV,uBAAuB,EAEvB,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAuBlF;;;;;;;;;;;;GAYG;AACH,wBAAsB,2BAA2B,CAC/C,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,GAAG,EAAE,mBAAmB,GACvB,OAAO,CAAC,mBAAmB,CAAC,CA4B9B;AAED,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,EAAE,wBAC0D,CAAC;AAMjG;;;GAGG;AACH,wBAAsB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAY5H;AAED,iGAAiG;AACjG,eAAO,MAAM,sBAAsB,EAAE,uBAAyG,CAAC"}