Skip to content

Add Google Drive document and folder creation - #301

Open
ndisidore wants to merge 4 commits into
mainfrom
feat/gk-google-drive-creation
Open

Add Google Drive document and folder creation#301
ndisidore wants to merge 4 commits into
mainfrom
feat/gk-google-drive-creation

Conversation

@ndisidore

@ndisidore ndisidore commented Aug 21, 2026

Copy link
Copy Markdown
Member
  • Add approval-gated creation of blank Google Docs, Google Sheets, and folders to account and shared-drive bindings.
  • Keep exact-file bindings and Drive-opened native Docs and Sheets read-only; request drive.file only for broad creation-capable grants.
  • Revalidate destination scope and current Drive capabilities at submission and apply time, persist durable action outcomes, retry provider creates idempotently, and move created items to trash on revert.
  • Update resource declarations, configurator copy, and Google Drive documentation for the new capability boundary.
Screenshot from 2026-08-24 17-10-44 Screenshot from 2026-08-24 17-25-27 Screenshot from 2026-08-24 17-28-28 2026-08-24_17-34
Open in Devin Review

@github-actions github-actions Bot added delivery Changes to CI or release delivery gatekeeper Changes to a gatekeeper integration labels Aug 21, 2026
@ndisidore
ndisidore force-pushed the feat/gk-google-drive-creation branch from 2a8cf1a to 60a28b8 Compare August 21, 2026 22:46
@github-actions github-actions Bot removed the delivery Changes to CI or release delivery label Aug 21, 2026
@ndisidore
ndisidore force-pushed the feat/gk-google-drive-creation branch from 60a28b8 to fb3e029 Compare August 24, 2026 22:02
@github-actions

Copy link
Copy Markdown

Preview: pr301-feat-gk-googl-52b434ba

https://pr301-feat-gk-googl-52b434ba-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

@ndisidore
ndisidore marked this pull request as ready for review August 24, 2026 22:26
@Maximo-Guk

Copy link
Copy Markdown
Member

Some GPT findings:

1. High: Ambiguous create failures can leave rejected files or produce duplicates. A timed-out POST stores no file ID; reject skips marker recovery, while retry may create again before marker search observes the first file. packages/gatekeeper-google/src/drive-creation.ts:244-253, :311-314
2. High: A shared-drive parent can move out of scope between validation and creation. The item is then created outside the binding, and cleanup refuses to trash it because it is out of scope. packages/gatekeeper-google/src/drive-creation.ts:242-251, :334-337
3. Medium: applyDriveCreation() treats a previously rejected action as successful. Concurrent reject/approve requests can therefore mark an action approved even though no file exists. packages/gatekeeper-google/src/drive-creation.ts:228-231
4. Medium: Destination metadata is validated before observation authorization. Failed probes reveal MIME type, trash state, creation marker, and write capability without audit or observer exclusion. packages/gatekeeper-google/src/drive-session.ts:273-280
5. Medium: Trash cleanup is not idempotent. If the file is already trashed, including after a crash between the PATCH and outcome write, retries can fail permanently on canTrash. packages/gatekeeper-google/src/drive-creation.ts:334-343
6. Medium: Creation names are unbounded and approval fencing is quadratic for long backtick runs, allowing oversized approval records and Worker CPU exhaustion. packages/gatekeeper-google/src/drive-creation.ts:145-147, packages/gatekeeper-google/src/approval-format.ts:7-10

@ndisidore
ndisidore force-pushed the feat/gk-google-drive-creation branch from fb3e029 to 682c677 Compare August 26, 2026 23:30

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@ndisidore
ndisidore force-pushed the feat/gk-google-drive-creation branch from 682c677 to 8c7215d Compare August 27, 2026 16:39
@ndisidore
ndisidore force-pushed the feat/gk-google-drive-creation branch from 8c7215d to ebbbc84 Compare September 1, 2026 17:09

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 new potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +486 to +496
async function trashCreatedFile(runtime: DriveCreationRuntime, fileId: string): Promise<void> {
let file = await runtime.api.getFile(fileId);
if (file.id !== fileId || runtime.scope.kind === "file" ||
!isDriveFileInScope(runtime.scope, file)) {
throw new Error("The requested file is outside this Drive binding.");
}
if (file.trashed === true) return;
if (file.capabilities?.canTrash !== true) {
throw new Error("The created Google Drive item cannot currently be moved to trash");
}
await runtime.api.trashFile(fileId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 Revert trusts stale file identity

trashCreatedFile never verifies the stored creation marker. A stale or corrupted ID can trash an unrelated in-scope file.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

storage: DriveCreationStorage, creationId: number, documentId: string, snapshot: DocSnapshot,
): string {
let edits = new DriveCreationStore(storage).docEdits(creationId);
let pending = edits.list().map(({ id, action }) => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High: This replays edits that the provider may already have committed. If batchUpdate() writes the content and named range but its response is lost, the action remains pending; the next read fetches the committed content and overlays the same append again (or invalidates a replacement whose old text is now gone). Filter pending edits against the document's gadgets-write-* markers, as the existing Google Doc path does.

}
let hasLaterEdits = store.listDocEdits(action.driveCreationId)
.some(record => record.id > actionId);
store.finishDocEdit(actionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High: Rejection needs to reconcile the provider marker before deleting this action. After a successful write whose response was lost, the action is still pending and the user can discard it; this path then reports rejection even though the document mutation remains applied.

}))
.filter(({ id, outcome }) => {
if (!Number.isFinite(id) || retainedCreationIds.has(id)) return false;
return "actionType" in outcome ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: This makes created Sheets and folders prunable after 100 newer terminal records, but their approvals advertise implementsRevert: true. Once pruned, revertDriveCreation() has lost the file ID and can never perform that advertised revert; getCreationResult() also turns a valid handle into an unknown action. Retain the creation identity for as long as the approved action can be reverted.

Comment on lines +3737 to +3747
`**Old:** ${previewMarkdown(oldMarkdown, 80)}\n\n` +
`**New:** ${previewMarkdown(newMarkdown, 80)}`,
);
}

async appendText(markdown: string): Promise<void> {
await this.#snapshot();
await this.#submitEdit(
{ type: "appendText", markdown },
"Append to app-created Google Doc",
`Append content to the app-created document:\n\n${previewMarkdown(markdown, 100)}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: These approval descriptions show only the first 80/100 characters and interpolate them as raw Markdown. An approver cannot see arbitrary content after the preview, and content can forge the surrounding labels/text, while the full payload is applied. ActionDescription requires a complete description; bound the mutation itself and render the complete values with formatApprovalField().

}
}

store.finishDocEdit(actionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: Completing the action removes only local state; the gadgets-write-* named range created above is never deleted. Every successful edit therefore permanently adds provider metadata and enlarges all later documents.get responses until document or response limits are reached. Persist a completion receipt, then clean up the exact marker like the existing Google Doc implementation.

): Promise<void> {
resolveDriveCreatedDocument(this.storage, this.creationId);
let store = new DriveCreationStore(this.storage);
let actionId = store.submitDocEdit({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: There is no capacity or payload bound for created-Doc edits. The existing 100-action check counts only creation actions, so a blocked gadget can persist unlimited edit payloads, and every simulated read enumerates and replays the entire backlog. Add an edit/action limit (and a payload bound) before storing this record.

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Submitted 6 actionable inline findings.

github run

Base automatically changed from feat/gk-google-drive-native-sessions to main September 1, 2026 18:22
@ndisidore
ndisidore force-pushed the feat/gk-google-drive-creation branch from ebbbc84 to 6d7794c Compare September 1, 2026 18:22

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +3535 to +3536
: await this.#creationCoordinator.reject(this.#creationRuntime(), actionId);
return restart ? { restart: true } : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Rejected creation strands queued edits

When rejection returns { restart: true }, the Workshop caller ignores it. Auto-approved edits behind that creation remain pending without another drain trigger.

Prompt for agents
Wire the Gatekeeper.rejectAction restart result into Workshop action processing. packages/gatekeeper-google/src/google.ts now returns restart when rejecting a creation or edit changes the simulated action chain, but packages/workshop-backend/src/overseer.ts AwaitedApiImpl.rejectAction currently discards the callback return and never restarts auto-approval draining. Consume the result and trigger the same gatekeeper-scoped drain used after submissions/rule changes, while preserving the rejection record update ordering. Add an integration test with a manually rejected creation followed by auto-eligible document edits.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

async applyAction(actionId: number): Promise<void> {
let store = new DriveCreationStore(this.ctx.storage.kv);
if (store.isDocEdit(actionId)) {
await this.#creationCoordinator.run(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High: Callbacks are serialized by action ID, so edits belonging to the same created Doc can still interleave. For example, while append A is applying, rejecting B can fetch the still-pre-A snapshot; after A commits, B resumes its rebuild from that stale snapshot and invalidates a later replace C that targets A. C then completes via the invalidated-action branch without applying its still-valid mutation. Serialize apply/reject/revert by driveCreationId (or with one binding-wide mutex), not by each action ID.

writeId: action.writeId,
...(action.invalidatedReason ? { invalidatedReason: action.invalidatedReason } : {}),
...action.edit,
...(action.edit.type === "appendText" ? { preserveSpacing: true as const } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: preserveSpacing makes simulation concatenate the raw fragment, but materialization parses that fragment as standalone Markdown before inserting it into the current final paragraph. With existing content text, appending # Heading simulates text# Heading, while markdownToDocRequests() treats the fragment as a heading and applies heading style to the whole existing paragraph, so the approved document reads back differently. Normalize the append boundary identically for simulation and provider requests.

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Submitted 2 actionable inline findings.

github run

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

Labels

gatekeeper Changes to a gatekeeper integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants