Skip to content

feat(server): add Google Drive file create/upload tool#2402

Open
benjaminshafii wants to merge 1 commit into
devfrom
feat/google-drive-create-file
Open

feat(server): add Google Drive file create/upload tool#2402
benjaminshafii wants to merge 1 commit into
devfrom
feat/google-drive-create-file

Conversation

@benjaminshafii

@benjaminshafii benjaminshafii commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds a drive_create_file action to the Google Workspace extension, closing the gap reported by users where the connected Drive scope only exposed drive_search_files, drive_read_file, and drive_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.
  • Supports two input modes:
    • 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).
  • Optional mimeType override and parentId to create the file inside a specific Drive folder.
  • Works under the existing base drive.file OAuth 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 for drive_create_file (inline text, binary file upload, and validation of mutually exclusive content/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.

Review in cubic

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.
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview, Comment Jun 30, 2026 11:30pm
openwork-den Ready Ready Preview, Comment Jun 30, 2026 11:30pm
openwork-den-worker-proxy Ready Ready Preview, Comment Jun 30, 2026 11:30pm
openwork-landing Ready Ready Preview, Comment, Open in v0 Jun 30, 2026 11:30pm

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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(

Comment on lines +1155 to +1157
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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";

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant