Skip to content

refactor(core,server): manager-layer boolean + debug logs for tenancy-scoped delete observability#18855

Closed
NikAiyer wants to merge 35 commits into
mainfrom
nikaiyer/mastra-4454-oss-observability-for-silent-tenancy-mismatch-no-ops-in
Closed

refactor(core,server): manager-layer boolean + debug logs for tenancy-scoped delete observability#18855
NikAiyer wants to merge 35 commits into
mainfrom
nikaiyer/mastra-4454-oss-observability-for-silent-tenancy-mismatch-no-ops-in

Conversation

@NikAiyer

@NikAiyer NikAiyer commented Jul 2, 2026

Copy link
Copy Markdown
Member

What

Adds an observability signal for scoped tenancy misses on datasets and experiments — without changing the storage abstract contract or adding new exports from @mastra/core/storage.

Why

Scoped storage get*ById calls return null and scoped delete* calls silently no-op when tenancy filters don't match. That's the correct posture — throwing would leak cross-tenant existence via error timing or message — but it left operators with no signal at all when scoped ops missed.

How

Abstract contract is unchanged. deleteDataset, deleteExperiment, and deleteExperimentResults still return Promise<void>, matching every other delete in the storage domain. Third-party adapter implementations do not need updating.

Manager-layer boolean. DatasetsManager.delete now returns boolean. It derives that locally by doing a pre-delete scoped getDatasetById probe, then calls the void adapter delete unconditionally. The tiny TOCTOU between probe and delete on concurrent recreate only affects debug-log fidelity — not correctness or the no-existence-leak contract.

Wire behavior change on DELETE /datasets/:id:

  • Scoped delete (with organizationId / projectId query params) that targets a missing id or a scope-mismatched row: 200 { success: false }. Legit 404 and scope mismatch remain indistinguishable to preserve the no-leak contract.
  • Unscoped delete that targets a missing id: 404 (unchanged — the handler throws DATASET_NOT_FOUND when nothing was deleted and no scope was supplied).

Manager-layer debug logs. DatasetsManager and the Dataset handle emit a debug-level log when a scoped get* returns null or a scoped delete/mutation targets a missing/mismatched record. Payload is plain { op, id, organizationId, projectId } — same identifiers already logged by the HTTP layer for scoped calls. Deployments that sanitize HTTP access logs should apply the same treatment to the debug channel if these identifiers are considered sensitive.

No peer-floor bump needed. Because there are no new named exports from @mastra/core/storage, none of the store adapters need a @mastra/core peer-floor bump.

Test plan

  • pnpm build:core — clean
  • pnpm --filter ./packages/core check — 0 tsc errors
  • pnpm --filter @mastra/{libsql,pg,mysql,mongodb,spanner} build:lib — all clean
  • pnpm build:server — clean
  • pnpm --filter ./packages/server check:core-imports — clean
  • ✓ Core focused tests (329) — pass
  • ✓ Server dataset handler tests (14) — pass
  • _test-utils tenancy suite: adapter-level delete* assertions kept as resolves.toBeUndefined() (abstract contract unchanged)
  • Manager-level scoped delete asserts boolean return
  • Server handler scope-mismatch delete asserts { success: false } — same shape as a legit 404 to the client

Related: MASTRA-4454

ELI5

This change makes dataset deletes safer and clearer when teams use tenant scoping. If you try to delete something outside your tenant, the system now quietly says “nothing happened” instead of leaking whether that item exists.

Summary

  • Moved scoped delete observability into the manager layer for datasets, keeping storage delete contracts unchanged.
  • DatasetsManager.delete now returns a boolean based on a scoped pre-check, while still calling the adapter delete as a no-op-safe operation.
  • DELETE /datasets/:id now returns { success: false } for scoped mismatches/misses, but still returns 404 for unscoped missing datasets.
  • Added debug logs for scoped get* misses and scoped delete/mutation no-ops in DatasetsManager and Dataset, including op, id, organizationId, and projectId.
  • Tightened experiment and dataset storage adapter tenancy mismatch handling, plus related docs/comments.
  • Updated dataset/server tests to match the new scoped delete behavior.

NikAiyer and others added 15 commits July 1, 2026 10:48
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>
Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
… 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>
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>
…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>
…sultById

ExperimentsStorage.getExperimentById and getExperimentResultById used to
look up experiments (or experiment results) by their primary key alone, so
any caller who knew the id could read the row regardless of tenant. This
pushes the tenancy predicate into the storage layer via an optional filter
(option (a) — no fetch-then-assert), mirroring how MASTRA-4438 fixed the
same class of leak for datasets.

- Storage contract: getExperimentById / getExperimentResultById now accept
  an optional { organizationId?, projectId? } filter. All adapters
  (inmemory, libsql, pg, mysql, mongodb, spanner) honor the predicate.
- Dataset handle: Dataset.getExperiment and the shared experiment-ownership
  gate forward this.#scope, so experiment reads and downstream mutations
  reached through a dataset handle (list results, update result, delete
  experiment) are automatically scoped to the owning tenant.
- Tests: shared _test-utils tenancy suite grows experiments coverage and
  auto-runs against every adapter; inmemory + Dataset.getExperiment unit
  tests added.

On tenancy mismatch the storage layer returns null — matching how a
missing id already behaves, so existence does not leak through error
timing or messages.

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

MASTRA-4445

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

Extend MASTRA-4445 tenancy scoping to the two ExperimentsStorage delete
paths. Prior to this commit, deleteExperiment({id}) and
deleteExperimentResults({experimentId}) keyed on the primary id alone,
so any caller that knew an id could delete the row regardless of tenant.
This is the same class of leak MASTRA-4438 already closed for datasets.

Both deletes now accept an optional filters: { organizationId?, projectId? }.
On mismatch the delete is a silent no-op, matching how a missing id
already behaves so existence does not leak through error timing or
messages. The deleteExperiment cascade to results and the standalone
deleteExperimentResults bulk delete are both gated on the parent
experiment matching the caller scope, so a leaked id cannot wipe
another tenant results.

Adapters updated the same way as the getter fix:
- inmemory: reuse the tenancy filter check helper
- libsql, pg: use a shared tenancyWhere helper (extracted to
  stores/{libsql,pg}/src/storage/domains/utils.ts; datasets impls
  refactored to import it too so we do not keep two copies drifting)
- mysql: gate via operations.load({keys: {id, organizationId, projectId}})
- mongodb: shared applyTenancyFilter extracted to
  stores/mongodb/src/storage/domains/utils.ts; datasets impls refactored
  to import it
- spanner: reuse this.getExperimentById as the tenancy gate before the
  cascading transactional delete

Dataset.deleteExperiment now forwards this.#scope alongside the existing
#assertExperimentOwnership check, so the storage-side delete is
defense-in-depth if a leaked handle or race bypasses the assertion.

The shared _test-utils experiments tenancy suite now covers:
- deleteExperiment silent no-op on mismatch (row + results preserved)
- deleteExperiment succeeds and cascades on match
- deleteExperiment backward-compatible when filters is omitted
- deleteExperimentResults silent no-op on mismatch
- deleteExperimentResults succeeds on match (parent experiment kept)

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

Related: MASTRA-4445

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
# Conflicts:
#	packages/core/src/datasets/dataset.ts
#	stores/libsql/src/storage/domains/datasets/index.ts
#	stores/libsql/src/storage/domains/experiments/index.ts
#	stores/mongodb/src/storage/domains/datasets/index.ts
#	stores/mysql/src/storage/domains/datasets/index.ts
#	stores/pg/src/storage/domains/experiments/index.ts
…city

Removes TOCTOU pattern of `SELECT id WHERE tenancy` -> `DELETE WHERE id`
across all 5 store adapters. Delete now folds the tenancy predicate into
the destructive statement itself:

- pg: single scoped DELETE, cascade via scoped subquery
- libsql: same, via batch() implicit transaction; adds buildScopedWhere
  helper in shared utils
- mysql: RW transaction with SELECT id ... FOR UPDATE, then scoped
  cascade DELETEs
- mongodb: tenancy filter applied to deleteMany (not only pre-check)
- spanner: gate + cascade folded into a single runTransactionAsync,
  tenancy predicate in both DELETE DMLs

Also:
- Shared TenancyScope aliased to core DatasetTenancyFilters |
  ExperimentTenancyFilters so shared helpers stay in sync with the
  authoritative shape
- Clarified base ExperimentsStorage JSDoc so downstream implementers
  know success does not imply a row was deleted and the tenancy
  predicate must live in the destructive DML
- Added code example to the changeset

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
Applies the same TOCTOU fix from experiments deletes to the datasets
adapter deletes that shipped with MASTRA-4438:

- pg: gate + cascade folded into a single transaction with SELECT ...
  FOR UPDATE, parent DELETE scoped by tenancy
- libsql: tenancy predicate folded into the parent DELETE inside the
  batch (single-writer serialized, so check + DML cannot race)
- mysql: gate moved inside the RW transaction with SELECT ... FOR
  UPDATE, parent DELETE scoped by tenancy
- mongodb: tenancy filter applied to the destructive parent deleteOne
  (not only the pre-check)
- spanner: gate + cascade folded into a single runTransactionAsync,
  parent DELETE scoped by tenancy

Also drops 7 e2e LLM recording JSONs under packages/core/__recordings__
that accidentally rode along on the merge from main (they were removed
on main in commit 575bc70 and reappeared in this branch's merge
commit).

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
Replace the "run best-effort DML, swallow errors" pattern in every
deleteDataset that has to detach experiments from a dataset. That
pattern was unsafe inside a Postgres transaction: when the experiment
tables aren't installed alongside datasets, the first DELETE raises
42P01 "relation does not exist", Postgres puts the connection into
25P02 "current transaction is aborted", the JS-level try/catch swallows
the original error, and every subsequent DELETE in the same tx
(including the dataset row itself) fails, so the whole transaction
rolls back and the dataset is never removed.

Spanner has similar semantics — once a statement inside a read-write
transaction fails, the tx is no longer a safe place to keep issuing
DMLs. libsql and mysql are safer (single-writer autocommit, and
ER_NO_SUCH_TABLE doesn't abort InnoDB), but running DMLs speculatively
and swallowing errors is still fragile.

Same shape everywhere now: probe the experiment tables once against
the schema catalog before the destructive block (`to_regclass` on pg,
`INFORMATION_SCHEMA.TABLES` on spanner/mysql, `sqlite_master` on
libsql), then only run the detach DMLs when both tables exist. Nothing
inside the transaction throws for "table missing" anymore.

Also strip trailing HTML tags from `.changeset/loud-planes-scope.md`
that would otherwise land in the GitHub release notes.

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

Document that when 'filters' is omitted, implementations MAY skip the
tenancy predicate entirely (backward compat: callers explicitly opt out
of scoping). This matches the observed divergence across adapters —
pg/mysql/spanner take an unscoped fast path while mongodb/libsql fold
the (empty) filter unconditionally — both are correct under the contract.

Related: MASTRA-4445

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
Adds a debug-level log at the storage layer when a tenancy-scoped operation
silently no-ops on a mismatch. Getters return null and deletes affect zero
rows by design (throwing would leak existence via error timing/text), so
operators had no signal when a scoped call was behaving unexpectedly.

Covered ops:
  - getDatasetById, deleteDataset
  - getExperimentById, deleteExperiment
  - getExperimentResultById, deleteExperimentResults

The log carries op, table, and an opaque 8-char correlation token derived
from sha256(id + ':' + organizationId + ':' + projectId). Raw id and
tenancy are never logged. Emission is gated on filters being tenancy-
scoped so unscoped calls that return null on a legit 404 don't emit noise.

Two shared helpers on the storage domain (logTenancyReadMiss,
logTenancyDeleteNoOp) plus isTenancyScoped gate; wired through inmemory,
libsql, pg, mysql, mongodb, and spanner adapters.

No behavior change to any storage contract.

Related: MASTRA-4454.
@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d108562

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

This PR includes changesets to release 23 packages
Name Type
@mastra/core Patch
@mastra/server Patch
@mastra/code-sdk Patch
mastracode Patch
@mastra/mcp-docs-server Patch
@internal/playground Patch
@mastra/client-js Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
@mastra/deployer Patch
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/nestjs Patch
@mastra/next Patch
@mastra/tanstack-start Patch
mastra Patch
@mastra/deployer-cloud Patch
@mastra/react Patch
@mastra/temporal Patch
@mastra/playground-ui 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 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR changes DatasetsManager.delete to return a boolean distinguishing scoped deletion hits from misses, adds debug-level logging for tenancy-scoped read/delete misses in DatasetsManager and Dataset, updates the DELETE /datasets/:id route to return { success: false } on scoped mismatches while preserving 404 for unscoped misses, refactors in-memory experiments tenancy checks, and removes redundant pre-delete existence probes in libsql and mongodb adapters.

Changes

Tenancy-miss observability

Layer / File(s) Summary
DatasetsManager scoped delete/get miss logging
packages/core/src/datasets/manager.ts, packages/core/src/datasets/__tests__/manager.test.ts
delete probes for existing record under scope, computes boolean deleted, logs debug on miss; get/getExperiment log debug on scoped miss; tests updated to expect false and verify scoped deleteDataset call args.
Dataset class scoped ownership miss logging
packages/core/src/datasets/dataset.ts
#assertScope, #assertExperimentOwnership, and getExperiment emit debug logs with id/org/project context on scoped ownership check failures.
DELETE /datasets/:id route contract change
packages/server/src/server/handlers/datasets.ts, packages/server/src/server/handlers/datasets.test.ts
Handler switches to delete-first flow returning { success: deleted } for scoped calls and throwing DATASET_NOT_FOUND only for unscoped misses; tests updated accordingly with a new unscoped-404 test.
In-memory experiments tenancy mismatch refactor
packages/core/src/storage/domains/experiments/inmemory.ts
Combines separate org/project mismatch checks into single orgMismatch/projMismatch computed early returns across four methods.
LibSQL and MongoDB delete no-op simplification
stores/libsql/src/storage/domains/datasets/index.ts, stores/mongodb/src/storage/domains/experiments/index.ts
Removes pre-delete existence probes; deletions rely on tenancy-scoped WHERE/filter to become natural no-ops on mismatch.
Storage contract documentation and changeset
packages/core/src/storage/domains/datasets/base.ts, packages/core/src/storage/domains/experiments/base.ts, .changeset/tenancy-miss-observability.md
JSDoc contracts clarified for silent no-op semantics; changeset documents boolean return change, debug logging, and route behavior while confirming unchanged abstract storage contract.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • mastra-ai/mastra#18750: Both PRs modify tenancy-scoped dataset get/delete handling in core, with this PR refining the same boolean-return and scoped-miss route semantics.
  • mastra-ai/mastra#18770: Shares the tenancy-scope experiments theme, aligning inmemory.ts mismatch handling with related experiment filters enforcement changes.

Suggested labels: Documentation, complexity: high

Suggested reviewers: abhiaiyer91, DanielSLew, wardpeet, TheIsrael1, mfrachet, rase-

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is descriptive, but it exceeds the length guideline and is not written in imperative mood. Shorten it to under 50 characters and rephrase it in imperative mood with proper capitalization.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 nikaiyer/mastra-4454-oss-observability-for-silent-tenancy-mismatch-no-ops-in

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

@vercel

vercel Bot commented Jul 2, 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 14, 2026 9:56pm
mastra-playground-ui Ready Ready Preview, Comment Jul 14, 2026 9:56pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mastra-studio-preview Ignored Ignored Preview Jul 14, 2026 9:56pm

Request Review

@dane-ai-mastra dane-ai-mastra Bot added the complexity: critical Critical-complexity PR label Jul 2, 2026
@dane-ai-mastra

dane-ai-mastra Bot commented Jul 2, 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 11 +22
Lines changed 269 +15
Author merged PRs 615 -20
Test files changed Yes -10
Final score 7

Applied label: complexity: low


Changed test gate

Changed tests failed against the base branch as expected.

Label: tests: green ✅

@dane-ai-mastra dane-ai-mastra Bot added the tests: green ✅ Changed tests failed against base as expected label Jul 2, 2026
Base automatically changed from MASTRA-4445 to main July 2, 2026 20:01
@NikAiyer NikAiyer requested review from DanielSLew and rase- as code owners July 2, 2026 20:01
…oss-observability-for-silent-tenancy-mismatch-no-ops-in

# Conflicts:
#	packages/core/src/storage/domains/experiments/inmemory.ts
#	stores/libsql/src/storage/domains/datasets/index.ts
#	stores/libsql/src/storage/domains/experiments/index.ts
#	stores/mongodb/src/storage/domains/experiments/index.ts
#	stores/mysql/src/storage/domains/datasets/index.ts
#	stores/mysql/src/storage/domains/experiments/index.ts
#	stores/pg/src/storage/domains/datasets/index.ts
#	stores/pg/src/storage/domains/experiments/index.ts
#	stores/spanner/src/storage/domains/datasets/index.ts
#	stores/spanner/src/storage/domains/experiments/index.ts
…e test

- Standardize on non-optional `getLogger().debug(...)` across all 5 scoped-miss
  debug log sites in DatasetsManager + Dataset. `Mastra.getLogger()` always
  returns a logger in real usage; the optional-chain was inconsistent (one site
  in `delete` used non-optional, the other five used `?.`).

- Add manager test locking in that `store.deleteDataset` is fired
  unconditionally, even when the pre-delete probe returns null. This is the
  right behavior — the scoped adapter's tenancy predicate remains the source of
  truth and concurrent recreate races resolve consistently — but it wasn't
  covered by an existing test.

Per PR #18855 review feedback.

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 3, 2026 00:05 Inactive
NikAiyer and others added 3 commits July 2, 2026 19:07
… preflight

Per PR #18855 second review nit: unscoped `DELETE /datasets/:id` now costs
2 round-trips (probe + delete) inside `DatasetsManager.delete`, but that's
identical to the prior implementation where the server handler did a
preflight `mastra.datasets.get()` before the delete. The probe just moved
from the handler into the manager so the boolean contract lives in one
place. No net cost change on either the scoped or unscoped path.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
Cut the 55-line explanation down to the essentials: what changed, what the
wire behavior is, and the fact that the abstract contract is unchanged.
Round-trip cost, TOCTOU trade-off, peer-floor rationale, and PII sanitization
notes belong in the PR description, not the changelog.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
@NikAiyer NikAiyer marked this pull request as ready for review July 3, 2026 00:09
@dane-ai-mastra dane-ai-mastra Bot added tests: failing ❌ Changed tests passed against base complexity: low Low-complexity PR and removed tests: green ✅ Changed tests failed against base as expected complexity: critical Critical-complexity PR labels Jul 3, 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.

Caution

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

⚠️ Outside diff range comments (1)
stores/libsql/src/storage/domains/datasets/index.ts (1)

410-438: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Cross-tenant cascade still reaches child rows on a scoped miss. The manager always calls deleteDataset, so this batch runs even when the parent DELETE no-ops. Because the experiment/item/version statements are only filtered by datasetId = ?, they can delete or detach rows for a dataset owned by a different tenant. Fold the tenancy predicate into every statement, or restore the scoped existence guard, so the cascade stays a true no-op on mismatch.

🤖 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/libsql/src/storage/domains/datasets/index.ts` around lines 410 - 438,
The deleteDataset cascade is not fully tenant-scoped because the child
DELETE/UPDATE statements only filter by datasetId, so a scoped miss can still
affect rows from another tenant. Update the batch built in deleteDataset to
apply the same tenancy predicate used in scopedWhere to every
experiment/version/item statement, or restore a parent existence check before
building the batch, so the cascade remains a true no-op on tenant mismatch.
🧹 Nitpick comments (3)
.changeset/tenancy-miss-observability.md (1)

6-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing code example for the new public API behavior.

This changeset introduces two consumer-facing behavior changes with no example: DatasetsManager.delete() now returns a meaningful boolean, and DELETE /datasets/:id now returns { success: false } instead of always { success: true } on scoped mismatch. As per path instructions, "If the change is a breaking change or is adding a new feature, ensure that a short code example is provided. This code example should show the public API usage."
[potential_issue]

📝 Suggested addition
 `DELETE /datasets/:id` now returns `{ success: false }` on a scoped miss
 (previously always `{ success: true }`). Unscoped missing-id still returns
 `404`.
+
+```ts
+const deleted = await mastra.datasets.delete({
+  id: datasetId,
+  organizationId,
+  projectId,
+});
+// `deleted` is `false` for both a scoped tenancy mismatch and a truly
+// missing id — the two are indistinguishable by design.
+```
🤖 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 @.changeset/tenancy-miss-observability.md around lines 6 - 21, Add a short
public API example for the new tenancy-miss behavior: show
`DatasetsManager.delete(...)` returning a boolean and note that `false` means
either a scoped mismatch or a missing id, and also show `DELETE /datasets/:id`
returning `{ success: false }` on scoped misses. Place the example near the
changeset content so readers can see how to consume the updated
`datasets.delete` and route behavior without guessing.

Source: Path instructions

packages/server/src/server/handlers/datasets.test.ts (1)

265-277: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten the new 404 test to assert status, like its sibling tests.

Other tenancy tests in this file (e.g. Lines 115-125, 134-145) explicitly assert err instanceof HTTPException and err.status === 404. This new test only checks .rejects.toThrow(), so it wouldn't catch a regression where the handler throws a different error type/status instead of the expected 404.

✅ Suggested tightened assertion
     it('throws 404 when unscoped delete targets a non-existent id (legacy behavior)', async () => {
       // Unscoped callers must still get a 404 on a missing id — this preserves
       // pre-boolean-return behavior for clients that do not send tenancy scope
       // query params. Only scoped calls return `{ success: false }` on miss.
-      await expect(
-        (async () =>
-          DELETE_DATASET_ROUTE.handler({
-            ...createTestServerContext({ mastra }),
-            datasetId: 'ds_does_not_exist',
-          } as any))(),
-      ).rejects.toThrow();
+      const err = await DELETE_DATASET_ROUTE.handler({
+        ...createTestServerContext({ mastra }),
+        datasetId: 'ds_does_not_exist',
+      } as any).then(
+        () => null,
+        (e: unknown) => e,
+      );
+      expect(err).toBeInstanceOf(HTTPException);
+      expect((err as HTTPException).status).toBe(404);
     });
🤖 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 265 - 277,
The new legacy 404 test in DELETE_DATASET_ROUTE.handler is too loose because it
only checks that an error is thrown. Update the test to match the other tenancy
tests by capturing the rejection and asserting it is an HTTPException with
status 404, using the same DELETE_DATASET_ROUTE and createTestServerContext
setup so it verifies both error type and status.
packages/core/src/datasets/dataset.ts (1)

478-502: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Debug log doesn't distinguish "missing" vs "wrong dataset owner".

Both #assertExperimentOwnership and getExperiment log the same message for two different miss conditions: the experiment truly doesn't exist under scope, vs. it exists but belongs to a different dataset (experiment.datasetId !== this.id). Since this PR is specifically about tenancy-miss observability, distinguishing these cases (e.g., a reason: 'not_found' | 'ownership_mismatch' field) would make the debug logs more actionable for diagnosing scope vs. ownership issues.

Also applies to: 507-527

🤖 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 478 - 502, The debug
logging in `#assertExperimentOwnership` (and the matching getExperiment path)
currently uses one message for two different failure cases, so update the log
payload to distinguish whether the experiment was missing under scope or found
but had a dataset ownership mismatch. Add a clear reason field such as reason:
'not_found' or reason: 'ownership_mismatch' alongside the existing context
(datasetId, experimentId, organizationId, projectId) and set it based on whether
experiment is absent or experiment.datasetId !== this.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.

Outside diff comments:
In `@stores/libsql/src/storage/domains/datasets/index.ts`:
- Around line 410-438: The deleteDataset cascade is not fully tenant-scoped
because the child DELETE/UPDATE statements only filter by datasetId, so a scoped
miss can still affect rows from another tenant. Update the batch built in
deleteDataset to apply the same tenancy predicate used in scopedWhere to every
experiment/version/item statement, or restore a parent existence check before
building the batch, so the cascade remains a true no-op on tenant mismatch.

---

Nitpick comments:
In @.changeset/tenancy-miss-observability.md:
- Around line 6-21: Add a short public API example for the new tenancy-miss
behavior: show `DatasetsManager.delete(...)` returning a boolean and note that
`false` means either a scoped mismatch or a missing id, and also show `DELETE
/datasets/:id` returning `{ success: false }` on scoped misses. Place the
example near the changeset content so readers can see how to consume the updated
`datasets.delete` and route behavior without guessing.

In `@packages/core/src/datasets/dataset.ts`:
- Around line 478-502: The debug logging in `#assertExperimentOwnership` (and the
matching getExperiment path) currently uses one message for two different
failure cases, so update the log payload to distinguish whether the experiment
was missing under scope or found but had a dataset ownership mismatch. Add a
clear reason field such as reason: 'not_found' or reason: 'ownership_mismatch'
alongside the existing context (datasetId, experimentId, organizationId,
projectId) and set it based on whether experiment is absent or
experiment.datasetId !== this.id.

In `@packages/server/src/server/handlers/datasets.test.ts`:
- Around line 265-277: The new legacy 404 test in DELETE_DATASET_ROUTE.handler
is too loose because it only checks that an error is thrown. Update the test to
match the other tenancy tests by capturing the rejection and asserting it is an
HTTPException with status 404, using the same DELETE_DATASET_ROUTE and
createTestServerContext setup so it verifies both error type and status.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5a476337-f7e8-4b46-86e5-9d7fb128d68f

📥 Commits

Reviewing files that changed from the base of the PR and between 2e0b377 and 30c9414.

📒 Files selected for processing (11)
  • .changeset/tenancy-miss-observability.md
  • packages/core/src/datasets/__tests__/manager.test.ts
  • packages/core/src/datasets/dataset.ts
  • packages/core/src/datasets/manager.ts
  • packages/core/src/storage/domains/datasets/base.ts
  • packages/core/src/storage/domains/experiments/base.ts
  • packages/core/src/storage/domains/experiments/inmemory.ts
  • packages/server/src/server/handlers/datasets.test.ts
  • packages/server/src/server/handlers/datasets.ts
  • stores/libsql/src/storage/domains/datasets/index.ts
  • stores/mongodb/src/storage/domains/experiments/index.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/storage/domains/datasets/base.ts

@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 3, 2026
@NikAiyer NikAiyer closed this Jul 14, 2026
@NikAiyer NikAiyer deleted the nikaiyer/mastra-4454-oss-observability-for-silent-tenancy-mismatch-no-ops-in branch July 14, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: low Low-complexity PR tests: green ✅ Changed tests failed against base as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant