Proposals

A proposal is an ops envelope submitted for human review instead of being applied. Nothing about the scene changes when a proposal is created; when a workspace member approves it, the ops land as a normal revision carrying full provenance (who authored the change, which proposal it came from, who approved it). Agents draft; humans decide.

Protected scenes

Every scene has a require_approval flag (default false). When it is on:

Toggle it with PATCH /v1/scenes/:id/protection and body { "requireApproval": true | false }. The toggle is a ratchet: any authenticated owner - including an API-key agent - may turn protection ON (self-restriction is safe, and lets a CI setup script protect the scenes it creates), but turning it OFF requires a signed-in human session. An agent can never unprotect a scene to bypass review.

Lifecycle

A proposal moves through exactly one of these paths:

open ── approve ──▶ approved   (ops applied; resultingRevision recorded)
open ── reject ───▶ rejected   (nothing applied; resolutionReason recorded)
open ── scene moved on ──▶ superseded   (nothing applied; re-propose)

Terminal states are immutable. Approving or rejecting an already-resolved proposal returns 409 PROPOSAL_ALREADY_RESOLVED.

Endpoints

EndpointAuthWhat it does
POST /v1/scenes/:id/proposalskey or sessionCreate. Ops are validated against the scene’s current revision (422 on failure) but NOT applied. A stale baseRevision returns 409 PROPOSAL_BASE_STALE.
GET /v1/scenes/:id/proposalskey or sessionList for one scene, newest first. ?status=open|approved|rejected|superseded.
GET /v1/scenes/:id/proposals/:pidkey or sessionFetch one proposal with ops, status, and resolution.
GET /v1/proposalskey or sessionCross-scene inbox for everything the caller owns. Summary items (ops replaced by opsCount, scene titles attached).
POST /v1/scenes/:id/proposals/:pid/approvesession onlyApply the ops as a new revision with provenance, mark the proposal approved. API keys get 403 SESSION_AUTH_REQUIRED.
POST /v1/scenes/:id/proposals/:pid/rejectsession onlyClose the proposal without applying. Optional { "reason": "..." } is stored as resolutionReason for the agent to read.
POST /v1/scenes/:id/proposals/:pid/previewkey or sessionApply the ops in memory against the current revision, render SVG, and return it with the structural diff. Nothing is persisted.
PATCH /v1/scenes/:id/protectionsee aboveToggle require_approval (ratchet).

MCP tools mirror the agent-side surface: dsp_propose_ops, dsp_list_proposals, dsp_get_proposal (since @zindex-ai/mcp 0.43.0). There is deliberately no MCP approve tool - approval is impossible over API-key auth, which is what makes “agents never approve their own proposals” a server-side guarantee rather than a convention.

Conflict semantics: never a silent rebase

The scene can move past a proposal’s baseRevision before a human reviews it. On approval the server re-validates the proposed ops against the CURRENT revision:

Creating a proposal against a stale baseRevision fails immediately with 409 PROPOSAL_BASE_STALE - re-read the scene first.

One rare fourth code: 409 PROPOSAL_APPLY_FAILED means the ops re-validated cleanly but the executor hit a conflict applying them (usually a write raced the approval). The proposal stays open; retry the approval.

Provenance model

Every revision records who produced it. The four fields appear in GET /v1/scenes/:id/revisions (see Revisions):

Direct writes record author kind and transport with null proposal fields. Revisions written before provenance recording landed have all four fields null.

Derive-proposals: syncing a protected scene with attested lineage

POST /v1/scenes/:id/proposals accepts a second body shape: { "format": "prisma" | "sql", "source": "<full schema snapshot>" } instead of ops. The server runs the deterministic derivation inside the proposal, so the proposed ops and the snapshot’s SHA-256 (deriveFormat, deriveSourceSha256) are recorded by the API rather than claimed by the agent. On approval the lineage carries onto the landed revision, and from there into evidence packs - the pack’s answer to an auditor’s “does this diagram match the source environment?” is the fingerprint of the schema it was derived from.

This is the recovery path when dsp_derive_scene returns 409 PROPOSAL_REQUIRED on a protected scene: re-submit the same format + source as a derive-proposal. Do not hand-copy derive-preview ops into a plain ops-proposal - the change lands the same, but the lineage is lost. A derive-proposal that finds nothing to change returns 200 { "proposed": false } and creates no proposal. Never send ops and format/source together.

Plan limits

Creating a proposal is free. Approval counts toward the workspace’s updatesPerMonth limit - the moment a revision lands is the moment usage is charged.