Skip to content

feat: share sessions with specific users (AGE-2577) - #1122

Merged
hary73 merged 10 commits into
mainfrom
haryahuja/age-2577-session-sharing-with-collaborators
Oct 28, 2026
Merged

hary73 merged 10 commits into
mainfrom
haryahuja/age-2577-session-sharing-with-collaborators

Conversation

@hary73

@hary73 hary73 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

First of two stacked PRs for AGE-2577: lets a session creator share a session with specific users.

This PR only adds things. Grants are stored but don't give read access yet; the follow-up PR (stacked on this branch) makes canReadSession honour them. No existing behaviour changes here: PATCH { shared }, the session.shared column, ISessionStore, session listing, subscribe, downloads and schedules are untouched.

Changes

  • New access_control table (Postgres and SQLite migrations 20261027_000001_access_control). It has one row per (tenant, session, subject_type, subject_id) and stores granted_by_subject and created_at. It references session(session_id) with ON DELETE CASCADE. Only subject_type = 'user' is written for now.
  • IAccessControlStore with Postgres and SQLite implementations (list, grant, revoke), plus a shared contract suite.
    • Grant is one multi-row INSERT ... ON CONFLICT DO NOTHING, so re-granting is a no-op.
    • Revoke is one DELETE for a batch of subjects; subjects without a grant are ignored.
  • Session grants API under /api/v1/sessions/{session_id}/grants: list, plus one update endpoint that grants and revokes together. Only the session creator may call it. The update runs both writes in one transaction, so the sessions router takes withTransaction again. See New APIs below.
  • IUserStore with generic { id, display_name, email } users:
    • TrueFoundry mode uses TrueFoundryUserStore, which calls ServiceFoundry GET v1/subjects/peek with the caller's token. Searches always send limit. Id lookups send no paging and are chunked 50 ids per call, to keep URLs short.
    • Standalone and OIDC use EmptyUserStore, so sharing with users is rejected there.
  • GET /api/v1/users (new Users tag): search with token pagination, or look up specific users with ids so the UI can show names and emails for a session's grants.

OpenAPI and SDK are not hand-edited; CI regenerates them.

New APIs

All endpoints need an authenticated caller. The /grants endpoints are restricted to the session creator (others get 403). Errors use { "error": { "message": "..." } }.

Method Path Fern Success
GET /api/v1/sessions/{session_id}/grants sessions.list_grants 200 ListSessionGrantsResponse
POST /api/v1/sessions/{session_id}/grants sessions.update_grants 200 ListSessionGrantsResponse
GET /api/v1/users?query=&limit=&page_token= or ?ids= users.list 200 ListUsersResponse

Update request (grant and revoke in one call)

{
  "grant": [{ "subject_type": "user", "subject_id": "u1" }],
  "revoke": [{ "subject_type": "user", "subject_id": "u2" }]
}
  • Both lists are optional, up to 50 subjects each, and at least one subject is required overall. Only user subjects in this PR.
  • Atomic: all changes apply or none do.
  • Duplicates are collapsed. Granting an existing subject, or revoking one without a grant, is a no-op.
  • 422 (nothing written) if a subject to grant is the creator or an unknown or inactive user, or if a subject appears in both lists.

List / update response

{
  "data": [{
    "subject": { "subject_type": "user", "subject_id": "u1" },
    "granted_by_subject": { "subject_id": "owner", "subject_type": "user", "subject_display_name": "Owner" },
    "created_at": "2026-10-22T08:30:00.000Z"
  }]
}

Users response

{
  "data": [{ "id": "u1", "display_name": "Alice", "email": "alice@example.com" }],
  "pagination": { "limit": 25, "next_page_token": "…" }
}
  • Search: query matches email or display name, limit defaults to 25 (max 100), and page_token pages through results.
  • Lookup: ids=u1,u2,… (up to 100) returns just those users, for showing names and emails of a session's grants. Unknown or inactive ids are omitted. It can't be combined with query or page_token.
  • Returns active users in the caller's tenant only, and is always empty outside TrueFoundry mode.

Not in this PR: read access from these grants, and the tenant (everyone) subject. Grants carry only ids; resolve names and emails with GET /users?ids=.

How was this tested?

  • New contract suite for the store, run against both SQLite and Postgres (test:store:sqlite, test:store:postgres), including batch revoke.
  • New unit tests:
    • grants API: owner-only rules and 404s; grant and revoke separately and together; atomicity, i.e. an invalid grant leaves existing grants untouched; duplicates and idempotency; subject in both lists; 422s; per-list limit of 50; body validation;
    • GET /users: search paging, plus ids lookup (omits unknown ids, limit of 100, can't combine with query/page_token);
    • TrueFoundryUserStore;
    • peekUsers: search sends limit/offset, id lookup doesn't, 424 on a malformed body.
  • A regression test asserts that a user with a grant still gets 403 on GET /sessions/{id}. The stacked PR flips it.
  • tsc --noEmit (src and both test configs), ESLint on changed files, full unit suite (734 tests).

Checklist

  • I have read the contributing guidelines
  • pnpm build, pnpm test, pnpm typecheck, pnpm lint:ci, and pnpm format:check pass locally
  • Tests added/updated where it makes sense
  • No hand-edits to generated code (packages/trueforge-sdk, python/trueforge_sdk, .github/fern/openapi/openapi.json, docs/openapi.json) — fork PRs omit SDK regen; maintainers regenerate after merge
  • Docs / .env.example updated if configuration or behavior changed

Note

Medium Risk
Adds authorization-adjacent APIs and grant storage with creator-only enforcement, but grants do not yet affect session read access; user validation depends on ServiceFoundry in TrueFoundry mode.

Overview
Adds per-user session sharing as stored grants plus HTTP APIs to manage them, without changing who can read a session today.

New access_control persistence (Postgres/SQLite) records explicit user grants per session, with list/grant/revoke via GET/POST /api/v1/sessions/{session_id}/grants. Only the session creator may list or update grants; updates apply grant and revoke lists atomically (up to 50 each), validate users against IUserStore, and return the full grant list.

GET /api/v1/users supports search or ids lookup so clients can resolve share targets. In TrueFoundry mode this calls ServiceFoundry v1/subjects/peek; standalone/OIDC use an empty store so grants to users are rejected. OpenAPI and SDK clients gain sessions.listGrants / updateGrants and users.list.

Grants are persisted only—canReadSession and existing shared behavior are unchanged; a test documents that grantees still get 403 on session reads until a follow-up PR.

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

@changeset-bot

changeset-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d9267e3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@truefoundry/trueforge Patch
@truefoundry/trueforge-core Patch

Not sure what this means? Click here to learn what changesets are.

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

@hary73
hary73 marked this pull request as ready for review October 21, 2026 21:11
Comment thread .github/fern/openapi/openapi.json Outdated
Comment thread .github/fern/openapi/openapi.json Outdated
Comment thread .github/fern/openapi/openapi.json Outdated
Comment thread .github/fern/openapi/openapi.json Outdated
Comment thread .github/fern/openapi/openapi.json Outdated
Comment thread packages/trueforge/src/app.ts Outdated
Comment thread packages/trueforge/src/truefoundry/accessToken.ts Outdated
Comment thread packages/trueforge/src/truefoundry/TrueFoundryServiceFoundryServerClient.ts Outdated
Comment thread packages/trueforge/src/truefoundry/TrueFoundryUserDirectory.ts Outdated
Comment thread packages/trueforge/src/schemas/sessionAccess.ts Outdated

@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 default effort and found 1 potential issue.

Fix All in Cursor

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

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 190b368. Configure here.

Comment thread packages/trueforge/src/truefoundry/TrueFoundryUserStore.ts Outdated
@hary73

hary73 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

How does user sharing work in opensource

In open source mode - either allow unvalidated users for sharing or restrict to only creator or tenant.

@ramantehlan ramantehlan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved

hary73 and others added 9 commits October 27, 2026 19:39
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Use "access" naming only: SessionAccessGrant -> SessionAccess, nesting the
  shared subject as `subject` so request and response use the same shape
- Drop display_name/email from access entries; names come from GET /users
- Rename UserDirectory -> IUserStore / userStore (TrueFoundryUserStore, EmptyUserStore)
- Use the existing callerAccessToken instead of moving requireUserCredential
- Avoid "SFY" in new code (PeekUser/PeekUsersResponse); require user email
- Cap subjects per request at 50
- Describe the caller-visible contract only: no creator-only wording,
  403 "User does not have permission.", simpler subject/user id descriptions
- Revoke: keep per-subject DELETE; 404 says the subject has no access

Co-authored-by: Cursor <cursoragent@cursor.com>
…-2577)

- Paths: /api/v1/sessions/{session_id}/grants for list, create and revoke
- Revoke takes subject_type and subject_id as required query params
  (DELETE /grants?subject_type=user&subject_id=...), so new identifying
  fields can be added later without changing the route
- Rename wire schemas and Fern methods: SessionGrant, SessionGrantSubject,
  ListSessionGrantsResponse, CreateSessionGrantsRequest,
  DeleteSessionGrantsRequestQuery, DeleteSessionGrantResponse;
  list_grants / create_grants / delete_grant

Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace POST /grants (create) and DELETE /grants with one
  POST /api/v1/sessions/{session_id}/grants taking { grant, revoke }
  (up to 50 each, at least one subject). Both lists apply atomically in
  one transaction; granting an existing subject or revoking one without
  a grant is a no-op. Fern: sessions.update_grants, returns 200 with
  every grant on the session.
- 422 when a subject to grant is the creator or unknown, or when a
  subject is in both lists; nothing is written.
- Store revoke takes a batch of subjects in one DELETE.
- GET /api/v1/users accepts ids=u1,u2,... (up to 100) so the UI can show
  names and emails for a session's grants; cannot be combined with
  query or page_token.
- peekUsers: id lookups no longer send limit/offset; searches still do.

Co-authored-by: Cursor <cursoragent@cursor.com>
…PI tests (AGE-2577)

- IAccessControlEntryStore -> IAccessControlStore (accessControlStore dep),
  PostgresAccessControlStore / SqliteAccessControlStore in access-control-store/,
  contract suite renamed to match
- Drop low-value GET /users tests (default page size, empty store,
  near-duplicate ids rejections)

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@hary73
hary73 force-pushed the haryahuja/age-2577-session-sharing-with-collaborators branch from 25826cd to 6c0cf64 Compare October 27, 2026 14:14
@hary73
hary73 merged commit 6722969 into main Oct 28, 2026
18 checks passed
@hary73
hary73 deleted the haryahuja/age-2577-session-sharing-with-collaborators branch October 28, 2026 04:43
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.

4 participants