Skip to content

docs(adr): ADR-056 §3.1–3.6 ownership overrides + multi-org membership; ADR-059 app-level entity registry - #677

Open
seanspeaks wants to merge 5 commits into
adr/056-app-profilesfrom
claude/internal-mcp-01-adr-056-059
Open

seanspeaks wants to merge 5 commits into
adr/056-app-profilesfrom
claude/internal-mcp-01-adr-056-059

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #670 (ADR-056). Driven by Left Hook's internal MCP server, where each staff member connects their own Google, Xero and HubSpot in an organization-primary Frigg app, with no integration around those connections.

ADR-056 §3, new subsections

  • §3.1 Where a scope comes from. Scope resolves request → binding → integration → app entity type → app → default. The most specific level that sets one wins; nothing intersects. The default follows user.primary, so apps that set nothing keep today's meaning.
  • §3.2 Ownership is decided by scope, not user.primary. Today an x-frigg-appuserid-only request to an org-primary app resolves to an undefined owner. A request missing the identity its scope needs now gets a 400.
  • §3.3 Integration ownership can be set explicitly. An organization-owned integration may not bind an individual entity.
  • §3.4 Lists say whose each record is. /api/v2/entities, /credentials and /integrations return mixed ownership, and each record carries scope, owner, organizationId and access.
  • §3.5 Entity proxy (ADR-052). It accepts only the scopes in managementApi.proxy.scopes, default ['individual'].
  • §3.6 A user may belong to several organizations. This fixes a defect. GetUserFromXFriggHeaders re-links an individual to whichever org a request names, because membership is a single organizationId column, so a person in two orgs flips membership on every request.
    • Membership becomes its own relation, with orgRole.
    • Each request has one active organization.
    • Individual entities and integrations are paired with the organization they were created in, which reverses §3's earlier "no organizationId column".
    • Lists are scoped to the active organization.

ADR-059 (new)

  • Adds an optional appDefinition.entities map, keyed by entity type name and merged with today's inference from integrations. Entities can then exist without an integration: agents and MCP servers, connect-first products, global entities.
  • One getEntityTypeRegistry replaces the five call sites that derive module definitions from integrations: router, backend-utils, app-definition-loader, encryption schema registry and health. The encryption schema therefore covers registry-only types.
  • $infer: false makes registration required for apps that want one explicit list.

Review

Daniel. These are the decisions the stacked implementation PRs follow, so comments here change those PRs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6


Generated by Claude Code


Note

Low Risk
Documentation-only ADR updates; no runtime code changes in this PR, though accepted decisions will drive substantial 2.1 auth and schema work later.

Overview
ADR-056 is revised (2026-10-15) by folding former ADR-058 into Decision §3 as §3.1–§3.6, so tenancy, scope resolution, and multi-org behavior are one document.

§3.1–§3.5 specify how scope is resolved (request → binding → integration → app entity type → app → default), that ownership follows resolved scope instead of user.getId() / user.primary, optional explicit integration ownership, v2 list payloads with scope / owner / organizationId / access, and entity-proxy scope defaults via managementApi.proxy.scopes.

§3.6 replaces single-column org membership with OrganizationMembership, per-request active org, and organizationId on Entity and Integration (reversing the earlier “no organizationId on Entity” note). orgRole moves to membership; org-admin rules differ by auth mode. Integration mixed-scope ownership rows are marked deferred for 2.1’s first cut (400 MIXED_ENTITY_SCOPES, global entities 404 on tenant API).

ADR-059 (new) adds an optional appDefinition.entities registry merged with today’s integration inference, getEntityTypeRegistry to replace five derivation call sites, optional $infer: false, and v2 /api/v2/entity-types plus authorize without an integration.

The ADR index lists ADR-056’s revision date and ADR-059.

Reviewed by Cursor Bugbot for commit 8eda42e. Bugbot is set up for automated code reviews on this repo. Configure here.

…hip; ADR-059 entity registry

Stacked on #670 (ADR-056).

ADR-056 §3 gains: scope resolution request → binding → integration → app
entity type → app → primary-derived default; ownership by scope instead
of user.primary; explicit integration ownership; mixed-ownership list
fields (scope, owner, organizationId, access); proxy scopes; and users in
several organizations (membership relation, one active org per request,
individual entities and integrations paired with their organization).

ADR-059: optional appDefinition.entities registry keyed by entity type,
merged with inference from integrations; one registry function replaces
the five derivations; `$infer: false` makes registration required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

Comment thread docs/architecture-decisions/056-app-profiles-tenancy-entity-scopes.md Outdated
…t-mode semantics

Record what F4 (#681) implemented: no backfill migration; User.organizationId
is a legacy pointer read as an implicit membership; orgRole defaults to
MEMBER; strictMembership refuses any non-member of a pre-existing org.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…ner-only, list/access rule)

From the Graphite review on #677: under 'all' the old text still paired
every individual entity with an org, so the opt-in could not work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

is no backfill: an existing person's org is honoured at read time, and the table holds only
memberships added since. This replaces an earlier plan for the migration to write one row per
existing `organizationId`. That would mean a data migration run against every adopter's live
database, and it buys nothing the read-time rule does not.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Legacy membership cannot be revoked

High Severity

User.organizationId is a permanent implicit membership and is never moved, while OrganizationMembership has no removed state. Leaving the first organization has nothing to delete, and deleting a later row for that org makes the implicit membership apply again. The original org membership therefore comes back after removal.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b090b64. Configure here.

```
owner = caller AND (organizationId = active org
OR (individualAcross = 'all' AND organizationId IS NULL))
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

List rules drop admin visibility

Medium Severity

§3.4 returns every record the caller may see under the permissions table, including an org admin's view access to other members' individual entities. §3.6 then restricts both lists and access checks to individual entities whose owner is the caller. An admin's metadata access and the view access value cannot both be implemented.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b090b64. Configure here.

…wrapper takes entities

The first implementation lists, reads and deletes an entity of an
unregistered type and refuses new ones, but does not emit
"registered": false yet. Says so, and how such an entity answers today.
getModulesDefinitionFromIntegrationClasses is kept as a deprecated wrapper
(#679 now restores it) with an optional entities argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…ntegrations deferred

§3.6 gains "Organization roles": the shared secret always manages; an
adopter token's role claim wins, else the stored membership orgRole, else
MEMBER; a Frigg bearer token keeps its pre-roles behaviour unless
user.orgAdminGateBearerTokens is set. The stored membership role is read
for authorization as the fallback.

The integration ownership table now says what is implemented: mixed
organization + individual integrations are refused with 400
MIXED_ENTITY_SCOPES and global entities give 404 at integration create.
Both mixed rows, and the workspace/me example that depends on them, are
deferred until ORPHANED and member removal exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
@sonarqubecloud

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8eda42e. Configure here.

itself.

The membership row is therefore read for authorization, not only written: it is the fallback when
the token is silent, never an override of what the token says.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stored role outlives IdP demotion

Medium Severity

An adopter JWT role claim wins so an IdP demotion applies on that request, but the membership row keeps the first token's role and is the fallback whenever a later token omits the claim. A demoted caller who next presents a silent token is authorized as their original role again.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8eda42e. Configure here.

@seanspeaks

Copy link
Copy Markdown
Contributor Author

ADR reconciliation needed after Stack A/B — drafted with Claude Code at Sean's request

Severity: must-reconcile. The scope vocabulary (individual|organization|global), the ownership table, ORPHANED and visible(context) are consistent with #670. These items are not:

ADR-056

ADR-059

Merge order: this PR merges into adr/056-app-profiles before #670 merges. #668's 052 amendment comes before #685.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants