Skip to content

fix(core): tenancy-scope datasets get and delete#18750

Merged
NikAiyer merged 9 commits into
mainfrom
MASTRA-4438
Jul 2, 2026
Merged

fix(core): tenancy-scope datasets get and delete#18750
NikAiyer merged 9 commits into
mainfrom
MASTRA-4438

Conversation

@NikAiyer

@NikAiyer NikAiyer commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

DatasetsManager.get({ id }) and DatasetsManager.delete({ id }) looked up a dataset by its primary key alone — no organizationId / projectId predicate. Any caller who knew a dataset id could read or delete it regardless of which tenant it belonged to. listDatasets already scopes correctly via filters, so this PR closes the asymmetry.

Fix pushes tenancy into the SQL WHERE clause at the storage layer (option (a) in the ticket — no fetch-then-assert, which would leak existence via error timing).

Linear: MASTRA-4438

What changed

Storage contract (packages/core/src/storage)

  • getDatasetById and deleteDataset accept optional filters?: DatasetTenancyFilters.
  • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

Adapters — inmemory, libsql, pg, mysql, mongodb, spanner

  • Push tenancy predicate into the query when passed.
  • Delete cascade (dataset items / versions) is gated by a scoped parent pre-check, so a mismatched delete is a silent no-op and cross-tenant data is never touched.

Core (DatasetsManager, Dataset)

  • DatasetsManager.get / .delete accept flat { id, organizationId?, projectId? }.
  • Tenancy is stashed on the returned Dataset handle and forwarded to getDetails, update, addItem, item batch ops, and startExperimentAsync.

Server (packages/server)

  • GET /datasets/:id, PATCH /datasets/:id, and DELETE /datasets/:id accept optional organizationId and projectId query params.

Client (client-sdks/client-js)

  • getDataset, updateDataset, deleteDataset accept optional tenancy args.

Behavior

  • Omitting tenancy → no predicate added, existing behavior preserved. Fully backwards-compatible.
  • Mismatch on get → storage returns null; manager throws NOT_FOUND; HTTP returns 404 — same as a truly missing id, so existence does not leak.
  • Mismatch on delete → silent no-op, matching the existing "delete non-existent is no-op" semantic.

Example

// Before
const ds = await mastra.datasets.get({ id });
await mastra.datasets.delete({ id });

// After — scoped to a tenant
const ds = await mastra.datasets.get({ id, organizationId, projectId });
await mastra.datasets.delete({ id, organizationId, projectId });
GET /datasets/abc123?organizationId=org_a&projectId=proj_1
DELETE /datasets/abc123?organizationId=org_a

Test plan

Automated

  • @mastra/core — 266 dataset tests pass, no type errors.
  • @mastra/server — 11 handler tests (4 preexisting + 7 new tenancy tests) pass. check:permissions + check:core-imports clean. Build clean.
  • @mastra/libsql — 46 dataset tests pass (includes new shared tenancy suite).
  • @mastra/pg — 91 dataset tests pass (includes new shared tenancy suite).
  • @mastra/mongodb, @mastra/mysql, @mastra/spanner — build clean; mysql unit tests pass. Their docker-backed integration tests should run in CI (containers were not up locally).
  • @mastra/client-js — typecheck clean.

New tests

  • 8 tenancy cases in the shared _test-utils datasets suite — auto-runs against every store adapter.
  • 6 DatasetsManager unit tests verifying tenancy is forwarded through get, delete, and back onto the returned Dataset handle.
  • 7 server handler tests covering GET / PATCH / DELETE × match / org-mismatch / project-mismatch, with assertions that the underlying dataset is untouched on rejected mutations.

Follow-ups (out of scope)

  • Tenancy query params for listDatasets at the HTTP + client-js layer.
  • Same audit for experiments and scorer-definitions get/delete.
  • Simplify the belt-and-suspenders read in services/agent-learning/exploratory/experiments/src/runExperiment.ts now that the manager enforces tenancy.

ELI5

This PR makes sure you can’t use a dataset ID to read or delete a dataset that belongs to a different tenant. If the dataset isn’t in the tenant you asked for, get behaves like it doesn’t exist (404/NOT_FOUND) and delete silently does nothing.

Summary

  • Extended dataset scoping to support optional tenancy filters (organizationId and/or projectId) for DatasetsManager.get and DatasetsManager.delete, including carrying that scope on the returned Dataset handle.
  • Updated the storage layer contracts and mutation input types to accept optional tenancy filters so adapters can apply the predicates in queries (rather than post-fetch assertions).
  • Implemented tenancy-aware behavior across adapters/storage backends (inmemory, libsql, pg, mysql, mongodb, spanner):
    • getDatasetById returns null on tenancy mismatch.
    • deleteDataset becomes a silent no-op on tenancy mismatch to avoid existence leakage.
    • Dataset/item mutation flows (including related item operations and bulk mutations) gate by scoped parent dataset lookup.
  • Updated server routes for GET, PATCH, and DELETE /datasets/:id to accept optional organizationId/projectId query params and forward them into Mastra dataset operations (with DELETE avoiding cross-tenant existence leakage via conditional preflight).
  • Updated client SDK and generated route typings so getDataset, updateDataset, and deleteDataset accept optional tenancy arguments and send them as query params.
  • Strengthened core protections and tests:
    • Dataset now reasserts scope/ownership before experiment mutations and result operations (not just dataset reads).
    • updateExperimentResult input now requires experimentId, with improved ownership/missing-id error behavior.
    • Added/extended tests to verify cross-tenant and cross-dataset ownership failures return the correct errors/no-op behavior without leaking existence.

DatasetsManager.get and DatasetsManager.delete used to look up a dataset by
its primary key alone, so any caller who knew the id could read or delete
it regardless of tenant. This pushes the tenancy predicate into the storage
layer via an optional filter (option (a) — no fetch-then-assert).

- Storage contract: getDatasetById / deleteDataset and item-mutation inputs
  accept optional filters. All adapters (inmemory, libsql, pg, mysql,
  mongodb, spanner) honor the predicate when set.
- Core: DatasetsManager.get / .delete accept flat organizationId / projectId
  args, stash them on the returned Dataset, and forward on every downstream
  storage call (getDetails, update, item ops, startExperimentAsync).
- Server: GET / PATCH / DELETE /datasets/:id accept organizationId and
  projectId query params.
- Client: getDataset, updateDataset, deleteDataset accept optional tenancy.
- Tests: shared _test-utils tenancy suite (auto-runs against every adapter),
  DatasetsManager unit tests, and server handler tests.

On tenancy mismatch, get throws NOT_FOUND (returns null at the storage
layer) and delete is a silent no-op — matching how a missing id already
behaves, so existence does not leak through error timing or messages.

Legacy calls that omit tenancy are unchanged, so this is fully
backwards-compatible.

MASTRA-4438

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc92a74

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

This PR includes changesets to release 27 packages
Name Type
@mastra/client-js Minor
@mastra/server Minor
@mastra/mongodb Patch
@mastra/spanner Patch
@mastra/core Minor
@mastra/libsql Patch
@mastra/mysql Patch
@mastra/pg Patch
mastracode Patch
@mastra/playground-ui Major
@internal/playground Patch
@mastra/react Patch
@mastra/deployer Minor
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/nestjs Patch
@mastra/next Patch
@mastra/tanstack-start Patch
@mastra/mcp-docs-server Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
mastra Patch
@mastra/deployer-cloud Minor
@mastra/temporal Patch
create-mastra 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

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f95784dc-aafd-45f3-a76f-4237894e1457

📥 Commits

Reviewing files that changed from the base of the PR and between e2766d9 and 0db24e0.

📒 Files selected for processing (2)
  • packages/core/src/datasets/__tests__/dataset.test.ts
  • packages/core/src/datasets/dataset.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/datasets/dataset.ts

Walkthrough

This PR adds optional tenancy scoping (organizationId/projectId) to dataset reads, updates, and deletes across the client SDK, core dataset/storage layers, server routes, and storage adapters. Mismatched reads and updates return not found; mismatched deletes no-op. Supporting tests and release notes were updated.

Changes

Tenancy scoping for dataset operations

Layer / File(s) Summary
Client SDK tenancy query params and generated types
client-sdks/client-js/src/client.ts, client-sdks/client-js/src/types.ts, client-sdks/client-js/src/utils/index.ts, client-sdks/client-js/src/route-types.generated.ts, packages/cli/src/commands/api/route-metadata.generated.ts
buildTenancyQuery builds optional org/project query params; getDataset, updateDataset, deleteDataset, and UpdateDatasetParams accept tenancy fields; generated route types and CLI metadata expose the new dataset query params.
Core DatasetsManager and Dataset handle scope propagation
packages/core/src/datasets/manager.ts, packages/core/src/datasets/dataset.ts, packages/core/src/datasets/experiment/index.ts, packages/core/src/datasets/experiment/types.ts, packages/core/src/datasets/__tests__/manager.test.ts, packages/core/src/datasets/__tests__/dataset.test.ts
scopeFromArgs derives tenancy filters; DatasetsManager.get/delete and Dataset forward filters to storage for details, updates, items, and experiments; ExperimentConfig gains an internal filters field excluded from StartExperimentConfig; tests cover scoping and mismatch behavior.
Storage contract and input type updates
packages/core/src/storage/domains/datasets/base.ts, packages/core/src/storage/types.ts
Abstract getDatasetById/deleteDataset gain optional filters; dataset and item mutation inputs add optional filters; base methods propagate filters to internal lookups.
In-memory storage tenancy enforcement
packages/core/src/storage/domains/datasets/inmemory.ts
matchesTenancy gates getDatasetById and deleteDataset on tenancy mismatch.
SQL/NoSQL storage adapters tenancy filtering
stores/libsql/..., stores/mongodb/..., stores/mysql/..., stores/pg/..., stores/spanner/..., stores/_test-utils/...
Adapter-specific tenancy helpers scope getDatasetById and gate deleteDataset with pre-checks; shared test-utils cover tenancy isolation across adapters.
Server route and schema wiring
packages/server/src/server/schemas/datasets.ts, packages/server/src/server/handlers/datasets.ts, packages/server/src/server/handlers/datasets.test.ts
tenancyQuerySchema and updated GET/PATCH/DELETE dataset routes parse and forward organizationId/projectId; tests verify mismatch behavior without leaking existence.
Changesets documenting tenancy behavior
.changeset/*.md
Version bumps and release notes describing tenancy scoping across client, core, and server.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: dane-ai-mastra, abhiaiyer91

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change to dataset tenancy scoping for get and delete operations.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MASTRA-4438

Comment @coderabbitai help to get the list of available commands.

@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
mastra-docs-1.x Ready Ready Preview, Comment Jul 2, 2026 5:35pm
mastra-playground-ui Ready Ready Preview, Comment Jul 2, 2026 5:35pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mastra-studio-preview Ignored Ignored Preview Jul 2, 2026 5:35pm

Request Review

@dane-ai-mastra

dane-ai-mastra Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked issue check skipped for core contributor @NikAiyer.


PR complexity score

Factor Value Score impact
Files changed 27 +54
Lines changed 1407 +60
Author merged PRs 607 -20
Test files changed Yes -10
Final score 84

Applied label: complexity: critical


Changed test gate

Changed tests failed against the base branch as expected.

Label: tests: green ✅

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
@NikAiyer NikAiyer requested a review from LekoArts as a code owner July 1, 2026 16:05
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x July 1, 2026 16:05 Inactive
@dane-ai-mastra dane-ai-mastra Bot added the tests: failing ❌ Changed tests passed against base label Jul 1, 2026
@dane-ai-mastra dane-ai-mastra Bot added tests: green ✅ Changed tests failed against base as expected and removed tests: failing ❌ Changed tests passed against base labels Jul 1, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

🧹 Nitpick comments (1)
stores/_test-utils/src/domains/datasets/index.ts (1)

84-237: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a scoped updateDataset contract test.

This suite covers scoped getDatasetById and deleteDataset, but not updateDataset even though scoped PATCH/update is part of this change. Adding a shared adapter test here would catch stores that still update by bare id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stores/_test-utils/src/domains/datasets/index.ts` around lines 84 - 237, Add
a tenancy-scoping contract test for updateDataset in the datasets test suite so
scoped PATCH/update behavior is covered alongside getDatasetById and
deleteDataset. Reuse the existing tenancy patterns in datasetsStorage and verify
that updateDataset with mismatched organizationId/projectId does not modify the
record, while the matching tenancy does; this will catch adapters that still
update by bare id.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/chatty-areas-grab.md:
- Around line 12-14: The changelog entry for the optional tenancy arguments
needs a wording fix because it incorrectly says `deleteDataset` returns a 404 on
tenant mismatch. Update the text in the changeset to keep the 404 behavior only
for `getDataset` and `updateDataset`, and rephrase the `deleteDataset` part so
it clearly states a silent no-op on mismatch without promising an error
response.

In `@packages/core/src/datasets/dataset.ts`:
- Around line 31-43: The Dataset tenancy scope is only applied to some reads and
mutations, but several handle methods still bypass it and can access
cross-tenant data. Update the remaining Dataset surface methods such as getItem,
listItems, deleteItem, listVersions, getItemHistory, and the experiment/listing
helpers to consistently forward `#scope` to storage calls. Use the existing scope
plumbing in Dataset.getDetails and Dataset.startExperimentAsync as the pattern
so all handle operations are tenancy-safe.

In `@packages/core/src/storage/types.ts`:
- Around line 2587-2607: Single-item dataset deletes are missing tenancy scoping
in the storage contract, so add an optional filters field to the delete-item
input alongside AddDatasetItemInput and UpdateDatasetItemInput. Update
DatasetsStorage.deleteItem(...) to first call getDatasetById({ id:
args.datasetId, filters: args.filters }) before proceeding to
_doDeleteItem(...), and make Dataset.deleteItem(...) pass this.#scope through so
scoped handles enforce the tenant.
- Around line 2562-2563: The update filter documentation in
DatasetTenancyFilters is incorrect because update operations do not no-op when
the scoped row is missing; the storage base and adapters throw NOT_FOUND
instead. Update the doc comment on the filters field in DatasetTenancyFilters to
describe the actual update behavior, and keep the wording aligned with the
storage base and adapter implementations that enforce the not-found error.

In `@packages/server/src/server/handlers/datasets.ts`:
- Around line 302-307: The dataset delete handler is doing a preflight
`mastra.datasets.get()` before `mastra.datasets.delete()`, which turns
tenant-scoped idempotent deletes back into a 404 on tenancy mismatch. Remove the
existence check from the delete path in `handler` and let
`mastra.datasets.delete` handle scoped deletes directly using `datasetId`,
`organizationId`, and `projectId` so the silent no-op behavior is preserved.

In `@stores/spanner/src/storage/domains/datasets/index.ts`:
- Around line 315-317: Scope the dataset update in the `update` flow by tenancy
instead of only on the readback. In `datasets/index.ts`, adjust the
`this.db.update` call inside the dataset update method so the write itself
includes the tenant-scoping criteria from `args.filters` (not just
`getDatasetById`), preventing cross-tenant mutation before the `NOT_FOUND`
check.

---

Nitpick comments:
In `@stores/_test-utils/src/domains/datasets/index.ts`:
- Around line 84-237: Add a tenancy-scoping contract test for updateDataset in
the datasets test suite so scoped PATCH/update behavior is covered alongside
getDatasetById and deleteDataset. Reuse the existing tenancy patterns in
datasetsStorage and verify that updateDataset with mismatched
organizationId/projectId does not modify the record, while the matching tenancy
does; this will catch adapters that still update by bare id.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35037395-3d53-4d7f-970d-31feccf98a4b

📥 Commits

Reviewing files that changed from the base of the PR and between bf62322 and a978dc7.

📒 Files selected for processing (26)
  • .changeset/chatty-areas-grab.md
  • .changeset/pink-mails-attend.md
  • .changeset/swift-things-push.md
  • .changeset/tired-donuts-fly.md
  • client-sdks/client-js/src/client.ts
  • client-sdks/client-js/src/route-types.generated.ts
  • client-sdks/client-js/src/types.ts
  • client-sdks/client-js/src/utils/index.ts
  • packages/cli/src/commands/api/route-metadata.generated.ts
  • packages/core/src/datasets/__tests__/manager.test.ts
  • packages/core/src/datasets/dataset.ts
  • packages/core/src/datasets/experiment/index.ts
  • packages/core/src/datasets/experiment/types.ts
  • packages/core/src/datasets/manager.ts
  • packages/core/src/storage/domains/datasets/base.ts
  • packages/core/src/storage/domains/datasets/inmemory.ts
  • packages/core/src/storage/types.ts
  • packages/server/src/server/handlers/datasets.test.ts
  • packages/server/src/server/handlers/datasets.ts
  • packages/server/src/server/schemas/datasets.ts
  • stores/_test-utils/src/domains/datasets/index.ts
  • stores/libsql/src/storage/domains/datasets/index.ts
  • stores/mongodb/src/storage/domains/datasets/index.ts
  • stores/mysql/src/storage/domains/datasets/index.ts
  • stores/pg/src/storage/domains/datasets/index.ts
  • stores/spanner/src/storage/domains/datasets/index.ts

Comment thread .changeset/chatty-areas-grab.md Outdated
Comment thread packages/core/src/datasets/dataset.ts
Comment thread packages/core/src/storage/types.ts Outdated
Comment thread packages/core/src/storage/types.ts
Comment thread packages/server/src/server/handlers/datasets.ts
Comment thread stores/spanner/src/storage/domains/datasets/index.ts
… updateDataset test, single-item deleteItem plumbing

Follow-up on the initial MASTRA-4438 patch, addressing CodeRabbit + audit findings:

- Dataset handle now gates unscoped read paths (getItem, listItems, listVersions,
  getItemHistory, listExperiments, getExperiment, listExperimentResults,
  updateExperimentResult, deleteExperiment) with a private #assertScope() that
  runs a scoped getDatasetById preflight and throws DATASET_NOT_FOUND on miss.
  Closes the last known handle-level cross-tenant read gap.

- DeleteDatasetItemInput now carries optional filters; base deleteItem enforces
  a parent-existence gate identical to batchDeleteItems, so a leaked itemId
  cannot delete an item from a dataset in another tenant. Updated inmemory +
  libsql/pg/mysql/mongodb/spanner adapters to the new signature (parent gate
  in base means adapter bodies unchanged).

- Shared _test-utils/datasets adds three scoped-updateDataset contract tests
  (cross-tenant NOT_FOUND, matching-tenant apply, mixed-tenancy sanity). Runs
  against every adapter via createTestSuite.

- Sharpen changeset wording: reads/updates throw NOT_FOUND (404 over HTTP),
  deletes silently no-op on tenancy mismatch.

- Server handler test for cross-tenant delete rewritten to reflect the
  silent-no-op contract (returns success, row untouched).

- Verified: @mastra/core 266 tests + tsc --noEmit; @mastra/libsql 817 tests
  (includes shared tenancy + new update-scope tests); @mastra/server 11
  handler tests + check:permissions + check:core-imports. Route metadata +
  route types regenerated (no diff — additive optional query params).

Related: MASTRA-4438

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/datasets/dataset.ts (1)

227-236: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Forward #scope into listItems() instead of relying on preflight.

ListDatasetItemsInput already accepts filters, but this path drops them after #assertScope(). Keep the tenancy predicate in the storage query to avoid fetch-then-use gaps.

Proposed fix
     return store.listItems({
       datasetId: this.id,
       search: args?.search,
       pagination: { page: args?.page ?? 0, perPage: args?.perPage ?? 20 },
+      filters: this.#scope,
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/datasets/dataset.ts` around lines 227 - 236, The dataset
item listing path in Dataset.listItems() currently relies on `#assertScope`() and
then drops the tenancy predicate before calling store.listItems(), which can
create a fetch-then-use gap. Update the listItems() flow to pass this.#scope
through ListDatasetItemsInput.filters (alongside datasetId, search, and
pagination) so the store query enforces tenancy directly, and keep the version
branch unchanged unless it also needs the same scope propagation.
♻️ Duplicate comments (1)
packages/core/src/datasets/dataset.ts (1)

209-212: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Bind child lookups and mutations to this dataset, not just the parent preflight.

#assertScope() only proves this.id is in scope. These methods then use caller-supplied item/experiment IDs without proving those child records belong to this.id, so a known cross-dataset child ID can still be read or mutated through a valid scoped handle. Add dataset/scoped storage predicates or verify ownership before returning/updating/deleting. This is the remaining unresolved part of the earlier handle-surface tenancy concern.

Also applies to: 287-290, 397-430

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/datasets/dataset.ts` around lines 209 - 212, The child
lookup/mutation paths are only doing the parent scope check via `#assertScope`(),
but still trust caller-provided item/experiment IDs that may belong to a
different dataset. Update the affected Dataset methods such as getItem and the
other child accessors/mutators in this class to verify ownership against this.id
before returning, updating, or deleting records, either by adding dataset-scoped
store predicates or by checking the fetched child record’s dataset association.
Keep the fix local to the Dataset API surface so cross-dataset child IDs cannot
be accessed through a valid scoped handle.
🧹 Nitpick comments (2)
packages/server/src/server/handlers/datasets.test.ts (2)

161-175: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add projectId mismatch coverage for update and delete.

GET covers projectId mismatch, but UPDATE and DELETE only exercise organizationId. Add parallel projectId: 'proj_2' cases so query-param forwarding for both tenancy dimensions is protected.

Also applies to: 205-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/server/handlers/datasets.test.ts` around lines 161 - 175,
Add missing projectId mismatch coverage for the update and delete dataset
handler tests. In the UPDATE_DATASET_ROUTE.handler and
DELETE_DATASET_ROUTE.handler cases, mirror the existing organizationId mismatch
assertions by also passing a different projectId (for example, proj_2) so both
tenancy dimensions are verified. Keep the new checks alongside the existing
dataset creation and rejection expectations in datasets.test.ts.

108-121: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert the 404 status, not just the exception type.

These tests would still pass if the handler threw an HTTPException with the wrong status. Since the requirement is “not found/no info leak,” assert the exception status/message shape as well.

Also applies to: 124-138, 161-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/server/handlers/datasets.test.ts` around lines 108 - 121,
The GET_DATASET_ROUTE.handler tests only assert that an HTTPException is thrown,
so they can pass even if the status is wrong. Update the affected cases in
datasets.test.ts to verify the exception’s 404/not found shape, not just the
type, by asserting the status/message on the thrown error for
GET_DATASET_ROUTE.handler and the related organization mismatch scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/core/src/datasets/dataset.ts`:
- Around line 227-236: The dataset item listing path in Dataset.listItems()
currently relies on `#assertScope`() and then drops the tenancy predicate before
calling store.listItems(), which can create a fetch-then-use gap. Update the
listItems() flow to pass this.#scope through ListDatasetItemsInput.filters
(alongside datasetId, search, and pagination) so the store query enforces
tenancy directly, and keep the version branch unchanged unless it also needs the
same scope propagation.

---

Duplicate comments:
In `@packages/core/src/datasets/dataset.ts`:
- Around line 209-212: The child lookup/mutation paths are only doing the parent
scope check via `#assertScope`(), but still trust caller-provided item/experiment
IDs that may belong to a different dataset. Update the affected Dataset methods
such as getItem and the other child accessors/mutators in this class to verify
ownership against this.id before returning, updating, or deleting records,
either by adding dataset-scoped store predicates or by checking the fetched
child record’s dataset association. Keep the fix local to the Dataset API
surface so cross-dataset child IDs cannot be accessed through a valid scoped
handle.

---

Nitpick comments:
In `@packages/server/src/server/handlers/datasets.test.ts`:
- Around line 161-175: Add missing projectId mismatch coverage for the update
and delete dataset handler tests. In the UPDATE_DATASET_ROUTE.handler and
DELETE_DATASET_ROUTE.handler cases, mirror the existing organizationId mismatch
assertions by also passing a different projectId (for example, proj_2) so both
tenancy dimensions are verified. Keep the new checks alongside the existing
dataset creation and rejection expectations in datasets.test.ts.
- Around line 108-121: The GET_DATASET_ROUTE.handler tests only assert that an
HTTPException is thrown, so they can pass even if the status is wrong. Update
the affected cases in datasets.test.ts to verify the exception’s 404/not found
shape, not just the type, by asserting the status/message on the thrown error
for GET_DATASET_ROUTE.handler and the related organization mismatch scenarios.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 975195b5-6516-45fd-8c44-6f9e89b846da

📥 Commits

Reviewing files that changed from the base of the PR and between a978dc7 and dd0dd10.

📒 Files selected for processing (13)
  • .changeset/chatty-areas-grab.md
  • packages/core/src/datasets/dataset.ts
  • packages/core/src/storage/domains/datasets/base.ts
  • packages/core/src/storage/domains/datasets/inmemory.ts
  • packages/core/src/storage/types.ts
  • packages/server/src/server/handlers/datasets.test.ts
  • packages/server/src/server/handlers/datasets.ts
  • stores/_test-utils/src/domains/datasets/index.ts
  • stores/libsql/src/storage/domains/datasets/index.ts
  • stores/mongodb/src/storage/domains/datasets/index.ts
  • stores/mysql/src/storage/domains/datasets/index.ts
  • stores/pg/src/storage/domains/datasets/index.ts
  • stores/spanner/src/storage/domains/datasets/index.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • .changeset/chatty-areas-grab.md
  • packages/core/src/storage/types.ts
  • stores/spanner/src/storage/domains/datasets/index.ts
  • stores/mysql/src/storage/domains/datasets/index.ts
  • packages/core/src/storage/domains/datasets/inmemory.ts
  • stores/libsql/src/storage/domains/datasets/index.ts
  • packages/core/src/storage/domains/datasets/base.ts
  • stores/pg/src/storage/domains/datasets/index.ts
  • packages/server/src/server/handlers/datasets.ts
  • stores/mongodb/src/storage/domains/datasets/index.ts

Address CodeRabbit round-2 review on MASTRA-4438:

- Dataset.getItem / getItemHistory / getExperiment / listExperimentResults
  now verify the returned child row's parent datasetId matches this.id
  before returning. Prevents a caller with a valid handle to dataset A from
  reading item/experiment rows belonging to dataset B via a caller-supplied
  child id.
- Dataset.updateExperimentResult / deleteExperiment throw NOT_FOUND on
  ownership mismatch, matching the existing deleteItem contract.
- Dataset.listItems now forwards this.#scope via ListDatasetItemsInput.filters
  instead of an extra preflight roundtrip.
- Server handler tests: assert HTTP 404 status on scope-mismatch (not just
  HTTPException) and add projectId-mismatch cases for update + delete
  routes.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x July 1, 2026 21:19 Inactive

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/core/src/datasets/dataset.ts (1)

422-425: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Re-run the tenant scope check before experiment mutations/results access.

#assertExperimentOwnership() validates only experiment.datasetId === this.id; it does not prove the parent dataset still matches #scope. Since listExperimentResults(), updateExperimentResult(), and deleteExperiment() rely on this helper, a scoped/stale handle can bypass the same per-operation tenancy check used by the other handle methods.

Suggested fix
 async `#assertExperimentOwnership`(experimentId: string): Promise<void> {
+  await this.#assertScope();
   const experimentsStore = await this.#getExperimentsStore();
   const experiment = await experimentsStore.getExperimentById({ id: experimentId });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/datasets/dataset.ts` around lines 422 - 425, Re-run the
tenant scope check inside `#assertExperimentOwnership` before allowing experiment
access or mutation. The helper currently only verifies experiment.datasetId
against this.id, so update it to also validate the dataset’s current scope via
`#checkScope` (or the equivalent dataset fetch/validation used by other handle
methods) before returning. Keep the fix localized in `#assertExperimentOwnership`
so listExperimentResults(), updateExperimentResult(), and deleteExperiment()
inherit the same per-operation tenancy guard.
🧹 Nitpick comments (1)
packages/core/src/datasets/__tests__/dataset.test.ts (1)

612-612: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the exact MastraError.id for these ownership failures.

These tests name specific failure modes, but .toThrow(MastraError) would also pass for the wrong storage/user error. Match EXPERIMENT_NOT_FOUND and EXPERIMENT_RESULT_MISSING_EXPERIMENT_ID so regressions in the ownership gate order are caught.

Suggested test tightening
-    await expect(ds.deleteExperiment({ experimentId: otherExp.id })).rejects.toThrow(MastraError);
+    await expect(ds.deleteExperiment({ experimentId: otherExp.id })).rejects.toMatchObject({
+      id: 'EXPERIMENT_NOT_FOUND',
+    });

Apply the same pattern to listExperimentResults, cross-dataset updateExperimentResult, and the missing-experimentId case.

Also applies to: 627-627, 639-645

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/datasets/__tests__/dataset.test.ts` at line 612, The
ownership-failure tests are too broad because they only assert a MastraError was
thrown, so they can miss the wrong error path. Tighten the assertions in
dataset.test.ts for deleteExperiment, listExperimentResults, cross-dataset
updateExperimentResult, and the missing experimentId case to check the exact
MastraError.id values, specifically EXPERIMENT_NOT_FOUND and
EXPERIMENT_RESULT_MISSING_EXPERIMENT_ID, using the relevant dataset methods to
verify the correct gate order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/datasets/dataset.ts`:
- Around line 461-475: The Dataset handle already enforces `experimentId` at
runtime in `Dataset.updateExperimentResult`, so tighten the method signature to
require `experimentId` by using `UpdateExperimentResultInput & { experimentId:
string }` while keeping the existing missing-ID guard for JavaScript callers.
Update the `updateExperimentResult` method in `Dataset` so TypeScript callers
cannot pass an input without `experimentId`, and preserve the current ownership
check via `#assertExperimentOwnership` before delegating to
`#getExperimentsStore().updateExperimentResult`.

---

Duplicate comments:
In `@packages/core/src/datasets/dataset.ts`:
- Around line 422-425: Re-run the tenant scope check inside
`#assertExperimentOwnership` before allowing experiment access or mutation. The
helper currently only verifies experiment.datasetId against this.id, so update
it to also validate the dataset’s current scope via `#checkScope` (or the
equivalent dataset fetch/validation used by other handle methods) before
returning. Keep the fix localized in `#assertExperimentOwnership` so
listExperimentResults(), updateExperimentResult(), and deleteExperiment()
inherit the same per-operation tenancy guard.

---

Nitpick comments:
In `@packages/core/src/datasets/__tests__/dataset.test.ts`:
- Line 612: The ownership-failure tests are too broad because they only assert a
MastraError was thrown, so they can miss the wrong error path. Tighten the
assertions in dataset.test.ts for deleteExperiment, listExperimentResults,
cross-dataset updateExperimentResult, and the missing experimentId case to check
the exact MastraError.id values, specifically EXPERIMENT_NOT_FOUND and
EXPERIMENT_RESULT_MISSING_EXPERIMENT_ID, using the relevant dataset methods to
verify the correct gate order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b5fcc0f5-b7c0-4fdb-8339-6da52716e3d5

📥 Commits

Reviewing files that changed from the base of the PR and between dd0dd10 and e2766d9.

📒 Files selected for processing (3)
  • packages/core/src/datasets/__tests__/dataset.test.ts
  • packages/core/src/datasets/dataset.ts
  • packages/server/src/server/handlers/datasets.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/src/server/handlers/datasets.test.ts

Comment thread packages/core/src/datasets/dataset.ts Outdated
…p tests

Address CodeRabbit round-3 review on MASTRA-4438:

- #assertExperimentOwnership now calls #assertScope() first, so stale
  scoped handles cannot bypass the per-operation tenancy check on
  listExperimentResults / updateExperimentResult / deleteExperiment.
- Narrow Dataset.updateExperimentResult's input type to require
  experimentId (kept the runtime guard for JS callers).
- Tighten ownership-failure tests to assert MastraError.id
  (EXPERIMENT_NOT_FOUND / EXPERIMENT_RESULT_MISSING_EXPERIMENT_ID)
  instead of only the error class.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
# Conflicts:
#	packages/core/src/datasets/dataset.ts
#	stores/mysql/src/storage/domains/datasets/index.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: critical Critical-complexity PR tests: green ✅ Changed tests failed against base as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants