Skip to content

v0.9.26: abuse prevention, project files - #9155

Open
waleedlatif1 wants to merge 21 commits into
mainfrom
staging
Open

waleedlatif1 wants to merge 21 commits into
mainfrom
staging

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

waleedlatif1 and others added 11 commits October 16, 2026 14:57
…erve (#9143)

The file serve route catches its own errors, so an abort thrown after the
client went away was logged at error and answered 500, bypassing the 499
classification withRouteHandler already applies. Rethrow when the request
signal has aborted, in the outer catch and in the cloud and local read
helpers that log before rethrowing.
…ecorded instead of failing them (#9139)

* fix(trigger): complete async workflow runs whose failure is already recorded instead of failing them

Every failed workflow-execution run fires the Trigger.dev error alert, and in
practice those runs are user/workflow errors (missing start block, Function
runtime errors, provider 4xx/5xx) that core already recorded as the run's
failed execution log. The worker meant to rethrow only unrecorded faults, but
it checked wasExecutionFinalizedByCore without awaiting the post-execution
promise that sets the mark, so the check almost never saw it. The webhook
worker already awaited it (#4860); workflow-execution was skipped.

- isRecordedWorkflowFailure awaits post-execution, then checks the mark. The
  webhook worker uses it with no behavior change; the schedule worker uses it
  so it no longer races core's completion (its caller swallows either way).
- workflow-execution completes with { success: false, error } for a recorded
  failure, and for non-retryable 402/403/404/429 preprocessing rejections
  (deleted or undeployed workflow, suspended account, exhausted quota) after
  the same log write as before. Timeouts, unrecorded errors, retryable
  rejections, and 5xx still fail the job. Trigger retries are maxAttempts 1
  and the database runner never retries, so no retry semantics change.
- Both queue backends' getJob read a completed workflow-execution job carrying
  that result as failed, with the same error and output (undefined on Trigger,
  null in the database) the thrown run had, so /api/jobs, the SDKs, and the v2
  run-status queue fallback are unchanged.

resume-execution is left throwing: executeResumeJob is also called inline by
the resume poller and enqueue path, which rely on the throw.

* fix(trigger): bound recorded workflow failure errors in UTF-8 bytes

The previous character cap did not bound bytes against Trigger.dev's 128 KiB
output offload threshold. Cap the error at 16 KiB of UTF-8, cut at a code point
boundary, so even sixfold JSON escaping stays inline; normal-size messages are
returned unchanged. Correct the comment: runs.retrieve resolves offloaded
outputs itself, and only a failed presigned fetch leaves the output missing.
Drop an unconsumed waitForPostExecution mock from the timeout test.

* chore(audits): shrink the unused-exports baseline for PreprocessExecutionError

* fix(trigger): keep alerting on deadline-passed and unlogged async workflow failures

Re-check the deadline after awaiting core's finalization, since that wait can
outlast it, so a timed-out run still fails its job. Complete a non-fault
preprocessing rejection only when the logging session confirms the failure
row was written (hasCompleted, the same signal core uses to mark a failure
finalized); otherwise throw as before so an unsaved rejection still alerts.

* refactor(trigger): share the recorded-failure completion and skip the redundant error write on a timed-out recorded failure
…run (#9144)

* fix(execution): end runs quietly when their workspace is deleted mid-run

Deleting an account hard-deletes its solely-owned workspaces, and the cascade
takes each in-flight run's workflow and execution log with it. A run caught in
that window kept executing, then failed every terminal write: completion threw
"Workflow log not found", the cost-only fallback failed the same way, run
counts threw and logged twice, pause persistence logged errors, and the
admission reservation leaked until its TTL. A run whose log insert failed on
the deleted workspace kept executing blocks with no log row, so nothing could
record or stop it. Along the way a file upload failure surfaced raw Drizzle SQL
and bound parameters (user id, workspace id, storage key) in a block error.

- ExecutionScopeDeletedError (no cause) marks the outcome; classifyFailure
  attributes it to the owner, so it logs at info.
- completeWorkflowExecution, when the log row is missing, checks the run's
  workspace by primary key (inside the completion transaction when the row
  vanished mid-completion) and throws the typed error before any redaction,
  storage, or billing work. A missing row under a live workspace still throws
  "Workflow log not found" as before.
- LoggingSession recognizes it in every completion path: info instead of
  error, no cost-only fallback, admission slot released, later completions
  and force-fails become no-ops, and getDeletedScope() reports it.
- A log start that fails on a foreign key checks whether the workspace or
  workflow still exists; if not, safeStart returns false without the
  fallback, releases the slot, and executeWorkflowCore stops before building
  the executor. Any other start failure behaves exactly as before.
- Run counts skip a deleted scope, and a zero-row update is an info no-op.
- Pause persistence returns quietly for a deleted scope.
- User-visible error text never carries a query failure's SQL: the execution
  upload wrapper keeps its cause and redacts its message, the block executor
  redacts any query failure in the cause chain, and the workspace file,
  file-output, video, image, TTS, ElevenLabs, LaTeX, and pause sites use a
  shared getUserFacingErrorMessage.

* fix(execution): close review gaps in deleted-scope run ending

- Rename the integration suite to scope-deleted.integration.ts so its name
  does not repeat its folder (check:file-names).
- Type the new execution-core tests through ExecutionSnapshot and
  LoggingSession instead of `as any` (check:explicit-any).
- Define FOREIGN_KEY_VIOLATION once in database-query-error and import it in
  the logger and the logging session.
- Cover the guards the first commit left untested: a minimal fallback start
  that hits the deleted scope's foreign key, a cost-only fallback that is the
  first to find the workspace deleted, and pause persistence skipping a run
  whose deletion its paused completion found.
- Give the csv-workflow-execution live test's LoggingSession stub the new
  getDeletedScope method.

Correction to the previous commit message: pause persistence is quiet only
when the paused completion itself found the workspace or workflow deleted. A
deletion that lands after the paused log was written still fails
persistPauseResult, which logs at error and force-fails the run, exactly as
before. The TSDoc now says so.

* test(logs): type the logged-message callbacks in the deleted-scope session tests

* fix(execution): keep wrapped causes and quiet usage logging for deleted workspaces

- The file-output wrapper in FileToolProcessor keeps the original failure as
  its cause, so diagnostics and the tool boundary still see a database
  failure underneath the redacted message.
- recordExecutionUsage logs a missing workflow at info, not error, when the
  run's whole workspace is gone. The completion then reports the deleted run.
  The lookup runs only on that missing-workflow branch, both branches record
  nothing as before, and a failed lookup or a live workspace keeps the error.

* refactor(execution): simplify deleted-scope handling after audit

Fold the deleted-scope note and slot release into one session helper, reuse
the admission-slot release on the normal completion path, share an
ExecutionScope type, drop the unread executionId field and a redundant guard.
* revert(search): restore the v0.9.23 Jira text search

Reverts the Jira summary tiers (#9111, #9115, #9121, #9129). A plain Jira search
again returns the all-terms text match in Jira's order, as in v0.9.23. The
stemmedTerms helper stays; cross-source ranking uses it.

* chore(search): stop exporting stemmedTerms now only text-match uses it
#9151)

* fix(trigger): do not alert on runs whose workspace was deleted mid-run

When an account deletion cascades a workspace (or its workflow) away while a
run is in flight, #9144 makes LoggingSession record the deleted scope and core
throw ExecutionScopeDeletedError when the scope is already gone at start. The
background tasks still rethrew both cases: a start-time ExecutionScopeDeletedError
is not "recorded by core", and a block that then fails (e.g. a file-upload FK
error) cannot be finalized because the log row is gone, so hasCompleted() is
false. The Trigger run FAILED and paged Slack for a workspace that no longer
exists.

workflow-execution and webhook-execution now treat a deleted scope as a quiet
outcome: when the error is ExecutionScopeDeletedError, or the session reports a
deleted scope once core's post-execution settles (or once the task's own failure
log discovers it), they log at info and complete with the #9139 failure result.
The deleted-scope check runs before the timeout rethrow: with the workspace gone
there is no owner to alert and no log to fix, and the admission slot is already
released. Live-workspace timeouts, unrecorded failures, and infra faults still
throw.

The /api/jobs projection reads the string `error` as `failed`, matching the
FAILED status these runs reported before.

schedule-execution already swallows execution errors into schedule bookkeeping
(no Trigger failure), and resume-execution has no task-level logging session,
so neither changes here.

* improvement(trigger): fold the deleted-scope log and result into one in-scope closure per task
…ed, removed or banned (#9145)

* fix(execution): stop in-flight workflow runs when an account is deleted, removed or banned

No removal path stopped workflow executions. Account deletion cascades the
doomed workspaces' log rows away, after which no cancel path can find the run,
so it kept spending hosted provider keys for an account that no longer existed
until it crashed on foreign keys at finalization. A ban archived the
workspaces and stopped nothing; an admin removal cascaded them the same way.

- Account deletion reads the running executions (and claimed resumes, which
  run under their own id) of the doomed workspaces in its transaction, while
  the workspace rows are locked, and records one outbox stop per workspace with
  the erasure. After commit the stops are delivered a few at a time; a stop
  that cannot be delivered stays in the outbox for its worker, and the
  committed deletion is never reported as failed.
- A stop sets the cancellation flag each engine watches and sweeps the job
  queue for the workspace: a new JobQueueBackend.cancelByWorkspace cancels
  queued, delayed and running workflow jobs by the workspaceId tag on
  Trigger.dev (pages of 100, ten cancels at a time, 1,000 runs per attempt,
  continued without spending an attempt) and by metadata.workspaceId on the
  database queue, so queued runs no longer fail preprocessing later.
- A missing Redis completes the stop: the flag is process-local by design and
  no retry could reach further. A refused Redis write or a failed sweep retries.
- A ban stops the runs of the workspaces it archived; the admin remove-user
  hooks read the runs before the bare user delete and record them after it
  commits. Both sit in page module graphs that cannot load the delivery, so
  they signal in-flight runs at once and leave the queue sweep to the outbox
  worker.

* fix(execution): keep removal stops off the deletion and ban critical paths

- Account deletion no longer awaits the full stop delivery before purging
  storage. A slow Trigger.dev listing could hold the response for minutes and
  widen the window in which a restart orphans erased objects. It now signals
  in-flight runs fire-and-forget right after commit, ahead of every other
  post-commit step, under a 10-second deadline; the committed outbox stops
  repeat the signal and sweep the job queue in the outbox worker.
- A failure to record a ban's stops is logged instead of thrown. The ban and
  archives have committed, so a 500 helped no one and a retry finds no
  unarchived workspace to stop.
- Claimed resumes are found from the scope's running and paused log rows and
  joined through resume_queue's parent index, not by scanning resume_queue
  for claimed rows inside the deletion transaction.
- Tests pin the database queue's job-type filter, so a workspace sweep never
  cancels a non-workflow job such as log cleanup.

* test(execution): give each removal-stop fixture workflow a unique name

* fix(execution): close the review's gaps in removal stops

- A ban records and signals each workspace's stops as soon as its archive
  commits, through the archive's post-commit stop hook, instead of after every
  archive's socket and webhook cleanup. It also stops runs in the owner's
  workspaces archived before the ban, which an archive leaves running.
- The outbox handler re-reads a workspace's running executions when it runs.
  An archived workspace keeps its rows, so a run admitted before the ban that
  logged after its stop was recorded is still signalled.
- The admin remove-user hook handlers move to lib/auth/admin-removal-stops.ts,
  and signal the removed runs even when recording their stops fails.
- The database queue's workspace sweep works in batches of 100 up to the
  caller's cap and checks its abort signal between them, like the Trigger.dev
  sweep, rather than one unbounded update reported as complete.

* refactor(execution): tighten removal stops after final audit

- disableUserResources reads owned workspaces once and stops earlier-archived ones up front, dropping the post-archive read that could fail the ban
- Trigger.dev cancelByWorkspace reuses scanAndCancelTriggerRuns; workspace tag shares one builder with enqueue
- database queue shares abortInlineJob with cancelJob and returns completeness from control flow
- removal stop handler parses its payload with zod and signals while it sweeps
- drop the unused event-id return and redundant bucketing state

* fix(execution): finish the removal sweep's edge cases

- The removal stop handler loads both job queues independently and sweeps
  whichever loaded, so a primary queue that fails to load no longer skips the
  database queue's sweep. The failure is still rethrown for the outbox to retry.
- A claimed resume is found under a parent log row in the legacy `paused`
  status too, one of the statuses a resume claims from.
- The database queue re-checks for a matching active job after a short batch,
  since a selected job that finished before the update reached it shrinks the
  batch while later jobs still match.
- The ban's completion log no longer reports its runs stopped when recording
  their stops failed.

* fix(execution): abort local runs before the removal signal deadline can skip them

The signal deadline bounds only the Redis flag writes. Aborting a run this
process holds needs no Redis, so every execution is aborted first, even after
the deadline has passed. Corrects the ban's note on why a failed stop record is
logged: banning again records the stops again.

* fix(execution): guard ban stops against transfers and refusing runs

- A ban records stops only for workspaces the user still owns, re-read in the
  recording transaction and held FOR SHARE until it commits, so a concurrent
  ownership transfer waits rather than having its new owner's runs stopped.
  Only workspace rows are locked, so no lock order is inverted.
- Trigger.dev's workspace sweep counts only cancelled runs toward its cap, with
  a separate bound on failures, so runs that keep refusing cancellation no
  longer hold every retry at the front of the listing. cancelByExecution is
  unchanged.

* fix(execution): sweep a removed workspace's Trigger.dev runs of any age

The workspace sweep searched only runs created in the last 14 days, so an older
run still queued or waiting survived the removal. It now passes only an upper
time bound, which Trigger.dev treats as no lower bound, and falls back to the
retention window only if Trigger.dev refuses that search as too expensive
before returning a run. cancelByExecution is unchanged.
* feat(files): restore Project file browser and Mothership integration

* fix(files): resolve CI audits and test types after restacking on staging

- Keep prepareProjectsForAccountDeletion returning the Project IDs to delete.
  The object return added an unused storageCleanupEventIds field; the
  durable cleanup outbox already processes those events, so restore the
  staging contract and its stronger integration assertions.
- Exempt lib/mothership/generated from the relative-import convention.
  contracts:sync copies those wire contracts verbatim and byte-checks them,
  so their sibling imports must stay relative; drop their baseline rows.
- Re-record the files page module-count baseline for the Project browser.
- Type the test fixtures that regressed, including a queue-recovery
  fixture that persisted the invalid completion status 'success'.

* fix(files): gate Project sandbox callbacks like workspace callbacks

Staging now holds resource-delegated Chat principals to content
admission, so a Project callback read with no gate was refused and the
sandbox got a 503. Run Project callbacks under the same workbench
session gate as workspace callbacks (the gate's workspace is optional,
since Projects have none) and share the withheld-result handling.

The Project CLI read suite now runs reads under the tool executor's
registry gate, as Chat does.

* fix(projects): keep Project chat tabs behind the rollout flag

The chat resources API stored a Project-owned tab without checking the
projects flag. Refuse new Project tabs while Projects are off; removing
or reordering stored tabs stays possible so they can be cleaned up.

Also drop the projects flag check in list_user_projects that could never
run (listProjects already refuses while the flag is off) and the content
delivery barrel export left unused by the shared sandbox delivery helper.

* refactor(files): drop the Project files UI from the Chat transport PR

The Project files UI will be built for the project-first redesign, so
this PR keeps only the backend and Chat transport behind the projects
flag. Restore the workspace Files page, file viewer, Chat resource
panel, add-resource and mention entries, and their client query hooks
and owner adapters to staging. The previous tip is tagged
project-files-ui-seed for that UI work.

Kept: the shared ChatContext wire type (Project contexts and file owners
reach the Chat server) and its exhaustive kind registry entry.

Also drop the UI-only browser constants and response types nothing uses
now, restore the files page module-count baseline, and type the
use-chat fixtures that returned with the staging test.

* fix(files): redact Project file text like workspace file text

Project grep and text reads admitted every Project artifact as opaque
bytes, so any file carrying a secret was refused to Chat instead of
having the secret masked, unlike the same file in a workspace.

Share one text extraction core between workspace text reads and private
file observation: parse, redact before a line window can cut a secret,
then slice. Project text reads now go through readProjectFileText, which
admits the source and every rendered input as complete text and returns
text only. The complete artifact read stays private to that use case, so
bytes never leave under a complete label; readProjectFileArtifact keeps
admitting opaque bytes, and visual reads of a secret still refuse.

* fix(sim-cli): keep unpublished groups in the Mothership command inventory

#9057 hid the projects group from --help and `sim cli` search until
launch, and the inventory printer dropped every hidden command, so the
Mothership worker lost the Project file commands it routes --project to.

List a hidden top-level group that is only unpublished, marked
`unpublished`, so the worker can route to it once Sim enables Projects
for a turn; other hidden commands stay out, and --help is unchanged.

* fix(files): address Cubic and Greptile review findings

Workspace regressions while Projects are off:
- Sandbox file copies between workspaces keep the workspace route when
  the lease has not negotiated the file-owner protocol; only pairs that
  name a Project owner need it.
- Chat upload contexts keep their uploads/ path in the workspace owner
  adapter.

Project transport:
- Project CLI requests refuse mutations during benchmark reference
  generation, as workspace requests do.
- Project grep defaults an omitted scope to files.
- Project files with unrecorded or empty exact provenance are readable
  when the caller holds a registry; a missing registry is still refused.

Mothership inventory:
- Unpublished Project commands carry response shapes from the
  launch-ready Project OpenAPI definitions.
- An operation whose delegated services exclude Chat no longer mints a
  Chat principal, so the refused-route list now matches runtime for
  Project file version history.

* fix(files): address the second review round

- Project `files read` returns binary metadata for undecodable files:
  the text route checked `workspaceFileTextFormat` against null, but it
  returns undefined.
- Workspace file resources keep `workspaceId` beside their new workspace
  owner, so current browsers can open and close those tabs in
  organization chats; the resource key is unchanged.
- The Project tab flag gate is covered against the database instead of
  through mocked collaborators.

* fix(files): choose a generated Project document's read by its rendered type

A generated PDF is stored under its source type (text/x-python-pdf), so
the Project text route treated `files read report.pdf` as text instead
of the default visual representation. Share the workspace reader's type
selection as observedMediaType: a generated document is read as the
type its name promises, otherwise as its effective stored type.

* fix(mothership): close review gaps in Project file transport

- Dispatch CLI services before file-owner adapters, so a service that
  carries a file owner gets the services validation instead of a bridge
  error. The unreachable Project-side service check is removed.
- Check caller-chosen names on Project file renames and folder create
  and update with the same secret-provenance gate as create and upload,
  including workbench history for sandbox callbacks.
- Move first-message file panel resolution into resolveFileAttachment
  and replace the mock-only post test with a database-backed check of
  the saved tab.

* fix(mothership): match chat tabs within the chat's workspace

- The chat resource store now keys tabs with the chat's workspace as the
  fallback scope, so an unscoped tab and the same tab addressed by that
  workspace merge instead of duplicating. Upsert, remove and reorder all
  use the same identity.
- Project file renames and folder writes recheck cancellation after the
  name check and never forward a cancelled write.
- Leave the .claude and .cursor import rules to staging, whose source
  conventions check now exempts generated contract copies itself.

* fix(mothership): bound Project name checks and keep equal owners

- Parse Project file renames and folder writes with their route contracts
  under the inline body limit before the name check, so an oversized body
  is refused instead of buffered. Only those routes copy the request.
- Treat an equal file owner as unchanged when merging chat tabs, so a
  repeated upsert stays a no-op.

* fix(project-files): harden owner hints, grep provenance, and tab identity

- Restore info/warn levels for stale or forbidden tab attachments and skip an unresolvable target instead of throwing
- Make Project owner hints best-effort so a failed lookup never fails the turn; document that a worker rollback needs projects off first
- Cache the worker file-owner protocol probe per base URL (lru-cache, 30s, success only)
- Answer an invalid copy body on an unnegotiated lease from the workspace route (v2 400) instead of a 403
- One tab-key function on server and client, keyed with the chat workspace on both sides
- Project grep withholds a file without provenance instead of failing the search or latching the registry, and stops listing once the file budget is full
- Bound copy effects: one copied file opens a tab, bulk and folder copies refresh the destination owner; org copies carry the destination workspace
- Bound list_user_projects capability fan-out
- Refuse unextractable or oversized Project text reads before download, with the text-extraction size message
- Share the create-file input mapper, the resource authoring version, and isProjectsEnabled

* fix(project-files): refuse secret-bearing raw Project downloads without a workbench tracker

* test(project-files): pin refusals and cover live owner-hint, tab-key and log-level paths

* refactor(project-files): move owner routing and admission behind the file-owner adapter

- copilot-request carries one opaque resource grant per request; the Project file
  and file-copy grants are minted next to their adapters, so the shared route layer
  no longer imports Project or copy policy
- One projectFileDelegationScope for the verifier, the Copilot adapter and the
  request grant; admitsCopilotResourceDelegation shared by the adapter and the grant
- Private v2 transports derive route patterns from their contracts
  (v2RoutePattern / isV2ContractRoute / tryMatchV2Route), including the name-write
  lookup, so a renamed route can no longer skip interception silently
- Project upload and write transports share their route prologue, cancellation
  check and failure envelope
- One forwarded-header strip list and one callback context for sandbox callbacks;
  SandboxResourceScope is exported instead of re-derived
- isOwnerScopedFile replaces adapter field comparisons; the adapter field is
  renamed invocationScope so it no longer reads like Principal.resourceScope
- authoringInvocation picks a turn's chat-or-workspace invocation in one place

* refactor(mothership): share chat tab keys, CLI session setup, and file read helpers

- chatResourceKeyFor binds the tab key to one chat's workspace for the store,
  reorder, stream handler and use-chat; resource removal looks keys up in a Set
- isFileResourceType names the file/filefolder check; shared owner refinement in
  the resource contracts
- prepareCliSession gives every CLI invocation its endpoint, workbench session,
  file provenance and effect sink
- Text-extraction refusals, the default-visual media check and MIME
  normalization are shared rather than copied
- Grep page cap and listing note are shared; a corpus states its own scope
  refusal instead of the engine naming Projects
- resource-effects builds an owned file path once and shares the owner refresh

* perf(project-files): skip the folder tree for grep reads and check hint gates before the flag

- Project grep reads ask the artifact read not to load the owner's whole folder
  tree per file; grep never reads file.folderPath
- File-owner hints run the route, mode and admission checks before awaiting the
  projects flag
- Project text reads reuse the workspace text-extraction refusals; Project grep
  uses the shared page cap and states its own scope refusal

* test(mothership): drive stale-tab and owner-hint failures through the real boundary

* test(project-files): pin upload cancellation during provenance and new-chat tab order

* fix(project-files): recheck cancellation before completing a workbench upload

Provenance resolution can outlast the invocation; completion now rechecks both
signals before it registers the file, as the create path does.

* fix(mothership): persist new-chat tabs in attachment order

Attachments resolve concurrently, so tabs were persisted in completion order.
Each authorized tab now keeps its attachment index.

* fix(mothership): store the recovery authoring marker only while Projects are on

Earlier builds parse recovery config strictly and skip recovery on an unknown
key. Only Project authoring reads the marker, so flag-off turns no longer
write it and stay recoverable across a rollback.

* fix(mothership): tag an unlabeled file selection as a bare mention

* test(project-files): give new suites their own reports and drop an unreachable adapter case

* refactor(files): return readSourceBuffer's TSDoc to its function

---------

Co-authored-by: Waleed Latif <waleed@sim.ai>
Co-authored-by: Waleed Latif <walif6@gmail.com>
…out touching live objects (#9146)

* fix(account-deletion): durably purge a deleted workspace's storage by prefix

Account deletion found the objects to erase by reading their rows before the
teardown transaction and deleting them in memory after commit. Anything that
read could not see stayed in storage forever: rows written between the read
and the workspace lock, rowless objects (table snapshots, compiled documents,
table-import leftovers, execution objects whose metadata insert failed),
everything past the 100k in-memory cap, everything after a crash following
commit, and the account's private files in workspaces that survive it, whose
rows cascade away with the user.

Inside the deletion transaction, after the workspace rows are locked, account
deletion now queues delayed outbox work that commits or rolls back with it:

- one prefix sweep per deleted workspace across workspace/, table-import/,
  execution/, table-snapshots/, copilot-doc-compiled/ and workspace-logos/
  in the bucket serving each, with a trailing-slash prefix built from a
  validated id;
- exact-key cleanup for what cascades outside those prefixes: knowledge-base
  documents, Copilot uploads and other non-prefixed files of deleted
  workspaces, and the account's private files elsewhere (attachments of its
  own chats, private contexts, files with no workspace). Handed-off, Project,
  organization files and attachments of someone else's chat are not queued.

Both wait out every upload URL signed before the deletion, refuse as a no-op
when the workspace or account row exists again (restore, PITR), and keep any
key a surviving workspace_files, workspace_file, workspace_file_version,
document or execution_large_values row still references. The sweep reuses the
Project prefix cleanup's provider listing and deletion, generalized by storage
context and given an in-run provider cursor; Project cleanup still lists one
page per run and deletes exactly as before. The existing in-memory purge is
unchanged and still runs.

Large execution values now pass cleanupOnMetadataFailure, so a failed
metadata insert deletes the freshly keyed object instead of orphaning it, as
execution files already do.

* fix(account-deletion): never purge objects that live data still names

Review of the durable storage sweep found ways it, or the in-memory purge it
backs up, could delete an object another tenant still uses.

Reference guard. Every account-deletion purge now runs one check before
deleting a key: the durable prefix sweep (per listed key), the durable
exact-key cleanup, and the post-commit in-memory purge. A key is kept when a
row still names it (workspace_files.key, workspace_file.key,
workspace_file_version.key, document.storage_key, execution_large_values.key)
or a stored asset URL names it: user.image, workspace.logo_url,
organization.logo, organization.whitelabel_settings, organization.metadata,
custom_block.icon_url, chat.customizations (deployed-chat logo and avatar) and
oauth_client.icon. URLs are mapped to keys with extractStorageKey, so serve
paths with or without a provider segment and direct bucket URLs both match;
over-matching only keeps an object. References embedded in free-form JSON or
text (workflow sub-block values, chat messages, Markdown links) are not
consulted; for the deleted account's own chat attachments that is the intended
erasure. This keeps whitelabel logos that were uploaded under an admin's
personal workspace prefix, and legacy kb/ objects shared across workspaces,
which the in-memory purge used to delete unchecked.

Exact-key cleanup also keeps any key under a workspace-keyed root whose
workspace still exists, since its members can read it by key through the
metadata fallback. Workspace-less rows are queued only for the Copilot
context: the workspace binding check was added NOT VALID, so legacy
shared-context rows without a workspace can name a surviving workspace's
object. Profile pictures are taken only from the account's own user.image,
never from rows, because whitelabel logos were once stored as
profile-pictures rows under an admin's account; organization logos are never
queued either.

Prefix deletion now counts only objects actually removed. A tolerated ENOENT
(locally, any file whose on-disk name no sanitized key resolves to) used to
count as progress, so a page of such files re-listed forever without spending
an attempt. The workspace sweep and the Project prefix cleanup (which had the
same flaw) now defer and spend an attempt when a full page removes nothing.
A local metadata sidecar is kept whenever its object is. Versions of the
account's private files are queued in keyset pages that stop at the cap
instead of one unbounded query inside the deletion transaction.

* fix(account-deletion): bound and harden the storage reference guard

A stored URL with malformed percent-encoding (for example
`/api/files/serve/%E0%A4%A`, which any workspace admin can store as a logo URL)
made extractStorageKey throw. The guard then rejected on every call, so every
storage purge on the platform failed closed and deleted nothing. The guard now
parses each value under its own try and falls back to the raw, undecoded path.
extractStorageKey itself is unchanged for its other callers.

The URL-column check read six unindexed tables on almost every guard call.
Only keys under the prefixes the file-serve route answers without a row or a
session (profile-pictures/, og-images/, workspace-logos/,
organization-logos/, per isPublicByKeyPrefix in the serve route) can be live
through a stored URL alone. The guard now reads URL columns only for such
keys, and only rows containing the key's longest URL-safe run, which
percent-encoding never splits. Each run is read at most once per guard, 50 per
query. A key with no run long enough falls back to the unfiltered read. Lookup
failures still throw, so nothing is deleted.

The workspace sweep now defers with a distinct reason when a full local page
removes nothing, so the test for it can tell a stall from a deadline.

* refactor(account-deletion): split the storage reference guard and share storage primitives

- keysNamedByUrl also yields a leniently decoded candidate when a stored URL's
  percent-encoding is malformed, so a legacy key holding a literal '%' is kept
- correct the guard's TSDoc: rowless keys stay reachable through key paths under a
  live workspace and object metadata, not only through a row
- move the reference guard to lib/users/storage-reference-guard.ts
- move prefix listing/deletion, the upload-TTL cleanup delay, and the outbox
  deadline margin from lib/projects/files/prefix-cleanup.ts to
  lib/uploads/core/prefix-sweep.ts; drop the projectUploadCleanupAvailableAt alias
- share WORKSPACE_SHARED_FILE_CONTEXTS between member handoff and account cleanup
- isPublicStorageKey replaces the public-prefix lists in the serve route and guard

* fix(account-deletion): read durable storage keys before the deletion locks

- collectAccountStorageObjects runs the exact-key keyset reads before the
  transaction, beside collectAccountStorageKeys; inside it,
  enqueueAccountStorageCleanupInTx now only inserts the prefix-sweep events and
  the pre-collected exact-key events, so teammates' writes in shared workspaces
  never wait on those reads
- Project prefix cleanup completes on a short listing page again, as before
- the file-serve route keeps its original public-prefix check

* fix(account-deletion): close the storage cleanup gaps review found

- integration fixture rows get unique names: the active-name unique index on
  workspace_files rejected the second 'fixture.txt' row, failing seed()
- the reference guard reads stored asset URLs unfiltered and decodes them, so
  a URL that percent-encodes the key's characters still keeps its object; the
  world-readable-key gate still decides whether any URL column is read
- enqueueAccountStorageCleanupInTx re-reads the account's rows written since
  the pre-transaction collection (via the user_id and uploaded_by indexes)
  under the account lock, so a late Copilot or knowledge-base upload is queued
- local listings walk keys in sorted order and page after the last key, so a
  workspace sweep advances past a full page of kept objects

* docs(account-deletion): describe the in-transaction delta read accurately

* fix(account-deletion): narrow stored-URL reads and re-check workspaces per page

- fix TS2769: the collection scope passed a readonly id list to notInArray
- the reference guard reads stored asset URL rows only where they mention a
  public key's owning id run, paged, and keeps a public key with no such run
  instead of reading every URL column
- the account deletion request no longer purges world-readable keys; the
  account's own avatar is queued for the delayed durable cleanup instead
- the workspace prefix sweep re-checks that the workspace is still gone before
  every page it deletes from

* fix(account-deletion): queue an avatar only when the account uploaded it

user.image accepts any serve URL, so it names a key without proving whose it
is. The avatar is queued for durable cleanup only when the account's completed
profile_picture upload session, or an older profile-picture workspace_files row
of the account, names the same key; otherwise it is left in place.

* fix(account-deletion): apply every purge guard on every call

- the reference guard keeps keys under a live workspace's keyed roots itself,
  so the request purge gets the check the durable cleanup already had
- stored asset URLs are re-read for every guard call rather than cached for
  the sweep, so a reference written mid-sweep still keeps its object
- an avatar key that is not a safe profile-pictures key is never queued

* fix(account-deletion): queue the account's avatar and let the guard keep shared ones

An ownership lookup left avatars uploaded more than a week before deletion in
storage, since their upload sessions are gone by then. The key user.image
names is queued again, and the run-time reference guard keeps it whenever
another account's user.image or a stored asset URL still names it.
… for text/markdown (#9154)

* improvement(landing): serve the homepage as Markdown when clients ask for text/markdown

* improvement(landing): describe the Markdown variant's cache policy accurately
…#9141)

* fix(billing): stop account deletion from resetting the free allowance

Self-service deletion cascades the usage ledger with the user row, and signup
grants every new user row the free allowance, so deleting an account and
signing up again on the same email, Google, GitHub, or Microsoft identity
reset the one-time free credit.

Deletion at the holder's request now keeps keyed digests (HMAC-SHA256 under a
purpose-bound key derived from BETTER_AUTH_SECRET) of the account's canonical
email and social sign-in subjects for 365 days, and a new account on a matching
identity is created without the free allowance. The SCIM provisioning rollback
records nothing, and nothing is kept where billing is disabled.

- The allowance is a per-user `user_stats.free_tier_eligible` fact, so plan
  re-syncs, the null-limit heal, usage reads, and admin grants keep it
  withheld while prepaid credit and paid plans are unaffected.
- Signup checks the email and any sign-in already linked; a social sign-in
  committed after the user hook is checked only when it was created in the
  same auth request as its user, so linking a sign-in to an existing or
  provisioned user later never withholds anything.
- A stats row created by the fallback (signup failed to create it) gets the
  same check, failing open without failing the caller.
- Forfeiting sets the flag and re-derives the limit in one transaction.
- The deletion dialog says free credits are granted once per person.

Migration 0433 adds the `deleted_account_identity` table and the
`free_tier_eligible` column (constant default, metadata-only).

* fix(billing): never let the identity record block an erasure, and keep later links unchecked

- Recording a deleted account's identity digests now runs in a savepoint and
  only logs on failure, so an anti-abuse write can never abort the holder's
  deletion; before this change nothing of the kind could block it.
- A stats row the fallback creates gets only the check signup would have made:
  the email always, and the linked sign-ins only for a user created in the
  same auth request. A legacy user without a stats row who links a sign-in
  later is no longer withheld through the fallback.
- Docs say exactly who is checked: every user Better Auth creates, SCIM- and
  SSO-provisioned ones included, is checked by email at creation; only a
  sign-in linked in a later request never withholds anything.

* fix(billing): keep deleted identities 30 days, drop Google subjects, leave the dialog unchanged

- Identity digests now expire 30 days after the deletion, inside the privacy
  policy's 30-day Account Data window.
- Google subject ids are no longer recorded or matched (the policy keeps
  Google API data only 7 days after deletion); a Google re-signup is still
  caught by its email digest. GitHub and Microsoft subjects are kept.
- The deletion dialog is unchanged from staging.
- Callers added on staging pass the deletion option.

* improvement(billing): skip locked rows in the identity purge, share the auth-request lookup

Concurrent account deletions no longer wait on each other's purge batch.
syncUsageLimitsFromSubscription reads in parallel again, and email
canonicalization reuses normalizeEmail.

* fix(billing): drain expired identity digests hourly, record only verified emails

- Expired deleted-account digests were removed only while recording a later
  deletion, so with no later deletions they outlived the 30-day window. The
  hourly OAuth-token cleanup cron now drains them first in bounded batches
  (indexed expires_at order, SKIP LOCKED, batch and time caps); a failed purge
  is logged and never holds up the token sweep.
- A deleted account's email is recorded only when it was verified, so an
  unverified signup can never cost the address's real owner the allowance.
  GitHub and Microsoft subjects are still recorded; matching is unchanged.
- The admin credit grant creates a missing stats row with the same email
  check as every other row creation instead of assuming eligibility, looked
  up before its transaction.
@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Oct 17, 2026 10:05am UTC

Request Review

@greptile-apps

greptile-apps Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[High impact] Broad changes to core application and API routes.

Add the required TSDoc to the new Oracle exports before merging; no new blocking runtime defect was established.

Findings

  1. P2 Exported functions lack required docs ▶

Summary

This release adds Project file access in Chat, strengthens account-removal cleanup and billing checks, and expands integrations.

  • Chat can search, read, and change files in a Project.
  • Data drains can send events to Microsoft Sentinel.
  • Databricks workflows can manage more jobs, compute, SQL, and Genie tasks.
  • Workflows can read and update Oracle Fusion Financials records.
  • Removed workspaces and accounts stop their active workflow runs.
  • Account deletion resumes its storage sweep and leaves live files untouched.
  • The homepage can return Markdown to clients that request it.
  • File serving, search, billing, and library pages get targeted updates.
  • The flaky-test issue reporter is removed from CI.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Resume saved storage position] --> B[List page]
  B --> C[Keep referenced objects]
  C --> D[Delete remaining objects]
  D --> E{Page completed?}
  E -->|No| F[Save current position]
  E -->|Yes| G[Advance listing position]
  G --> H{Time remaining?}
  H -->|Yes| B
  H -->|No| F
Loading

Reviews (2) · Last reviewed commit: "feat(databricks): expand API coverage an..." · Reviewed by Greptile

Comment thread apps/sim/lib/mothership/tools/server/open-resource.ts
Comment thread apps/sim/lib/mothership/resources/store.ts
Comment thread apps/sim/lib/users/storage-sweep.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

Review completed against the latest diff

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/sim/lib/users/workspace-storage.ts
Comment thread apps/sim/lib/auth/admin-removal-stops.ts
Comment thread apps/sim/lib/mothership/chat/process-contents.ts
Comment thread apps/sim/lib/workflows/lifecycle.ts
Comment thread apps/sim/lib/core/async-jobs/types.ts Outdated
Comment thread apps/sim/lib/uploads/utils/file.ts
Comment thread apps/sim/lib/mothership/agent-cli/project-file-provenance.ts
Comment thread apps/sim/lib/users/storage-reference-guard.ts
Comment thread apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
…#9158)

* fix(search): count two-letter query terms when ranking across sources

queryRelevance took its terms from the snippet tokenizer, which drops words
under three characters, while the title-share denominator counted every
title word. For 'AI agents', a page titled 'AI agents' scored below one
titled 'Agents'. Ranking now keeps two-letter terms and stoplists
two-letter function words; snippets and the Atlassian half-terms filter
keep the three-character minimum.

* fix(search): keep two-letter terms whole and capitalized acronyms ranked

singularKey folded two-letter words to one letter (OS to o, IE to i), so
they matched unrelated single letters; words of two letters or fewer are
now their own key. A two-letter word written in capitals (IT, US) is an
acronym, so it is no longer dropped as a stopword.
…ved listing position (#9159)

* fix(account-deletion): resume a workspace storage sweep from its saved listing position

A sweep that hit its deadline checkpointed only the family, so the next run
re-listed the family from page one. When the early pages held only kept
objects and walking them filled the handler window, every run re-walked them,
deleted nothing, spent an attempt, and eventually dead-lettered, leaving the
unreferenced objects behind them.

A stopped run now checkpoints { family, pageToken } and the next run resumes
there; moving the saved position counts as progress, so the run continues
without spending an attempt. Completing a family clears the token. A saved
token that no longer lists (expired or rejected by the provider) restarts the
family from the start once, and that restart does not count as progress, so a
cursor that never lists stays bounded by the attempt budget. Payloads without
a token still parse; a present token must be a non-empty string of at most
4096 characters.

* fix(account-deletion): bound position-only sweep resumes and reject foreign local page tokens

Provider cursors are opaque, so a moved listing position alone no longer
counts as progress past 1000 resumes per event; past that a run that only
moved its position spends an attempt, keeping the sweep bounded even by a
cursor that revisits positions. A local page token that is not a key under
the prefix (one saved under another storage provider) now fails to list, so
the sweep restarts the family instead of skipping every key before it.

* fix(account-deletion): retry a saved sweep position once before restarting its family

A single failed listing from a saved page token restarted the family from
the prefix start, so a transient provider error at a deep position threw
the position away. The first failure now records tokenFailures: 1 and
rethrows, so the outbox retries from the same position under its normal
attempt budget; only a second failure from that token restarts the family.
Any successful listing clears the count.
* fix(chat): open and close Project file tabs by their owner

Closing a Project-owned file tab dropped its owner, so the removal request
never matched the server's owner-keyed tab and a history refresh restored
it. Tab removal, the persistence queue, and the selection reset now take
one ChatResourceAddress and match it through getChatResourceKey; the
DELETE body carries the owner. Workspace tabs key exactly as before.

A Project file tab borrowed the chat's workspace and showed 'File not
found' or 'no workspace address'. It now renders a read-only FileViewer
fed by the Project file routes, with no workspace-only tab actions.

* fix(chat): remove tabs by their stored address and refresh open Project files

Tab removal matches through the chat-scoped key that adding uses, then
removes, un-queues, and deletes the stored tab, so a caller that names a
workspace tab by type and id alone deletes it with its workspace. The
persistence queue keys with the same chat-workspace fallback.

A Project file upsert in the stream now refetches the open Project file
view's record and bytes. Also drops a stale title argument to
isEphemeralResource.
…nt resolution (#9161)

* fix(chat): treat browser tab metadata as untrusted and bound attachment resolution

Browser tab attachments put the client-sent page title and URL straight into
the turn context as prose. The URL now goes through safeBrowserSelectionUrl
(credentials and non-http(s) URLs dropped, title kept), the title is capped,
and both are quoted inside an untrusted JSON block. The block format is shared
with the browser and terminal selection formatters via formatUntrustedJsonBlock.

Open-tab attachments resolved with an unbounded Promise.allSettled fan-out; they
now run through mapWithConcurrency at CONTEXT_RESOLUTION_CONCURRENCY with a
total mapper, so per-tab failures are still logged and skipped in order.

The project file read integration suite now deletes its fixtures' search
chunks and builds, which have no cascade from files.

* improvement(chat): settle attachment resolution through a shared bounded helper

Adds mapSettledWithConcurrency next to mapWithConcurrency, as that helper's contract asks, and uses it for open-tab attachments. The Project file read teardown now finds search builds by fixture owner, so builds whose file is already gone are removed too.

* fix(chat): keep truncated browser tab titles within the title bound
* feat(data-drains): add Microsoft Sentinel ingestion

* fix(data-drains): trim pasted Sentinel client secrets
…jections, stats-row creator, sweep pages) (#9164)

* fix(trigger): complete runs whose workflow was deleted or undeployed before they started

Not-found, archived, undeployed, unattached and suspended rejections come
from a successful read and never wait on an execution log (not-found and
undeployed never write one), so they complete without the hasCompleted gate.
Quota rejections (402/429) still complete only once their log is saved.

* fix(projects): finish a retired Project's storage sweep only on the provider's last page

Azure Blob, GCS and S3-compatible providers can return a short or empty page
that is not the last, which the sweep read as done and left the rest of the
prefix orphaned. The sweep now ends only on page.done, and follows a non-final
page that removed nothing to the next one within the run.

* fix(account-deletion): keep a public object a stored URL with a fragment still names

A relative /api/files/serve/<key>#frag kept the fragment in its candidate key,
so a live reference could be missed and the object purged. The guard now also
reads the path without its fragment, on both the decoded and fallback paths.

* fix(billing): withhold the free allowance from stats rows created outside signup

A storage write or an ownership transfer that creates a missing user_stats row
gave it the default free allowance without the deleted-identity check
ensureUserStatsExists makes. When either insert creates the row, the same
email-only check now runs on the caller's transaction, in a savepoint that
fails open; an existing row costs no extra query.

* docs(async-jobs): scope cancelByWorkspace to persisted queue jobs

* refactor(trigger): classify preprocessing rejections by meaning, not status code

Preprocessing tags not-found, archived, not-attached and not-deployed
rejections with an internal workflowState (isWorkflowStateRejection); these
write no execution log by design, so the job completes at once. Quota and
account rejections (usage limit, spent reservation headroom, rate limit,
suspended account) complete only once their log is saved; a suspended account
goes back behind that gate.

The tag is a separate field rather than a code because v2 execute, its
streaming error and MCP serve copy error.code into their responses; a new code
on a 404 would change those outputs. Headroom denials are matched through a
new isReservationHeadroomDenial next to their descriptors instead of joining
the deterministic-rejection set, which webhooks and polling read to stop
retries and which deliberately leaves headroom out.

* refactor(billing): create every fallback user_stats row through one creator

createUserStatsIfMissing inserts the row on the caller's executor and, only
when it created the row, runs the deleted-identity check and forfeits the
allowance (a transaction on db, a savepoint inside a caller's transaction),
failing open. ensureUserStatsExists, the storage write and the ownership
transfer all use it; the two forfeit paths merge into one.

One behavior delta: a stats row the ownership transfer creates for the new
owner now starts at getFreeTierLimit() instead of the schema default, the same
as every other creator (identical unless FREE_TIER_COST_LIMIT is set).

The admin credits route keeps its own insert: it is not behavior-identical
(short id, no usageLimitUpdatedAt, a plain insert that throws on a concurrent
create rather than a no-op), and it rewrites the limit right after.

* refactor(billing): drop the unused executor from the deleted-identity email check

isDeletedAccountIdentity reads on db as it did before; only
userHasDeletedAccountIdentity takes a caller's transaction, so its savepoint
wrapper moves inline. Hoist the headroom descriptors to a module constant.
* feat(oracle-fusion): add core financial business operations

* fix(oracle-fusion): preserve resource keys and tighten validation fixtures

* fix(oracle-fusion): distinguish empty forms and expose array inputs
…9163)

* feat(databricks): expand API coverage and preserve paginated results

* fix(databricks): preserve saved controls and correct output contracts

* fix(databricks): clarify operation parameter descriptions
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

Comment on lines +123 to +126
export function isOracleFusionFinancialsToolId(
value: string
): value is OracleFusionFinancialsToolId {
return TOOL_ID_SET.has(value)

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.

P2 Exported functions lack required docs

The new isOracleFusionFinancialsToolId and executeOracleFusionFinancialsOperation exports have no TSDoc. The repository requires TSDoc for exported APIs, so this requirement must be satisfied before merging. Add short comments describing their inputs and results, including how the operation runner reports failures.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@cubic-dev-ai cubic-dev-ai 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.

8 issues found across 427 files

Confidence score: 2/5

  • cancel_all_runs.ts can cancel every active workspace run when called with credentials alone. Require an explicit scope before sending the cancellation request.
  • token-resolution.ts can mint managed Slack OAuth credentials for a service-account-only tool. Validate that managed credentials match the tool’s required credential kind.
  • use-file-content-source.tsx serves embedded Project images with a cache policy that does not enforce no-store. Use the shared no-store policy for inline images.
  • genie_update_space.ts drops an explicit empty description, so callers cannot clear an existing Genie description. Preserve "" while continuing to omit undefined.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/oauth/token-resolution.ts">

<violation number="1" location="apps/sim/lib/oauth/token-resolution.ts:310">
P2: The managed-OAuth branch still bypasses `credentialKind`, so a managed Slack OAuth credential can be minted for a service-account-only tool such as `slack_add_bookmark`. Validate managed credentials as OAuth before `resolveManagedOAuthCredentialToken.execute`.</violation>
</file>

<file name="apps/sim/tools/databricks/repair_run.ts">

<violation number="1" location="apps/sim/tools/databricks/repair_run.ts:96">
P2: A successful non-JSON Databricks response makes `response.json()` throw an unmarked parse error, which Sim records as an internal failure. Catch this parse error and mark it `third_party_server` at the provider boundary.</violation>
</file>

<file name="apps/sim/hooks/use-file-content-source.tsx">

<violation number="1" location="apps/sim/hooks/use-file-content-source.tsx:171">
P2: This new Project source routes embedded images through the file-ID inline endpoint, whose response is cacheable (`private, no-cache`) rather than `private, no-store`. Use the shared no-store policy for Project inline responses so previously authorized private bytes do not remain cached after access is revoked or the file is deleted.</violation>
</file>

<file name="apps/sim/tools/databricks/genie_create_space.ts">

<violation number="1" location="apps/sim/tools/databricks/genie_create_space.ts:82">
P2: A 2xx response with a non-JSON body is surfaced as a raw JSON parse failure, which misclassifies a Databricks server response as an internal tool error. Parse successful responses through a provider-server error path and classify invalid 2xx payloads as `third_party_server` at this boundary.

(Based on your team's feedback about non-JSON tool responses.)</violation>
</file>

<file name="apps/sim/tools/databricks/get_job.ts">

<violation number="1" location="apps/sim/tools/databricks/get_job.ts:95">
P2: `JOB_TASK_OUTPUT` omits common task-specific fields such as `spark_python_task`, `sql_task`, and `run_job_task`. `transformResponse` preserves them, but this output schema drives selectable paths, so extend the shared schema before replacing the generic task object.</violation>
</file>

<file name="apps/sim/tools/databricks/genie_update_space.ts">

<violation number="1" location="apps/sim/tools/databricks/genie_update_space.ts:90">
P2: This drops an explicit empty description before the PATCH, so callers cannot clear a populated Genie description. Preserve an empty string while still omitting an undefined parameter.</violation>
</file>

<file name="apps/sim/tools/databricks/cancel_all_runs.ts">

<violation number="1" location="apps/sim/tools/databricks/cancel_all_runs.ts:23">
P1: Both cancellation controls are optional, allowing a credentials-only call to send an empty body and cancel every active run in the workspace. Require an explicit scope or add a clearly deliberate workspace-wide control before issuing this destructive request.</violation>
</file>

<file name="apps/sim/tools/databricks/genie_agent_cancel.ts">

<violation number="1" location="apps/sim/tools/databricks/genie_agent_cancel.ts:56">
P2: This agent cancel endpoint is mapped with the chat-message mapper, so successful cancellations lose the agent response’s `output` items and expose misleading empty message fields. Map the agent response with an agent-specific output contract, or return a simple cancellation-success result.</violation>
</file>

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Turn on auto-fix | Re-trigger cubic

version: '1.0.0',
params: {
...DATABRICKS_AUTH_PARAMS,
jobId: {

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2026 •

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.

P1: Both cancellation controls are optional, allowing a credentials-only call to send an empty body and cancel every active run in the workspace. Require an explicit scope or add a clearly deliberate workspace-wide control before issuing this destructive request.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/tools/databricks/cancel_all_runs.ts, line 23:

<comment>Both cancellation controls are optional, allowing a credentials-only call to send an empty body and cancel every active run in the workspace. Require an explicit scope or add a clearly deliberate workspace-wide control before issuing this destructive request.</comment>

<file context>
@@ -0,0 +1,58 @@
+  version: '1.0.0',
+  params: {
+    ...DATABRICKS_AUTH_PARAMS,
+    jobId: {
+      type: 'number',
+      required: false,
</file context>
Fix with cubic

@@ -271,6 +307,13 @@ export async function resolveCredentialToken(
return { ok: false, status: 403, error: authz.error || 'Unauthorized' }
}

const bindingError = validateToolCredentialBinding(

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2026 •

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.

P2: The managed-OAuth branch still bypasses credentialKind, so a managed Slack OAuth credential can be minted for a service-account-only tool such as slack_add_bookmark. Validate managed credentials as OAuth before resolveManagedOAuthCredentialToken.execute.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/lib/oauth/token-resolution.ts, line 310:

<comment>The managed-OAuth branch still bypasses `credentialKind`, so a managed Slack OAuth credential can be minted for a service-account-only tool such as `slack_add_bookmark`. Validate managed credentials as OAuth before `resolveManagedOAuthCredentialToken.execute`.</comment>

<file context>
@@ -271,6 +307,13 @@ export async function resolveCredentialToken(
         return { ok: false, status: 403, error: authz.error || 'Unauthorized' }
       }
 
+      const bindingError = validateToolCredentialBinding(
+        input.toolId,
+        resolved.providerId,
</file context>
Fix with cubic

},
transformResponse: async (response) => {
if (!response.ok) throw new Error(await readDatabricksError(response, 'Failed to repair run'))
const data = await response.json()

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2026 •

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.

P2: A successful non-JSON Databricks response makes response.json() throw an unmarked parse error, which Sim records as an internal failure. Catch this parse error and mark it third_party_server at the provider boundary.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/tools/databricks/repair_run.ts, line 96:

<comment>A successful non-JSON Databricks response makes `response.json()` throw an unmarked parse error, which Sim records as an internal failure. Catch this parse error and mark it `third_party_server` at the provider boundary.</comment>

<file context>
@@ -0,0 +1,106 @@
+  },
+  transformResponse: async (response) => {
+    if (!response.ok) throw new Error(await readDatabricksError(response, 'Failed to repair run'))
+    const data = await response.json()
+    return { success: true, output: { repairId: data.repair_id ?? null } }
+  },
</file context>
Fix with cubic

t: opts?.version == null && opts?.bust ? String(Date.now()) : undefined,
},
}),
`/api/projects/${encodeURIComponent(projectId)}/files/inline`,

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2026 •

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.

P2: This new Project source routes embedded images through the file-ID inline endpoint, whose response is cacheable (private, no-cache) rather than private, no-store. Use the shared no-store policy for Project inline responses so previously authorized private bytes do not remain cached after access is revoked or the file is deleted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/hooks/use-file-content-source.tsx, line 171:

<comment>This new Project source routes embedded images through the file-ID inline endpoint, whose response is cacheable (`private, no-cache`) rather than `private, no-store`. Use the shared no-store policy for Project inline responses so previously authorized private bytes do not remain cached after access is revoked or the file is deleted.</comment>

<file context>
@@ -142,6 +147,32 @@ export function createPublicFileContentSource(
+              t: opts?.version == null && opts?.bust ? String(Date.now()) : undefined,
+            },
+          }),
+    `/api/projects/${encodeURIComponent(projectId)}/files/inline`,
+    { entityType: 'project', entityId: projectId }
+  )
</file context>
Fix with cubic

transformResponse: async (response) => {
if (!response.ok)
throw new Error(await readDatabricksError(response, 'Failed to create genie space'))
const data = await response.json()

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2026 •

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.

P2: A 2xx response with a non-JSON body is surfaced as a raw JSON parse failure, which misclassifies a Databricks server response as an internal tool error. Parse successful responses through a provider-server error path and classify invalid 2xx payloads as third_party_server at this boundary.

(Based on your team's feedback about non-JSON tool responses.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/tools/databricks/genie_create_space.ts, line 82:

<comment>A 2xx response with a non-JSON body is surfaced as a raw JSON parse failure, which misclassifies a Databricks server response as an internal tool error. Parse successful responses through a provider-server error path and classify invalid 2xx payloads as `third_party_server` at this boundary.

(Based on your team's feedback about non-JSON tool responses.) </comment>

<file context>
@@ -0,0 +1,97 @@
+  transformResponse: async (response) => {
+    if (!response.ok)
+      throw new Error(await readDatabricksError(response, 'Failed to create genie space'))
+    const data = await response.json()
+    return {
+      success: true,
</file context>
Fix with cubic

},
}
},

outputs: {
tasks: JOB_TASK_OUTPUT,

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2026 •

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.

P2: JOB_TASK_OUTPUT omits common task-specific fields such as spark_python_task, sql_task, and run_job_task. transformResponse preserves them, but this output schema drives selectable paths, so extend the shared schema before replacing the generic task object.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/tools/databricks/get_job.ts, line 95:

<comment>`JOB_TASK_OUTPUT` omits common task-specific fields such as `spark_python_task`, `sql_task`, and `run_job_task`. `transformResponse` preserves them, but this output schema drives selectable paths, so extend the shared schema before replacing the generic task object.</comment>

<file context>
@@ -65,11 +81,64 @@ export const getJobTool: ToolConfig<DatabricksGetJobParams, DatabricksGetJobResp
   },
 
   outputs: {
+    tasks: JOB_TASK_OUTPUT,
+    jobClusters: JOB_CLUSTERS_OUTPUT,
+    description: { type: 'string', nullable: true, description: 'Job description' },
</file context>
Fix with cubic

return filterUndefined({
serialized_space: optionalDatabricksString(params.serializedSpace),
title: optionalDatabricksString(params.title),
description: optionalDatabricksString(params.description),

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2026 •

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.

P2: This drops an explicit empty description before the PATCH, so callers cannot clear a populated Genie description. Preserve an empty string while still omitting an undefined parameter.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/tools/databricks/genie_update_space.ts, line 90:

<comment>This drops an explicit empty description before the PATCH, so callers cannot clear a populated Genie description. Preserve an empty string while still omitting an undefined parameter.</comment>

<file context>
@@ -0,0 +1,115 @@
+      return filterUndefined({
+        serialized_space: optionalDatabricksString(params.serializedSpace),
+        title: optionalDatabricksString(params.title),
+        description: optionalDatabricksString(params.description),
+        warehouse_id: optionalDatabricksString(params.warehouseId),
+        etag: optionalDatabricksString(params.etag),
</file context>
Suggested change
description: optionalDatabricksString(params.description),
description: params.description?.trim(),
Fix with cubic

transformResponse: async (response) => {
if (!response.ok)
throw new Error(await readDatabricksError(response, 'Failed to cancel Genie agent response'))
return { success: true, output: mapGenieMessage(await response.json()) }

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2026 •

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.

P2: This agent cancel endpoint is mapped with the chat-message mapper, so successful cancellations lose the agent response’s output items and expose misleading empty message fields. Map the agent response with an agent-specific output contract, or return a simple cancellation-success result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/tools/databricks/genie_agent_cancel.ts, line 56:

<comment>This agent cancel endpoint is mapped with the chat-message mapper, so successful cancellations lose the agent response’s `output` items and expose misleading empty message fields. Map the agent response with an agent-specific output contract, or return a simple cancellation-success result.</comment>

<file context>
@@ -0,0 +1,59 @@
+  transformResponse: async (response) => {
+    if (!response.ok)
+      throw new Error(await readDatabricksError(response, 'Failed to cancel Genie agent response'))
+    return { success: true, output: mapGenieMessage(await response.json()) }
+  },
+  outputs: GENIE_MESSAGE_OUTPUT_PROPERTIES,
</file context>
Fix with cubic

… copy parsing (#9169)

* fix(chat): release-audit follow-ups for Project file tabs and sandbox copy parsing

* fix(chat): exempt only Project-owned file tabs from the stream workspace guard

* fix(chat): read a sandbox copy body once under the cap before routing it

Parsing a clone of the live stream hung on an oversized body, because cancelling one teed branch waits for the unread other. The copy body is now buffered once at the entry, and both the protocol check and either copy path read that in-memory request.

This branch was previously deployed

1 inactive deployment
Preview — 477028de Deployed Oct 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants