Appearance
Proposals
The propose / apply workflow lets a token (typically an agent's) stage a change without mutating live state, hand the resulting blast radius to a human for review, and only commit on approval. See Agents and AI → Propose / apply for the conceptual flow.
The Proposal object:
json
{
"id": "9f7a32b5-…",
"envId": "1234abcd-…",
"kind": "set_default_value_flag",
"resourceType": "flag",
"resourceKey": "ui.theme",
"diff": { "defaultValue": "midnight" },
"status": "pending",
"liveVersion": 142,
"expiresAt": "2026-04-29T15:33:00Z",
"createdAt": "2026-04-29T14:33:00Z",
"proposerTokenId": "5678ef01-…",
"proposerUserId": null,
"blastRadius": [
{
"context": { "userId": "u_42", "plan": "enterprise" },
"live": { "ui.theme": { "value": "classic", "defaultValue": "classic", "reason": { "kind": "default" } } },
"preview": { "ui.theme": { "value": "midnight", "defaultValue": "midnight", "reason": { "kind": "default" } } }
}
],
"reason": "switch default theme; 0 of 1 contexts flip"
}Statuses: pending → applied / cancelled / expired. A proposal is single-use; any second apply or cancel against a non-pending row returns 410 proposal_gone.
Create a proposal
http
POST /api/v1/proposals
Content-Type: application/json
{
"envId": "1234abcd-…",
"kind": "set_default_value_flag",
"resourceKey": "ui.theme",
"diff": { "defaultValue": "midnight" },
"spotCheck": [
{ "userId": "u_42", "plan": "enterprise" }
],
"expiresInSeconds": 3600,
"reason": "switch default theme"
}Bearer scope: propose action. Session role: editor+.
Stages the change without mutating live state. The server runs the proposed change against the supplied spotCheck contexts via the preview pipeline and stores the resulting blastRadius on the proposal row.
kind selects the verb. The supported verbs:
| Resource | Kinds |
|---|---|
| Flag | set_default_value_flag, set_rules_flag, kill_flag, delete_flag |
| Config | set_default_value_config, set_rules_config, delete_config |
| Segment | set_rules_segment, delete_segment |
diff is kind-specific:
set_default_value_*→{ "defaultValue": <typed> }set_rules_*→{ "rules": [<rule>, ...] }(segments also acceptallowList?,denyList?)kill_flag→{}(boolean flags only — setsdefaultValue=false, rules=[])delete_*→{}
spotCheck is required, capped at 50 contexts. expiresInSeconds is optional (1–86400; default 3600). reason is optional free-text.
Response: 201 → Proposal with Location: /api/v1/proposals/{id} header. Errors: 400 invalid_request, 403 scope_denied, 404 not_found, 409 in_use (delete_segment only — segment is referenced), 503 ruleset_not_cached.
Get a proposal
http
GET /api/v1/proposals/{id}Re-read a proposal — useful when an agent host's context window has been compacted and you need to refresh the blast radius before applying.
Bearer scope: read action on the underlying resource.
Response: 200 → ProposalErrors: 403 scope_denied, 404 not_found.
Apply a proposal
http
POST /api/v1/proposals/{id}/applyCommits the proposal's diff via the canonical write path (audit + cache + version bump). The audit row carries reason="proposal:<id>" and the proposal back-links via appliedAuditId.
Bearer scope: the action the apply requires (write for set/kill kinds, delete for delete kinds). A token without that action 403s — a Proposer-level token can stage changes but cannot apply them. Session role: editor+.
Version drift: if the env's version has moved since the proposal was created (someone else wrote to it), the apply returns 409 version_drift with { liveVersion, proposedVersion }. The proposal stays terminal-eligible (still pending until the sweeper expires it); re-propose against the new live state to capture a fresh blast radius.
Response: 200 → ApplyProposalResponse
json
{
"proposalId": "9f7a32b5-…",
"status": "applied",
"appliedVersion": 143,
"appliedAuditId": "5678ef01-…",
"resolvedAt": "2026-04-29T14:33:00Z"
}Errors: 403 scope_denied, 404 not_found, 409 version_drift, 409 in_use (delete_segment with a new referrer landed mid-flight), 410 proposal_gone.
Cancel a proposal
http
POST /api/v1/proposals/{id}/cancel
Content-Type: application/json
{ "note": "human reviewed; not shipping this change" }Reachable by:
- The original proposer (regardless of action scope) — an agent can withdraw its own pending proposal.
- A token with the action the apply would have required — an Operator+ token can reject any pending proposal in scope.
- A session caller with
editor+role.
Body is optional. The optional note is copied to resolverNote (and to the proposal's reason if not already set) and surfaced in the audit row.
Response: 200 → Proposal with status="cancelled" and resolverNote populated. Errors: 403 scope_denied, 404 not_found, 410 proposal_gone.
Expiration
A background sweeper inside the server flips pending rows past their expiresAt to expired. Expired proposals can't be applied. Re-propose if a human takes too long.