Repository navigation
Flow: person-signed approver grants, no self-approval, one-tap phone approval on the decision portal - #1234
Merged
Conversation
…phone approval - An agent, rule or system approves saves only under an approver grant a person signed with a policy_signers key (reusing the JIT authority's Ed25519 registry). An unsigned, tampered, expired or agent-signed grant means no delegate approval; the run falls back to a person. - Nobody approves their own work: each run records who started it, and a delegate can't answer a run it (or whoever it acts for) started, or a run whose starter is unknown. - One-tap phone approval (opt-in): waiting saves go to Cloud over the runner's own connection; a person approves from their phone after an AAL2 sign-in. The run accepts only a Cloud-signed decision for the exact request digest from a pinned key, writes it once, and re-checks the form before the save. Desktop keeps working and wins if it answers first. - item-approval show and the report name who decided: a person (Desktop or phone) or a named delegate with its grant hash.
…, not a Cloud relay
Drop the separate Cloud phone relay (phone_relay, cloud_decision,
--phone-approval, runtime.phone_approval, item-approval pin-cloud-keys and
apply-cloud-decision). A waiting save is now a HumanDecisionTaskV1 with
task_kind save_approval / question approve_save, served by the attended
console the decision portal already relays to:
GET /api/save-approvals
GET /api/save-approvals/{request_id} signed task + local presentation
GET /api/save-approvals/{request_id}/frame exact screenshot, digest-checked
POST /api/save-approvals/{request_id}/actions/{verify_and_resume|reject}
The task's capability_digest is the request digest (run, step, screenshot,
form state). The phone echoes the task digest and signature; the answer is
written once on the new decision_portal person route, and the run still
re-reads the form before saving. The hosted lane does not carry save
approvals: Cloud never receives the screenshot.
abrichr
added a commit
that referenced
this pull request
Oct 18, 2026
An accepted admission now admits the workflow to the problems_only approval mode: its runs stop only for problems (a failed check, a changed screen, a step outside what was qualified) instead of before every save. openadapt-flow run reads this from the bundle's admission-decision.json and prints who admitted it. A flag or runtime.approval_mode still wins. The admitter is a person, or a delegated agent acting under an approver grant a registered person signed (#1234's openadapt-flow/approver-grant/v1), replacing the unsigned delegate decision. The grant must verify, name the agent, cover the workflow and let an agent approve consequential saves. The decision records the grant's digest, its signer and the signed grant itself. The mode applies only to the exact admitted bundle, and an agent's admission only while its grant still verifies. problems_only keeps its own gates, so every save still waits for a person until the switch is earned. MCP: openadapt-agent serve --admissions DIR --delegate-approvals ID --approver-grant FILE.
This was referenced Oct 18, 2026
…-phone-approval # Conflicts: # packages/flow/openadapt_flow/console/app.py
A run that gets no answer in time stops before the save and leaves a durable item_approval pause. The command line carried a later answer to that pause; the phone (decide_save_approval) only wrote it. So a phone approval sat until someone ran `resume`, and a phone decline left the pause open while the receipt said the run would continue. The carry step moves to approval_mode/durable_carry.py, and both the CLI and the decision portal call it right after writing the answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves item_approval.py: keeps the person-signed grant checks from this branch and the run-start policy pin from #1232. _recorded_policy() now returns the pinned policy once the run has written one. The pinned-policy test now expects the earlier refusal: a planted policy without a signed grant can't produce an agent's answer at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Do not merge while the main merge freeze is in place.
What changes
Person-signed approver grants. An agent, rule or system can answer a save only under an approver grant (
openadapt-flow/approver-grant/v1) that a person signed with apolicy_signerskey in the JIT authority'sprincipals.json. It reuses #1207's domain-separated Ed25519 signing. The grant names the delegates, the workflows it covers and its expiry (366 days at most).--approver-policynow takes a signed grant. An unsigned policy that allows a non-person, a tampered or expired grant, or a grant signed by an agent's key gives no delegate approval: the run warns and waits for a person. New commands:approver-grant sign(needs a terminal and a typedyes) andapprover-grant verify.Nobody approves their own work. Each run records who started it (
--started-by; the agent runner always passes it). A delegate is refused for a run it started, a run started by whoever it acts for, and a run whose starter is unknown. The grant is re-verified on every answer.One-tap phone approval, on the decision portal. A save waiting for a person is now a signed
openadapt.human-decision-task/v1(task_kind: save_approval, questionapprove_save) served by the attended console the decision portal already relays to:GET /api/save-approvals: the waiting saves.GET /api/save-approvals/{id}: the signed PHI-free task plus a local-only presentation block.GET /api/save-approvals/{id}/frame: the exact screenshot, checked against its digest.POST /api/save-approvals/{id}/actions/{verify_and_resume|reject}: the answer. A decline needs one closed reason.The task's
capability_digestis the request digest (run, step, screenshot, form state). The phone echoes the task digest and signature. A changed or foreign task is refused. The answer is written once on the newdecision_portalperson route, so a replay or second tap has nothing to answer. The run still re-reads the form before the save. Nothing new leaves the runner. Local-only and no-outbound installs work the same way, and the hosted lane doesn't carry save approvals, so Cloud never gets the screenshot. The earlier Cloud relay (--phone-approval,runtime.phone_approval,item-approval pin-cloud-keys/apply-cloud-decision,openadapt.cloud-item-decision/v1) has been removed.Who decided.
item-approval showreturnsdecided_by. The report's approval rows carry the route (desktop_run_it,decision_portal, or the delegate's route) and, for a delegate, its identity and grant hash (policy_ref).Docs
docs/APPROVAL_AND_VERIFICATION.mdanddocs/APPROVAL_MODES.mdhave new sections:Tests
tests/test_approval_signed_grant.pycovers grants, tampering, expiry, an agent's key signing, self-approval, and the run's starter.tests/test_console_save_approvals.pycovers the portal task (PHI-free, signed, stable), the exact screenshot, single-use answers, closed decline reasons, foreign/forged tasks, expiry, a swapped screenshot, and attended-only answering.tests/test_approval_delegated.pynow uses signed grants.HumanDecisionTaskV1/V2schemas regenerated for the two new enum members. The public artifact inventory is refreshed.Related: OpenAdaptAI/openadapt-desktop#207 (portal relay for these routes), OpenAdaptAI/openadapt-desktop#206 and OpenAdaptAI/openadapt-cloud#419 (who approved, in Desktop and Cloud run views).