UX audit fixes: accurate is_new on run tools, approver reason capture, human-readable approval pages - #5
Merged
Conversation
- workflow.run / intake.run: report is_new truthfully (was hardcoded True despite the documented dedupe contract); start_run accepts an optional caller run id so a dedupe hit is detectable race-free. - approval.decide / web approvals: record the approver's reason (input was documented and the decision model had the field, but resolve_gate never accepted one); reason now lands in the decision record and audit stream. - /approvals web UI: render human-readable confirmation and error pages instead of raw JSON, show the workflow being approved, and add a reason field to the form. Status codes unchanged.
Setup installs git-lfs (required by the PR publication hook) and syncs all extras; check runs the suite, ruff, and strict mypy.
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.
Summary
Audit of the main user flows (workflow run/start tools, approval gates + web UI, webhooks, deadline tools) surfaced three verifiable papercuts, all fixed here:
workflow.run/intake.runalways reportedis_new: true— both tools documentedis_newas "False if idempotent return", but the value was hardcoded (is_new=True # simplified) or a tautology (run.trigger.dedupe_key == idempotency_key). An agent resubmitting a duplicate lead could never tell the run was deduped.start_runnow accepts an optional caller-supplied run id; comparing it to the returned run's id detects a dedupe hit race-free (the store'screate_rundedupe check remains the single source of truth).The approver's
reasonwas silently dropped.approval.decideadvertised an optionalreason, andApprovalDecision.reasonexists in the model and is persisted by both stores — butresolve_gatenever accepted one. The reason is now threaded through to the decision record and the hash-chained audit stream (gate.approved/gate.rejectedpayloads).The approval web UI returned raw JSON to humans. An attorney clicking an approval link from email saw
{"detail": "Token has expired."}on error and a JSON blob after clicking Approve/Reject. The pages now:Known issue found, intentionally not changed here
With no authenticated session, the web/email channels submit
actor="web_anonymous", which never satisfiesresolve_gate's role check — every web approval is denied 403 today. Fixing that means deciding the authorization model for token-possessed approvers (token-as-credential vs. session auth), which is a security-design decision, not a papercut. Flagged for a follow-up; the new HTML error page at least makes the denial readable.Verification
uv run pytest -q: 549 passed, 1 skipped (baseline 542 + 7 new tests), including Postgres-backed suites run against a live Postgres 16 container.is_newforintake.runandworkflow.run(E2E-05 extended + new E2E-06), reason recorded on decision + audit stream, reason defaults toNone, approval page shows workflow + reason field, HTML 401 on invalid token, confirmation page records reason/channel.uv run ruff check src tests: clean.uv run mypy src/cam(strict): clean, 145 files.