Skip to content

flow: approve an API step's exact request under per-item approval - #1355

Merged
abrichr merged 3 commits into
mainfrom
flow/api-step-approval
Oct 19, 2026
Merged

abrichr merged 3 commits into
mainfrom
flow/api-step-approval

Conversation

@abrichr

@abrichr abrichr commented Oct 19, 2026

Copy link
Copy Markdown
Member

First of the small PRs that put the API step, pipelined read-back and self-repair in one production profile. This one handles approval gating.

Why

The API step compiled from network capture (#1269, docs/API_TIER.md) fell back to the GUI steps under each_item and problems_only. The person approved a screenshot of a form the API step never fills. Flow 2 gates every saving run by default, so the default profile never used the API step.

What changes

The person now approves the exact request the API step sends.

  1. Before asking, the run prepares the request. It reads the form page, builds the body and runs the pre-checks; only reads are sent. If the request can't be prepared, nobody is asked and the GUI steps run as before.
  2. The approval request has form_state_kind: "api_request" and an api_request block: method, path, and payload_sha256, the digest of every byte sent (session values included). Its form_state_sha256 is the operation digest, which binds the payload, the record the check reads back and the record check's contract hashes.
  3. Desktop shows an image of the request and the phone shows rows: the values the run supplies, the record the check reads back, and the request line. Session values are counted but never shown.
  4. After the approval the run prepares the request again, with a fresh form and fresh pre-checks. The last-point gate compares the digests. If the request, the record or the record check changed, the item stops before the save (form_changed_after_approval). A request that can't be prepared again also stops (form_state_unreadable). In both cases nothing is sent and the GUI steps don't redo it.
  5. The run sends exactly the request it checked, once.

WebFormActuator is split into prepare() and send(), and actuate() calls both. An actuator without prepare() still falls back to the GUI under a gate, with a stated reason.

Not in this PR

Governed (every CLI run), durable and multi-surface runs still use the GUI steps. The next PRs cover durable checkpoints and reconciliation, the admitted-bundle binding, and the compatibility matrix test with docs/COMPATIBILITY.md.

Tests

  • tests/test_api_step_approval.py (new): an approved request is sent once and its digest equals the digest of the bytes sent; a session value that changes while the person looks means nothing is sent and the GUI doesn't redo it; a duplicate that appears after approval means nothing is sent; a decline sends nothing; a request that can't be prepared asks nobody and runs the GUI; the operation digest changes with the record, the record check or the body.
  • tests/test_api_tier_web_form.py: the old "only an ungated run uses API steps" test now checks that a gated run uses API steps only when the actuator can prepare the request.
  • Locally: 260 passed across the API tier, approval mode, console approvals and delegated approver suites. ruff check, ruff format --check and strict mypy on the four changed modules all pass.

Synthetic data only.

🤖 Generated with Claude Code

abrichr and others added 3 commits October 19, 2026 12:03
Under each_item and problems_only the API step used to fall back to the
GUI steps, because the person approved a screenshot of a form the API
step never fills. Flow 2 gates every saving run, so the API step never
ran in the default profile.

Now the run prepares the request first (reads only: form page, body,
pre-checks) and the person approves that request. The approval request
has form_state_kind "api_request", an api_request block (method, path,
payload_sha256 over every byte sent) and an operation digest that binds
the payload, the record the check reads back and the record check's
contract hashes. After the approval the run prepares the request again;
the last-point gate compares digests, and a changed or unpreparable
request stops the item before the save, with no GUI redo. The run then
sends exactly the request it checked, once.

WebFormActuator splits into prepare() and send(); actuate() is both. An
actuator without prepare() still falls back to the GUI under a gate.

Governed, durable and multi-surface runs still use the GUI steps; those
come in the next pull requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant