Skip to content

v0.8.18: sandbox files mounting, ttl tables column type - #7283

Open
waleedlatif1 wants to merge 15 commits into
mainfrom
staging
Open

v0.8.18: sandbox files mounting, ttl tables column type#7283
waleedlatif1 wants to merge 15 commits into
mainfrom
staging

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

TheodoreSpeaks and others added 10 commits August 29, 2026 03:47
#7275)

A Slack-triggered run's subject is the external Slack user, and a schedule or
public-API run has no subject at all. list_groups, list_people, and send_invite
demanded a Sim user, so every unattended run got "Credential Group user access
required" — including reads that need no actor. Authority for an actorless
caller comes from the deployment the workspace layer already checks.

Invitations no longer name an inviter when there is no person to name, rather
than borrowing the run's actor and claiming someone invited when they did not.
* feat(function): mount referenced files into the code sandbox

Reference a file's path in Function block code and it is mounted for you:
`<block.file.path>` resolves to its location on the sandbox filesystem, so
any language can open it. It is the counterpart to `.base64`, which inlines
the bytes and only works in JavaScript, leaving Python and Shell with no way
to read a referenced file at all.

Resolution happens long before a sandbox exists, and mount paths are only
assigned once the whole set is planned together, so the resolver leaves a
marker that the function runtime swaps for the real path — the same shape as
LargeValueRef.

Files written to /tmp/sim/outputs come back as platform file objects, so they
can be attached or uploaded with no intermediate step. Harvesting is automatic
on runs already in the remote sandbox; isolate runs pay nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(execution): cover the sim.* helpers in a real isolate

isolated-vm.test.ts mocks the spawn, so nothing proved the sim.* namespace
was reachable from user code — only that the process plumbing was called.
These run the real worker and assert values cross the boundary both ways.

Pins the fast runtime's global surface, which turned out narrower than
assumed: plain ECMAScript plus fetch, console and sim.*, with no Buffer,
require, process, crypto, TextDecoder, atob or setTimeout. That list is
exactly what decides whether a block needs an import and so moves to the
remote sandbox, so it is asserted rather than described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: regenerate tool metadata and integration docs

Changing function_execute's params and adding file_write's fileInput made
both generated artifacts stale, which check:audits catches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(function): address review findings on sandbox file I/O

- Classify harvested output by content, not by file name. Gating the
  provenance scan on a filename-derived MIME type let a resolved secret be
  written as plaintext under a binary-looking extension and skip the only
  guard before upload. Bytes that round-trip as UTF-8 are scannable
  whatever they are called.
- Format the .path replacement through the shared context-aware helper.
  Returning a bare identifier inserted __blockRef_N literally in Shell and
  inside quoted strings instead of the mounted path.
- Enforce the mount ceiling on the combined set. Marker-derived mounts
  bypassed the contract's max, which only bounded the explicit files param.
- Reuse one marker per file key, so referencing a path twice mounts once.
- Raise a coded error when the code deletes the output directory, so it
  reports as a 400 with remediation rather than an opaque 500.
- Drop declared paths from the discovered set; counting a file in both
  rejected a single output larger than half the byte ceiling.
- defineProperty when rebuilding context values, so an own __proto__ key
  survives instead of hitting Object.prototype's setter.
- Give the directory sentinel a collision-resistant name, and cap
  file_write's fileInput at the destination's own limit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(function): include files in the FAQ's output answer

Adding <function.files> to the outputs table left the FAQ still saying the
block returns two outputs, and the reference FAQ never mentioned that a
file is read by referencing its path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(function): scan every harvested output for resolved secrets

Gating the scan on whether the bytes looked textual was defeatable twice
over: name the file .png, or append a single invalid byte, and a plaintext
secret skipped the only guard before upload.

A lossy UTF-8 decode preserves ASCII runs, so a literal secret is findable
in any buffer. The scan is now unconditional. What remains out of reach is
a secret carried in transformed form, which no substring scan can see —
an inherent limit of scanning rather than a hole in the gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(function): correct provenance, mount caps and runtime file plumbing

Review findings from the sandbox file I/O change, traced to root cause.

A file_write carrying fileInput copied its bytes into a new workspace
file without the source's secret lineage, so a file the platform had
locked as secret-derived became readable again under a new id. It now
derives provenance from the source through the same helper archiving
uses, which also marks a source with no workspace row unknown rather
than empty.

The tool half of that feature had shipped without the block half: the
File block's write mapper listed four params and dropped fileInput, and
no sub-block could supply one, so the documented binary path was
unreachable from the canvas and from an agent holding the File tool.
Adds the canonical pair, maps it, and drops content's unconditional
required now that the two are mutually exclusive sources.

The harvest counted secret names the matcher had already discarded as
too short to identify anything, so a workflow whose every secret was
under the substitutable-literal minimum built no matcher, classified
every output unknown, and refused each one while claiming it held a
secret. The counter now applies the matcher's own predicate.

The execution file index was lazily created on the per-call context
clone, so files a tool produced were recorded onto a throwaway and the
next call in the run never saw them. Materializing it on the source
before the spread makes both objects share one map.

A .path reference inside a quoted Python or JavaScript string was
JSON-encoded, putting literal quote characters inside the path the code
then opened. Mount paths are sanitized to a safe character set, so they
splice raw.

URL mounts now carry a ceiling that curl enforces on the bytes actually
served, rather than trusting a recorded size, and declared sandbox
outputs opt out of the harvest explicitly instead of relying on which
branch returns first. Also stops rebuilding non-plain context values
into stripped objects, and removes an agent tool-description branch that
no longer had a sub-block to read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): send one write source, and bound mounts by what they were charged

The write card sent `content` unconditionally, and the contract counts any
defined `content` as "text was provided" — so an untouched Content box,
which serializes as an empty string, collided with a selected file and
rejected every binary write. The mapper now emits only the source the card
carries, leaving the contract to catch filling both or neither.

A generated document that references other files needs a principal to
resolve them; without one the resolver can only serve an already-published
artifact and throws. The write path now passes it.

URL mounts are granted exactly the byte count they were charged against the
aggregate, rather than being charged a reported size while permitted the
global per-file maximum — twenty mounts each claiming a byte could
otherwise be allowed 500MB apiece. An honest size fetches normally and an
understated one is refused.

`--max-filesize` only refuses a transfer up front when the response
declares a Content-Length, so a chunked reply slipped past it. The
delivered file is now measured and removed if it overran, in the same
command rather than a second round trip.

Also documents that naming an explicit sandbox output path excludes the
harvest directory, in both the block docs and the tool description the
model reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): accept the picker shape, and bound a mount while it downloads

The write path required an already-complete UserFile, but the file picker
stores {name, path, key, size, type} with no id or url — so selecting a
file in the new basic field was rejected as not a file object before any
bytes moved. It now runs through the same normalizer every other operation
in that file applies to its own input, while a block reference or an
agent-resolved id still passes through as-is.

Writing a deliberately empty text file was also rejected, because an empty
string read as "no text". The selected file is what disambiguates now: with
one present an empty Content box means unused, and with none, content
always goes through.

--max-filesize only refuses a transfer that declares a Content-Length, so
the previous size check ran after the whole object had already been written
to sandbox disk. The fetch now streams through a byte cap, so at most one
byte over the limit can ever land, and curl's status travels through a file
so a 403 on an expired URL is still distinguishable from an empty download.

A non-finite mount size made every comparison false, so the aggregate check
passed while the mount was charged the per-file maximum anyway; the size is
resolved once now, before either test.

Mount resolution failures are the caller's files — unreadable, oversized,
or over the aggregate — and now answer 400 with the message naming the
file, rather than a 500. Files already uploaded when a later one in the
same harvest is refused are removed, since the export is all-or-nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): clamp a declared mount cap, and demand identity not full metadata

sandboxFiles reaches the sandbox layer from the request body, so a declared
maxBytes is a caller's number. It may now lower its own mount's ceiling but
never raise it past the one that layer guarantees.

The write path required full UserFile metadata, but size is never read
before the download and the download reports the real content type — so a
reference carrying id, key, url and name was rejected over two fields
nothing depends on. It now asks only for identity and fills the rest.

Uploads already made are also discarded when a later upload throws, not
only when a later file is refused for carrying a secret. Both exits leave
the harvest all-or-nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): answer null for a key a file input cannot be classified by

fileInputToUserFile normalizes caller-supplied file objects and returns
null for anything it cannot use — but it classified the storage key with
the throwing form, so a key without a recognized context prefix escaped as
a 500 from every operation that normalizes a file input, not just write.

Adds tryInferContextFromKey beside inferContextFromKey, sharing the one
prefix list so a new context cannot be added to half of them. The throwing
form stays right where an unclassifiable key means the platform built one
wrong; the nullable form is for keys that arrived in a request, where an
unrecognized prefix only means this is not a file we can use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The admin workspace move was restricted to personal/grandfathered sources;
`assertWorkspaceMovable` refused anything already owned by an organization, so
support could only re-home a workspace with manual SQL.

Relax that guard to a drift-only check and handle the source organization.
`changeWorkspaceStoragePayerInTx` already accepted an arbitrary source payer,
so the storage-ledger rebalance needed no change.

Moving a workspace between organizations is the first operation capable of
separating an artifact from the organization that owns it, so two invariants
nothing has ever had to defend are enforced here:

- A custom block and its bound workflow always share an organization.
  `getCustomBlockAuthority` resolves by the consumer's org and
  `admitCustomBlockChildExecution` skips its concurrency reservation on the
  strength of that, so a stranded row would run a foreign tenant's workflow
  under its owner's credentials, billed to the wrong payer. The move
  unpublishes those blocks through the product's own `deleteCustomBlock` and
  records the loss in the source organization's audit view.
- A fork parent and child always share an organization. `resolveForkEdge` has
  no org check at all, so the move refuses while a cross-org edge would result.

Move-time checks alone cannot hold either invariant, because the writers can
commit after them. `publishCustomBlock` now validates and inserts under the
organization mutation lock the move holds, and `createFork` row-locks its
parent so the move and the organization-attach path — both of which take
`FOR NO KEY UPDATE` on that row — serialize against it.

Pending invitations block: re-stamping an org-scoped invitation would convert
a pending membership in the source org into one in the destination, consuming
a seat for an invitation the destination never issued.

An entitlement downgrade blocks. When entitlement is subscription-backed, both
organizations are re-evaluated under the locks rather than trusting a
pre-transaction read that is stale in both directions. The two modes where
`resolveOrganizationEnterprisePlan` grants entitlement by deployment
configuration are excluded through a shared `isSubscriptionBackedEntitlement`
predicate, so a missing subscription row is never misread as a lapse.

Both organizations are locked, ascending by id, mirroring
`acquireOrganizationUserMutationLocks`. The source id is read optimistically
before the transaction and re-verified under the locks, retrying through the
existing loop when it moved.
…orkflows Beyond HubSpot and Zapier (#7280)

* feat(library): AI Agents for Marketing Automation: Building Agentic Workflows Beyond HubSpot and Zapier

* fix(library): use Chat and Sim instead of the prohibited Mothership name

The constitution reserves "Sim" for the agent and "Chat" for the surface;
"Mothership" names no product surface a reader can find.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…7281)

* fix(provenance): let a run that never started report why it failed

A copilot-run workflow that fails before reaching the engine crossed
back with no provenance, which latched the tool's registry and reduced
the result to "result unavailable". The caller was told its run failed
but not that the workflow was undeployed, or the input invalid, or the
slot unavailable — the reasons this layer produces before any block
runs, naming no secret because none had been resolved yet.

The executor attaches its execution result to every throw, so the
absence of one is proof that no block ran: output, logs and error are
all undefined and the only content is a message this layer wrote. That
is an absence, not an inability to vouch, so the crossing now carries
an exact-empty envelope. The message still passes the tool boundary's
egress projection against the same registry, so anything that registry
knows is still redacted. A run that did execute and could not vouch
hands back its incomplete envelope exactly as before, and that still
latches.

Make the attach total rather than conditional to keep that inference
sound. A block failure is already normalized on the way in, so the old
`instanceof Error` guard held in practice; what it did not give was a
guarantee covering a non-Error raised by the engine's own synchronous
work. toError is identity-preserving, so ordinary failures keep their
type.

The empty envelope moves to the registry module, which owns the
vocabulary, replacing a private copy in the logging session so one
definition states what "vouched for, naming nothing" is.

* fix(provenance): keep the post-run crossing window out of the never-started claim

Review round 1, both findings accepted.

The post-run crossing runs inside the same try as the executor call, so
when that import is what throws, the catch sees an error carrying no
execution result — the same evidence a run that never started leaves.
The previous condition read that as "nothing crossed" and vouched for
it, when in fact an execution exists and its provenance was never
imported, which is exactly the content that cannot be vouched for.

Record whether the executor returned and require both facts before
claiming the absence: not past the executor, and no result attached.
Everything else hands back whatever envelope it has, and an incomplete
one still latches.

The executor test also could not fail against the old gated attach: a
block failure is normalized on the way in, so its rejection already
arrived as an Error. Drive it through the cancellation subscribe run()
awaits before the queue instead, which is its own synchronous work and
reaches the catch untouched — the case the total attach exists for.

* fix(provenance): carry the run's result through post-execution failures

Round 2, cubic's finding accepted — and it was a distinct window, not a
restatement of round 1. The executor's post-execution work runs after
the run has produced a result but before `executeWorkflow` returns, so
a failure there reached callers with no result attached: the run threw
nothing itself, and the flag added last round could not be set yet.
Every consumer that reads a missing result as "no block ran" was wrong
in that window, this crossing included.

Fix it where the result lives rather than at each reader. The executor
attaches its own on the throws it raises; `executeWorkflow` now does the
same for failures raised after it holds one, skipping the case the
executor already recorded. Logging and trace spans get the same benefit
for free — they read the identical signal.

That makes an absent result total again, so the boolean flag goes and
the crossing reads one thing: the result from the error, or the one
already returned when the failure came later still, from the crossing
itself. Only a failure with neither can claim nothing ran. The
post-return case now describes content with the run's real envelope
rather than latching blind, which is strictly more accurate than either
prior behaviour.

* fix(provenance): normalize a post-execution failure so it can carry the result

Round 3, cubic's finding accepted. The guard added last round required
the caught value to already be an `Error`, so a non-Error raised by
post-execution work skipped the attach and was rethrown bare — the same
hole this branch closed in the executor, left open one layer up by my
own change. A Copilot run would have reported an executed workflow as
never started and vouched for content it cannot describe.

Normalize once at the top of the catch and use that value throughout,
including the rethrow, matching what the executor does. `toError`
returns an `Error` unchanged, so a custom error class keeps its
identity and every ordinary failure is untouched — the existing
identity assertion on the rejection path still holds.

Two tests: the result reaches an ordinary post-execution failure, and a
non-Error one is normalized so it can carry the result too. The second
fails against the previous guard.
* feat(billing): record function sandbox usage

* fix(billing): charge function user-code failures

* fix(billing): correct sandbox trace cost boundaries

* fix(billing): tighten sandbox completion boundaries

* test(billing): check the metered sandbox amount against a real provider

The pricing unit test pins the arithmetic and the conformance suite proves a
cost is produced, attached to the right outcomes, and routed — but that suite
stubs the provider and mocks Date.now() with a counter advancing one
millisecond per call. Under that clock `total > 0` is the strongest claim
available, and it holds equally well if the metered window is anchored to the
wrong instants or the resource constants are wrong.

Bounds the charge between what the sleep must cost and what the wall clock
could justify, so a wrong rate, a wrong vCPU/memory constant, and a
mis-anchored window all fail. Provider-agnostic via resolveProvider, opt-in
behind SANDBOX_BILLING_SMOKE=1 like the sibling smoke suites.

Verified against both providers: E2B billed 9.031s of a 9.302s call at
$0.1656/hr, Daytona 8.435s of 8.721s at $0.16668/hr — both matching published
rates, both excluding ~275ms of Sim-side overhead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(billing): meter the sandbox a cloud Pi session runs in

Pi's own sandbox was never metered. withPiSandbox called createSandbox
without the meterUsage argument, so only sandboxes created through
executeFunctionRequest were charged — and Pi's is the larger consumer by an
order of magnitude. A Function block holds one for seconds; a Pi session
holds one for a minimum lifetime of 31 minutes.

The gap was worst exactly where it was least visible. A Pi coding agent
normally runs BYOK, so its model cost is zero by definition, and the ledger
bills a model row on total > 0. With the sandbox unmetered, such a run
produced a zero-cost model_unbilled row and Sim collected only the flat
execution fee while paying its provider for the whole session.

Threads a cost sink through PiRunContext, which is the seam backends
already receive and the only one that reaches all four cloud modes. The
handler owns one sink covering both sandbox sources — Function tools in
local mode, the agent's own sandbox in cloud mode — so neither can be
dropped where the cost is folded into the block's output. It rides in
toolCost for the same reason the Function tool cost already does: that is
what survives the BYOK zeroing.

Unlike the Function path this charges on creation rather than on a
completed session. A Function run is seconds long, so absorbing one the
provider failed to deliver is cheap and reads as fair; tens of minutes of
Pi compute is consumed whether the agent finished, errored, or was
cancelled, and billing only clean endings would mean paying for every other
one. A create that throws still costs nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(billing): check the Pi sandbox charge against a real provider

The handler test mocks the backend and writes into the cost sink by hand,
so it proves the wiring from a backend to the block's cost and nothing
more — it would still pass if withPiSandbox never metered at all, which is
precisely the bug that path had.

Holds a real Pi sandbox open for a known interval and bounds the charge
between what that interval must cost and what the whole session could
justify. Verified to fail against the original unmetered call with
"expected 0 to be greater than or equal to 0.00023", and to pass once the
sink is threaded: 5.949s billed of a 6.141s session on E2B.

The second case pins the other half of the contract — a caller that
supplies no sink is not charged, which is what keeps mothership and other
internal Pi sandboxes free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(billing): charge a Pi session only when it completed

Aligns Pi with the outcome policy the Function path already applies rather
than keeping the divergence the previous commit introduced. A session that
ends by throwing — a provider crash, a lifetime limit, a cancellation — is
absorbed, because a charge nobody can tie to delivered work is not one
worth defending, and consistency across the two sandbox paths is worth more
than recovering the cost of runs that failed.

A command exiting non-zero is still billed: the callback returns normally
there and the agent produced its answer, which is the same reason the
Function path bills its own non-zero exits.

The window still closes at teardown, so a completed session is charged for
the whole time the provider held its sandbox.

Verified on both providers, including that the new case fails when the
charge is applied unconditionally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(billing): keep the charge on completed runs that fail after execution

Three paths dropped cost the sandbox had already earned.

A harvest that cannot return what the run produced — more files than the
export limit, nesting past the listing depth, or an output directory the
code deleted — was excluded from the billable-error set. All three arrive
only after the sandbox has executed and all three are the caller's to fix,
so they belong with the post-completion export failures the policy already
bills rather than the provider failures it absorbs. A completed run whose
code wrote one file too many went free.

That also left the route with nothing to read: it already consults
readTrustedSandboxOutputCost for these errors, so attaching the cost at the
sandbox layer is what carries it into the response.

Separately, a Function block whose handler succeeded could still fail in
the steps that follow it — base64 hydration, and large-value redaction that
throws rather than emit unredacted data. Those errors carry no cost of
their own, so the completed sandbox went unbilled. The handler's cost is
now held across that window, in the same way streamingPartialOutput already
is, and used only when the error has none.

The new conformance case was confirmed to fail against the narrower catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(billing): carry the Pi charge onto a session its agent failed

A backend that returns a result carrying `totals.errorMessage` has already
run: the sandbox was billed and the sink holds the charge. But that path
throws instead of reaching `buildOutput`, which is what publishes the cost,
so the charge was accumulated and then dropped — lost revenue rather than
an over-charge.

Both failure paths now carry it on the error they raise, the same way the
Function handler carries its tool cost, so `handleBlockError` can pick it
up. An agent that ran and then reported a failure consumed the same tokens
and sandbox seconds as one that succeeded, which is why the cost
computation is now shared between the two rather than duplicated.

Also corrects the sink's doc comment. Local mode does fill it — the agent
runs on the caller's own machine and costs Sim nothing, but a
`function_execute` among the Sim tools it calls bills its own remote
sandbox into the same total.

The new case was confirmed to fail without the attach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(tables): add column type extension points

* Address PR review feedback (#7119)

- preserve explicit nulls from source-owned conversion normalization
- normalize hooked values before select migration
- cover null and select conversion rewrites

* refactor(tables): short-circuit unlimited column types

* refactor(tables): keep CSV coercion in import switch

* test(tables): cover rebased column dropdown
* feat(tables): add row TTL expiration

* chore(api): regenerate table API artifacts

* fix(tables): make TTL cleanup fair across tables

* fix(tables): reject rolled-over TTL dates

* fix(copilot): remove unrelated catalog drift

* chore(helm): bump chart for TTL cron

* fix(ci): regenerate CLI table restore type

* refactor(tables): keep TTL CSV parsing in import switch

* docs(tables): clarify TTL epoch-second storage

* docs(tables): clarify TTL cleanup timing

* fix(tables): prioritize oldest TTL cleanup rows

Page cleanup by created_at and id, with a supporting index, so capped runs make progress on the oldest rows first.

* improvement(tables): rename TTL column to Expiration

* chore(tables): run TTL cleanup every fifteen minutes

* feat(tables): gate row TTL expiration

* improvement(tables): align Expiration feature messages

* chore(db): format TTL migration metadata

* test(tables): cover TTL column dropdown

* fix(tables): close TTL extension gaps

* fix(tables): keep TTL import coercion lightweight
* fix(timezone): consolidate table wall-clock conversion

* fix(timezone): preserve ambiguous date semantics

* fix(tables): prevent early TTL expiration

* fix(tables): wait for timezone before TTL edits

* improvement(tables): improve timezone loading state

* test(tables): preserve editor timezone during edits

* fix(tables): guard row modal timezone edits
* feat(tables): trigger workflows on row deletes

* fix(tables): bound delete trigger snapshots

* fix(tables): scope and verify delete triggers
@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (182 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 30, 2026 9:26pm

Request Review

…e integration's output, timeout, and redirect defects (#7276)

* fix(elasticsearch): resolve a Cloud ID to the real Elasticsearch host

* fix(elasticsearch): reject a malformed parent-domain port in a Cloud ID

* fix(elasticsearch): correct get_index output, cluster-health timeout, and redirect credentials

* fix(elasticsearch): reject a surviving colon in a decoded Cloud ID component

* fix(elasticsearch): stop a cloud deployment falling back to a stale host

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

14 issues found and verified against the latest diff

Confidence score: 2/5

  • Invalid or inconsistently loaded timezones can either crash TTL table rendering or cause inline date edits to write a different instant; harden cell-render.tsx, inline-editors.tsx, and timezone.ts with validated fallbacks before formatting or saving values.
  • Malformed TTL handling can silently blank imported CSV cells without rejection metrics, while ttl.ts may interpret empty values as January 1, 1970; reject invalid values consistently and preserve onValueRejected reporting.
  • Advanced function execution can treat serialized files JSON as one file ID, breaking sandbox inputs; parse array-form JSON before the generic file[] hydrator in apps/sim/tools/function/execute.ts.
  • Several workflow paths still need contract and error-boundary fixes: admin-move.ts loses credential summaries, create-fork.ts turns expected conflicts into 500s, and service.ts makes row deletion wait on webhook dispatch; preserve response/error semantics and keep trigger dispatch asynchronous.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/workspaces/admin-move.ts">

<violation number="1" location="apps/sim/lib/workspaces/admin-move.ts:1057">
P2: After confirmation or reload, the operation response reports zero credentials even when the workspace has secrets, environment variables, or BYOK keys. Preserve or reconstruct the credential summary instead of returning `EMPTY_CREDENTIAL_SUMMARY`.</violation>

<violation number="2" location="apps/sim/lib/workspaces/admin-move.ts:1830">
P2: When an admin reloads a completed move from a personal workspace, the response adds a false “source organization was not persisted” notice. Distinguish a missing property from an explicit `null` before emitting the legacy warning.</violation>
</file>

<file name="apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx">

<violation number="1" location="apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx:145">
P2: When a persisted user setting contains an invalid timezone, formatting any non-null TTL cell throws `RangeError` and prevents the table from rendering. Validate or fall back the effective timezone before passing it into the cell formatter.</violation>
</file>

<file name="apps/sim/lib/table/column-types/import-coercion.ts">

<violation number="1" location="apps/sim/lib/table/column-types/import-coercion.ts:10">
P2: When a CSV contains an invalid non-empty TTL, `parseTtlEpochSeconds` returns `null` but this fallback returns the raw string. The import then blanks the cell without invoking `onValueRejected`, so `cellsRejected` and the import rejection summary under-report the data loss. Return `null` for parse failures so the existing rejection hook records the rejected cell.

(Based on your team's feedback about surfacing rejections across all CSV import paths.)</violation>
</file>

<file name="apps/sim/blocks/blocks/file.ts">

<violation number="1" location="apps/sim/blocks/blocks/file.ts:1241">
P2: When `content` resolves to a non-string value while a file is selected, `params` drops it and writes the file successfully instead of letting the file-write schema reject the invalid content. Omit only an explicitly empty string; preserve other defined values for validation.</violation>
</file>

<file name="apps/sim/lib/table/constants.ts">

<violation number="1" location="apps/sim/lib/table/constants.ts:165">
P2: When `TABLE_MAX_ROW_SIZE_BYTES` exceeds `DELETE_SNAPSHOT_BATCH_MAX_BYTES`, this fallback still allows a single row larger than the claimed snapshot budget through trigger dispatch. Reject or cap row-size configurations above the snapshot budget, or explicitly account for the larger single-row payload in the budget.</violation>
</file>

<file name="apps/sim/background/cleanup-table-row-ttl.ts">

<violation number="1" location="apps/sim/background/cleanup-table-row-ttl.ts:254">
P2: When a later table cleanup fails after an earlier table has deleted rows, this function exits before emitting the earlier table's change event, leaving open grids stale. Signal each successful batch or from a `finally` path so partial cleanup remains visible.</violation>
</file>

<file name="apps/sim/ee/workspace-forking/lib/create-fork.ts">

<violation number="1" location="apps/sim/ee/workspace-forking/lib/create-fork.ts:214">
P2: When the parent changes organizations during a fork, Drizzle wraps this transaction error before the route can handle it. The API therefore returns a generic 500 instead of the intended 409; preserve `ForkError` through the transaction cause chain or classify it in the fork route before rethrowing.</violation>
</file>

<file name="apps/sim/lib/table/column-types/ttl.ts">

<violation number="1" location="apps/sim/lib/table/column-types/ttl.ts:71">
P2: When an unset or malformed TTL cell reaches the formatter or conversion hook, `Number(value)` turns it into epoch zero and can display or convert it as January 1, 1970. Reject `null`/`undefined` and empty non-numeric strings before numeric coercion, then preserve the blank through the callers.</violation>
</file>

<file name="apps/sim/lib/core/utils/timezone.ts">

<violation number="1" location="apps/sim/lib/core/utils/timezone.ts:158">
P2: When a caller passes an empty timezone string, `getWallClockParts` treats it as omitted and uses the runtime's local timezone. Check specifically for `undefined` so invalid timezone input is rejected instead of producing host-dependent wall-clock results.</violation>
</file>

<file name="apps/sim/lib/table/rows/service.ts">

<violation number="1" location="apps/sim/lib/table/rows/service.ts:1921">
P2: When a table has matching webhooks, `deleteRow` waits for lookup, preprocessing, admission, and enqueueing before returning. Dispatch this trigger fire-and-forget, matching the trigger contract and insert/update paths, so a slow trigger cannot delay or time out the DELETE request.</violation>
</file>

<file name="apps/sim/tools/function/execute.ts">

<violation number="1" location="apps/sim/tools/function/execute.ts:170">
P2: When advanced-mode resolution supplies `files` as a JSON string, the generic `file[]` hydrator treats the entire JSON document as one file ID before `normalizeSandboxInputFiles` runs. Parse serialized arrays in the shared file-parameter hydration step, or otherwise bypass that step for this representation so valid mounts reach the new normalizer.</violation>
</file>

<file name="apps/sim/lib/table/dates.ts">

<violation number="1" location="apps/sim/lib/table/dates.ts:336">
P2: When `timezone` is supplied for a valid year from `0000` through `0099`, this call resolves the wall clock using 1900-1999 timezone rules because JavaScript remaps those years in `Date.UTC`. Fix the shared resolver to preserve the full year before resolving, or reject unsupported years before this call.</violation>
</file>

<file name="apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx">

<violation number="1" location="apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx:114">
P2: When a regular date editor opens before general settings finish loading, this ref freezes the browser fallback instead of the user's saved timezone. Editing the time/date then writes a different offset and instant; keep date editing gated until the timezone is ready before capturing `initialTimeZone`.</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.
Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread helm/sim/Chart.yaml Outdated
Comment thread apps/sim/executor/variables/resolver.ts
return formatLocalFieldsAsWall(parsed, offsetMinutes)
const wallClock = isoWallClock ?? localizedWallClock ?? parseNaiveWallClockAsUtc(trimmed)
if (!wallClock) return null
return zonedWallClockWithOffset(wallClock, options.timezone, {

@cubic-dev-ai cubic-dev-ai Bot Aug 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When timezone is supplied for a valid year from 0000 through 0099, this call resolves the wall clock using 1900-1999 timezone rules because JavaScript remaps those years in Date.UTC. Fix the shared resolver to preserve the full year before resolving, or reject unsupported years before this call.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/table/dates.ts, line 336:

<comment>When `timezone` is supplied for a valid year from `0000` through `0099`, this call resolves the wall clock using 1900-1999 timezone rules because JavaScript remaps those years in `Date.UTC`. Fix the shared resolver to preserve the full year before resolving, or reject unsupported years before this call.</comment>

<file context>
@@ -238,11 +331,12 @@ export function normalizeDateCellValue(
-    return formatLocalFieldsAsWall(parsed, offsetMinutes)
+    const wallClock = isoWallClock ?? localizedWallClock ?? parseNaiveWallClockAsUtc(trimmed)
+    if (!wallClock) return null
+    return zonedWallClockWithOffset(wallClock, options.timezone, {
+      ambiguousTime: options.ambiguousTime ?? 'earlier',
+      offsetMinuteRounding: options.offsetMinuteRounding,
</file context>
Fix with cubic

* response, which is exactly when the operator most needs to see what the
* move did and where it came from.
*/
const recordedSourceOrganizationId = operationPayload.audit.sourceOrganizationId ?? null

@cubic-dev-ai cubic-dev-ai Bot Aug 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When an admin reloads a completed move from a personal workspace, the response adds a false “source organization was not persisted” notice. Distinguish a missing property from an explicit null before emitting the legacy warning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/workspaces/admin-move.ts, line 1830:

<comment>When an admin reloads a completed move from a personal workspace, the response adds a false “source organization was not persisted” notice. Distinguish a missing property from an explicit `null` before emitting the legacy warning.</comment>

<file context>
@@ -986,8 +1820,52 @@ export async function getWorkspaceMoveOperation(
+   * response, which is exactly when the operator most needs to see what the
+   * move did and where it came from.
+   */
+  const recordedSourceOrganizationId = operationPayload.audit.sourceOrganizationId ?? null
+  const sourceOrganization = recordedSourceOrganizationId
+    ? await getSourceOrganization(recordedSourceOrganizationId)
</file context>
Fix with cubic

summary: await getMovedWorkspaceSummary(tx, params.workspaceId, destination, {
sourceOrganization: replayedSourceOrganization,
sourceOrganizationImpact: EMPTY_SOURCE_IMPACT,
credentials: EMPTY_CREDENTIAL_SUMMARY,

@cubic-dev-ai cubic-dev-ai Bot Aug 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: After confirmation or reload, the operation response reports zero credentials even when the workspace has secrets, environment variables, or BYOK keys. Preserve or reconstruct the credential summary instead of returning EMPTY_CREDENTIAL_SUMMARY.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/workspaces/admin-move.ts, line 1057:

<comment>After confirmation or reload, the operation response reports zero credentials even when the workspace has secrets, environment variables, or BYOK keys. Preserve or reconstruct the credential summary instead of returning `EMPTY_CREDENTIAL_SUMMARY`.</comment>

<file context>
@@ -548,17 +1022,50 @@ export async function moveWorkspaceToOrganization(params: {
+            summary: await getMovedWorkspaceSummary(tx, params.workspaceId, destination, {
+              sourceOrganization: replayedSourceOrganization,
+              sourceOrganizationImpact: EMPTY_SOURCE_IMPACT,
+              credentials: EMPTY_CREDENTIAL_SUMMARY,
+              entitlements: {
+                sourceIsEnterprise: false,
</file context>
Fix with cubic

const popoverPointerAtRef = useRef(0)
const timeZone = useTimezone()
/** Keep one wall-clock interpretation for the lifetime of this edit. */
const editTimeZoneRef = useRef(initialTimeZone)

@cubic-dev-ai cubic-dev-ai Bot Aug 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When a regular date editor opens before general settings finish loading, this ref freezes the browser fallback instead of the user's saved timezone. Editing the time/date then writes a different offset and instant; keep date editing gated until the timezone is ready before capturing initialTimeZone.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx, line 114:

<comment>When a regular date editor opens before general settings finish loading, this ref freezes the browser fallback instead of the user's saved timezone. Editing the time/date then writes a different offset and instant; keep date editing gated until the timezone is ready before capturing `initialTimeZone`.</comment>

<file context>
@@ -68,9 +110,11 @@ function InlineDateEditor({
   const popoverPointerAtRef = useRef(0)
-  const timeZone = useTimezone()
+  /** Keep one wall-clock interpretation for the lifetime of this edit. */
+  const editTimeZoneRef = useRef(initialTimeZone)
+  const timeZone = editTimeZoneRef.current
 
</file context>
Fix with cubic

Comment thread apps/sim/providers/index.ts
Comment thread apps/sim/lib/execution/payloads/sandbox-file-mount-ref.ts
}

function epochSecondsToIso(value: unknown): string | null {
const seconds = typeof value === 'number' ? value : Number(value)

@cubic-dev-ai cubic-dev-ai Bot Aug 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When an unset or malformed TTL cell reaches the formatter or conversion hook, Number(value) turns it into epoch zero and can display or convert it as January 1, 1970. Reject null/undefined and empty non-numeric strings before numeric coercion, then preserve the blank through the callers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/table/column-types/ttl.ts, line 71:

<comment>When an unset or malformed TTL cell reaches the formatter or conversion hook, `Number(value)` turns it into epoch zero and can display or convert it as January 1, 1970. Reject `null`/`undefined` and empty non-numeric strings before numeric coercion, then preserve the blank through the callers.</comment>

<file context>
@@ -0,0 +1,122 @@
+}
+
+function epochSecondsToIso(value: unknown): string | null {
+  const seconds = typeof value === 'number' ? value : Number(value)
+  if (!isRepresentableEpochSeconds(seconds)) return null
+  return new Date(seconds * 1000).toISOString().replace('.000Z', 'Z')
</file context>
Fix with cubic

Comment thread apps/sim/lib/execution/remote-sandbox/index.ts Outdated
…7284)

* improvement(integrations): refresh Slack integration page SEO copy

* fix(integrations): correct Slack trigger name and tool categories in SEO copy
appVersion has been pinned at v0.7.44 since chart 1.2.0 while the app moved
through forty-odd releases. It is the default tag for every first-party image
(app, realtime, migrations, pii, copilot), so a helm install that does not pin
image.tag deploys an application far behind the chart shipping with it — and
any values key added by a newer chart is silently inert, because the running
image has no code that reads it.

The release tag is published by the main-branch merge commit that cuts it, so
this lands on main together with v0.8.18.

The kind install test also stops resolving its images through appVersion. That
job installs the default configuration, so it pulled the tag appVersion names —
which, on the very PR that raises appVersion, has not been published yet. The
install would sit in ImagePullBackOff until --wait timed out. Pinning CI to the
published :latest removes the circularity that kept appVersion frozen.
)

A .chart document is author-controlled and rendered with setOption() straight
into the app document, including on the anonymous /f/<token> share route.
confineOptionToCanvas closed the innerHTML/document.write paths but left the
navigation ones open: title.link, title.sublink, and a link on a treemap or
sunburst data item each reach windowOpen, which assigns the URL to
location.href, so a javascript: URL executed on the app origin on one click.

Drop the link keys everywhere in the walk alongside toolbox. A chart has no
reason to navigate its viewer, so they are stripped rather than
scheme-checked, which would still leave an open redirect on an authenticated
origin.
Two ceilings on a Function run's sandbox files were charged per source
rather than per execution.

Mounts: planUserFileMounts assigned a path per element, so one storage
key named by two sources became two mounts. `files` is `user-or-llm`
and deduped nowhere, so a model repeating an id — or naming a file the
code also references with `<block.file.path>` — produced a duplicate
that cost a presign, a second transfer of identical bytes, and a second
charge against both the byte budget and the 20-file mount ceiling,
either of which then refuses a request that fits. Collapse by storage
key, first occurrence wins. The contract already requires a non-empty
key, so there is no keyless case to carry.

Exports: MAX_SANDBOX_OUTPUT_FILES is documented as what one execution
may export "whether declared by path or discovered by harvesting", and
collectExportedFiles already runs the byte ceiling that way. The count
ceiling did not, so a request declaring paths and harvesting a directory
could export 20 of each. Count declared and discovered together, with a
declared path inside the directory dropped from the discovered set so it
is not billed on both sides. With no declared paths — every call
execute-request makes, since it sets outputSandboxDir only when nothing
declares a sandboxPath — the check and its message are unchanged.

The resolver's marker reuse is no longer what keeps a twice-referenced
file to one mount; its comment said otherwise.

Fixture keys in sandbox-mounts.test.ts were identical across files the
tests meant to be distinct; they now differ, which is what those tests
always claimed to set up.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

6 participants