Skip to content

flow: run an admitted API step under a governed run and Standard/Regulated - #1361

Merged
abrichr merged 4 commits into
mainfrom
flow/api-step-governed
Oct 19, 2026
Merged

abrichr merged 4 commits into
mainfrom
flow/api-step-governed

Conversation

@abrichr

@abrichr abrichr commented Oct 19, 2026

Copy link
Copy Markdown
Member

Stacked on #1358, which is stacked on #1355. Merge those first.

Third of the small PRs that put the API step, pipelined read-back and self-repair in one production profile. This one handles governed runs.

Why

Every openadapt-flow run carries a run authorization, and the API step fell back to the GUI under any of them ("a governed run uses its qualified GUI path"). Separately, a compiled proposal alone made a bundle fail the Standard/Regulated identity gate, because the gate counted the unadmitted API path as executable and that path had no identity contract.

What changes

  • The authorization names the API step. GovernedRunAuthorization.api_steps lists each admitted API step: the digest of the binding the person admitted, the folded-checks digest, and the record check's contract hashes. build_runtime_authorization fills it in. The runtime runs an API step only when all three still match; otherwise it runs the GUI steps and says why. The field is left out when empty, so other authorizations and Cloud's dispatch binding digest serialize exactly as before.
  • Right record. openadapt-flow api admit --identity KEY=PARAM (for example subject_name=lname) names the parameter that identifies the record. The request must send that parameter's run value as-is in a form field, and the record check must select by it (in match, or in new_records for a create). The runtime checks this before sending and the outcome classifier checks it again. Form field names belong to the page (form_lname), not the record (lname), so for web forms the field and the record check are tied by the shared parameter rather than by name. Both sides use one shared helper.
  • Path projection. A web-form API step that no person admitted is GUI-only in executable_actuation_paths. The runtime never ran it, so the gate and qualification no longer count it. An admitted one without --identity is still refused by the identity gate, whose message now names the step and the flag.
  • Outcome. The classifier accepts api_folded skips only when the API step's own save in the same report ran on the API path and succeeded, and every covered step was folded. The save is then checked like any API action. The default profile (Regulated, durable, governed, each_item) now reads VERIFIED with one person approval.

Named refusals, not silent fallback

These run the GUI steps, each with its reason in api_fallback:

  • Qualification cases. Qualification needs representative and fault cases on the API path for an admitted API step, and a case run doesn't run API spans yet. As a result, a bundle with an admitted API step can't complete qualification today, which was already true on main. A run under a production qualification admission therefore uses the GUI. Supporting API-path cases for folded spans is the next PR.
  • Managed dispatch. Cloud's authorization doesn't list API steps yet.

Tests

tests/test_api_step_governed.py (new, 13 tests):

  • the default profile runs the admitted API step, sends one request, and reads VERIFIED and approved;
  • admitted without a right-record parameter: the gate refuses and names api admit --identity;
  • unadmitted: the gate passes and the run stays on the GUI;
  • not in the authorization: the run uses the GUI;
  • qualification case, production admission and managed dispatch each name their reason;
  • api_steps is serialized only when non-empty;
  • api admit --identity writes the contract; a bad parameter, a malformed flag or an unknown key is refused; a record check that doesn't select by the parameter is refused.

Locally: 1709 passed across the gate, execution profile, qualification, governed authorization, runner/dispatch, approval and API suites. ruff and strict mypy pass.

Synthetic data only.

🤖 Generated with Claude Code

abrichr and others added 2 commits October 19, 2026 12:43
…lated

Every CLI run carries a run authorization, and the API step fell back to
the GUI under any of them. Now it runs there, under these rules.

- The run authorization lists the admitted API steps (api_steps): the
  admitted binding's digest, the folded-checks digest and the record
  check's contract hashes. The runtime runs an API step only when all
  three still match; anything else runs on the GUI with the reason. The
  field is left out when empty, so other authorizations and Cloud's
  dispatch binding digest are unchanged.
- api admit --identity KEY=PARAM names the parameter that identifies the
  record. The request must send its run value as-is in a form field, and
  the record check must select by it (match, or new_records for a
  create). The runtime and the outcome check both verify it; form field
  and record field are tied by the parameter, not by name.
- An API step no person admitted is GUI-only in executable_actuation_paths,
  so a compiled proposal alone no longer fails the Regulated identity gate.
  An admitted one without --identity is refused by that gate, which now
  names the step and the flag.
- The outcome check accepts api_folded skips only when the API step's own
  save in the same report ran on the API path, succeeded, and every
  covered step was folded. The default profile then reads VERIFIED.

Qualification cases, production qualification admissions and managed
dispatch still use the GUI steps, each with a named reason: qualifying an
API step's path needs case support that is the next pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr and others added 2 commits October 19, 2026 13:31
With --identity, a form save's API step now checks that the request and the
record check select the same record in ungoverned and durable runs too, not
only governed ones, so the durable checkpoint has the save's identity
evidence. A save that checks the right record on screen without a contract
still runs on the GUI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from flow/api-step-durable to main October 19, 2026 22:22
@abrichr
abrichr merged commit 142aeea into main Oct 19, 2026
35 checks passed
@abrichr
abrichr deleted the flow/api-step-governed branch October 19, 2026 22:27
abrichr added a commit that referenced this pull request Oct 19, 2026
…ed-step digest

After merging main (#1361): the run authorization's admitted_api_step
compared folded_checks_digest over the steps before the save, while #1372
now digests every replaced step including the save. It uses the same range
as the replayer and the proposal. Tests that edit the save re-propose its
digest first, as openadapt-flow api propose does.

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