Skip to content

feat: tenant-scoped composite project IDs in SpiceDB#1996

Open
omar-inkeep wants to merge 4 commits intomainfrom
PRD-5976
Open

feat: tenant-scoped composite project IDs in SpiceDB#1996
omar-inkeep wants to merge 4 commits intomainfrom
PRD-5976

Conversation

@omar-inkeep
Copy link
Contributor

Summary

Migrates SpiceDB project IDs from plain format (project:{projectId}) to tenant-scoped composite format (project:{tenantId}/{projectId}) to prevent cross-tenant collisions and fix missing organization links.

Changes

  • Migration script (pnpm spicedb:migrate-ids): Deletes old plain-ID relationships and ensures every project has the correct organization link on its composite ID
    • Dry run by default, --apply to write changes
  • Permissions/sync: Updated to use toSpiceDbProjectId(tenantId, projectId) everywhere
  • Middleware: projectAccess, runAuth, and routes updated for composite IDs
  • Tests: Updated for new tenant-scoped ID format

How to run the migration

pnpm spicedb:migrate-ids          # dry run
pnpm spicedb:migrate-ids --apply  # apply changes

- Migrate SpiceDB project IDs from plain format (project:{projectId}) to
  tenant-scoped composite format (project:{tenantId}/{projectId})
- Add migration script (pnpm spicedb:migrate-ids) with dry-run and --apply modes
- Detect and repair missing project→organization links in SpiceDB
- Update permissions, sync, and middleware to use composite project IDs
- Update tests for new tenant-scoped ID format
@vercel
Copy link

vercel bot commented Feb 13, 2026

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

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Feb 13, 2026 10:46pm
agents-docs Ready Ready Preview, Comment Feb 13, 2026 10:46pm
agents-manage-ui Ready Ready Preview, Comment Feb 13, 2026 10:46pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 13, 2026

⚠️ No Changeset found

Latest commit: ee0deec

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(6) Total Issues | Risk: High

🔴❗ Critical (1) ❗🔴

Inline Comments:

  • 🔴 Critical: runAuth.ts:245-248 Missing required tenantId parameter in canUseProjectStrict call breaks Slack JWT authorization

🟠⚠️ Major (3) 🟠⚠️

🟠 1) permissions.ts, sync.ts Silent failures when parsing composite SpiceDB IDs

files: packages/agents-core/src/auth/authz/permissions.ts:152-159, permissions.ts:177-184, sync.ts:366-374

Issue: Multiple locations use try { ... } catch { return []; } patterns that silently swallow errors when parsing composite SpiceDB project IDs via fromSpiceDbProjectId(). There is no logging when IDs fail to parse.

Why: If SpiceDB returns malformed IDs (during migration, data corruption, or bugs), users silently lose access to projects with no error trail. This makes debugging authorization issues extremely difficult in production. The listAccessibleProjectIds, listUsableProjectIds, and listUserProjectMembershipsInSpiceDb functions all have this pattern.

Fix: Add logging in catch blocks to aid debugging while preserving graceful degradation:

} catch (error) {
  console.warn(`Failed to parse SpiceDB project ID: ${id}`, error);
  return [];
}

Consider using a structured logger if available in this module.

Refs:


🟠 2) permissions.test.ts Missing test coverage for listUsableProjectIds cross-tenant filtering

Issue: The listUsableProjectIds function includes tenant filtering logic that silently drops IDs from other tenants, but there is no unit test verifying this security-critical behavior. The similar function listAccessibleProjectIds has a test for cross-tenant filtering (lines 235-248), but listUsableProjectIds does not.

Why: If the tenant filtering logic were accidentally removed or broken, users could see projects from other tenants in API responses (e.g., the availableAgents endpoint). This is a cross-tenant data exposure risk.

Fix: Add a test case to permissions.test.ts:

describe('listUsableProjectIds', () => {
  it('should filter out projects from other tenants', async () => {
    mockLookupResources.mockResolvedValue([
      'test-tenant/project-1',
      'other-tenant/project-2',
      'test-tenant/project-3',
    ]);

    const result = await listUsableProjectIds({
      userId: 'user-1',
      tenantId: 'test-tenant',
    });

    expect(result).toEqual(['project-1', 'project-3']);
  });
});

Refs:


🟠 3) config.ts Missing unit tests for core ID transformation functions

Issue: No unit tests exist for toSpiceDbProjectId and fromSpiceDbProjectId functions. These are the core ID transformation functions that the entire tenant-scoped authorization system depends on.

Why: If the composite ID format parsing/generation logic breaks (wrong separator, incorrect substring handling), all SpiceDB operations would silently fail or produce incorrect results. The fromSpiceDbProjectId function throws on invalid format, but this error path is untested.

Fix: Add tests (consider creating config.test.ts):

describe('toSpiceDbProjectId', () => {
  it('should compose tenant-scoped ID', () => {
    expect(toSpiceDbProjectId('tenant-abc', 'proj-123')).toBe('tenant-abc/proj-123');
  });
});

describe('fromSpiceDbProjectId', () => {
  it('should parse tenant-scoped ID', () => {
    const result = fromSpiceDbProjectId('tenant-abc/proj-123');
    expect(result).toEqual({ tenantId: 'tenant-abc', projectId: 'proj-123' });
  });

  it('should throw on invalid format', () => {
    expect(() => fromSpiceDbProjectId('invalid-no-slash')).toThrow('Invalid SpiceDB project ID format');
  });

  it('should handle projectId containing separator', () => {
    const result = fromSpiceDbProjectId('tenant/project/with/slashes');
    expect(result).toEqual({ tenantId: 'tenant', projectId: 'project/with/slashes' });
  });
});

Refs:

Inline Comments:

  • 🟠 Major: permissions.ts:152-159 Silent failure when parsing composite SpiceDB IDs (see inline for details)

🟡 Minor (2) 🟡

🟡 1) sync-spicedb.sh:249 Typo in migration command documentation

Issue: Comment references pnpm spicedb:migrate-ids:apply but the actual command is pnpm spicedb:migrate-ids --apply.
Why: Operators copy-pasting will get "command not found".
Fix: Change to pnpm spicedb:migrate-ids --apply.

Inline Comments:

  • 🟡 Minor: sync-spicedb.sh:249 Typo in migration command (see inline for 1-click fix)

💭 Consider (2) 💭

💭 1) migrate-spicedb-project-ids.ts Database connection cleanup

Issue: The migration script creates a database client but doesn't explicitly close the connection before process.exit(0).
Why: While process.exit() will terminate regardless, explicit cleanup is best practice for scripts that may be run repeatedly during dry-run testing.
Fix: Consider adding connection cleanup in a finally block if the Drizzle client exposes a close method.

💭 2) .changeset/ Missing changeset for agents-core public API change

Issue: This PR adds new public exports (toSpiceDbProjectId, fromSpiceDbProjectId) to @inkeep/agents-core's authz subpath.
Why: Per AGENTS.md, public surface changes to published packages should have a changeset.
Fix: pnpm bump patch --pkg agents-core "Add tenant-scoped SpiceDB project ID utilities" (or minor if these are considered new features).


🚫 REQUEST CHANGES

Summary: This PR makes important security improvements by introducing tenant-scoped composite project IDs in SpiceDB, but there is one critical blocking bug: the canUseProjectStrict call in trySlackUserJwtAuth (runAuth.ts:245-248) is missing the newly required tenantId parameter. This will break all Slack work app authenticated requests. Please fix this before merging.

Additionally, the silent error handling in ID parsing functions should be addressed to improve debuggability, and test coverage for the cross-tenant filtering logic should be added given the security-sensitive nature of these changes.

Discarded (4)
Location Issue Reason Discarded
migrate-spicedb-project-ids.ts Migration doesn't preserve user role relationships on old IDs Upon review, the script correctly deletes OLD plain-ID relationships — if user roles exist on composite IDs (created by sync functions), they're preserved. The migration only fixes org links, which is the stated purpose.
permissions.test.ts Missing test for canUseProjectStrict Lower priority — middleware integration tests cover the happy path, and the CRITICAL bug needs to be fixed first anyway.
sync.test.ts Missing test for revokeAllProjectMemberships tenant prefix Lower priority — the behavior change is correct, testing is nice-to-have.
sync.test.ts Missing test for listUserProjectMembershipsInSpiceDb filtering Lower priority — membership listing is lower risk than permission checks.
Reviewers (7)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-security-iam 1 0 0 0 1 0 0
pr-review-errors 5 1 0 0 0 0 4
pr-review-tests 5 2 0 0 0 0 3
pr-review-devops 5 0 2 0 1 0 2
pr-review-standards 1 0 0 0 0 0 1
pr-review-consistency 2 0 0 0 0 0 2
pr-review-types 1 0 0 0 0 0 1
Total 20 3 2 0 2 0 13

Note: Multiple reviewers independently identified the same CRITICAL bug (missing tenantId in runAuth.ts), demonstrating high confidence in this finding. Findings were deduplicated and merged.

Comment on lines +152 to +159
return compositeIds.flatMap((id) => {
try {
const parsed = fromSpiceDbProjectId(id);
return parsed.tenantId === params.tenantId ? [parsed.projectId] : [];
} catch {
return [];
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🟠 MAJOR Silent failure when parsing composite SpiceDB IDs

Issue: The catch block silently returns an empty array for any parsing error, with no logging. This catches ALL exceptions, not just parsing errors from fromSpiceDbProjectId.

Why: If SpiceDB returns malformed IDs (e.g., during migration or data corruption), users silently lose access to projects without any indication why. Debugging becomes extremely difficult because there's no log of which IDs failed to parse.

Fix: Add logging for parse failures to aid debugging:

return compositeIds.flatMap((id) => {
  try {
    const parsed = fromSpiceDbProjectId(id);
    return parsed.tenantId === params.tenantId ? [parsed.projectId] : [];
  } catch (error) {
    // Log but don't throw - graceful degradation
    console.warn(`Failed to parse SpiceDB project ID: ${id}`, error);
    return [];
  }
});

Refs:

@github-actions github-actions bot deleted a comment from claude bot Feb 13, 2026
@inkeep
Copy link
Contributor

inkeep bot commented Feb 13, 2026

📚 Documentation Review

No documentation updates needed for this PR.

The tenant-scoped composite project ID changes are internal to SpiceDB authorization and don't affect user-facing APIs, configurations, or behaviors. The migration script (pnpm spicedb:migrate-ids) is an internal operator tool that handles the transition transparently.

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(5) Total Issues | Risk: High

🔴❗ Critical (1) ❗🔴

Inline Comments:

  • 🔴 Critical: runAuth.ts:245-248 Missing required tenantId parameter in canUseProjectStrict call breaks Slack work app authorization

🕐 Pending Recommendations (4)

Prior issues from the previous review that remain unaddressed:

  • 🟠 permissions.ts:152-159 Silent failure when parsing composite SpiceDB IDs — same pattern in permissions.ts:177-184 and sync.ts:366-374
  • 🟠 permissions.test.ts Missing test coverage for listUsableProjectIds cross-tenant filtering
  • 🟠 config.ts Missing unit tests for core ID transformation functions (toSpiceDbProjectId, fromSpiceDbProjectId)
  • 💭 .changeset/ Consider adding changeset for agents-core public API addition

✅ Resolved Since Last Review

  • 🟡 sync-spicedb.sh:249 — Typo in migration command documentation (fixed via 1-click suggestion ✓)

🚫 REQUEST CHANGES

Summary: The delta since the last review fixed the documentation typo (thanks!), but the CRITICAL bug remains: trySlackUserJwtAuth in runAuth.ts:245-248 is still missing the required tenantId parameter. This will cause all Slack work app authenticated requests to fail because the SpiceDB permission check will use an incorrectly formatted project ID.

The previous review's inline comment on this file appears to not have posted correctly, so I've added it again with a 1-click suggestion to fix it.

Discarded (0)

No new findings to discard in this delta review.

Reviewers (1)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator 1 0 0 0 1 4 0
Total 1 0 0 0 1 4 0

Note: Delta review — subagent reviewers not dispatched for 1-line documentation fix. Critical bug from prior review verified as still present.

@vercel vercel bot temporarily deployed to Preview – agents-docs February 13, 2026 21:59 Inactive
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) New Issues | (4) Pending | Risk: Medium

✅ Resolved Since Last Review

🔴 CRITICAL bug fixed: The missing tenantId parameter in trySlackUserJwtAuth (runAuth.ts:245-248) has been addressed. The fix correctly passes tenantId: payload.tenantId to canUseProjectStrict, ensuring Slack work app authenticated requests use the tenant-scoped composite project ID format.

The fix is minimal and correct — no new issues introduced.

🕐 Pending Recommendations (4)

Prior issues from previous reviews that remain unaddressed:

  • 🟠 permissions.ts:152-159 Silent failure when parsing composite SpiceDB IDs — same pattern in permissions.ts:177-184 and sync.ts:366-374. Add logging to catch blocks for debuggability.
  • 🟠 permissions.test.ts Missing test coverage for listUsableProjectIds cross-tenant filtering (security-critical behavior)
  • 🟠 config.ts Missing unit tests for core ID transformation functions (toSpiceDbProjectId, fromSpiceDbProjectId)
  • 💭 .changeset/ Consider adding changeset for agents-core public API addition (toSpiceDbProjectId, fromSpiceDbProjectId exports)

💡 APPROVE WITH SUGGESTIONS

Summary: The CRITICAL blocking bug is now fixed — Slack work app authorization will work correctly with the new tenant-scoped composite project IDs. 🎉

The pending recommendations above are quality/observability improvements that would strengthen this security-sensitive change but are not blocking. The silent error handling in ID parsing functions and missing test coverage for cross-tenant filtering are worth addressing before or shortly after merge to ensure production debuggability and maintain confidence in the tenant isolation guarantees.

Discarded (0)

No new findings to discard in this delta review.

Reviewers (1)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator 0 0 0 0 0 4 0
Total 0 0 0 0 0 4 0

Note: Delta review — the only change was a 1-line fix for the CRITICAL bug identified in the prior review. No subagent reviewers dispatched as the fix is straightforward and correct.

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) New Issues | (4) Pending | Risk: Medium

Delta Analysis

This review was triggered by a pull_request:synchronize event. The 10 commits since the last review (81618863c4a8..HEAD) are entirely from a merge of main into this branch — bringing in unrelated work (Slack work app fixes, changesets, version bumps).

No substantive changes to PR code. The only change to PR files is an automated version bump in packages/agents-core/package.json (0.47.5 → 0.48.2).

🕐 Pending Recommendations (4)

Prior issues from previous reviews that remain unaddressed:

  • 🟠 permissions.ts:152-159 Silent failure when parsing composite SpiceDB IDs — same pattern in permissions.ts:177-184 and sync.ts:366-374. Add logging to catch blocks for debuggability.
  • 🟠 permissions.test.ts Missing test coverage for listUsableProjectIds cross-tenant filtering (security-critical behavior)
  • 🟠 config.ts Missing unit tests for core ID transformation functions (toSpiceDbProjectId, fromSpiceDbProjectId)
  • 💭 .changeset/ Consider adding changeset for agents-core public API addition (toSpiceDbProjectId, fromSpiceDbProjectId exports)

✅ Previously Resolved

  • 🔴 runAuth.ts:245-248 — CRITICAL missing tenantId parameter (fixed in commit 81618863)
  • 🟡 sync-spicedb.sh:249 — Typo in migration command documentation (fixed via 1-click suggestion)

💡 APPROVE WITH SUGGESTIONS

Summary: No new code changes in this delta — just a merge from main. The previous review's approval stands. 🎉

The pending recommendations above are quality/observability improvements that would strengthen this security-sensitive change but are not blocking. The silent error handling in ID parsing functions and missing test coverage for cross-tenant filtering are worth addressing before or shortly after merge.

Discarded (0)

No new findings — delta contained no substantive PR changes.

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
Total 0 0 0 0 0 4 0

Note: No subagent reviewers dispatched — delta contained only a merge from main with no substantive changes to PR code.

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.

1 participant