Repository navigation
Conversation
A gated save counted as approved by a person when the answer said so. --decided-by human with --unauthenticated, or a claimed --auth-method, was enough, and the run accepted any answer file of that shape. Any process that could run the CLI or write the run directory, such as the agent that started the run, could approve its own save. The durable approve named whoever the OS user was (getpass). A person's answer now counts only with a HumanAttestation: an Ed25519 signature over the answer by a key registered under the new `approvers` role in principals.json (jit register --role approver), using the route's own method (desktop_key for Desktop's Run it, portal_key for the decision portal). The run verifies it on every answer. A claimed Cloud sign-in is refused because nothing here can verify it, and self-approval is refused whatever the answer claims. No key directory, an unregistered key, an agent's key, or a changed answer is a refusal. - The durable approve signs its ApprovalRecord with --approver-key and names the key's registered approver; resume refuses a stored approval without a valid signature (item approvals and business decisions keep their own signed authority). - The decision portal signs with OPENADAPT_PORTAL_APPROVER_KEY and records no answer without it. - Desktop's Run it answers with its approver key and refuses, giving the exact one-time terminal setup commands, when none is registered. `openadapt-desktop approver-key setup` creates the keys. - Demos that play a person register a throwaway key in their own temporary key directory. - Tests that answered as a person now sign; tests that asserted the old claims were accepted now assert refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr
enabled auto-merge
October 21, 2026 04:41
This branch has not been deployed
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.
Problem
A gated save counted as approved by a person when the answer file said so. Any one of these was enough:
item-approval approve --decided-by human --unauthenticated;--approver-id X --auth-method cloud_aal2;The run accepted all of these. Any process that could run the CLI or write the run directory, including the agent that started the run, could approve its own gated save. The durable
approverecorded whoever the OS user was (getpass.getuser()) as the approver.Fix
A person's approval now needs a verified signature:
ItemApprovalAnswercarries aHumanAttestation, an Ed25519 signature over the answer by a key registered under a newapproversrole inprincipals.json(jit register --role approver). The signature must use the route's own method:desktop_keyfor Desktop's Run it,portal_keyfor the decision portal. The run verifies it for every answer it reads (approval_mode/human_attestation.py).--decided-by humanand--unauthenticatedon their own are refused (human_answer_unsigned).auth_method_unverified), because nothing here can verify it.openadapt-flow approve --approver-keysigns theApprovalRecordand names the key's registered approver.getpassis gone.resumerefuses a storedapproval.jsonwithout a valid signature. Item approvals and business decisions keep their own signed authority.OPENADAPT_PORTAL_APPROVER_KEYand records no answer without it.openadapt-desktop approver-key setupcreates the keys. In-app registration (Keychain, Touch ID) is a separate follow-up.Delegated (agent or system) answers under a person-signed grant are unchanged.
Flows that changed
item-approval approve|declineneeds--approver-keyfor a person, andapprove(durable) needs--approver-key.OPENADAPT_PORTAL_APPROVER_KEYon the runner.test_approval_mode_item_approval(claimedcloud_aal2);test_approval_delegated(claimed Cloud, unsigned copy);test_approval_mode_cli::test_a_person_approves_once;test_cli_new_commands::test_approve_flips_status_and_is_idempotent(no OS-user approve);test_desktop_check_it_cli_mockmed.Tests
test_human_approval_verified.py:--decided-by human --unauthenticatedis refused;cloud_aal2is refused, with and without a key;approveneeds the key and no longer trusts the OS user;resumerefuses an unsigned stored approval.test_console_save_approvals::test_a_portal_without_an_approver_key_records_no_answer.test_layout_drift::test_real_render[kw3-font_size_changed], a local font-rendering test that fails the same way on main.openadapt-agententry point and don't involve approvals.check_release_consistency.pyare clean.gitleaks git --log-opts=origin/main..HEAD: no leaks.Known follow-ups
desktop-release-intentmay flag that this touches Desktop without a version bump. A bump would auto-publish Desktop, so it's left to the release decision.resumereads the registry from the default key directory. Anapprove --key-diroverride isn't carried toresume.🤖 Generated with Claude Code