feat(server): add Google Drive file create/upload tool#2402
feat(server): add Google Drive file create/upload tool#2402benjaminshafii wants to merge 1 commit into
Conversation
Adds drive_create_file to the Google Workspace extension, closing the gap where the connected Drive scope only exposed search/read/update (plain-text-only) actions with no way to create or upload a new file (e.g. binary formats like .docx). Supports either inline text content or uploading a local workspace file as binary content (base64 MIME multipart upload), reusing the existing workspace-root path validation used by Gmail attachments.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
3 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/server/src/extensions/google-workspace.test.ts">
<violation number="1" location="apps/server/src/extensions/google-workspace.test.ts:457">
P2: Missing `await` on `expect().rejects.toThrow()` — assertion may not run before test completes, creating risk of false-positive passes.</violation>
</file>
<file name="apps/server/src/extensions/google-workspace.ts">
<violation number="1" location="apps/server/src/extensions/google-workspace.ts:1155">
P2: Empty inline content is treated as absent, so the tool cannot create an empty text file and misses the content/path conflict when content is an empty string. Track whether the content field was provided instead of testing truthiness.</violation>
<violation number="2" location="apps/server/src/extensions/google-workspace.ts:1169">
P2: Inline mimeType is not validated before being inserted into the multipart Content-Type line. Reject invalid MIME values so CR/LF or malformed values cannot corrupt or inject multipart headers.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| accounts: [accountRecord("one@example.com", "sub-one")], | ||
| }); | ||
|
|
||
| expect(callGoogleWorkspaceExtensionAction(config, "drive_create_file", { name: "f.txt", content: "a", path: "f.txt" }, {})).rejects.toThrow( |
There was a problem hiding this comment.
P2: Missing await on expect().rejects.toThrow() — assertion may not run before test completes, creating risk of false-positive passes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/extensions/google-workspace.test.ts, line 457:
<comment>Missing `await` on `expect().rejects.toThrow()` — assertion may not run before test completes, creating risk of false-positive passes.</comment>
<file context>
@@ -373,6 +373,95 @@ describe("Google Workspace extension", () => {
+ accounts: [accountRecord("one@example.com", "sub-one")],
+ });
+
+ expect(callGoogleWorkspaceExtensionAction(config, "drive_create_file", { name: "f.txt", content: "a", path: "f.txt" }, {})).rejects.toThrow(
+ new ApiError(400, "invalid_payload", "Provide only one of path or content, not both"),
+ );
</file context>
| expect(callGoogleWorkspaceExtensionAction(config, "drive_create_file", { name: "f.txt", content: "a", path: "f.txt" }, {})).rejects.toThrow( | |
| await expect(callGoogleWorkspaceExtensionAction(config, "drive_create_file", { name: "f.txt", content: "a", path: "f.txt" }, {})).rejects.toThrow( |
| const content = typeof args.content === "string" ? args.content : ""; | ||
| if (!path && !content) throw new ApiError(400, "invalid_payload", "Either path or content is required"); | ||
| if (path && content) throw new ApiError(400, "invalid_payload", "Provide only one of path or content, not both"); |
There was a problem hiding this comment.
P2: Empty inline content is treated as absent, so the tool cannot create an empty text file and misses the content/path conflict when content is an empty string. Track whether the content field was provided instead of testing truthiness.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/extensions/google-workspace.ts, line 1155:
<comment>Empty inline content is treated as absent, so the tool cannot create an empty text file and misses the content/path conflict when content is an empty string. Track whether the content field was provided instead of testing truthiness.</comment>
<file context>
@@ -1110,6 +1148,37 @@ async function googleWorkspaceUpdateFile(config: ServerConfig, args: Record<stri
+ const name = readStringField(args, "name");
+ if (!name) throw new ApiError(400, "invalid_payload", "name is required");
+ const path = readStringField(args, "path");
+ const content = typeof args.content === "string" ? args.content : "";
+ if (!path && !content) throw new ApiError(400, "invalid_payload", "Either path or content is required");
+ if (path && content) throw new ApiError(400, "invalid_payload", "Provide only one of path or content, not both");
</file context>
| const content = typeof args.content === "string" ? args.content : ""; | |
| if (!path && !content) throw new ApiError(400, "invalid_payload", "Either path or content is required"); | |
| if (path && content) throw new ApiError(400, "invalid_payload", "Provide only one of path or content, not both"); | |
| const hasContent = typeof args.content === "string"; | |
| const content = hasContent ? args.content : ""; | |
| if (!path && !hasContent) throw new ApiError(400, "invalid_payload", "Either path or content is required"); | |
| if (path && hasContent) throw new ApiError(400, "invalid_payload", "Provide only one of path or content, not both"); |
| mimeType = gmailAttachmentMimeType(resolvedPath, requestedMimeType || undefined); | ||
| } else { | ||
| body = content; | ||
| mimeType = requestedMimeType || "text/plain"; |
There was a problem hiding this comment.
P2: Inline mimeType is not validated before being inserted into the multipart Content-Type line. Reject invalid MIME values so CR/LF or malformed values cannot corrupt or inject multipart headers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/extensions/google-workspace.ts, line 1169:
<comment>Inline mimeType is not validated before being inserted into the multipart Content-Type line. Reject invalid MIME values so CR/LF or malformed values cannot corrupt or inject multipart headers.</comment>
<file context>
@@ -1110,6 +1148,37 @@ async function googleWorkspaceUpdateFile(config: ServerConfig, args: Record<stri
+ mimeType = gmailAttachmentMimeType(resolvedPath, requestedMimeType || undefined);
+ } else {
+ body = content;
+ mimeType = requestedMimeType || "text/plain";
+ }
+
</file context>
| mimeType = requestedMimeType || "text/plain"; | |
| if (requestedMimeType && !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(requestedMimeType)) throw new ApiError(400, "invalid_payload", "mimeType must be a valid MIME type"); | |
| mimeType = requestedMimeType || "text/plain"; |
Summary
drive_create_fileaction to the Google Workspace extension, closing the gap reported by users where the connected Drive scope only exposeddrive_search_files,drive_read_file, anddrive_update_file(plain-text-only replace), with no way to create a brand-new Drive file or upload binary content (e.g..docx,.pdf, images) without corrupting formatting.content: inline plain text for simple text files.path: a local workspace file (binary-safe), uploaded as base64 multipart content with its MIME type inferred from the extension (reusing the same workspace-root path validation already used for Gmail attachments).mimeTypeoverride andparentIdto create the file inside a specific Drive folder.drive.fileOAuth scope — no new scope/consent required.Test plan
cd apps/server && bun test src/extensions/google-workspace.test.ts— 19 pass, including 3 new tests fordrive_create_file(inline text, binary file upload, and validation of mutually exclusivecontent/path).cd apps/server && pnpm typecheck— passes.Note: per repo guidance, PRs should include a video/screenshot proof of the end-to-end flow. This change is server-side extension/tool surface (Google OAuth + Drive API), not a UI experience, so it's not driven through the desktop app's UI — verified via the unit test suite and typecheck above instead.