From 0ca4c5cf8ce141889688cf2504f8446fdcb2095f Mon Sep 17 00:00:00 2001 From: mavneox Date: Thu, 30 Jul 2026 20:37:56 -0500 Subject: [PATCH] GEN-4797: document the two-phase 428 confirm on destructive vidsheet deletes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deleteLayer (the publicly advertised DELETE) gains its 428 response + confirm_token query param in openapi.yaml; llms.txt and llms-full.txt document the confirm protocol on the layers endpoint and in the MCP vidsheet tool list (gen_delete_variable is publicly advertised there). Co-ships with gen-backend-v2 #2002, which introduces the gate — an unannounced 428 on a documented DELETE would break external agents mid-flow. Co-Authored-By: Claude Opus 5 (1M context) --- public/llms-full.txt | 14 ++++++++++++++ public/llms.txt | 7 ++++++- public/openapi.yaml | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/public/llms-full.txt b/public/llms-full.txt index 9db45e0..735fea2 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -590,6 +590,20 @@ POST .../layers/{layer_id}/duplicate?agent_id={id} PUT .../layers/update_positions?agent_id={id} ``` +**Destructive deletes are two-phase.** When the layer carries content (media, styling, or a generated output), the first DELETE does not execute — it returns `428` with an authoritative preview and a single-use token: + +```json +{ + "error_code": "confirmation_required", + "operation": "video_layer.destroy", + "would_destroy": {"type": "video_layer", "id": 12, "name": "Hook clip", "layer_type": "video", "content_digest": "…"}, + "confirm_token": "…", + "expires_in": 300 +} +``` + +Repeat the same DELETE with `&confirm_token=` to execute. The token is single-use, expires in 5 minutes, and is bound to the exact target state — if the layer changed in between, the request re-gates with a fresh preview (`token_rejected: "target_changed"`). Bare layers (name/type/position only) delete in one step. The same protocol covers destructive column deletes and global-variable deletes (a variable with a value, or one still referenced by `{{name}}` in any cell — its preview lists the referencing cell ids that would break). + ## Generations (trigger + poll) ``` diff --git a/public/llms.txt b/public/llms.txt index 9d0ed6a..54d483a 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -431,6 +431,11 @@ POST /v1/vidsheet/:id/cells/:cell_id/layers?agent_id= GET /v1/vidsheet/:id/cells/:cell_id/layers/:layer_id?agent_id= PATCH /v1/vidsheet/:id/cells/:cell_id/layers/:layer_id?agent_id= DELETE /v1/vidsheet/:id/cells/:cell_id/layers/:layer_id?agent_id= + # Two-phase when the layer carries content: first DELETE returns 428 + # {error_code:"confirmation_required", would_destroy, confirm_token, expires_in}. + # Repeat with &confirm_token= to execute (single-use, 5-min TTL, invalid + # if the layer changed). Bare layers delete in one step. Same protocol + # applies to destructive column and global-variable deletes. POST /v1/vidsheet/:id/cells/:cell_id/layers/:layer_id/duplicate?agent_id= PUT /v1/vidsheet/:id/cells/:cell_id/layers/update_positions?agent_id= @@ -801,7 +806,7 @@ The current MCP exposes 150 `gen_*` tools, 9 prompts, and 3 guidance resources. **Social publishing:** `gen_get_social_connect_url` (OAuth connect flow), `gen_list_connected_socials`, `gen_schedule_post` (platforms: tiktok|instagram|facebook|youtube|x), `gen_list_scheduled_posts`, `gen_get_post_status`, `gen_update_scheduled_post`, `gen_delete_scheduled_post`, `gen_disconnect_social`. On **X**, `gen_schedule_post` also supports up to 4 images per tweet (`media_urls`), **threads** (`thread`: an ordered list of `{text, media_urls?}` segments), and **replies** (`reply_to_tweet_id`). `thread` and `reply_to_tweet_id` are X-only. -**Vidsheets:** `gen_list_templates`, `gen_clone_template`, `gen_create_engine`, `gen_get_engine`, `gen_clone_engine`, `gen_list_columns`, `gen_create_column`, `gen_list_rows`, `gen_create_row`, `gen_get_cell`, `gen_update_cell`, `gen_create_variable`, `gen_update_variable`, `gen_delete_variable`. +**Vidsheets:** `gen_list_templates`, `gen_clone_template`, `gen_create_engine`, `gen_get_engine`, `gen_clone_engine`, `gen_list_columns`, `gen_create_column`, `gen_list_rows`, `gen_create_row`, `gen_get_cell`, `gen_update_cell`, `gen_create_variable`, `gen_update_variable`, `gen_delete_variable`. Destructive deletes (a variable with a value or live `{{name}}` references, a populated column, a content-bearing layer) are two-phase: the first call returns `confirmation_required` with a `would_destroy` preview and a single-use `confirm_token`; repeat with the token to execute. **Watchlists:** `gen_create_watchlist`, `gen_list_watchlists`, `gen_query_watchlist`, `gen_add_watchlist_source`, `gen_remove_watchlist_source`. diff --git a/public/openapi.yaml b/public/openapi.yaml index ac4757f..9af795c 100644 --- a/public/openapi.yaml +++ b/public/openapi.yaml @@ -1224,15 +1224,49 @@ paths: operationId: deleteLayer x-phase: edit summary: Delete a layer + description: >- + Deleting a layer that carries content (media, styling, or a generated + output) is a two-phase operation for API-key callers. The first DELETE + returns 428 with a `would_destroy` preview and a single-use + `confirm_token` (5-minute TTL, bound to the exact target state); repeat + the DELETE with `confirm_token` to execute. If the layer changed in + between, the request re-gates with a fresh preview and token + (`token_rejected: "target_changed"`). Bare layers (name/type/position + only) delete in one step. tags: [Layers] parameters: - $ref: '#/components/parameters/AgentId' - $ref: '#/components/parameters/SheetId' - $ref: '#/components/parameters/CellId' - $ref: '#/components/parameters/LayerId' + - name: confirm_token + in: query + required: false + schema: {type: string} + description: Single-use confirmation token issued by a prior 428 response. responses: '204': description: Layer deleted + '428': + description: >- + Confirmation required — the layer holds content. Repeat the DELETE + with the returned `confirm_token` within `expires_in` seconds. + content: + application/json: + schema: + type: object + properties: + error: {type: string} + error_code: {type: string, enum: [confirmation_required]} + operation: {type: string, example: video_layer.destroy} + would_destroy: + type: object + description: Authoritative server-computed preview of what the delete destroys. + confirm_token: {type: string} + expires_in: {type: integer, example: 300} + token_rejected: + type: string + description: Present when a supplied token was refused (target_changed, invalid_or_expired, already_used_or_expired, actor_mismatch). /vidsheet/{sheet_id}/cells/{cell_id}/layers/{layer_id}/duplicate: post: