feat(backtests): honor per-user concurrent dashboard slots - #342
feat(backtests): honor per-user concurrent dashboard slots#342Allan-Feng wants to merge 3 commits into
Conversation
Add per-user max_concurrent_backtests and credits, admin list/patch/bootstrap APIs, and an Admin page with site stats. Signup stays role=user. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace process-wide single-flight with entitlement-aware slots, poll every in-flight live_run_id for card progress, and keep Configure clickable while a run is in progress. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@Allan-Feng is attempting to deploy a commit to the allan-feng's projects Team on Vercel. A member of the Team first needs to authorize it. |
Mocked workers never finalize slots, so the process-wide cap of 20 refused later /backtest/run calls. Also register the new /api/admin routes and bump cache-buster floors. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Heads-up from #348 — cache-buster collision on this branch, and CI cannot see it. This PR ships
The guard only compares |
Security review fixes for Open-Finance-Lab#341. The admin routes themselves held up — one correct `require_admin` gate, a transactional PATCH, a constant-time compare, and both twins serializing the one-shot promotion. What did not hold up was the plane around them: the entitlement was built as a control and wired where it could not bind, and bootstrap's own budgets could be turned on the operator. - Close the two cap bypasses. `resolve_owner_cap_context` returned None the instant `owner_user_id` was falsy, and agent creation needs no login at all (`_require_owner_context` accepts a self-chosen X-Session-Id), so signing in was strictly worse than staying anonymous: one shared account budget versus the per-agent cap times however many agents you registered. Unclaimed agents are now billed to the browser session that created them, resolved inside the store (`list_owner_scope_agent_ids`, both twins) because `_public_agent` withholds `owner_browser_session` on purpose — `_owner_context` accepts that value AS an ownership credential. - Cap the legacy `/api/v1/backtest/*` surface. It authenticates nothing and writes no `protocol_runs` row, so all three protocol caps were blind to it: a bare POST spawned a thread and pinned a bar window, unbounded, beside a quota system holding every authenticated agent to its limit. Per-session and global budgets, counted and inserted under one lock. Opt-in, so the protocol path (already capped three ways) is not charged twice. - Raise the default quota to 5 and make it env-overridable. Nothing seeds `user_entitlements` at signup and nothing backfills, so `1` was not a default but every existing account's live limit on deploy day — a silent demotion from 5x(agents), with no remedy short of an admin. - Bootstrap: check the global budget AFTER the compare. Checked first, 20 wrong guesses a window — re-spent every window, from any account, and signup is open — refused the correct secret indefinitely, and the window it blocked is exactly the fresh-deploy window the route exists for. Require >= 32 chars (refused as if unset), and answer 403 for unset/weak/wrong alike so a caller cannot learn whether a deployment is bootstrappable. - Lock FixedWindowRateLimiter. The module had no `threading` import at all, and the buckets are mutated on every call — `_pruned` popleft()s from inside `check`, not only `record`. `allow()` is now atomic. - Reject control characters in emails at the validator. strip() only touched the ends, so an interior newline validated, was stored, and forged lines in the admin console's role-change confirm() — a native dialog with no markup to escape. One fix at the source beats one guard per renderer. - Get the store off the event loop in signup/login. This PR added a sync `get_entitlements()` to `_auth_json`, called from both async handlers, which the threadpool guard cannot see (it pins plain-`def` routes, and those two are exempt for already awaiting). Bundled with `create_session` into one `asyncio.to_thread` hop, plus the guard that can see it. Also: router-level `require_admin` so a route added later is gated by default, a 403 case for all four routes enumerated from the router, an audit line on every role/quota change (ids only — no email reaches a print sink), quota floor 0 so a quota can actually suspend, `credits` labelled not-enforced in the console it is editable from, and the first Postgres tests for `promote_first_admin` — prod's copy of the one-shot predicate had none. Cache busters to 100/100: Open-Finance-Lab#342 also ships styles.css?v=97 and Open-Finance-Lab#344 ships 99/98, so 98/97 would have collided with no git conflict (Open-Finance-Lab#347/Open-Finance-Lab#348). Backend suite: 2797 passed, 76 skipped. SDK suite: 171 passed, 2 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QCiPGs3ZYVjyFYVLa6E7bZ
Summary
live_run_idso each running agent card shows its own progress.Stacking
Depends on #341 (
feat/admin-entitlements). This branch includes that commit plus the concurrent follow-up. Review the latest commit for concurrent-only changes; merge #341 first, then this.Test plan
pytest dashboard/backend/tests/test_admin_users.py dashboard/backend/tests/test_backtest_progress_card.py -qMade with Cursor