Conversation
Enable better-auth's mcp plugin (wraps oidcProvider) so the Gram-hosted MCP server authenticates users via OAuth ("Sign in with Google") instead of a pasted API key.
- add OauthApplication/OauthAccessToken/OauthConsent tables (+ migration)
- register Gram as an env-driven trusted OAuth client (inert when env unset)
- HybridAuthGuard: validate MCP OAuth tokens via getMcpSession and bind org
explicitly from active memberships (device-agent pattern) — single org binds,
multiple orgs fail closed to avoid wrong-tenant access
- add 6 tests for the MCP OAuth path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…scovery Add Rule 11 to the api-endpoint-contract skill + cursor rule: every endpoint needs a meaningful @apioperation summary/description (already enforced by openapi-docs.spec.ts), now correctness-critical because Gram dynamic toolsets discover tools via semantic search over descriptions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The offboarding-checklist endpoints were missing @apioperation summaries/descriptions, failing the OpenAPI quality contract (openapi-docs.spec.ts: missingSummaries + invalidSeo). Added a meaningful summary + ~120-155 char description to all 15 endpoints (the SEO check requires the generated description to be >= 80 chars) and regenerated packages/docs/openapi.json. Descriptions also power the hosted MCP's dynamic-toolset semantic search, so these tools are now discoverable by agents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gram is Comp AI's own hosted MCP client, so the user's login is the authorization — no consent screen needed. Avoids building a consent page UI for v1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multi-org users can now use the MCP instead of being blocked. A per-user McpOrgBinding (set at connect time) records which org their MCP/OAuth token acts on; HybridAuthGuard uses it: single-org binds automatically, multi-org uses the saved choice (if still a member), otherwise returns a helpful 'choose your org' error instead of a dead 401. Adds GET/PUT /v1/mcp/organization (web-app management endpoints, deny-listed from the public spec + MCP tools so the agent can't switch tenants). Tests: guard binding cases + service. Migration is additive (1 table). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mcp plugin registers /api/auth/mcp/{authorize,token,register}, not /oauth2/* (those aren't registered). Gram's OAuth proxy targets /mcp/*.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an AI / MCP organization section to User Settings so multi-org users can choose which org their MCP connection acts on, and switch it anytime (applies on the next request, no reconnect). Renders only for users in more than one org; calls GET/PUT /v1/mcp/organization via a useSWR hook with optimistic update + rollback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 401 makes MCP clients re-run sign-in in a loop; the token is valid and the user just needs to pick an org, so 403 is correct and surfaces the message to the agent. Also re-throw HttpExceptions from the session-auth catch so the 403 propagates instead of collapsing to 401. Updates the message to 'try again' (no reconnect needed). Adds a proactive 'pick an organization' warning callout in the User Settings AI/MCP section when nothing is selected yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An MCP token is only usable by a member of at least one organization. The membership check now runs before the skipOrgCheck shortcut, so a user with zero memberships (a stranger who completed Google sign-in, or someone removed from all orgs) is rejected with 403 on EVERY MCP tool — including org-agnostic ones. They can never reach any org's data, and a user can only ever act on orgs they belong to. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MCP now follows the same access rule as the web app: a user can only use it if their role grants app access (app:read) in the operative org. Owner/admin/auditor and custom roles with the App Access toggle qualify; Portal-only roles (employee/contractor) are rejected with 403. Combined with the existing 'must be a member of an org' check, this means: only customers, and only app-access roles, can use the MCP. Adds a reusable hasAppAccess(orgId, roleString) helper (built-in + custom roles, comma-separated union). The settings picker now only offers app-access orgs, and setOrganization validates app access before saving. 24 unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cursor rule now auto-attaches on apps/api controllers + DTOs via globs (not just description-triggered), so it applies reliably when editing endpoints. AGENTS.md (Codex/agents) gains the rule that every endpoint needs a meaningful @apioperation summary/description — CI-enforced and required for dynamic-toolset discovery — keeping it in sync with the Claude skill + Cursor rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses cubic P1: the MCP controller had HybridAuthGuard only, so the PUT bypassed API-key scope enforcement and the mutation audit hook (AuditLogInterceptor only logs when @RequirePermission is present). Now matches the rest of the API (e.g. people controller): class-level HybridAuthGuard + PermissionGuard, with @RequirePermission('app','read') on both endpoints — gating on app access (consistent with the MCP access rule) and logging the PUT mutation. Dropped @SkipOrgCheck (these are web-only, deny-listed from MCP; an active org is present for web sessions, which PermissionGuard + the audit log need).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1: PermissionGuard rejected MCP OAuth calls — it authorizes via session-based auth.api.hasPermission, but OAuth tokens aren't sessions, so every permission-gated MCP call would 403. Added an isMcpOAuth marker + a guard branch that authorizes from the roles already resolved by HybridAuthGuard (via resolveRolePermissions). This was the critical one — the whole OAuth flow was broken without it. P1: MCP controller used @userid() with no session-only guard, so a scoped API key would pass the guards then crash at @userid() with a 500. Added SessionOnlyGuard (clean 403 for API keys/service tokens). P2: app-access.ts hardened — guarded JSON.parse (malformed custom-role perms no longer throw) and own-property role lookup (a custom role named e.g. 'constructor' is no longer misclassified as built-in). P2: mcp.service resolves app-access concurrently (Promise.all) instead of a serial N+1 loop. Adds tests for the MCP OAuth authorization path + the robustness cases (46 auth/mcp tests green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P2: platform admins were blocked by the MCP app-access gate in HybridAuthGuard before PermissionGuard's isPlatformAdmin bypass could apply, incorrectly denying privileged users whose member role lacks app access. Now the gate is skipped for platform admins, consistent with the normal session path. P2: useMcpOrganization rolled back to a snapshot on save error, which can restore a stale selection. Now revalidates from the server instead (matches the team's SWR norm). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(api): keyless hosted MCP via OAuth (Gram)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Contributor
There was a problem hiding this comment.
1 issue found across 28 files
Confidence score: 2/5
- There is a high-risk security concern in
packages/db/prisma/migrations/20260528213310_add_oauth_provider_tables/migration.sql: OAuth secrets/tokens are stored in plaintext, which increases the chance of credential compromise if database contents are accessed. - Given the severity (8/10) and solid confidence (7/10), this is more than a minor follow-up and should be addressed before relying on this migration in production.
- Pay close attention to
packages/db/prisma/migrations/20260528213310_add_oauth_provider_tables/migration.sql- token/secret storage needs protection (for example, encryption-at-rest strategy at the application/data model level) rather than plaintext persistence.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/db/prisma/migrations/20260528213310_add_oauth_provider_tables/migration.sql">
<violation number="1" location="packages/db/prisma/migrations/20260528213310_add_oauth_provider_tables/migration.sql:22">
P1: OAuth secrets/tokens are being persisted as plaintext, which creates direct credential compromise risk if DB data is exposed.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
nest build emits .d.ts (declaration:true), and the better-auth mcp plugin's internal MCPOptions type leaks into the inferred type of the exported 'auth' but isn't exported/nameable, so declaration emit fails with TS4023 and breaks the Docker build. The API is an app, not a library — it never consumes its own .d.ts — so declaration emit is unnecessary. Disabled it in tsconfig.build.json (type-checking via tsc --noEmit is unaffected; this also future-proofs against other plugins leaking internal option types). Verified with tsc -p tsconfig.build.json (the path nest build uses). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(api): disable declaration emit to unblock staging build (TS4023)
The employee portal showed policies that had been archived/replaced by a framework version sync. The sync stamps `archivedAt` on superseded policies but leaves `status: 'published'` and `isRequiredToSign: true` untouched (framework-sync-apply.ts), so the portal's queries — which filtered on `status` + `isRequiredToSign` only — kept rendering them. The main app's API (policies.service.ts findAll) and the public Trust Center already filter `isArchived: false, archivedAt: null`; the portal was the lone divergent surface. Add both archive filters to the two portal policy-list queries: - OrganizationDashboard (the "Accept All" review list) - Signed Policies page (was missing only `archivedAt: null`) This matches the rule documented on the Policy schema: hide a policy if EITHER `isArchived` or `archivedAt` is set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leak fix(portal): hide archived policies from employee policy lists
…pack CS-423 [BUG] Unable to download Altorney evidence pack
tofikwest
added a commit
that referenced
this pull request
May 29, 2026
Addresses cubic P1 on PR #2956: OAuth credentials in the better-auth OIDC provider tables were stored in plaintext. Sets oidcConfig.storeClientSecret to 'encrypted' (symmetric encryption keyed by BETTER_AUTH_SECRET) instead of the default 'plain', so any client persisted in oauth_application (DCR) is encrypted at rest. No migration risk today: the Gram client is a config-only trustedClient (never persisted) and DCR is disabled, so oauth_application is empty. Verified storeClientSecret exists in the locked better-auth 1.4.22 OIDCOptions; typecheck clean for auth.server.ts. The accessToken/refreshToken in oauth_access_token are generated and looked up by raw value by better-auth's oidcProvider, so there is no supported hook to hash/encrypt them without breaking token validation. They rely on DB encryption-at-rest and short access-token TTLs; a full opaque-token-hashing layer would be a larger, riskier follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tofikwest
added a commit
that referenced
this pull request
May 29, 2026
Replaces the breaking attempt to set `storeClientSecret: 'encrypted'` (cubic P1 on PR #2956) with a warning comment. That option would have broken the Gram OAuth flow: better-auth verifies every confidential client through the same decrypt/hash path, including config `trustedClients` whose secret is the plaintext GRAM_OAUTH_CLIENT_SECRET, so verification would fail (`invalid_client`). There is also nothing to encrypt: the Gram client is config-only and DCR is disabled, so no client secrets are persisted to `oauth_application`. The `accessToken`/`refreshToken` in `oauth_access_token` are generated and looked up by raw value by better-auth, so they can't be hashed/encrypted at our layer without breaking token validation; they rely on DB encryption-at-rest + short TTLs. Net: no safe app-level fix exists; documented to prevent re-introduction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
[BUG] Reactivate the previously-deactivated users in GWS sync
Contributor
|
🎉 This PR is included in version 3.66.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is an automated pull request to release the candidate branch into production, which will trigger a deployment.
It was created by the [Production PR] action.
Summary by cubic
Enables keyless hosted MCP via OAuth (Gram) with per-user RBAC, multi‑org support, and a settings org picker. Adds an Overview nudge center and Trust Portal setup prompt, fixes Google Workspace sync timestamps and reactivates users, and prevents large evidence export timeouts.
New Features
better-authmcpplugin with a trusted Gram client (env‑driven); exposes/api/auth/mcp/*.GET/PUT /v1/mcp/organization; settings UI picker shows only orgs with app access.oauth2(authorization code) targeting/api/auth/mcp/{authorize,token}and offered alongside the API key; added summaries/descriptions to offboarding endpoints; excluded/v1/mcp/*from public docs; CI enforces summaries/descriptions.@trycompai/mcp-serverto v0.0.2 with clearer tool descriptions.lastSyncAtafter GWS sync; reactivate previously deactivated users when active again in GWS (clearsoffboardDate).EXPORT_INFO.txtfirst; tests updated.gram.json+ workflow to pushpackages/docs/openapi.jsonto Gram onmainwith least‑privilege token.Migration
apps/api:GRAM_OAUTH_CLIENT_ID,GRAM_OAUTH_CLIENT_SECRET,GRAM_OAUTH_REDIRECT_URI(optional:MCP_OAUTH_LOGIN_PAGE,MCP_RESOURCE_URL).GRAM_API_KEYto enable the Gram sync workflow.Written for commit d682a94. Summary will update on new commits.