Add Google Drive document and folder creation - #301
Conversation
2a8cf1a to
60a28b8
Compare
60a28b8 to
fb3e029
Compare
Preview:
|
|
Some GPT findings: |
fb3e029 to
682c677
Compare
682c677 to
8c7215d
Compare
8c7215d to
ebbbc84
Compare
There was a problem hiding this comment.
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)
| 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); |
| storage: DriveCreationStorage, creationId: number, documentId: string, snapshot: DocSnapshot, | ||
| ): string { | ||
| let edits = new DriveCreationStore(storage).docEdits(creationId); | ||
| let pending = edits.list().map(({ id, action }) => ({ |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 || |
There was a problem hiding this comment.
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.
| `**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)}`, |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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.
|
Submitted 6 actionable inline findings. |
ebbbc84 to
6d7794c
Compare
There was a problem hiding this comment.
Devin Review found 1 new potential issue.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| : await this.#creationCoordinator.reject(this.#creationRuntime(), actionId); | ||
| return restart ? { restart: true } : undefined; |
There was a problem hiding this comment.
🟡 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.
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( |
There was a problem hiding this comment.
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 } : {}), |
There was a problem hiding this comment.
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.
|
Submitted 2 actionable inline findings. |
drive.fileonly for broad creation-capable grants.