Proposals (Öneri)
The consent mechanism for agent work (ADR 0027 §2). An Ajan token's live mutations are never executed: each is dry-run previewed and recorded as an item of the token's one open proposal, answered with HTTP 202 {proposed: true, auditId: null}. "No audit row ⇔ nothing applied" stays literally true: a proposal stores envelopes and previews only: never data snapshots, no storefront bump, nothing visible until apply.
Proposal statuses
| status | meaning |
|---|---|
open | the token's working set: every recorded mutation appends an item (one open proposal per token; idle proposals expire after 7 days) |
submitted | proposal.submit with a title + summary; the owner is mailed a review link; expires unreviewed after 14 days |
applying | the owner approved: proposal.approve is the audited consent event (owner sessions only; tokens structurally cannot approve) |
paused | a stale or failed item stopped the run; the owner skips, re-approves, or rejects |
applied | every item ran (or was skipped) |
rejected | owner refused (proposal.reject) |
withdrawn | the agent (or owner) dropped it (proposal.withdraw) |
expired | the open-idle or submitted window lapsed |
Item statuses: pending → applied, or stale (preview drift), failed (execution error), skipped (owner's proposal.item.skip). A proposal holds at most 500 items (proposalMaxItems).
Preview-hash drift
At record time the item stores its dry-run preview and sha256(canonicalJson(preview)). At apply, each item is re-previewed against the current world and the hashes must match; design items additionally require the workspace hash unchanged and liveSeq === baseSeq. A mismatch means the world moved under the proposal (a price edited, an order shipped, the live design advanced): the item goes stale with the current preview attached, the proposal paused. The owner skips the item or re-approves to accept the new preview; POST /v1/proposals/{prp}/devam resumes.
Apply semantics
- Items run in order, stop at the first failure, and are never rolled back: every applied item is an ordinary audited command the owner undoes by the usual means.
- Each item executes as the approver's merchant actor with
idempotencyKey = command_uid(crash-retry replays produce one outcome), attributed{viaProposalId, proposedByToken}: proposer and approver land on ONE audit row. - Route items replay through the same handler functions the admin buttons call, so mail and provider side effects match exactly.
proposal.approvepinsexpectedItemCount: an item appended between render and click invalidates the approval.