feat(msteams): add thread copilot approvals#66210
feat(msteams): add thread copilot approvals#66210sudie-codes wants to merge 3 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryAdds a durable approval flow for Microsoft Teams channel-thread side effects (post summary, create poll, pin message, upload artifact), backed by a new generic Confidence Score: 5/5Safe to merge; all findings are P2 style and cleanup suggestions that do not affect correctness at runtime. No P0/P1 issues found. The four inline comments are all P2: auth-check ordering that affects only audit-trail attribution (not security enforcement), an extensions/msteams/src/thread-approval.ts — four P2 style issues noted inline. Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/msteams/src/thread-approval.ts
Line: 502-526
Comment:
**Authorization check runs after expiry handling**
The `approvalExpired` branch (lines 502–522) runs before the approver-ID guard (line 523), so any sender who clicks an expired card triggers `claimActionApprovalFlow` with their `ctx.senderId` recorded as `actorId` — embedding an unauthorized user's ID in the expiry audit record. Moving the approver check before the expiry block prevents this.
```suggestion
if (!ctx.senderId || !loaded.snapshot.approverIds.includes(ctx.senderId)) {
await ctx.respond.reply({ text: "You are not allowed to approve this action." });
return { handled: true };
}
const now = Date.now();
const approvalExpired =
loaded.state.status === "expired" ||
(loaded.state.expiresAt !== undefined && now >= loaded.state.expiresAt);
if (approvalExpired) {
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/msteams/src/thread-approval.ts
Line: 571
Comment:
**`as never` type escape**
`as never` is being used as a shortcut to satisfy the `JsonValue` parameter, but `never` is assignable to every type, making it a silent type bypass that hides future mismatches. Prefer an explicit widening cast or annotate `executeThreadAction`'s return type as `{ ok: true; result: JsonValue }` upstream so no cast is needed here.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/msteams/src/thread-approval.ts
Line: 107-112
Comment:
**Unused `toolContext` parameter**
`normalizeApproverIds` accepts `toolContext` in its params object but never uses it — only `rawParams` is forwarded to `readMSTeamsApproverIds`. If context-based approver resolution is not planned, the parameter can be removed to avoid misleading callers.
```suggestion
function normalizeApproverIds(params: {
rawParams: Record<string, unknown>;
}): string[] {
return readMSTeamsApproverIds(params.rawParams);
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/msteams/src/thread-approval.ts
Line: 279-283
Comment:
**`normalizeMediaLocalRoots` called twice**
`normalizeMediaLocalRoots(params.toolContext)` is invoked twice — once to check truthiness and again to read the value. Extracting it to a variable avoids the redundant call:
```suggestion
const mediaLocalRoots = normalizeMediaLocalRoots(params.toolContext);
return {
...base,
operation,
text,
mediaUrl,
...(readTrimmedString(params.rawParams.filename)
? { filename: readTrimmedString(params.rawParams.filename) }
: {}),
...(mediaLocalRoots ? { mediaLocalRoots } : {}),
};
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(msteams): dedupe and expire thread a..." | Re-trigger Greptile |
| const now = Date.now(); | ||
| const approvalExpired = | ||
| loaded.state.status === "expired" || | ||
| (loaded.state.expiresAt !== undefined && now >= loaded.state.expiresAt); | ||
| if (approvalExpired) { | ||
| const claimed = claimActionApprovalFlow<ThreadActionSnapshot>({ | ||
| taskFlow, | ||
| flowId: decoded.flowId, | ||
| expectedRevision: decoded.expectedRevision, | ||
| snapshotHash: decoded.snapshotHash, | ||
| actorId: ctx.senderId, | ||
| now, | ||
| }); | ||
| await editApprovalCard( | ||
| ctx, | ||
| claimed.code === "expired" || approvalExpired | ||
| ? "Approval expired." | ||
| : `Approval could not be applied (${claimed.code}).`, | ||
| ); | ||
| return { handled: true }; | ||
| } | ||
| if (!ctx.senderId || !loaded.snapshot.approverIds.includes(ctx.senderId)) { | ||
| await ctx.respond.reply({ text: "You are not allowed to approve this action." }); | ||
| return { handled: true }; | ||
| } |
There was a problem hiding this comment.
Authorization check runs after expiry handling
The approvalExpired branch (lines 502–522) runs before the approver-ID guard (line 523), so any sender who clicks an expired card triggers claimActionApprovalFlow with their ctx.senderId recorded as actorId — embedding an unauthorized user's ID in the expiry audit record. Moving the approver check before the expiry block prevents this.
| const now = Date.now(); | |
| const approvalExpired = | |
| loaded.state.status === "expired" || | |
| (loaded.state.expiresAt !== undefined && now >= loaded.state.expiresAt); | |
| if (approvalExpired) { | |
| const claimed = claimActionApprovalFlow<ThreadActionSnapshot>({ | |
| taskFlow, | |
| flowId: decoded.flowId, | |
| expectedRevision: decoded.expectedRevision, | |
| snapshotHash: decoded.snapshotHash, | |
| actorId: ctx.senderId, | |
| now, | |
| }); | |
| await editApprovalCard( | |
| ctx, | |
| claimed.code === "expired" || approvalExpired | |
| ? "Approval expired." | |
| : `Approval could not be applied (${claimed.code}).`, | |
| ); | |
| return { handled: true }; | |
| } | |
| if (!ctx.senderId || !loaded.snapshot.approverIds.includes(ctx.senderId)) { | |
| await ctx.respond.reply({ text: "You are not allowed to approve this action." }); | |
| return { handled: true }; | |
| } | |
| if (!ctx.senderId || !loaded.snapshot.approverIds.includes(ctx.senderId)) { | |
| await ctx.respond.reply({ text: "You are not allowed to approve this action." }); | |
| return { handled: true }; | |
| } | |
| const now = Date.now(); | |
| const approvalExpired = | |
| loaded.state.status === "expired" || | |
| (loaded.state.expiresAt !== undefined && now >= loaded.state.expiresAt); | |
| if (approvalExpired) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/thread-approval.ts
Line: 502-526
Comment:
**Authorization check runs after expiry handling**
The `approvalExpired` branch (lines 502–522) runs before the approver-ID guard (line 523), so any sender who clicks an expired card triggers `claimActionApprovalFlow` with their `ctx.senderId` recorded as `actorId` — embedding an unauthorized user's ID in the expiry audit record. Moving the approver check before the expiry block prevents this.
```suggestion
if (!ctx.senderId || !loaded.snapshot.approverIds.includes(ctx.senderId)) {
await ctx.respond.reply({ text: "You are not allowed to approve this action." });
return { handled: true };
}
const now = Date.now();
const approvalExpired =
loaded.state.status === "expired" ||
(loaded.state.expiresAt !== undefined && now >= loaded.state.expiresAt);
if (approvalExpired) {
```
How can I resolve this? If you propose a fix, please make it concise.| expectedRevision: claimed.flow.revision, | ||
| snapshotHash: claimed.snapshotHash, | ||
| actorId: ctx.senderId, | ||
| result: executed.result as never, |
There was a problem hiding this comment.
as never is being used as a shortcut to satisfy the JsonValue parameter, but never is assignable to every type, making it a silent type bypass that hides future mismatches. Prefer an explicit widening cast or annotate executeThreadAction's return type as { ok: true; result: JsonValue } upstream so no cast is needed here.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/thread-approval.ts
Line: 571
Comment:
**`as never` type escape**
`as never` is being used as a shortcut to satisfy the `JsonValue` parameter, but `never` is assignable to every type, making it a silent type bypass that hides future mismatches. Prefer an explicit widening cast or annotate `executeThreadAction`'s return type as `{ ok: true; result: JsonValue }` upstream so no cast is needed here.
How can I resolve this? If you propose a fix, please make it concise.| function normalizeApproverIds(params: { | ||
| rawParams: Record<string, unknown>; | ||
| toolContext: ThreadToolContext; | ||
| }): string[] { | ||
| return readMSTeamsApproverIds(params.rawParams); | ||
| } |
There was a problem hiding this comment.
normalizeApproverIds accepts toolContext in its params object but never uses it — only rawParams is forwarded to readMSTeamsApproverIds. If context-based approver resolution is not planned, the parameter can be removed to avoid misleading callers.
| function normalizeApproverIds(params: { | |
| rawParams: Record<string, unknown>; | |
| toolContext: ThreadToolContext; | |
| }): string[] { | |
| return readMSTeamsApproverIds(params.rawParams); | |
| } | |
| function normalizeApproverIds(params: { | |
| rawParams: Record<string, unknown>; | |
| }): string[] { | |
| return readMSTeamsApproverIds(params.rawParams); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/thread-approval.ts
Line: 107-112
Comment:
**Unused `toolContext` parameter**
`normalizeApproverIds` accepts `toolContext` in its params object but never uses it — only `rawParams` is forwarded to `readMSTeamsApproverIds`. If context-based approver resolution is not planned, the parameter can be removed to avoid misleading callers.
```suggestion
function normalizeApproverIds(params: {
rawParams: Record<string, unknown>;
}): string[] {
return readMSTeamsApproverIds(params.rawParams);
}
```
How can I resolve this? If you propose a fix, please make it concise.| : {}), | ||
| ...(normalizeMediaLocalRoots(params.toolContext) | ||
| ? { mediaLocalRoots: normalizeMediaLocalRoots(params.toolContext) } | ||
| : {}), | ||
| }; |
There was a problem hiding this comment.
normalizeMediaLocalRoots called twice
normalizeMediaLocalRoots(params.toolContext) is invoked twice — once to check truthiness and again to read the value. Extracting it to a variable avoids the redundant call:
| : {}), | |
| ...(normalizeMediaLocalRoots(params.toolContext) | |
| ? { mediaLocalRoots: normalizeMediaLocalRoots(params.toolContext) } | |
| : {}), | |
| }; | |
| const mediaLocalRoots = normalizeMediaLocalRoots(params.toolContext); | |
| return { | |
| ...base, | |
| operation, | |
| text, | |
| mediaUrl, | |
| ...(readTrimmedString(params.rawParams.filename) | |
| ? { filename: readTrimmedString(params.rawParams.filename) } | |
| : {}), | |
| ...(mediaLocalRoots ? { mediaLocalRoots } : {}), | |
| }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/thread-approval.ts
Line: 279-283
Comment:
**`normalizeMediaLocalRoots` called twice**
`normalizeMediaLocalRoots(params.toolContext)` is invoked twice — once to check truthiness and again to read the value. Extracting it to a variable avoids the redundant call:
```suggestion
const mediaLocalRoots = normalizeMediaLocalRoots(params.toolContext);
return {
...base,
operation,
text,
mediaUrl,
...(readTrimmedString(params.rawParams.filename)
? { filename: readTrimmedString(params.rawParams.filename) }
: {}),
...(mediaLocalRoots ? { mediaLocalRoots } : {}),
};
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
openclaw/extensions/msteams/src/monitor-handler.ts
Lines 406 to 410 in c308ffa
The invoke dispatcher now only branches for message/submitAction and adaptiveCard/action, so Teams OAuth callbacks (signin/tokenExchange and signin/verifyState) fall through without token exchange/persistence. Any tenant using Teams SSO login cards will stop completing the sign-in flow.
ℹ️ 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".
| "undici": "^8.0.2", | ||
| "uuid": "^13.0.0", | ||
| "ws": "^8.20.0", | ||
| "yaml": "^2.8.3", |
There was a problem hiding this comment.
Restore required OpenAI runtime dependency
The root dependency list no longer includes openai, but production code still imports it (for example src/agents/openai-transport-stream.ts and extensions/memory-lancedb/index.ts). On a clean install this causes runtime/module-resolution failures as soon as OpenAI transport or LanceDB embedding paths are loaded.
Useful? React with 👍 / 👎.
| "test:fast": "node scripts/run-vitest.mjs run --config vitest.unit.config.ts", | ||
| "test:force": "node --import tsx scripts/test-force.ts", | ||
| "test:gateway": "node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts", | ||
| "test:gateway": "node scripts/run-vitest.mjs run --config vitest.gateway.config.ts", |
There was a problem hiding this comment.
Use existing Vitest config paths in npm scripts
Several test scripts now point to config files like vitest.gateway.config.ts and vitest.unit.config.ts, but this repo’s configs are under test/vitest/ (for example test/vitest/vitest.gateway.config.ts). As written, these scripts fail before tests run, which breaks local verification and CI lanes that rely on them.
Useful? React with 👍 / 👎.
| const pendingFile = getPendingUpload(uploadId); | ||
| if (pendingFile) { |
There was a problem hiding this comment.
Restore FS-backed lookup for file-consent uploads
File consent handling now reads pending uploads only from in-memory state (getPendingUpload). The proactive CLI path persists uploads to disk via storePendingUploadFs for cross-process delivery, so accepted fileConsent/invoke callbacks after the sender process exits will be treated as expired and the upload will never complete.
Useful? React with 👍 / 👎.
|
Thanks for the Teams work. Closing this as a broad dirty external-plugin candidate rather than a safe core PR; please resubmit the smallest current-main Teams approval slice with owner context. |
What this changes
This adds a helper for long Teams channel threads so someone can quickly get the summary, decisions, and next steps from a conversation. It can also prepare follow-up actions like posting a summary, creating a poll, pinning something important, or sharing an artifact.
Why it matters
Busy threads become easier to understand and act on. People can get the important outcomes from a conversation quickly, and the assistant can help prepare the next step without acting publicly on its own.
Safety
Notes
This branch carries the supporting approval groundwork it needs so the feature can be reviewed as a full experience.
Verification