Sloth-ninja gives firms the keys without sidelining lawyers

JessicaOSS now lets a firm fund shared AI-provider access while preserving each member's personal choice.

multi-tenantsecurity

Firm administrators can now manage a shared set of provider keys for the whole firm. A member's own key still takes priority, while self-hosted setups without firm accounts continue as before.

  • Shared provider access: admins can provision firm-wide access without exposing the underlying key material.
  • Member management: admins can view members and change roles within their own firm, with a safeguard against removing the final administrator.
  • Stronger controls: changing keys or roles requires an extra MFA check.
  • Clear boundaries: members can see when a firm supplies access, while the new policies area is explicitly a preview rather than a promise of enforcement.

The work is tested across key precedence, encryption, access control and the new admin journeys.

So what Worth a GitHub look for legal teams that want centrally funded AI tools without forcing every lawyer into one shared configuration.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

2 commits from Sloth-ninja/JessicaOSS, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
9fba26ab WS8 PR C: firm API keys, admin area, member roles sloth-ninja 2026-07-22 ↗ GitHub
commit body
What
- Firm-level (shared) provider API keys managed by admins, layered into
  key resolution as user > firm > env. Orgless/unmigrated deployments are
  unchanged.
- New /admin area (routes/admin.ts) behind requireAuth + requireAdmin:
  GET/PUT firm-keys, GET members, PATCH member role (MFA-gated mutations).
- Admin "Firm settings" screen: members (role badges, promote/demote with
  confirm + MFA), firm API keys (per-provider rows), read-only Policies
  preview. Sidebar gains a "Firm admin" group, visible only to admins.

Why
- First user-visible firm-administration surface for the pilot, building on
  the PR A (#34) organisation foundation. Lets a firm provision one shared
  set of provider keys for all members while a member's own key still wins.

How
- Shared AES-256-GCM crypto extracted to lib/apiKeyCrypto.ts (scheme
  unchanged); userApiKeys.ts keeps thin wrappers. Firm-key CRUD in
  organisationApiKeys.ts. Member listing + setMemberRole (org-scoped writes,
  last-admin guard) in organisations.ts. No migration - organisation_api_keys
  already exists from PR A.
- account/api-keys surfaces the new "firm" source ("Provided by your firm").

Tested
- Backend: tsc clean; vitest 199/199 (rewrote userApiKeys precedence matrix;
  new organisationApiKeys, organisationMembers, routes/admin tests covering
  authz 403, last-admin 409, cross-firm scoping 404).
- Frontend: tsc clean; eslint src 34 errors/77 warnings - identical to main
  baseline (changed files add zero).
- evals:smoke from the main checkout: 4/4 pass.
- Independent code review: no blockers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f0c3edb5 WS8 PR C: address review (firm-layer resilience, listUsers paging, email, skeleton) sloth-ninja 2026-07-22 ↗ GitHub
commit body
Approve-with-fixes follow-up:
- userApiKeys: wrap the firm layer in try/catch in getUserApiKeys and
  getUserApiKeyStatus - any organisation_api_keys read error is logged with a
  scoped tag and the firm layer skipped (env fallback intact), so a transient
  error can never break chat key resolution or profile status for a whole firm.
  New tests cover both paths.
- organisations.listOrganisationMembers: paginate auth.admin.listUsers until
  drained (guarded at 20 pages); perPage is a project-wide cap, not per-firm.
- organisations.setMemberRole: last-admin count now filters via normaliseRole
  so it can't miscount non-normalised data; success payload now populates the
  member's email (degrades to null on lookup failure).
- firm-settings: FirmApiKeysSection gains a loading skeleton so "Not set" never
  flashes before status loads.
- BUILD_LOG: document the last-admin recovery path (operator re-promotes via
  service_role SQL) + updated test count (201).

Verified: backend tsc clean, vitest 201/201; frontend tsc clean, eslint src
34 errors/77 warnings (main baseline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-1158.md from inside the repo you want the changes in.

⬇ Download capture-thread-1158.md