fix(msteams): handle fileConsent/invoke callback for bot-to-user file upload (#55386)#64087
Conversation
Greptile SummaryThis PR fixes a cross-process bug in the MSTeams Confidence Score: 5/5Safe to merge — the core fix is correct, tests are comprehensive, and remaining findings are P2 quality-only concerns. No P0 or P1 issues found. The malformed-accept edge case (else branch conflation) and the missing isSafeUploadId guard on pruned keys are both P2 suggestions — neither affects the primary file-upload path or introduces a practical data-loss or security risk under normal Teams behaviour. extensions/msteams/src/monitor-handler.ts (else branch logic) and extensions/msteams/src/pending-uploads-fs.ts (pruneExpired key validation)
|
| function pruneExpired( | ||
| index: PendingUploadFsIndex, | ||
| nowMs: number, | ||
| ttlMs: number, | ||
| ): { index: PendingUploadFsIndex; expiredIds: string[] } { | ||
| const kept: Record<string, PendingUploadFsEntry> = {}; | ||
| const expired: string[] = []; | ||
| for (const [id, entry] of Object.entries(index.entries)) { | ||
| if (nowMs - entry.createdAt > ttlMs) { | ||
| expired.push(id); | ||
| continue; | ||
| } | ||
| kept[id] = entry; | ||
| } | ||
| return { index: { version: 1, entries: kept }, expiredIds: expired }; | ||
| } |
There was a problem hiding this comment.
Index keys not validated before blob unlink in pruneExpired
pruneExpired collects id values directly from Object.entries(index.entries) and passes them to unlinkBlobIfPresent without running isSafeUploadId. A tampered index.json with a path-traversal key would cause blobPath(storeDir, id) to resolve outside the store directory. isSafeUploadId already exists for exactly this purpose — consider applying it to pruned keys as well:
for (const [id, entry] of Object.entries(index.entries)) {
if (!isSafeUploadId(id)) {
expired.push(id); // drop invalid keys from the index but skip blob unlink
continue;
}
if (nowMs - entry.createdAt > ttlMs) {
expired.push(id);
continue;
}
kept[id] = entry;
}Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/pending-uploads-fs.ts
Line: 88-103
Comment:
**Index keys not validated before blob unlink in pruneExpired**
`pruneExpired` collects `id` values directly from `Object.entries(index.entries)` and passes them to `unlinkBlobIfPresent` without running `isSafeUploadId`. A tampered `index.json` with a path-traversal key would cause `blobPath(storeDir, id)` to resolve outside the store directory. `isSafeUploadId` already exists for exactly this purpose — consider applying it to pruned keys as well:
```typescript
for (const [id, entry] of Object.entries(index.entries)) {
if (!isSafeUploadId(id)) {
expired.push(id); // drop invalid keys from the index but skip blob unlink
continue;
}
if (nowMs - entry.createdAt > ttlMs) {
expired.push(id);
continue;
}
kept[id] = entry;
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f34fb7255
ℹ️ 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".
| // Write the blob FIRST (best-effort crash safety: if we crash between | ||
| // blob write and index update, a stale blob without an index entry is | ||
| // just disk litter that the next prune cycle can skip). | ||
| await fs.promises.writeFile(blobPath(storeDir, id), new Uint8Array(params.buffer)); |
There was a problem hiding this comment.
Write pending-upload blobs with 0600 permissions
The new filesystem store writes raw attachment bytes with fs.promises.writeFile(...) defaults, which creates files using process umask (commonly world/group-readable like 0644), and mkdir(..., { recursive: true }) also defaults to permissive directory modes. In deployments where OPENCLAW_STATE_DIR (or its parents) is shared/traversable by other users, pending uploads can be read before consent is handled, exposing sensitive file contents; this path should use explicit restrictive modes for both directory and blob files.
Useful? React with 👍 / 👎.
| const { activity, uploadId } = await prepareFileConsentActivityFs({ | ||
| media: { buffer: media.buffer, filename: fileName, contentType: media.contentType }, | ||
| conversationId, | ||
| description: messageText || undefined, |
There was a problem hiding this comment.
Fallback to in-memory consent store when FS write fails
This path now always persists pending uploads to disk before sending the consent card, but there is no fallback if that write fails (for example read-only/unavailable state dir), so sendMessageMSTeams aborts and sends nothing. That is a regression for in-process sends (such as outbound/action flows in the running gateway) where the existing in-memory pending-upload store would still allow consent handling to work even without filesystem persistence.
Useful? React with 👍 / 👎.
- Adds consentCardActivityId to PendingUpload so the consent card activity can be replaced in-place after upload succeeds - Uses context.updateActivity() to replace the FileConsentCard with the file info card; falls back to sendActivity if update fails - Adds updateActivity to MSTeamsTurnContext type - Fixes timer leak in pending-uploads: clears TTL setTimeout on explicit removal and on clearPendingUploads() - Adds pending-uploads.test.ts covering all new timer/cleanup paths
…th updateActivity test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3f34fb7 to
cbed481
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cbed48138e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const activity = await buildActivity( | ||
| message, | ||
| params.conversationRef, | ||
| params.tokenProvider, | ||
| params.sharePointSiteId, |
There was a problem hiding this comment.
Keep buildActivity inside retry loop
sendMessageInContext now builds the activity before entering sendWithRetry, so retries only wrap ctx.sendActivity. buildActivity includes non-trivial prep steps (including OneDrive/SharePoint upload paths) that can fail transiently, and those failures now bypass the configured retry path entirely. In environments where media upload/auth calls intermittently return retryable failures, this regresses reliability by failing the whole send on the first error instead of retrying the full attempt.
Useful? React with 👍 / 👎.
… upload (openclaw#55386) (openclaw#64087) * fix(msteams): update FileConsentCard after user accepts upload - Adds consentCardActivityId to PendingUpload so the consent card activity can be replaced in-place after upload succeeds - Uses context.updateActivity() to replace the FileConsentCard with the file info card; falls back to sendActivity if update fails - Adds updateActivity to MSTeamsTurnContext type - Fixes timer leak in pending-uploads: clears TTL setTimeout on explicit removal and on clearPendingUploads() - Adds pending-uploads.test.ts covering all new timer/cleanup paths * msteams: wire consentCardActivityId from send response + add happy-path updateActivity test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(msteams): retry consent uploads end-to-end --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
… upload (openclaw#55386) (openclaw#64087) * fix(msteams): update FileConsentCard after user accepts upload - Adds consentCardActivityId to PendingUpload so the consent card activity can be replaced in-place after upload succeeds - Uses context.updateActivity() to replace the FileConsentCard with the file info card; falls back to sendActivity if update fails - Adds updateActivity to MSTeamsTurnContext type - Fixes timer leak in pending-uploads: clears TTL setTimeout on explicit removal and on clearPendingUploads() - Adds pending-uploads.test.ts covering all new timer/cleanup paths * msteams: wire consentCardActivityId from send response + add happy-path updateActivity test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(msteams): retry consent uploads end-to-end --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
… upload (openclaw#55386) (openclaw#64087) * fix(msteams): update FileConsentCard after user accepts upload - Adds consentCardActivityId to PendingUpload so the consent card activity can be replaced in-place after upload succeeds - Uses context.updateActivity() to replace the FileConsentCard with the file info card; falls back to sendActivity if update fails - Adds updateActivity to MSTeamsTurnContext type - Fixes timer leak in pending-uploads: clears TTL setTimeout on explicit removal and on clearPendingUploads() - Adds pending-uploads.test.ts covering all new timer/cleanup paths * msteams: wire consentCardActivityId from send response + add happy-path updateActivity test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(msteams): retry consent uploads end-to-end --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
Summary
Bot-to-user file uploads via
openclaw message send --mediawere silently failing because thefileConsent/invokecallback was never handled across processes. The CLI sender registered the pending upload in its own memory, but the webhook receiver ran in a different process and never saw it.Fix
pending-uploads-fs.ts) under<stateDir>/msteams-pending-uploads/so CLI sender and webhook receiver share statefileConsent/invokehandler: on accept → PUT file to upload URL + send FileInfoCard; on decline → drop pending uploadTest plan
Fixes #55386
🤖 Generated with Claude Code