Skip to content

[comp] Production Deploy#2956

Merged
tofikwest merged 49 commits into
releasefrom
main
May 29, 2026
Merged

[comp] Production Deploy#2956
tofikwest merged 49 commits into
releasefrom
main

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot commented May 29, 2026

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

    • Hosted MCP OAuth: better-auth mcp plugin with a trusted Gram client (env‑driven); exposes /api/auth/mcp/*.
    • Auth: HybridAuthGuard + PermissionGuard accept MCP OAuth bearer tokens, bind org (auto or saved), enforce app‑access, block org‑less users, return 403 to choose org, and bypass for platform admins; consistent RBAC for OAuth tokens.
    • MCP org selection: session‑only, permission‑guarded GET/PUT /v1/mcp/organization; settings UI picker shows only orgs with app access.
    • OpenAPI: declared 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.
    • MCP server: regenerated @trycompai/mcp-server to v0.0.2 with clearer tool descriptions.
    • Overview nudges: stacked nudge center (Offboarding, Trust Portal setup, Framework updates) with tests; Trust page shows a getting‑started card.
    • Trust Portal: auto‑publish for new orgs via a guarded settings warm‑up; “configured” detection utility powers nudges.
    • Integrations: record lastSyncAt after GWS sync; reactivate previously deactivated users when active again in GWS (clears offboardDate).
    • Tasks/Evidence export: prevent proxy timeouts on large orgs by flushing headers early and appending an EXPORT_INFO.txt first; tests updated.
    • Portal: hide archived policies from employee lists.
    • Build/CI: disabled TS declaration emit; gram.json + workflow to push packages/docs/openapi.json to Gram on main with least‑privilege token.
  • Migration

    • Run Prisma migrations.
    • Configure env in apps/api: GRAM_OAUTH_CLIENT_ID, GRAM_OAUTH_CLIENT_SECRET, GRAM_OAUTH_REDIRECT_URI (optional: MCP_OAUTH_LOGIN_PAGE, MCP_RESOURCE_URL).
    • In GitHub, set repo secret GRAM_API_KEY to enable the Gram sync workflow.

Written for commit d682a94. Summary will update on new commits.

Review in cubic

chasprowebdev and others added 18 commits May 27, 2026 14:15
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)
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comp-framework-editor (staging) Ready Ready Preview, Comment May 29, 2026 6:07pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
app (staging) Skipped Skipped May 29, 2026 6:07pm
portal (staging) Skipped Skipped May 29, 2026 6:07pm

Request Review

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

tofikwest and others added 2 commits May 28, 2026 20:32
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)
@vercel vercel Bot temporarily deployed to staging – portal May 29, 2026 00:40 Inactive
tofikwest and others added 4 commits May 29, 2026 12:56
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
@vercel vercel Bot temporarily deployed to staging – portal May 29, 2026 17:23 Inactive
@vercel vercel Bot temporarily deployed to staging – app May 29, 2026 17:23 Inactive
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>
@vercel vercel Bot temporarily deployed to staging – portal May 29, 2026 18:05 Inactive
@vercel vercel Bot temporarily deployed to staging – app May 29, 2026 18:05 Inactive
@tofikwest tofikwest merged commit 9546f91 into release May 29, 2026
12 of 13 checks passed
@claudfuen
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.66.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants