* fix(quota): fail closed when quota verification errors (#331)
Quota enforcement fell open on unexpected errors: the outer `except` in
`authorize_workflow_run_start` returned `has_quota=True`, so a degraded
database or a config-resolution bug let a billable run start unverified.
Billing and abuse protection are control-plane functions, so this is the
wrong default under exactly the degraded conditions that matter.
- Fail closed by default: the outer handler now returns
`has_quota=False` / `quota_check_failed`, reusing the existing message.
- Add `QUOTA_FAIL_MODE=closed|open` (default `closed`) so OSS self-hosters
can explicitly opt back into availability; the open path logs loudly.
- Narrow the try-scope so `get_user_by_id` / `get_workflow_run` DB read
failures surface as their specific `user_not_found` /
`workflow_run_not_found` codes instead of the generic handler.
- Tests cover the config-resolution and DB-read failure paths (denied,
not `has_quota=True`) and the `QUOTA_FAIL_MODE=open` escape hatch.
Fixes#331
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(quota): route DB read failures through the fail-mode policy gate
Review (greptile) flagged that the narrowed get_user_by_id / get_workflow_run
catches returned user_not_found / workflow_run_not_found before the outer
QUOTA_FAIL_MODE handler ran, so QUOTA_FAIL_MODE=open never applied to a DB
failure -- the exact "degraded database" case the escape hatch documents.
Revert the two narrowed catches so DB read exceptions fall through to the
single outer policy gate: closed -> quota_check_failed, open -> allow. The
None checks still return the specific not_found codes for genuinely missing
rows; an exception is a "cannot verify" condition, not a definitive absence.
Add a regression test asserting QUOTA_FAIL_MODE=open allows a run when a DB
read throws, and update the two DB-error tests to expect quota_check_failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(quota): scope the fail-mode comment to credit-verification failures (#331)
Review (cubic) flagged the outer-handler comment as overclaiming: it said the
handler is the single gate for "all cannot-verify errors", but the earlier
workflow-load and org-membership catches always deny with workflow_not_found
regardless of QUOTA_FAIL_MODE. That distinction is intentional (those are
authorization/existence gates, not credit verification), so scope the comment
accordingly. Comment-only, no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(quota): fail open only when MPS is unreachable
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
* fix(auth): allow invited org members to start workflow runs
Users invited to an org could not start workflows belonging to that org
because the authorization check compared actor.selected_organization_id
directly against workflow.organization_id. An invited user's selected
org correctly reflects the invited org, but if the Stack Auth token
resolves to a different org id than expected the strict equality fails.
Per api/AGENTS.md: "Whenever you read or write an organization-scoped
field, you must filter or validate by organization_id." The correct
policy is org membership, not selected-org identity.
- Add is_user_member_of_organization() to OrganizationClient; queries
the organization_users association table directly (no lazy-load risk).
- Replace the identity check in authorize_workflow_run_start() with a
membership lookup. Deny when actor_user.id is not in the org's member
set; error_code stays workflow_not_found to avoid leaking existence.
- Update test: rename rejects_actor_from_another_org to
rejects_actor_not_a_member (reflects actual policy), add positive test
allows_invited_member that seeds membership and asserts has_quota=True.
Closes#491
* fix(auth): skip membership check for personal workflows (organization_id=None)
When workflow.organization_id is None (personal or legacy workflow with no
org), the membership lookup was still called, producing a SQL IS NULL
comparison that matched nothing and denied the run.
Guard the check so it only runs when the workflow is org-scoped.
Adds a regression test confirming that an actor with a known id can start a
personal workflow without triggering is_user_member_of_organization.
* fix(auth): fail closed on workflow membership lookup errors
---------
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
* feat: add model config v2
* chore: centralize user org selection
* chore: move preferences to platform settings
* fix: decouple org preference and ai model preferences
* filter out local sdp candidates on non local environment
* feat: add FORCE_TURN_RELAY variable
* add FORCE_TURN_RELAY option in docker-compose
* fix: fix github workflow