Sloth-ninja puts firms in charge of personal AI access

Firm policies now actively control whether members can manage their own AI keys and chat-tool connections.

complianceworkflow

The fork turns two administrative settings into real guardrails. A firm can set the rule, and members see a clear firm-managed state instead of a confusing dead end.

  • Personal AI keys: firms can prevent members from adding, changing, or deleting their own provider credentials.
  • Chat-tool connectors: firms can stop members creating, reconfiguring, removing, or authorising personal connections to external tools. Existing connections can still be read and maintained so they keep working in chat.

Administrators can change either policy from firm settings, with an extra identity check. The same restrictions apply to admins as to other members.

So what Managing partners and legal-ops leads who need consistent control over firm AI access should take a look.

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
5d524088 WS8 PR B: firm policy enforcement (keys + connectors) sloth-ninja 2026-07-22 ↗ GitHub
commit body
Make the two firm policies from PR C real: block the matching personal
write routes when a firm's policy is OFF, hide the matching member UI, and
let admins flip the policies live from Firm settings. Orgless self-hosters
and policy-ON firms are unchanged. No migration, no new env vars.

Backend:
- requireMemberPolicy(policy, detail) middleware (middleware/auth.ts) reuses
  resolveUserOrganisation; orgless + policy-ON pass through, policy-OFF gets a
  fixed 403. Admins are NOT exempt (they use the firm surface). Fails OPEN on
  any lookup error (availability > a brief self-correcting policy gap), fully
  try-wrapped so it can never leak an unhandled rejection or hang a request.
- Gated writes (routes/user.ts): PUT /user/api-keys/:provider (+null-save
  delete); POST /mcp-connectors, PATCH/DELETE /mcp-connectors/:id, POST
  /mcp-connectors/:id/oauth/start. Reads + refresh/tool-toggle stay open so
  existing connectors keep working in chat.
- organisations.updateOrganisationPolicies + PATCH /admin/policies
  (requireAdmin + MFA), scoped to the caller's own firm.

Frontend:
- account/layout.tsx hides the API Keys / Connectors tabs per policy; direct
  nav renders a neutral "Managed by your firm" card. Model Preferences shows a
  firm-provided note; company-search empty-state points policy-OFF members at
  their firm admin. Firm settings Policies card is now live AccountToggles
  (optimistic + rollback, MFA-guarded, reloadProfile on success).

Tests: backend 215 pass (new middleware/auth.policy.test.ts policy matrix +
extended admin.test.ts PATCH /policies authz/MFA/validation/persistence).
Frontend tsc + eslint clean on changed files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f9d1999b WS8 PR B: review round 2 - inert personal keys under policy-off sloth-ninja 2026-07-22 ↗ GitHub
commit body
Address independent review (approve-with-fixes):

1. Pre-existing personal keys under a keys-off firm policy are now honestly
   inert, not silently active:
   a. requireMemberPolicy gains an optional shouldGate(req) predicate; on
      PUT /user/api-keys/:provider only a real (non-empty) SAVE is gated -
      a null/empty api_key (removal) always passes, so members can always
      remove their own keys.
   b. getUserApiKeys / getUserApiKeyStatus resolve org id + allow_member_api_keys
      in ONE query (getUserOrganisationKeyContext, reused from the firm-key
      layer - no second lookup); policy-off SKIPS the personal-key layer
      (firm > env; status source never "user"). Fail-open preserved: on a
      lookup error the personal key still applies. Saved-but-unused keys are
      reported via ApiKeyStatus.inertPersonalKeys.
   Frontend: the "managed by your firm" api-keys card lists any lingering
   personal key with a Remove action + copy, MFA-guarded, reloadProfile on
   success; inertPersonalKeys threads through UserProfileContext.

2. Comment at GET /mcp-connectors/oauth/callback noting it is transitively
   protected by the gated oauth/start.

3. Corrected the account/layout.tsx default-permissive comment to state the
   real tradeoff.

Tests: backend 224 pass (userApiKeys policy-off resolution + inert reporting +
fail-open; auth.policy save-only gate). Frontend tsc + eslint clean on changed
files.

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-1157.md from inside the repo you want the changes in.

⬇ Download capture-thread-1157.md