Skip to content

feat(approvals): one approval record, shared RiskTier, pending inbox query (#13421 S1) - #18404

Open
mrveiss wants to merge 6 commits into
mainfrom
issue-13421-approval-record
Open

mrveiss wants to merge 6 commits into
mainfrom
issue-13421-approval-record

Conversation

@mrveiss

@mrveiss mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Thinking Path

#13421 folds seven approval surfaces into one record. approvals is already the canonical table (#17043), so S1 extends it with nullable columns rather than adding a record type, and adds the single risk ladder and the inbox query the frontend slice (S3) builds on. Owner decisions of 2026-10-12 are followed as recorded on the issue.

What Changed

  • models/approval.py: nullable source, risk_tier, expires_at, payload_hash, resume_ref; ApprovalSource enum (terminal_command, agent_tool, workflow_plan, plugin_capability, autoresearch, skill_governance, company, mcp_tool_schema).
  • Migration 20261012_098 (after the single head 20260928_097): adds the five columns plus two indexes; asserts row count unchanged and every new column NULL (the 094 pattern); downgrade refuses once any row carries a value in one of them.
  • Migration 20261012_099 (found on the way, see below): adds the missing task_approval_links.created_at.
  • autobot_shared/risk_tier.py: RiskTier (low|medium|high|critical, asserted to be a subset of Severity values), risk_tier_from_severity, risk_tier_from_command_risk, parse_risk_tier (accepts the terminal's uppercase and command_execution.RiskLevel by member name). Grepped first: no existing approval tier. Severity is not widened; UNKNOWN raises instead of guessing; DANGEROUS/FORBIDDEN map to critical.
  • services/approval_execution.py: register_resume_handler / resume_decided_approval, keyed by source, run after every decision (approve, reject, revision) when resume_ref is set; failures are recorded like bug(approvals): an approved action that fails or has no handler leaves only a log line, while the approval still reads APPROVED #17141, never raised.
  • services/approval_gate_service.py: create_approval takes keyword-only record fields validated by the new services/approval_record_fields.py (sourced approval must be graded; resume_ref needs a registered handler; expires_at aware and future; payload_hash defaults to sha256 of canonical context); _transition resumes through approval_execution; list_pending is the inbox query (all sources, unscoped rows only, so no company-tenant leakage).
  • api/approval_gates.py: GET /approval-gates/pending plus source/risk_tier filters on the list route; response carries all five fields. Schemas moved to new api/schemas_approvals.py because schemas_workflows.py may not grow (ceilings lowered in both records, 3011 to 2917).
  • store_authority: approval concept registered (Postgres is the record).
  • Generated types: autobot-frontend/src/types/generated/api.ts regenerated with the CI recipe (separate commit, diff is approval-only: +86/-3).

Security choices: the HTTP create route does NOT accept source, risk_tier, resume_ref, expires_at or payload_hash (tested): a caller-chosen source would let any user pose as the terminal, and a caller-chosen resume_ref would pick what the server resumes. Only server-side emitters set them.

Defect found and fixed here: on a freshly migrated database selectinload(Approval.task_links) (used by list_approvals and list_pending) failed with column task_approval_links.created_at does not exist; migration 099 fixes it. The same drift on seven other tables is filed once as #18392, not patched here.

For S2 (emitters), recorded as asked

  • chat_workflow/approval_baseline.py on main (refactor(tools): one tool-effect catalogue, every mutating tool gated by category with an SLM-only relaxation (#18294) #18310): baseline categories are ALL valid categories minus those the SLM setting chat.relaxed_approval_categories relaxes; callers can only add (union_categories), never remove; SLM unreachable or malformed relaxes nothing (fail closed). S2e (agent tool emitter) must take its gate set from baseline_approval_categories() and must not offer a per-session or per-approval way to lift a category.
  • The pending list has the same visibility as the existing list route (any authenticated user). Terminal and tool approvals belong to a chat session, so S2a/S2e must carry the owning session in context and the inbox needs a session-ownership filter (THREAT_MODEL section 2) before those sources are shown to non-owners. Not done in S1 because no emitter sets a session yet.
  • expires_at is stored and published, not enforced: nothing moves a stale pending row to EXPIRED yet.
  • store_authority approval declares no Redis projection; S2a must add the terminal pending projection with its rebuild, per the note on the entry.
  • Moved line: services/approval_gate_service.py global-channel publish baseline entry 388 to 429 (same pre-existing finding, security: approval and checkpoint notifications broadcast workflow/session ids on 'global' #17373).

Verification

  • Postgres (disposable local cluster, real alembic upgrade): test_approval_record_columns_098.py + test_approval_inbox_query_13421.py: 7 passed (rows untouched, round trip, refusal, inbox query across sources, resume through the real table).
  • Unit: risk_tier_test.py 29, approval_record_test.py 19, approval_gates_inbox_13421_test.py 7: 55 passed; existing approval suites (execution, proposer, action validation, orphan chain, human decider, gate service, global-channel baseline) pass.
  • Guards: symbol_fork_ratchet_17312 18 passed; python_file_size_ratchet 31 passed; ceiling_record_parity_17872 11 passed; guard_reach_meta 23 passed; store_authority 16 passed; reach_scope_claim_17844 (alone) 10 passed; docstring_restatement 6, enum_hand_copy 7, frontend_api_contract_ratchet 12 passed.
  • Push: pytest part of pre-push passed; its vue-tsc step timed out at 159s, so the push used AUTOBOT_PREPUSH_ALLOW_TIMEOUT=1. The only frontend change is the generated api.ts.
  • alembic heads: single head 20261012_099.

Model Used

Claude Sonnet 5.5

Single-issue rationale: S1 of the multi-slice #13421 is the schema every later slice (S2a-e, S3) builds on and it must merge first; the slices are sequenced by design, so none can ride with it.

Refs #13421

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 90a628fc-f31f-4195-8cc9-4f2938ca47eb

📥 Commits

Reviewing files that changed from the base of the PR and between 13be1b4 and 8e0e123.


⛔ Files ignored due to path filters (3)
  • autobot-frontend/src/types/generated/api.ts is excluded by !**/generated/**
  • repo_tests/python_file_size_ratchet_baseline.py is excluded by !repo_tests/python_file_size_ratchet_baseline.py
  • scripts/python_file_size_known_large.py is excluded by !scripts/python_file_size_known_large.py

📒 Files selected for processing (20)
  • autobot-backend/api/approval_gates.py
  • autobot-backend/api/approval_gates_inbox_13421_test.py
  • autobot-backend/api/schemas_approvals.py
  • autobot-backend/api/schemas_workflows.py
  • autobot-backend/migrations/versions/20261012_098_approval_record_columns.py
  • autobot-backend/migrations/versions/20261012_099_task_approval_links_created_at.py
  • autobot-backend/models/approval.py
  • autobot-backend/services/approval_execution.py
  • autobot-backend/services/approval_gate_service.py
  • autobot-backend/services/approval_gate_service_test.py
  • autobot-backend/services/approval_record_fields.py
  • autobot-backend/services/approval_record_test.py
  • autobot-backend/tests/migrations/test_approval_inbox_query_13421.py
  • autobot-backend/tests/migrations/test_approval_record_columns_098.py
  • autobot-backend/tests/security/test_approval_human_decider_17042.py
  • autobot_shared/risk_tier.py
  • autobot_shared/risk_tier_test.py
  • autobot_shared/store_authority.py
  • repo_tests/docstring_restatement_baseline_17165.py
  • repo_tests/global_channel_carries_no_tenant_payload_17354_test.py

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Notice: 29 open PRs — past the runaway threshold (25)

This is not a request to defer this PR. The actual limit is the open-PR cap (AUTOBOT_OPEN_PR_CAP, default 40), enforced at pre-push against NEW branches only -- pushing more commits to this PR's own branch is never blocked by it.

This notice only means the count is high enough to be worth a glance for a runaway — something opening PRs in a loop, or a merge pipeline that has stalled so nothing is draining.

Currently open:

If the queue is draining normally, ignore this. Otherwise:

  1. Check whether CI is dispatching at all — see the ci-dispatch-watchdog status on these PRs
  2. Merge the ones whose CI has finished and review has passed: gh pr merge <number> --squash --delete-branch
  3. Look for a loop opening near-identical PRs

Warn-only runaway detector — .github/workflows/pr-queue-gate.yml. It never blocks a merge.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Security review (Opus) at 428598b: blocked. Nothing is exploitable through today's sources, but these must be settled first:

Severity Location Finding Fix
High (merge) migrations/versions/20261012_098_approval_record_columns.py:33 down_revision = "20260928_097", the same parent as #18343's 20261011_098_audit_logs_canonical_columns.py:46. Whichever lands second leaves two alembic heads, and alembic upgrade head fails. #18343 is in merge train 7b, so it lands first. After train 7b merges, rebase this PR. Renumber 098 → 099 with down_revision set to #18343's revision, and the current 099 → 100.
Medium services/approval_gate_service.py _transition (~L380-417) The status check is a plain SELECT followed by a write, with no lock or conditional UPDATE. Two concurrent approves both pass, so resume_decided_approval runs twice. It is latent today (no register_resume_handler caller), but this PR adds the execution trigger. Filed as #18413, which blocks S2. Fix it here with a conditional UPDATE … RETURNING, or keep it in #18413.
Medium _transition expires_at is not enforced at decision time. Filed as #18412.
Low api/approval_gates.py:205-229 /approval-gates/pending has no owner or session filter and returns context. That is no worse than main today, but it must be fixed before S2 sources. Filed as #18413.

Verified fine:

  • The create route rejects the new fields, and there is no mass-assignment path.
  • RiskTier is a subset of Severity, with mappers and no fork.
  • 099 is additive and idempotent.
  • The 098 downgrade refuses while any row holds a value in the new columns, so no data is lost.
  • api.ts matches the routes.

@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Review fixes pushed (head 8e0e123): atomic decision claim (one conditional UPDATE ... RETURNING; the loser matches no row and does not resume; concurrent approve/approve and approve/reject tests on Postgres), owner-scoped pending list (new nullable owner_user column in migration 098; admin sees all, a user sees their own plus legacy unsourced gates, a sourced row with no owner is admin-only; sourced creates must name an owner), and expires_at enforcement (past-expiry decision refused and the row moved to EXPIRED and resumed as expired; expired rows hidden from the pending list; expire_stale() once-only). The periodic scheduler for expire_stale() is not wired (#18412 stays open, comment there). Still to do: renumber 098/099 after #18343 lands. Refs #18413, #18412.

@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Security delta review (Opus) at 8e0e123: approve on everything except the migration renumber. The renumber stays deferred: 098's down_revision still collides with #18343's 098, and #18343 is in merge train 7b. Renumber once that is on main.

Verified:

Conditions and LOWs:

This branch has not been deployed

No deployments
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