Skip to content

Commit b790a04

Browse files
TheodoreSpeakswaleedlatif1BillLeoutsakosvl346Bill Leoutsakosmzxchandra
authored
chore(api): integrate staging into v2 endpoints (#6498)
* fix(logger): stop a server-side jsdom window from silencing all logging in production (#6339) * fix(logger): stop a server-side jsdom window from silencing all logging in production * fix(logger): widen the stubbed process cast so type-check passes * fix(logger): never let structured serialization throw into the caller (#6331) * fix(logger): never let structured serialization throw into the caller In production the JSON branch merged caller-supplied arguments into the log entry and stringified it with no error handling. A cyclic reference, a BigInt, or a throwing getter in that metadata raised a TypeError out of `logger.info` and friends: the line was lost and the caller's code path aborted. Dev was unaffected — the colorized branch already routes objects through `formatObject`, which catches — so this class of bug is invisible locally and only surfaces in production, where it reads as structured logs disappearing while raw stack traces keep shipping. Build and serialize through `serializeEntry`, which falls back to a cycle/BigInt-tolerant replacer and then to a minimal entry flagged with `serializationError`. * fix(logger): keep hostile child metadata from throwing into the caller * fix(logger): keep a throwing toJSON from escaping the final fallback * fix(logger): keep repeated references out of the circular-reference fallback * fix(scripts): make the sql Date-binding audit precise and crash-proof (#6340) * fix(scripts): make the sql Date-binding audit precise and crash-proof Resolve the drizzle `sql` tag from its import binding, scope Date bindings lexically, tolerate unparseable files, accept the allow annotation above a multi-line template, and scan the root scripts directory. * fix(scripts): honor shadowed bindings and defaulted destructured Dates * fix(scripts): audit drizzle sql tags bound through a dynamic import * chore(scripts): drop the sql Date-binding unit tests and the exports that served them * chore(scripts): drop the script unit tests and the exports that served them * fix(files): render audio and video stored as application/octet-stream (#6341) * fix(files): render audio and video stored as application/octet-stream The file viewer built the blob backing <audio>/<video> from the record's stored content type with a truthiness fallback, so a stored application/octet-stream was passed straight through and the element could not determine the format. Downloading the same file worked because the download path derives its content type from the filename. - Add resolveEffectiveMimeType, which resolves a generic stored type against the filename, and use it for the media blob, the type column, and the type filter (an octet-stream video was also invisible to the Audio/Video/Image filters) - Map .webm to video/webm rather than audio/webm: a <video> element plays an audio-only stream, an <audio> element drops the picture - Preview .bmp, .avif and .ico, which upload accepts but the viewer sent to the download-only path; serve them with their real content type so nosniff does not block them. .tiff and .heic stay unsupported - no browser renders them - Open .jsonl in the text editor, and fill the extension-to-mime gaps for .mmd, .diff, .patch and .fish * fix(files): settle the audio/video container ambiguity at the call site Follow-up to the review pass on this branch. - Revert the global .webm -> video/webm remap. EXTENSION_TO_MIME is shared with non-viewer callers, and a .webm with an empty stored type would have started taking the STT route's video branch (stt/route.ts:211 -> extractAudioFromVideo), which 500s where no ffmpeg binary is on PATH. The ambiguity is now settled in resolveMediaMimeType, which knows which element the caller is rendering - Resolve the public share route's Content-Type from the filename via getContentType, matching the workspace serve route, instead of echoing the client-declared stored type into a public unauthenticated response. Add the audio/video entries contentTypeMap was missing so a shared media file keeps a real Content-Type (disposition is unchanged - none are inline-safe) - Make resolveEffectiveMimeType total (string, not string | null); the null contract only bought one label edge case and cost a ?? at every call site, one of which was dead - Drop .jsonl from the text-editable set. The editor loads the whole file and only CSV has a byte cap, so a large .jsonl would trade a download-only fallback for a crashed tab. Needs the size guard generalized first - Trim two comments that restated their code * fix(files): resolve dual audio/video containers to the kind the app presents The viewer routes .webm to the video player, but the Type column and the audio/video filters resolved it through EXTENSION_TO_MIME and read audio/webm, so one file showed as Audio and opened in a <video>. resolveEffectiveMimeType now consults a DUAL_CONTAINER_MIME map first. It stays out of EXTENSION_TO_MIME because the speech-to-text and ElevenLabs routes read that table directly, where a video/* label pushes a .webm into ffmpeg audio extraction it does not need. * fix(files): keep the dual-container video default out of the persisted type resolveFileType writes user_file.content_type, and it delegated to resolveEffectiveMimeType, so DUAL_CONTAINER_MIME could persist video/webm. The speech-to-text route reads that back as file.type, which sends the upload into the ffmpeg extraction path the previous commit set out to avoid. resolveFileType now resolves through EXTENSION_TO_MIME alone; the video default stays on the presentation path. Both share an identifiesFormat predicate. * fix(deployment): prevent trigger registry initialization crash (#6342) * fix(deployment): initialize block registry before triggers * fix(triggers): break the triggers <-> blocks initialization cycle Replaces the import-order guard from the previous commit with the structural fix. Block configs spread `getTrigger('...').subBlocks` while their module body runs, so `blocks/*` depends on `triggers/*` by design. Thirteen edges closed the loop back the other way, which made module evaluation order load-bearing: enter the graph through `@/triggers` and a block config calls `getTrigger()` before `TRIGGER_REGISTRY` is initialized, throwing ReferenceError: Cannot access 'TRIGGER_REGISTRY' before initialization Eleven deployment routes crashed on import: `POST /api/workflows/[id]/deploy`, the v1 public and admin deploy/rollback/activate routes, both deployment-version routes, and the three custom-tool deployment routes. All of them funnel through `lib/webhooks/deploy.ts`, which stayed safe only because it imported a value from `@/blocks` — biome sorts that above `@/triggers`, so the safe barrel always evaluated first. #6272 deleted that import as unused cleanup and took the whole surface with it. The reverse edges came from two places, both layering violations rather than anything inherent to triggers: - `triggers/index.ts` imported the mock-payload generator from `trigger-utils`, which imports `@/blocks` for unrelated helpers. The generator is pure, so it moves to `lib/workflows/triggers/mock-payload.ts` and both callers import it there. - Eleven trigger modules statically imported the editor's Zustand stores to read sub-block values inside `fetchOptions`/`fetchOptionById`. Those reads now go through `triggers/editor-state.ts`, which loads the stores with a dynamic `import()` — resolved when the resolver is called, not during module evaluation, so it carries no initialization-order obligation. Side effect: `@/triggers` drops from 744 statically reachable modules to 526. The block registry, the workflow Zustand stores and their React Query graph are no longer pulled into every server module that imports a trigger. `scripts/check-trigger-block-cycle.ts` fails the build if a static edge returns, and reports the shortest offending chain. The existing suite could not have caught this — `deploy.test.ts` mocks both `@/blocks/registry` and `@/triggers`, and `vitest.setup.ts` mocks `@/blocks/registry` globally, so it passed 18/18 against the broken code. --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed Latif <walif6@gmail.com> * fix(chat): stop chats storing a resource they can never send with (#6344) * fix(chat): stop chats storing a resource they can never send with A chat resource persisted with a blank id made every later message fail: the write contract accepted `id: ''` while the send schema required `min(1)`, so the request 400d before a stream existed and the client's reconnect 404d. The tab could not be removed either, since the delete route requires a non-empty id. Twelve production chats were in this state. The id came from an agent-written file chip that carried only a filename: the client filled the missing id with `''` when the file was absent from its list, which it always is for a file the agent just created. - model the unresolved state (`WorkspaceResourceRef`) instead of faking an id, and resolve chip refs at one choke point that may refuse - close the stale-cache race by fetching the file list before giving up, so clicking a just-created file opens it instead of doing nothing - reject blank ids at the stream, write and send boundaries, and drop them wherever stored resources are read, which self-heals affected chats - collapse the 5-6 duplicate POSTs every resource add was firing - log rejected chat bodies, which previously left no trace at all * fix(chat): require a file chip's reference to resolve before opening it A rendered link collapses a resource's id and path into one href, so the click handler cannot tell them apart. Classifying on a separator got a bare filename in `path` wrong, and the resolver then trusted it as an id — opening and persisting a tab pointing at nothing. Drop the classifier and let the resolver try each candidate as an id, a VFS path and a unique name. A file ref must now match a record the workspace actually has; the stale-list case is covered by the refetch, so an id that never resolves was never an id. * fix(chat): tell the user when a resource chip resolves to nothing The chip renders as a button with a hover state, so refusing to open it silently reads as a broken control. Say what happened instead. * fix(chat): do not report an unreachable workspace as a missing file A failed refetch and a successful one that found nothing were both collapsed to an empty list, so a network blip told the user the file does not exist. Keep the two apart and say which happened. * feat(embeddings): multi-provider Embeddings block on a shared core (#6317) * feat(embeddings): multi-provider Embeddings block on a shared core The Embeddings block was OpenAI-only with a bare fetch: no batching, no retry, no metering, and no hosted-key support. Meanwhile the knowledge-base indexing path already had a real multi-provider engine. Nothing bridged the two, so the block could not reach Gemini and the KB engine could not be reached from a workflow. Extract the shared core into lib/embeddings/ first, then build breadth on top of it, so both the KB path and the block resolve models and providers from one catalog and one set of adapters instead of a third parallel implementation. - lib/embeddings/: catalog, client, key resolution, batching, L2 normalization, and adapters for OpenAI, Azure OpenAI, Gemini, Cohere, and Mistral - lib/knowledge/embeddings.ts becomes a thin KB wrapper with its exported signatures unchanged; the 1536-dimension vector invariant does not move - one tool per provider from a shared factory, behind a single /api/tools/embeddings route and contract - new `embeddings` block type; the `openai` block is left functionally untouched and only leaves the discovery surfaces via hideFromToolbar plus sunset.replacedBy, so placed instances keep working unmigrated - openai_embeddings is now an alias of embeddings_openai, so legacy instances pick up batching, retry, and metering with no visible change * fix(embeddings): report an unsupported dimension as a client error The route validated the model and the provider match up front but left `dimensions` to be checked inside embed(), where resolveDimensions throws and the generic catch maps it to 502. A typo in the block's dimension field, or a reference expression resolving to an out-of-range value, was reported as an upstream gateway failure rather than bad input. Resolve dimensions in the route alongside the other boundary checks and return 400. The throw stays the single source of the message, so the two call sites cannot drift. Adds route tests covering auth, the response shape, each boundary rejection, input normalization, and the 502 path for genuine provider failures. * fix(embeddings): only send a dimension when the caller asked to reduce resolveDimensions() returns the model's native size when no reduction is requested, and that resolved value was handed straight to the adapter. The adapters guard on `dimensions !== undefined`, so the field was always populated and always sent. Models that support Matryoshka reduction accept their own native size, so this was invisible for text-embedding-3-*, gemini-embedding-001, embed-v4.0, and codestral-embed. Models that do not support the parameter at all reject it outright: every unreduced request to text-embedding-ada-002 and mistral-embed failed with a 400, which is both of the models whose catalog entry has no supportedDimensions. Track the caller's explicit reduction separately from the resolved dimensionality. The resolved value still drives reporting and billing; only the requested one reaches the wire. Found by driving the live provider matrix against all four providers. * test(knowledge): de-flake the sync-engine suite Every test dynamically imported the module under test, so the first one to run paid the whole cold-load cost inside its own 10s timeout and failed intermittently under load. The dynamic imports were working around a hoisting problem: mockMapTags is a top-level const read by a vi.mock factory, and vi.mock is hoisted above it, so a static import of the module under test crashes with a use-before-initialization error. Declaring the mock through vi.hoisted() removes that constraint, which is the pattern the testing guidelines already call for. One static import replaces 42 dynamic ones. The file drops from ~15s to ~2s and passed 5 consecutive runs. * fix(embeddings): drop a capability the selected model no longer offers The per-model Dimensions and Task Type dropdowns each share one subblock id, and nothing clears a stored subblock value when its dependsOn fields change — dependsOn only feeds rendering. A choice made for one model therefore outlives a switch to another. Picking 3072 on text-embedding-3-large and switching to -3-small left 3072 stored while the dropdown offered at most 1536, and the block forwarded it. Same for a task type: 'similarity' chosen on Gemini survived a switch to Cohere, which has no equivalent input type. The guards only checked that the model declared the capability at all, not that the value was one it lists. Check membership so a stale value falls back to the model's native size, or is omitted, instead of being sent and rejected. The user cannot have deliberately chosen an option the dropdown stopped presenting. * feat(embeddings): use the latent-constellation mark for the block icon Replaces the scatter-plot-on-axes placeholder with a centre node, four neighbours, and the rays between them — a point and its nearest neighbours in embedding space, which is what the block actually produces. The axes mark read as a generic chart and said nothing specific to embeddings. Nodes are filled so they hold their shape at small sizes. The rays carry less weight than the nodes to keep the hierarchy, but at 1.6/0.9 rather than the 1.4/0.75 they were drawn at, so they do not thin out to loose dots in the 14px block-search row. Kept byte-identical between the app and docs icon sets. * fix(embeddings): declare the outputs the legacy openai block returns openai_embeddings became an alias of embeddings_openai, so the legacy block's runtime payload gained `provider` and `dimensions`. Its declared outputs still listed only embeddings/model/usage, so the tag picker never offered two fields every run demonstrably returns, and downstream blocks could not reference them. Declaring them is additive and does not touch execution. Asserts the legacy block's output keys match the replacement's, since both run the same tool and neither should expose fields the other lacks. * fix(copilot): resolve same-id subblock variants before validating A block may declare one field id several times, each variant conditioned on another field — the embeddings block declares model, dimensions, and taskType once per provider, and the image and video generators do the same. Validation keyed a map by id alone, so whichever variant was declared last silently became the validator for every write to that field. Programmatic edits to an embeddings block were therefore checked against Mistral's option lists whatever the saved provider: `text-embedding-3-small` was rejected as not one of mistral-embed/codestral-embed, and dimensions valid only elsewhere (3072, 768) could not be set at all. Values that happened to overlap the last variant passed, so automation saw partial success rather than a clean failure. Keep every candidate per id and pick the one whose condition holds, evaluating against the mutation's inputs merged over the block's saved values so a partial write still resolves. When no condition matches, fall back to the union of all variants' options rather than guessing. Conditions still never gate whether a field may be written — that was a deliberate choice and a hidden field stays writable. They only select which definition describes the field, and an unresolved condition widens the accepted set instead of narrowing it. * fix(copilot): prefer a conditioned variant over an unconditioned catch-all An unconditioned same-id variant matches every set of values, so it would shadow a genuinely selected variant purely by being declared first. Prefer a variant that actually asserted something about the current values. No block in the registry currently declares a catch-all ahead of a conditioned variant on a field where it would change validation, so this is a guard against the pattern rather than a fix for a live case. * chore(embeddings): scope this branch to the multi-provider block Two changes made while building the Embeddings block are not part of it and ship separately, so their files are restored to staging here: - copilot edit-workflow validation resolving same-id conditional subblock variants. The embeddings block surfaced it, but it is a platform fix affecting ~20 blocks that declare a field id more than once, and it narrows what programmatic edits accept — that deserves its own review. - the sync-engine test de-flake, which is unrelated test hygiene. Both are preserved in full on feat/embeddings-full-snapshot. Note this restores the reported bug where a programmatic edit to an embeddings block validates model/dimensions against the last-declared provider variant. The block is unaffected in the editor and at runtime. * fix(embeddings): honor per-model token limits and bound the JSON input path Review round 1. Batching used one 8,000-token constant for every model, inherited from the knowledge-base engine this branch extracted. `batchByTokenLimit` truncates any single text above the limit it is given, so that constant both sent oversized input to models with a lower ceiling and silently dropped content models with a higher one accept: - Gemini declares 2,048, so a 3,000-token text passed through whole and the provider rejected it, surfacing as a 502. This also affected knowledge-base indexing on staging, which uses the same constant. - Cohere declares 128,000, so anything past 8,000 was truncated for no reason. Batch against the selected model's own `maxInputTokens` instead. Using the per-input ceiling as the per-batch budget also keeps every individual text within it. The contract bounds the array arm of `input`, but a JSON-encoded array arrives as a plain string and `normalizeInput` only expands it after validation — so neither the 1,000-input cap nor the non-empty checks applied to the reference-expression path the route was written to accept. `"[]"` also reported success with no vectors. Re-check the normalized list so the bounds hold for both shapes. * chore(embeddings): regenerate tool metadata for the new embedding tools CI's tool-metadata:check gate failed: registering embeddings_openai, embeddings_gemini, embeddings_cohere, and embeddings_mistral left the generated tool-ids/metadata/outputs artifacts stale. * fix(embeddings): project before batching, and keep the sunset block's docs icon Review round 2. Projection ran inside callEmbeddingAPI, after batchByTokenLimit had already measured and truncated the original text. The projector rewrites resolved secrets to placeholders, which changes length, so batching sized against a string that was never sent: a lengthening projection then pushed input past the model's ceiling and the provider rejected it, and a shortening one discarded document content that would have fit. Project once up front, then batch the projected text, so truncation measures what actually goes to the provider. This also keeps projection to exactly one call per embed(), so no retry can re-project. Separately, marking the legacy openai block hideFromToolbar dropped it from the generated docs icon map, which only retains hidden blocks when they are versioned. integrations/openai.mdx is deliberately kept — docsLink is baked into every placed instance — so BlockInfoCard lost its icon and fell back to a text tile. A sunset block keeps its docs page for the same reason a hidden versioned block does, so the generator now treats it the same way. The sim-side integrations map still omits it, which is intended: that feeds the discovery page a sunset block should not appear on, and placed blocks render from the registry's own icon reference. * fix(embeddings): override stale block params instead of omitting them Review round 3. The generic handler merges the params() result over the original inputs (`{ ...inputs, ...transformedParams }`), so omitting a key leaves the stale value in place. The previous round dropped an unsupported taskType or dimensions by omission, which was therefore a no-op through the executor path: a reduction or task type chosen for one model still reached the tool after a model switch. Rewrite each stale field to an explicit `undefined`, which does override in a spread. Same class of bug for `model` itself, which was forwarded whenever present without checking it belongs to the selected provider. Every provider's model dropdown shares the `model` id, so switching provider kept the previous provider's model and failed at the route as a mismatch. It now falls back to the provider's default unless the saved model actually belongs to it. Tests assert the merged result rather than the returned object, since the return shape alone cannot distinguish an omitted key from an overridden one — which is exactly why the previous fix looked correct and was not. * fix(embeddings): discount the batch ceiling when the tokenizer is foreign Review round 4. Batching measures with tiktoken, which only has encodings for OpenAI models — every other id falls back to cl100k_base. Gemini's 2048, Cohere's 128k, and Mistral's 8192 were therefore enforced in OpenAI token units, so an input near one of those ceilings could still be rejected upstream or trimmed more than needed. A true fix needs per-provider tokenizers, which the repo does not have: estimateTokenCount is a chars-per-token heuristic, and truncation needs a real encode/decode pair to slice on a token boundary. So the ceiling is discounted for foreign tokenizers rather than trusted exactly. The discount is one-sided on purpose. Overshooting means the provider rejects the whole request; undershooting only trims a text that was already at the limit, so the margin errs toward the second. resolveBatchTokenCeiling is a pure function tested directly, rather than inferred from truncation behavior, so the guarantee holds per model as the catalog grows. * fix(embeddings): keep the batch ceiling exact and warn before truncating Review round 5. Reverts the safety margin from round 4. The two review findings were in direct tension: round 4 flagged that a foreign model's ceiling is measured in tiktoken units, and the margin added to absorb that error reintroduced the round 3 harm — valid content truncated below the provider's declared limit. The margin was the wrong trade. It swapped a loud failure for a silent one: an undercount surfaces as a provider rejection the caller can see and act on, while shortening an embedding's input produces a degraded vector that is indistinguishable from a good one at every layer above it. Silent quality loss in a retrieval index is the worse outcome, and it is also the harder one to ever notice. So the declared ceiling is applied exactly, and truncation is no longer silent: an input above the limit now logs a warning naming the model, the limit, and whether the count was approximate. hasApproximateTokenCount records which models are counted with a foreign tokenizer without being used to shrink anything. The tokenizer imprecision itself remains, and cannot be fixed without per-provider BPE the repo does not have — estimateTokenCount is a chars-per-token heuristic, and truncation needs a real encode/decode pair to slice on a token boundary. * refactor(embeddings): drop dead surface and enforce OpenAI's item cap Audit follow-ups on the multi-provider embeddings work: - Enforce OpenAI's documented 2048-entry `input` array cap in the OpenAI and Azure adapters. Nothing bounded item count on the OpenAI path — batching bounds tokens per request, so a batch of many short inputs could exceed it. - Make the provider item cap single-source. It was declared both on the catalog entry and on the adapter, read through a `??`; the adapter is the wire-protocol owner, so the catalog copy is gone. - Have the knowledge-base view call `getKbEligibleModels()` instead of re-deriving the same `kbEligible` filter inline. - Remove dead surface: the unused `EMBEDDING_TASK_TYPES` constant, `EmbeddingToolDefinition`, `HOSTED_KEY_PROVIDERS`, and the five request-body fields (`workspaceId`, `workflowId`, `executionId`, `userId`, `useHostedCostTracking`) the route never reads. - Trim `@/lib/embeddings` to what callers outside the module use. - Drop the route's manual request-id plumbing; `withRouteHandler` supplies it. - Fix two comments that had drifted onto the wrong declaration. * fix(embeddings): normalize reduced Cohere output; correct OpenAI token ceiling Second validation pass against provider documentation. - Cohere: normalize locally when `output_dimension` reduces below native. Cohere documents the parameter as Matryoshka truncation but never states that it renormalizes, and an unnormalized vector silently skews cosine similarity. `l2Normalize` is idempotent, so this is a no-op if Cohere already returns unit vectors and a correctness fix if it does not. Covered by a test that fails without it. - OpenAI: raise the per-input ceiling from 8191 to the 8192 the API reference documents, so a maximal input is no longer truncated by one token. - Share the OpenAI response type with the Azure adapter instead of declaring an identical copy, mirroring how the mail providers share `_nodemailer`. - Rewrite the Gemini item-cap comment to say the 100-item limit is observed rather than documented, which is what Google's reference actually supports. Docs: add a manual intro to the Embeddings page covering providers, models, inputs, outputs, and comparability rules. The generated Input tables are empty because `createEmbeddingTool` builds params programmatically and the docs generator only reads literals, so the manual section carries that reference. * fix(embeddings): split per-input and per-request token limits; close provider gaps Four gaps found in the validation pass. Gemini token counts were estimated, not measured. `BatchEmbedContentsResponse` carries `usageMetadata.promptTokenCount`; without reading it the client fell back to tiktoken, which has no Gemini encoding and silently used `cl100k_base` — the wrong tokenizer on a count knowledge-base runs bill against. `maxInputTokens` was doing two jobs: the per-input ceiling that decides truncation, and the per-request budget that decides how many inputs share a batch. These are different provider limits, and conflating them meant Cohere packed batches against its 128k per-document ceiling while OpenAI's documented 300,000-token request cap went unenforced. They are now separate fields. Truncation moves out of `batchByTokenLimit` and into `embed`, so it happens once, against the per-input ceiling, and always logs. The request budget is floored at that ceiling — a budget below it would truncate inputs the provider accepts. Batch sizes are unchanged everywhere except Gemini, which rises from 2048 to the 8192 the other providers already used. codestral-embed now offers its documented 3072 maximum. Its API default is 1536, so the offered sizes straddle the default; the catalog invariant relaxes from "native size first" to "native size present", which is what the block relies on. The Mistral API-key field no longer differs from the other three. Sim stocks `MISTRAL_API_KEY` — `mistral_parse` already hides its key field on hosted — so one field with `hideWhenHosted` replaces the conditional pair. Docs: correct the API-key row, which described the old Mistral-only behavior. * refactor(embeddings): derive block options from the catalog; use shared helpers Findings from a four-angle quality review. Reuse: `splitByItemLimit` and `processWithConcurrency` were reimplementations of `chunkArray` (`@sim/utils`) and `mapWithConcurrency` (`@/lib/core/utils/concurrency`), so `lib/embeddings/batching.ts` is gone. That helper's doc forbade a throwing mapper; embedding legitimately wants a failed batch to fail the call, since a partial vector set is not a usable result, so the contract is reworded to cover both intents rather than forked. The block no longer hand-copies the catalog. Its model, task-type, and dimension dropdowns are derived from `EMBEDDING_MODELS`, which deletes roughly 150 lines of literals that had to be kept in step by a drift test. The comment claiming this was impossible was wrong: `generate-docs.ts` only reads `subBlocks` looking for an `id: 'operation'` entry, which this block does not have. Verified by regenerating — `embeddings.mdx` and `integrations.json` come out byte-identical. Single-sourced two maps that were stated twice: BYOK provider ids (which encode the non-obvious gemini -> google mapping) and the per-provider default model. The route previously took its default from `getModelsForProvider(provider)[0]`, which silently depended on catalog key order. Azure's `endpoint` and `apiVersion` are required on their own context type instead of optional on the shared one, so the adapter can no longer be built without them and emit an `undefined/...` URL. Also: contract enums now `satisfies` the catalog unions so they cannot drift, the barrel exports only what callers outside the module use, the redundant `requestedDimensions` field is a parameter, the bare `getEmbeddingModelInfo()` call is a named `assertKbEmbeddingModel`, and the route checks payload size before scanning entries rather than copying the body first. * docs(embeddings): correct comments that drifted from the code A comment pass over the feature found four that no longer matched what they sat on, all introduced by earlier rounds of this work. The contract's `satisfies` note promised that adding a catalog provider could not leave the wire enum stale. It cannot deliver that: `satisfies` proves every listed member is valid, not that the list is exhaustive, so an addition stays silently absent. Reworded to say what it does and does not catch. The client cited Gemini as a provider that omits usage, which the Gemini adapter now contradicts — it reads `usageMetadata.promptTokenCount`. Every adapter defines `parseTokens`, so the fallback is about a response lacking a usage block, not about a particular provider. `l2Normalize` documented only Gemini, though Cohere now calls it for a different and stronger reason, and "normalizes in place" read as mutation when the function returns a copy. The route's new size-guard comment claimed it avoids copying the payload; nothing there copies. The real reason is that summing lengths gates before the per-entry character scan. Also: split the derived-sub-block TSDoc so both constants carry hover text, gave the payload cap its own doc, dropped one comment that restated a signature, and tightened two long blocks without losing a fact. * fix(docs): generate tool inputs for factory-built tools The four embeddings tools rendered header-only Input tables. `extractToolInfo` finds a tool's `params` by regex over the tool's own file, and these files hold nothing but a `createEmbeddingTool({...})` call — the params live in the factory's module. There was already a fallback for a same-file `...spread` base, so this adds the cross-module equivalent: follow the factory's import and read `params` from there. Two things surfaced once the tables populated. `hosting` was not in the set of keys that terminate the `params` capture, so the non-greedy match ran past it to `request:` and swallowed the whole hosting block. Every tool with a `hosting:` section between `params:` and `request:` was publishing `pricing` and `rateLimit` as if they were user-facing inputs — this drops those rows from eight unrelated integration pages as well. The shared apiKey description was a template literal, which the regex emitted verbatim as `${name} API key`. It is now a static string, matching how every other tool in the repo declares one. Docs: the Embeddings page keeps a prose intro in its MANUAL-CONTENT block like other integrations, with the hand-written input/output tables removed now that the generated ones are correct. The sunset `openai` page loses its `encodingFormat` row — page generation skips hidden blocks, so that page is frozen and would otherwise keep advertising a parameter the aliased tool no longer accepts. --------- Co-authored-by: Waleed Latif <walif6@gmail.com> * fix(tables): resolve active selector before schema enrichment (#6345) * fix(search): restore cmd+k autofocus on the search input (#6347) * feat(files): let the agent read HEIC photos (#6346) * feat(files): let the agent read HEIC photos iPhone photos reach the model as HEIC, which no vision model accepts - the Claude Messages API takes JPEG, PNG, GIF and WebP only - so the agent saw nothing. 75 HEIC files are already in production, 64 of them in one workspace uploaded over the last two days. sharp cannot cover this: its prebuilt libvips ships libheif with AV1 but not HEVC (sharp.format.heif.input.fileSuffix is ['.avif']), so a real iPhone photo fails with 'Security limit exceeded'. Verified against both a HEVC-coded sample (sharp fails, heic-convert decodes 2.99MB to a 3992x2992 JPEG in ~950ms) and an AV1-coded mif1 sample (sharp decodes it natively). Decoder selection is capability-based, not brand-based: sharp is always tried first and the WebAssembly decoder runs only on bytes it could not read. The container brand cannot identify the codec anyway - mif1 carries either - so choosing from it would push AV1 files down the slow path. This mirrors how PhotoPrism layers libvips over libheif. Also route the image path on the effective MIME type, since a phone upload commonly stores as application/octet-stream and would otherwise be read as a binary the model never sees, and stop reporting an undecodable image as 'too large'. * refactor(files): gate every vision passthrough on model-supported media types Review found two passthroughs that still handed the model bytes it cannot decode. The sharp-load-failure branch returned raw HEIF, and the already-small-enough branch returned raw AVIF, TIFF, BMP or ICO — all of which isImageFileType accepts and no vision model does. Gating all three on the existing MODEL_SUPPORTED_IMAGE_MIME_TYPES subsumes the ad-hoc isHeifContainer re-sniff, and re-encoding an unsupported format falls out of the resize ladder that was already there. Also drop two constants that were pure indirection (a one-use alias for 'image/jpeg', and a quality value identical to heic-convert's default), trim the oversized comments, log successful transcodes so the ratio is visible in prod, and replace a detection test that could not fail. * fix(files): read HEIF compatible brands, not just the major brand A standards-valid HEIF may carry a generic major brand such as isom and declare heic, heix or mif1 only among the compatible brands that follow the minor_version at offset 12. Reading bytes 8-11 alone classified those as non-HEIF, skipping the fallback decode and leaving a small undecodable file to reach the model as raw bytes. * fix(files): bound the HEIF fallback decode input (#6348) Uploads allow 100MB and prepareImageForVision runs sharp with limitInputPixels: false, so nothing upstream capped what could reach the single-threaded WebAssembly decoder. A tenant-controlled file could therefore spend unbounded CPU and memory on one read. Cap the transcode input at 20MB — generous headroom over any phone photo, which runs 1-4MB. Pixel-dimension bombs stay bounded by libheif's own security limits during parse. * fix(utils): drop the .js specifiers Turbopack cannot resolve (#6351) * fix(utils): drop the .js specifiers Turbopack cannot resolve Every dev server on staging is currently returning 500 from any route whose module graph reaches the `@sim/utils` barrel: Module not found: Can't resolve './errors.js' > 1 | export { getErrorMessage, getPostgresErrorCode, toError } from './errors.js' Import trace: ./packages/utils/src/index.ts ./apps/sim/lib/embeddings/client.ts ./apps/sim/lib/knowledge/embeddings.ts ./apps/sim/app/api/knowledge/route.ts `packages/utils/src/index.ts` addresses its siblings as `./errors.js` while the files are `./errors.ts`. webpack rewrites that through `resolve.extensionAlias`; Turbopack has no equivalent (vercel/next.js#82945). `next build` is webpack and `next dev` is Turbopack, so this passes CI and breaks every local dev server — #6317 went green. Nothing required the extensions: the repo is on `moduleResolution: "bundler"`, and no other package barrel uses them. Two changes, either of which fixes the symptom; both are here because they fail differently: - `packages/utils/src/index.ts` drops all 12 `.js` specifiers. Fixes the barrel for every current and future consumer. - `apps/sim/lib/embeddings/client.ts` imports `chunkArray` from `@sim/utils/helpers` rather than the barrel. #6317 added the only bare-barrel `@sim/utils` import in the monorepo; the subpath form is the documented convention (CLAUDE.md, "Common Utilities") and resolves to one module instead of pulling twelve. `scripts/check-import-specifiers.ts` fails the build on either shape and runs in CI. Verified it goes red by restoring both halves of the bug. It scans only bundler-compiled source — vitest and standalone `bun run` scripts resolve `.js` -> `.ts` themselves, so flagging their specifiers would be noise. Verified against a real dev server with production env: `/api/knowledge`, `/api/tools/embeddings` and `/api/workflows/[id]/deploy` all go 500 -> 401, `/workspace` renders, and the Turbopack log is free of resolution errors. `tsc --noEmit` clean, `packages/utils` 147/147. * refactor(scripts): resolve specifiers instead of pattern-matching one mistake The first version banned `.js` specifiers by regex, which catches the bug that happened and nothing adjacent to it. This runs the actual resolution algorithm with Turbopack's rules — extensionAlias deliberately absent — and fails on anything that does not land on a real file. That covers the whole "Module not found" class rather than one shape of it: `.js` specifiers, typo'd paths, files moved or deleted with a stale importer left behind, `@/` aliases pointing nowhere, and `@sim/*` subpaths a package does not export. Verified against three synthetic breakages the regex version passed clean: '@/lib/webhooks/providerz' — '@/' alias matches a tsconfig path but nothing is there './does-not-exist' — no file at that path '@sim/utils/chunking' — @sim/utils does not export './chunking' Getting to zero false positives on 37,307 specifiers needed three things the naive version got wrong: - tsconfig `paths` are per-workspace. `@/*` is `apps/sim/*` inside apps/sim but `apps/realtime/src/*` inside apps/realtime, and apps/sim maps `@sim/db/*` straight at the package directory, legitimately bypassing that package's exports map. One hardcoded alias produced ~30 false positives in apps/realtime alone. - `exports` maps have wildcards. `@sim/emcn` publishes `"./*": "./src/*"`, so `@sim/emcn/components/code/code.css` is valid despite no literal entry. - TSDoc contains example imports. `packages/db/triggers.ts` documents `import { ensureRowCountTriggers } from '@sim/db/triggers'` — a subpath the package deliberately does not export. Comments are now blanked in place, preserving byte offsets so reported line numbers stay exact. * fix(scripts): close three coverage gaps in the specifier audit Review round 1 on #6351. All three findings were real and all three let the exact regression this guard exists for slip through. - Reported line numbers were one early. `SPECIFIER_RE` opens with `(?:^|\n)`, so `m.index` is the newline ENDING the previous line, not the start of the statement. `./helpers.js` on line 13 was reported as line 12. Anchoring to the specifier's own offset is exact, and for a multi-line import it points at the `from '...'` line — where the reader needs to look anyway. - `require()` was not scanned. This repo uses lazy requires deliberately to break import cycles: `tools/params.ts` reaches `@/blocks` that way and `blocks/blocks/agent.ts` reaches `@/blocks/registry`, 22 first-party call sites in total. Those edges resolve exactly like static ones, so a bad specifier in one fails identically. Verified by pointing `tools/params.ts` at a non-existent module and watching the audit catch it. - `apps/docs` was not scanned, despite being a second Next.js app with its own `next.config.ts` — so it carries identical Turbopack exposure. Now covered, and clean. Side-effect imports and dynamic `import()` were called out in the same round but are already covered: the optional `from` group in `SPECIFIER_RE` matches bare `import '...'`, and `DYNAMIC_RE` handles `import('...')`. That review ran against 1c6073e0, before the resolver rewrite. Coverage goes from 37,307 specifiers across 11,182 files to 37,438 across 11,243, still with zero violations. * chore(tools): regenerate the stale tool metadata `bun run tool-metadata:check` has been failing on staging since #6317, so every PR branched off it inherits a red CI regardless of its own contents. Reproduced against a clean `origin/staging` to confirm it is not this branch's doing. #6317 rewrote the embeddings tools' `apiKey` descriptions from provider-specific strings to one generic string in `tools/embeddings/factory.ts`, but did not regenerate `tools/generated/tool-metadata.ts`. The whole delta is 89 bytes of description text — the tool set is unchanged at 4380 ids, none added, none removed: - "description":"Cohere Embeddings API key" + "description":"API key for the selected embedding provider" The old strings no longer exist anywhere in source, so the generated file was the stale side. `tool-metadata:check` passes after regenerating, and the generator's own resolver cross-check agrees. `mship:check` and `mship-tools:check` also fail locally, but neither is a CI gate and both fail only because they read contracts from the sibling copilot repo, which is not checked out here. Left alone. * fix(scripts): substitute every wildcard in a resolved target CodeQL js/incomplete-sanitization, two instances, both correct. `String.replace('*', x)` fills only the first occurrence. Node's `exports` resolver uses a global regex, so a target carrying more than one `*` — e.g. `"./src/*/index-*.ts"` — gets every occurrence substituted. Replacing only the first leaves a literal `*` in the path, so `probe()` finds nothing and the audit reports a perfectly valid subpath as missing. TypeScript `paths` allows at most one `*`, so the tsconfig branch was already correct in practice; it changes for consistency and because nothing enforces that assumption. Not a suppression — the resolver now matches Node's behaviour. 37,438 specifiers still resolve clean. * fix(scripts): do not assert on generated output in the specifier audit CI red on a fresh checkout, green locally — the tell that the audit was depending on build state rather than on source. apps/docs/lib/source.ts imports '@/.source/server'. apps/docs maps '@/.source/*' at './.source/*', which fumadocs-mdx generates and apps/docs/.gitignore excludes. It exists on any machine that has built the docs and is absent from CI's checkout, so the audit reported a valid import as unresolvable. A path landing in output the scanner itself refuses to read as source — node_modules, a build directory, any dot-directory — is now treated as unverifiable rather than missing. That is the consistent rule: if we do not scan it as source, we cannot assert on its presence, and asserting anyway makes the verdict depend on build order. Applied to all three resolution paths (relative, tsconfig paths, exports map), with a GENERATED sentinel keeping 'matched but generated' distinct from 'matched and genuinely missing'. Only the repo-relative portion is inspected. Checking the absolute path would match the '.claude/worktrees/...' a git worktree lives under and silently skip every specifier in the repo. Verified both directions: passes with apps/docs/.source moved away (CI's state), and still catches a require('@/blocks/still-not-real') planted in tools/params.ts. * refactor(scripts): trim the specifier audit's comments The audit shipped at 24% comment lines — the header alone retold the whole incident. Cut to 15% (452 -> 401 lines) by collapsing the narrative and keeping only what the code cannot say: the webpack/Turbopack extensionAlias divergence, why '.js' is a probed extension but not a fallback, why paths resolve per-workspace, why targets substitute with replaceAll, why generated output is unverifiable, and the '.claude/' worktree trap in the relative-path check. No behaviour change: 37,437 specifiers still resolve clean. * fix(chat): stop classifying secret-free binary sandbox exports as unknown (#6349) * fix(execution): stop classifying secret-free binary sandbox exports as unknown * fix(execution): fail closed when files are mounted without a provenance envelope The binary classifier read an absent mounted-file scanner as "no mounted secrets". That is absence of evidence, not evidence of absence: the request contract permits _sandboxFiles without the provenance envelope, so a caller that mounts secret-bearing bytes and omits the envelope would have a derived binary persisted as provably secret-free. Not reachable today — the route is internal-JWT-only and its one file-mounting caller always emits the envelope — but the classification rested on an invariant nothing enforced. - the copilot handler emits the envelope on the same condition that produces the mount, so tables ship one too and the two cannot drift apart - a mount with no verified scanner now counts as secret material in scope, so the classification is never stronger than what the caller attested to Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(execution): treat partial and unscannable mount attestations as unknown Two ways the envelope could read as stronger evidence than it was. The copilot handler preserved `_sandboxFiles` that arrived on the params and then exported provenance from `mountedRegistry`, which knows only about the files it resolved itself. The route would have read that partial envelope as a complete attestation over every mounted byte. The envelope now covers the whole mounted set or is not emitted at all, and a mount with no envelope already fails closed. `hasSecrets` was derived from whether entries produced scannable literals, so an envelope listing entries that all failed to decrypt reported false and let a derived binary be marked exact-empty. It now reflects what the envelope attested to: entries that yield no plaintext make the mount less classifiable, not more. Neither was reachable — `_sandboxFiles` is absent from the copilot tool schema, so nothing can populate the preserved-mount branch — but both had the classification resting on a property nothing enforced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(tools): regenerate stale tool metadata `bun run tool-metadata:check` fails on origin/staging as well as here, so this is not from this branch — #6317 landed the artifact generated from a factory that still built a per-provider apiKey description, and the source was later genericized without regenerating. Regenerating changes exactly the five embeddings entries' apiKey description to the text `tools/embeddings/factory.ts:74` actually produces. The per-provider strings appear nowhere in source. Included here only because the gate is red on every branch cut from staging until someone lands it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(execution): count the runtime payload as secret material in scope An execution with no mounted files and no env secret still carries `params` and `contextVariables` into the sandbox — the runtime payload is serialized into a private-input file, so resolved block outputs and workflow variables land as plaintext regardless of `_sandboxFiles`. The scope predicate only looked at mounts and env secrets, so a binary derived from them was classified exact-empty. The route has no catalog for those values and cannot tell a secret-bearing one from an ordinary one, so they count as in scope. Only an execution with nothing at all in scope earns an exact-empty binary. This narrows where the relaxation applies rather than regressing anything: every binary export was unknown before this branch, so a workflow Function block carrying block references keeps exactly the behavior it has today. The mothership path is unaffected — its tool sets no contextVariables, blockData, or workflowVariables, which is the case this branch exists to fix. Values, not keys, for the params check: `executionParams._context` is set to undefined before the context is built, so a key count reads every execution as carrying params. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Revert "fix(execution): count the runtime payload as secret material in scope" This reverts commit 754e37cedb. The classifier's secret catalog is the Secrets feature and nothing else: `outputSecretNamesByScanLiteral` and `outputSecretPlaintextsByName` are built only from `envVars`, and mounted-file entries trace back to the same place. `contextVariables`, `blockData`, and `workflowVariables` are ordinary workflow data — resolved block outputs the user already sees in logs — and the text export path does not scan them either. Treating their mere presence as secret material was a heuristic, not a security property, and it created exactly the asymmetry rejected two rounds earlier: a binary derived from a context variable would be `unknown` while a text export of the same bytes stays exact-empty. Stricter than the text path for the same content is not a boundary. It was also nearly inert. `scopeEnvironmentVariables` returns every workspace secret when scope is `all` (the default), so any workflow Function block with secrets configured already trips the env branch. The only slice it changed was executions with no env vars at all, where the workspace has no secret for a context variable to carry. A Secret resolved into an upstream block's output and arriving here through blockData is a real gap, but it is pre-existing, identical for text exports, and belongs at the executor -> route boundary as a provenance envelope for params — not as a presence check in this classifier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(scripts): make the specifier audit path-separator agnostic (#6355) Review round: isGeneratedPath split the repo-relative path on '/', but path.relative returns backslashes on Windows, so '.source' and 'node_modules' never matched a segment and generated output was treated as source. The repo does support Windows dev — scripts/setup branches on win32. The finding named one site; there were three. isCompiledSource compared against 'apps/sim/scripts/' with the same assumption, and workspaceFor matched `${w.dir}/`, which on Windows never matches an absolute path and would have dropped every file out of its own workspace — silently disabling tsconfig paths resolution rather than erroring. Normalized behind a repoPath() helper, with workspaceFor using path.sep against absolute paths. Reported paths now go through it too, so output is identical on either platform. spec.split('/') is left alone: import specifiers are always '/'-separated regardless of host. Verified by simulating win32 separators through the same predicates, and posix behaviour is unchanged at 37,437 specifiers. * improvement(sandbox): exempt caller-consumed streams from the output retention budget (#6353) * fix(sandbox): exempt caller-consumed streams from the output retention budget A Pi agent turn emits one JSONL event per step and passes the 10 MB process output budget on an ordinary session, killing the run. The bytes were never a result: `handleChunk` parses every chunk as it arrives and keeps none of it, and the accumulated copy is only ever read back to build an error message. The budget bounds what Sim RETAINS, so a stream the caller consumes itself is exempt and only a 64 KB diagnostic tail is kept. The limit is unchanged for everything else. Gated per stream, not per command: a caller that streams stdout but not stderr still has stderr fully bounded. Both adapters gate on the handler's presence, so the calls that parse markers out of stdout (Pi's clone/prepare/push, which do not stream) keep full retention and full budgeting — the case daytona.ts already warns about. E2B's SDK still accumulates internally, so this bounds what Sim retains rather than the provider's peak; Daytona accumulates locally and is bounded outright. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(sandbox): drop explicit any from the new conformance stream mocks The two new E2B mocks annotated their arguments as `any`, which both violates the repo's no-`any` rule and defeats the point of a mock: an invalid SDK shape would type-check. Matches the sibling mock a few lines above (`async (_code, options) =>`) and infers from the `vi.fn()` signature instead of naming a type, so the mock stays bound to whatever the adapter actually calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sandbox): cut Daytona's retained tail to the same bound as E2B `appendStreamedSandboxOutput` deliberately lets the accumulator grow to twice the tail before collapsing, so a single re-cut is amortized across chunks rather than paid on every one. That leaves it anywhere inside that band when the stream ends. E2B tails the value it returns, Daytona returned the accumulator as-is, so a stream finishing between one and two tails came back roughly 96 KB on Daytona and 64 KB on E2B. The two adapters must agree — a divergence here surfaces as changed behavior during a failover, which is the one moment nobody wants surprises. Daytona now takes the same final cut on every return path. The conformance test that should have caught this asserted the bound as `tail * 2`, which is satisfied by both the correct and the incorrect value. It now asserts the tail plus the truncation note, and a second case exercises the band between one and two tails where the two providers could disagree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf(typecheck): run the native TypeScript 7 compiler (#6356) A bare `tsc` was silently resolving to the JavaScript TypeScript 6 compiler. `apps/sim` depends on `@typescript/typescript6` for its runtime TypeScript AST API, which pulls in `@typescript/old` (an alias of `typescript@6`) declaring its own `tsc` bin. Package managers pick bin winners by lexical sort rather than dependency depth, so `@typescript/old` beat `typescript` and won `node_modules/.bin/tsc`. Identical diagnostics, ~10x slower, and it fails silently: the check still passes, it just burns minutes. Both compilers check an identical 11,066-source- file program with byte-identical diagnostics; the only `--listFiles` delta is lib relocation plus TS7 deduping nested .d.ts copies. The `@typescript/native` alias sorts ahead of `@typescript/old` and reclaims the bin. This is the TypeScript team's own recommendation on typescript-go#4567 -- the original blog example was wrong. Every `type-check` script is unchanged; `bunx tsc` and ad-hoc invocations are fixed too. apps/sim cold 83s -> 8.5s; all 23 workspaces 96s -> 9.4s. The alias is invisible-load-bearing: nothing imports it, so removing it looks like dead-dependency cleanup and costs 10x with no visible failure. check:native-typecheck asserts a bare `tsc` reports 7.x and fails CI otherwise. Also drops NODE_OPTIONS=--max-old-space-size=8192 from apps/sim's type-check -- it only ever mattered for the JS compiler's V8 heap. * feat(files): preview HEIC photos in the file viewer (#6350) * feat(files): preview HEIC photos in the file viewer The agent can read HEIC since #6346, but the Files page still showed 'Preview not available' — an <img> pointed at the serve route got the stored HEIF under nosniff, which no browser outside Safari renders. The serve route now resolves a JPEG derivative for HEIF bytes, cached in the artifact store and keyed by the source's storage key. Workspace keys are regenerated on every content replacement, so the key is already a content version and using it avoids streaming the original just to hash it. Caching matters here in a way it did not for the vision path: a preview is re-fetched on every view and the WASM decode costs roughly a second for a phone photo. The original stays the stored object — downloads and raw=1 serve it untouched, so this never changes what a user gets back. compileDocumentIfNeeded becomes resolveServableBytes, since it now resolves images as well as generated documents. .tif/.tiff stay download-only: nothing decodes those on either side. * fix(files): make the preview derivative opt-in and never show a broken image Five issues from review, all interlocking around one decision. The derivative is now requested with preview=1 rather than suppressed with raw=1. raw=1 would have corrupted generated-document downloads: every non-markdown workspace download routes through the serve route and relies on resolveServableDocBytes compiling stored source into the real binary. Opt-in separates the three consumers cleanly — previews get the JPEG, downloads get untouched stored bytes, and doc compilation stays unconditional. - Public shares resolve the derivative too, with the same preview/download split; the viewer requests it, the download button does not. - Split the brand predicate. isHeifContainer stays broad for the vision path, where it only runs after sharp has already failed. The serve path runs first, so it uses isHevcHeifContainer — an AVIF was costing a storage round-trip, a WASM load and a misleading warn per request. - A derivative that cannot be produced (past the 20MB ceiling, or a decode failure) now falls back to 'Preview not available' instead of a broken image. UnsupportedPreview moved to preview-shared to avoid a module cycle. - The chat composer chip requests the derivative, so HEIC attachments stop rendering as broken thumbnails. * fix(files): reset the image preview when the file is overwritten An overwrite preserves the storage key, which is what the parent keys this component on, so only the URL version changes and it never remounts. The previous bytes' outcome therefore stuck, leaving a replaced image parked on 'Preview not available' until something else forced a remount. Reset on URL change during render rather than in an effect — this is derived state, and an effect would render the stale outcome first. * improvement(files): drop the dead preview reset and cap the ftyp brand scan - Content writes mint a new storage key, so the parent's key={file.key} already remounts ImagePreview; the render-phase reset was unreachable and made renames flash a loading overlay. - Clamp the ftyp compatible-brand scan to a real box size. The declared size is attacker-controlled and this now runs on every preview request. - UnsupportedPreview takes a primitive name so memo is load-bearing. - Fix the hardcoded ? in the public preview URL builder. * improvement(copilot): only ask for a preview derivative on image thumbnails A video has no derivative path, so preview=1 there only spent a brand sniff per request. Adds the missing test coverage for the helper. * fix(tooltip): dismiss floating tooltip when its trigger is hidden without pointer events (#6354) * fix(tooltip): dismiss floating tooltip when its trigger is hidden without pointer events * fix(tooltip): catch display: none triggers in the legacy visibility fallback * feat(smartlead): add Smartlead integration (#6352) * feat(smartlead): add Smartlead integration Adds a Smartlead block with 22 tools covering campaigns, sequences, leads, analytics, and webhooks. Every request path, parameter, enum, and response mapping was verified against the live Smartlead API rather than its documentation, which proved unreliable: - `POST /campaigns/new` (documented) 404s; the real path is `/campaigns/create` - `GET /campaigns/{id}` and `/sequences` return bare payloads, not the documented `{success, data}` envelopes - `/statistics` returns paginated per-email rows, not the documented aggregate - `POST /campaigns/{id}/leads` returns import counters under entirely different field names than documented - documented `/leads/{id}`, `/top-level-analytics`, `/all-leads-activities`, `/lead-lists/`, and `/lead-tags/` all 404 Enum values (campaign status, track settings, stop-lead settings, webhook event types, engagement status) were probed value-by-value against the API. Notes on the API's shape, encoded in the mappers: - string-encoded numbers (`total_leads: "1"`, `sent_count: "0"`) are normalized to numbers so a field never changes type between operations - `seq_delay_details` is read as `delayInDays` but written as `delay_in_days` - webhook writes echo `event_type_map`/`category_id_map` objects while the list endpoint returns `event_types`/`categories` arrays; both map to arrays - `track_settings` reads back in a vocabulary it will not accept on write Statistics rows and lead message-history entries pass through unmapped: no account could produce a non-empty sample, so no field names were invented. Email-account tools and a webhook trigger are omitted for the same reason. Adds a `smartlead-errors` extractor since the API's 400s put the useful text in `message` while `error` is only "Bad Request". * feat(smartlead): expand to the core workflow surface and fix review findings Grows the block from 22 to 47 tools and fixes every defect found in review. New tools (all executed against the live API end to end): campaign email accounts (list/add/remove), duplicate, delete, CSV lead export, webhook delete + delivery summary, lead + mailbox statistics, top-level analytics by date, lead activities, get lead by id, unsubscribe from campaign, unsubscribe globally, mark complete, delete from campaign, master-inbox replies, lead lists (list/get/create/update/delete), email accounts, clients. The endpoint inventory was rebuilt by extracting method+path from all 212 reference pages, which corrected several earlier conclusions: get-lead-by-id is `/leads/{id}` (not under `/campaigns/`), lead lists are `/lead-list/` (singular), and lead activities are …
1 parent bc54274 commit b790a04

1,451 files changed

Lines changed: 128202 additions & 19638 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/add-column-type/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This was not always true: adding `currency` originally took ~40 edits across 32
1515
Do **not** hunt for places to edit. Add your type to the `ColumnType` union first and let `tsc` produce the list:
1616

1717
```bash
18-
cd apps/sim && bunx tsc --noEmit -p tsconfig.json
18+
cd apps/sim && bun run type-check
1919
```
2020

2121
You will get two errors, naming `column-types/registry.ts` and `column-types/registry.server.ts`. Register in both.
@@ -153,7 +153,7 @@ Registering the *type* is compiler-enforced. Registering its *metadata* is not,
153153

154154
## Final Validation (Required)
155155

156-
1. **`cd apps/sim && bunx tsc --noEmit -p tsconfig.json`** — must be clean. If any file *outside* `column-types/` errors, that file has a hardcoded type list; fix it to read the registry.
156+
1. **`cd apps/sim && bun run type-check`** — must be clean. If any file *outside* `column-types/` errors, that file has a hardcoded type list; fix it to read the registry.
157157
2. **Grep for leaks**`grep -rnE "(===|!==) '{id}'|case '{id}':" apps/sim --include='*.ts' --include='*.tsx' | grep -v column-types/`. (All three forms: a plain `!==` and a `case` are how half of `currency`'s real branches are written.) Hits are expected; judge each. A hit is fine when it mounts a specific React component or encodes a genuinely one-off behavior (`json`'s mono textarea, `date`'s timezone-aware parsing). A hit is a **leak** when it restates something the registry could answer — an icon, a label, a colour, an operator set, a cast, a coercion. Leaks get a registry field, not a new branch.
158158
3. **Run the suite**`bunx vitest run lib/table 'app/workspace/[workspaceId]/tables' lib/api app/api/table app/api/v1 lib/copilot/tools/server/table`. Existing tests must pass **unchanged**; needing to edit one means you changed behavior for the other types.
159159
4. **`bun run lint:check`, `bun run check:api-validation`, `bun run check:client-boundary`** from the repo root.

.agents/skills/add-enrichment/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export const ENRICHMENT_REGISTRY: EnrichmentRegistry = {
128128

129129
## Step 5: Verify
130130

131-
1. `bunx tsc --noEmit` (from `apps/sim`, `NODE_OPTIONS=--max-old-space-size=8192`) and `bunx biome check` on the changed files.
131+
1. `bun run type-check` (from `apps/sim`) and `bunx biome check` on the changed files.
132132
2. In a table → **+ New column → Enrichments** → pick the new enrichment, map its inputs to columns, name the output column(s), Save. Confirm it appears in the catalog with its icon/description.
133133
3. With hosted keys (or a workspace BYOK key) configured for each provider's service, run a row and confirm the cell fills; the dev-server log shows `Enrichment hit { provider }`. A row whose providers all miss completes blank; a row where every provider errored shows an error cell.
134134

.agents/skills/add-integration/SKILL.md

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,24 @@ service's official documentation or an unambiguous local execution path proves t
131131
field is consumed by an AI model. If that cannot be established, preserve existing tool behavior
132132
and leave the field unannotated.
133133

134-
- **Ordinary provider/API input:** leave it unchanged. Do not add blanket result sanitization.
134+
- **Ordinary provider/API input:** leave it unchanged. Explicit `{{...}}` references resolve and are
135+
sent with their normal request semantics. A URL, domain, resource ID, control field, or opaque
136+
payload is not model-visible merely because the provider is AI-backed or may process the
137+
referenced resource later.
135138
- **Text or structured content consumed by an AI model:** declare `request.modelInput` with
136139
`mode: 'project'` and select only the exact model-visible fields. The shared executor replaces
137140
activated Sim secrets with canonical `{{NAME}}` labels before request formatting. For nested or
138141
JSON-string fields, use a small shared selector plus `applyProjected`; verify that selecting the
139142
rebuilt params reproduces the projected selection.
140-
- **Opaque model input sent directly to an external provider** such as a model-read URL or image
141-
payload: declare `request.opaqueModelInput` with `mode: 'reject-resolved-secrets'` and select only
142-
the exact effective value. The shared `executeTool` preflight rejects incomplete or secret-bearing
143-
committed provenance before URL/body formatting or network I/O, preserves safe request bytes,
144-
and sends no provenance metadata to the provider.
145-
- **Opaque model input owned by an authenticated internal route** such as uploaded audio, image,
146-
video, file bytes, or signed URLs: add `privateProvenance` to a projected request, or use
147-
`mode: 'private-provenance'` when there is no textual projection. The route must call
143+
- **Serialized model content sent directly to an external provider:** include the serialized
144+
top-level param in `request.modelInput`. Project the private copy before the existing request
145+
formatter parses it; keep formatter behavior deterministic when a whole-value placeholder is not
146+
valid in the serialized grammar. Do not introduce a second hard-rejection path.
147+
- **Opaque model input owned by an authenticated internal route** such as inline audio, image,
148+
video, or document bytes: add `privateProvenance` to a projected request, or use
149+
`mode: 'private-provenance'` when there is no textual projection. Do not select storage keys,
150+
paths, signed URLs, or ordinary remote URLs as byte provenance; the owning route must authorize
151+
stored bytes independently at model egress. The route must call
148152
`validateOpaqueModelInputProvenance` before downloading or sending content to the model and must
149153
apply the workspace-file provenance guard before reading a persisted workspace file.
150154
- **Sim-owned durable storage or internal execution handoff** that can later enter a workflow/model
@@ -160,9 +164,9 @@ Hard rules:
160164
- Never substitute secret plaintext into source or serialize plaintext provenance.
161165
- Never hand-roll private provenance headers/envelopes; the shared `executeTool` boundary owns
162166
transport and strips private metadata from functional results.
163-
- Never attach private provenance to an external URL or to `directExecution`. Use the centralized
164-
`opaqueModelInput` rejection mode for external/direct opaque model inputs, or an authenticated
165-
internal route when encrypted provenance must cross the boundary.
167+
- Never attach private provenance to an external URL or to `directExecution`. Project proven
168+
model-visible external fields with `request.modelInput`; otherwise preserve ordinary request
169+
semantics. Use an authenticated internal route when encrypted provenance must cross the boundary.
166170
- Never sanitize arbitrary third-party tool results. Projection applies only to secrets activated
167171
by Sim's resolved-secret provenance for that execution/tool call.
168172
- Do not add provenance merely because a value is persisted, returned by a tool, or appears in a
@@ -173,12 +177,11 @@ Hard rules:
173177
provider responses, filenames, URLs, and errors remain unchanged when Sim did not resolve a
174178
secret into them.
175179

176-
Add focused tests covering named projection, ordinary identical text without provenance, nested
177-
shape preservation, malformed/incomplete private metadata failing closed, centralized external
178-
opaque rejection before formatting/I/O without byte changes or metadata transport, headerless
179-
legacy requests, and absence of private metadata in the public tool result. For durable sinks, also
180-
cover legacy `NULL` markers, exact-empty new writes, tracked secret writes, stale/missing sidecars,
181-
and scope isolation.
180+
Add focused tests covering named projection, ordinary identical text without provenance, nested and
181+
serialized shape handling, unchanged ordinary external inputs, malformed/incomplete private metadata
182+
failing closed, headerless legacy requests, and absence of private metadata in the public tool result.
183+
For durable sinks, also cover legacy `NULL` markers, exact-empty new writes, tracked secret writes,
184+
stale/missing sidecars, and scope isolation.
182185

183186
## Step 3: Create Block
184187

@@ -594,8 +597,8 @@ If creating V2 versions (API-aligned outputs):
594597
- [ ] Registered all tools in `tools/registry.ts`
595598
- [ ] Ran `bun run tool-metadata:generate` and committed the regenerated artifacts
596599
- [ ] Classified every model-visible, opaque, Sim-durable, and internal-execution request field
597-
- [ ] Added shared model-input projection, centralized opaque rejection, or private provenance only
598-
where required
600+
- [ ] Added shared model-input projection or private provenance only where required; ordinary
601+
external resource locators and control inputs retain their request semantics
599602
- [ ] Confirmed ordinary third-party tool results are not generically sanitized
600603
- [ ] Added provenance compatibility and fail-closed boundary tests where applicable
601604

.agents/skills/add-tools/SKILL.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,16 @@ export const {serviceName}{Action}Tool: ToolConfig<
150150
- Leave ordinary external API inputs and third-party results unchanged. Add provenance handling only
151151
when an exact field is proven to cross a Sim model, durable-storage, or internal-execution boundary.
152152
- Project AI-consumed text/structured fields with the smallest exact `request.modelInput` selector.
153-
- Reject resolved secrets in opaque model input sent directly to an external provider with
154-
`request.opaqueModelInput`; never attach private metadata to an external URL or `directExecution`.
155-
- For authenticated internal routes, use `privateProvenance` for opaque model input or
156-
`request.secretProvenance` for durable writes and execution handoffs. Authenticate first, validate
157-
the exact selection and scope, strip the private envelope, then import or propagate provenance at
158-
the receiving boundary. Preserve documented headerless legacy behavior.
153+
- Treat URLs, domains, resource IDs, and control fields as ordinary request values unless the exact
154+
field is proven model-visible. For serialized external model content, project the serialized
155+
top-level param through `request.modelInput` before the existing formatter parses it; do not add a
156+
separate hard-rejection mechanism.
157+
- For authenticated internal routes, use `privateProvenance` for actual inline/raw model bytes or
158+
`request.secretProvenance` for durable writes and execution handoffs. Do not treat a storage key,
159+
path, signed URL, or remote URL as provenance for fetched bytes; authorize tracked stored bytes at
160+
the owning model-egress boundary. Authenticate first, validate the exact selection and scope,
161+
strip the private envelope, then import or propagate provenance at the receiving boundary.
162+
Preserve documented headerless legacy behavior.
159163
- Never substitute secret plaintext into source, serialize plaintext provenance, hand-roll private
160164
headers, or blanket-sanitize tool results.
161165
- Add focused tests for named projection, identical unproven public text, malformed/incomplete

.agents/skills/ship/SKILL.md

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,10 @@ When the user runs `/ship`:
6565
echo "❌ block registry audit failed — do not ship"
6666
exit 1
6767
}
68-
rm -f /tmp/ship-audit-results
69-
for s in check:boundaries check:api-validation:strict check:openapi \
70-
check:desktop-bridge check:desktop-ipc \
71-
check:utils check:zustand-v5 \
72-
check:react-query check:client-boundary check:bare-icons check:icon-paths \
73-
check:realtime-prune check:tool-registry-boundary check:tool-request-boundary \
74-
check:sql-date-binding tool-metadata:check \
75-
integration-catalog:check skills:check agent-stream-docs:check; do
76-
( bun run "$s" >"/tmp/ship-audit-${s//:/-}.log" 2>&1; echo "$? $s" >>/tmp/ship-audit-results ) &
77-
done
78-
wait
79-
# any non-zero line is a failing audit — read its /tmp/ship-audit-<name>.log and fix before shipping.
80-
# `exit 1` on failure preserves the original sequential checks' semantics (their non-zero exit is
81-
# what an agent gates on); never use `grep … && echo ❌ || echo ✅` here — it always exits 0.
82-
if grep -vE '^0 ' /tmp/ship-audit-results; then echo "❌ audit(s) failed — do not ship"; exit 1; fi
83-
echo "✅ all audits passed"
68+
# Runs every audit CI runs, concurrently, and replays the output of any that fail.
69+
# Do not hand-list the audits here: the list is derived in scripts/run-audits.ts, and the
70+
# copy that used to live in this file had already drifted five audits behind package.json.
71+
bun run check:audits || { echo "❌ audit(s) failed — do not ship"; exit 1; }
8472
```
8573
If Phase A regenerated a file, its matching `:check` in Phase B now passes trivially — that parity is the point. Do not ship with any generator or audit failing; fix the cause (never silence it) and re-run. `check:migrations` and `type-check` are covered by steps 5 and CI respectively and are not repeated here.
8674
7. **Stage and commit** the changes with the generated message — including any files Phase A regenerated in step 6

.agents/skills/validate-integration/SKILL.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -141,21 +141,25 @@ search, extraction, or "AI-powered" marketing terminology.
141141

142142
- [ ] AI-consumed text/structured fields use `request.modelInput` with `mode: 'project'` and a
143143
minimal exact selector; nested/JSON-string adapters preserve shape through `applyProjected`
144-
- [ ] Opaque AI-consumed values sent directly to an external provider or `directExecution` use
145-
`request.opaqueModelInput` with `mode: 'reject-resolved-secrets'` and an exact effective-value
146-
selector; the central executor rejects incomplete/secret-bearing committed provenance before
147-
formatting or I/O, leaves safe bytes unchanged, and sends no provenance metadata externally
148-
- [ ] Opaque AI-consumed files/bytes/URLs owned by an authenticated internal route use
144+
- [ ] Ordinary external URLs, domains, resource IDs, and control fields retain normal request
145+
semantics unless the exact field is proven model-visible; an AI-backed provider or later model
146+
processing of the referenced resource is not sufficient evidence
147+
- [ ] Serialized content proven to be sent directly to an external model is selected by
148+
`request.modelInput`, projected before the existing formatter parses it, and has deterministic
149+
formatter behavior when a whole-value placeholder is invalid for the serialized grammar
150+
- [ ] Actual inline/raw AI-consumed bytes owned by an authenticated internal route use
149151
`privateProvenance` (or `mode: 'private-provenance'`), and the route validates
150-
`validateOpaqueModelInputProvenance` before any download or model call
152+
`validateOpaqueModelInputProvenance` before model egress; storage keys, paths, signed URLs,
153+
and ordinary remote URLs are not treated as byte provenance, while tracked stored bytes are
154+
authorized independently at the owning model-egress boundary
151155
- [ ] Persisted workspace-file contents are checked with the shared provenance guard only when
152156
their bytes or decoded content cross into a model/tool-result boundary; ordinary file APIs
153157
remain unchanged. Unsupported secret-bearing file paths are rejected at `file_write`
154158
- [ ] Sim-owned durable writes and internal execution handoffs that can enter workflows/models use
155159
field-scoped `request.secretProvenance`; authenticated receivers validate the exact selection
156160
and scope, strip private metadata, and persist, import, or propagate it at the owning boundary
157-
- [ ] Private provenance is never attached to external URLs or `directExecution`; those paths use
158-
centralized `opaqueModelInput` rejection when their opaque values are model-bound
161+
- [ ] Private provenance is never attached to external URLs or `directExecution`; proven
162+
model-visible external fields use projection, while other external inputs remain unchanged
159163
- [ ] No tool performs raw secret plaintext/source substitution or serializes plaintext provenance
160164
- [ ] No `transformResponse` or tool-local helper blanket-sanitizes ordinary third-party results;
161165
only execution-scoped, activated Sim provenance is projected at shared model/log boundaries
@@ -166,10 +170,9 @@ search, extraction, or "AI-powered" marketing terminology.
166170
metadata, provider results, or API payloads
167171
- [ ] Diagnostic projection is applied only to values carrying execution-scoped provenance;
168172
ordinary provider responses, filenames, URLs, and errors are unchanged
169-
- [ ] Tests cover named `{{NAME}}` projection, unproven identical public text, nested shape
170-
preservation, malformed/incomplete metadata, centralized opaque rejection before formatting
171-
or I/O with safe-byte preservation, headerless legacy requests, metadata stripping, and
172-
durable legacy/stale/scope cases when applicable
173+
- [ ] Tests cover named `{{NAME}}` projection, unproven identical public text, nested and serialized
174+
shape handling, unchanged ordinary external inputs, malformed/incomplete metadata, headerless
175+
legacy requests, metadata stripping, and durable legacy/stale/scope cases when applicable
173176

174177
Treat a missing or bypassed model, durable, or internal-execution provenance boundary as
175178
**critical**. Do not fix it with a tool-specific string replacer or by sanitizing every provider
@@ -348,8 +351,7 @@ Group findings by severity:
348351
- Service-account metadata disagrees with the canonical OAuth service configuration
349352
- `tools.config.tool` returning wrong tool ID for an operation
350353
- Type coercions in `tools.config.tool` instead of `tools.config.params`
351-
- AI-consumed request fields bypass the shared projection, centralized opaque rejection, or
352-
private-provenance boundary
354+
- Proven model-visible request fields bypass the shared projection or private-provenance boundary
353355
- Opaque model input is downloaded or sent before provenance and workspace-file checks
354356
- A Sim-owned durable sink or internal execution handoff drops encrypted provenance or breaks
355357
legacy headerless/`NULL` data

.claude/commands/add-column-type.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This was not always true: adding `currency` originally took ~40 edits across 32
1414
Do **not** hunt for places to edit. Add your type to the `ColumnType` union first and let `tsc` produce the list:
1515

1616
```bash
17-
cd apps/sim && bunx tsc --noEmit -p tsconfig.json
17+
cd apps/sim && bun run type-check
1818
```
1919

2020
You will get two errors, naming `column-types/registry.ts` and `column-types/registry.server.ts`. Register in both.
@@ -152,7 +152,7 @@ Registering the *type* is compiler-enforced. Registering its *metadata* is not,
152152

153153
## Final Validation (Required)
154154

155-
1. **`cd apps/sim && bunx tsc --noEmit -p tsconfig.json`** — must be clean. If any file *outside* `column-types/` errors, that file has a hardcoded type list; fix it to read the registry.
155+
1. **`cd apps/sim && bun run type-check`** — must be clean. If any file *outside* `column-types/` errors, that file has a hardcoded type list; fix it to read the registry.
156156
2. **Grep for leaks**`grep -rnE "(===|!==) '{id}'|case '{id}':" apps/sim --include='*.ts' --include='*.tsx' | grep -v column-types/`. (All three forms: a plain `!==` and a `case` are how half of `currency`'s real branches are written.) Hits are expected; judge each. A hit is fine when it mounts a specific React component or encodes a genuinely one-off behavior (`json`'s mono textarea, `date`'s timezone-aware parsing). A hit is a **leak** when it restates something the registry could answer — an icon, a label, a colour, an operator set, a cast, a coercion. Leaks get a registry field, not a new branch.
157157
3. **Run the suite**`bunx vitest run lib/table 'app/workspace/[workspaceId]/tables' lib/api app/api/table app/api/v1 lib/copilot/tools/server/table`. Existing tests must pass **unchanged**; needing to edit one means you changed behavior for the other types.
158158
4. **`bun run lint:check`, `bun run check:api-validation`, `bun run check:client-boundary`** from the repo root.

.claude/commands/add-enrichment.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export const ENRICHMENT_REGISTRY: EnrichmentRegistry = {
127127

128128
## Step 5: Verify
129129

130-
1. `bunx tsc --noEmit` (from `apps/sim`, `NODE_OPTIONS=--max-old-space-size=8192`) and `bunx biome check` on the changed files.
130+
1. `bun run type-check` (from `apps/sim`) and `bunx biome check` on the changed files.
131131
2. In a table → **+ New column → Enrichments** → pick the new enrichment, map its inputs to columns, name the output column(s), Save. Confirm it appears in the catalog with its icon/description.
132132
3. With hosted keys (or a workspace BYOK key) configured for each provider's service, run a row and confirm the cell fills; the dev-server log shows `Enrichment hit { provider }`. A row whose providers all miss completes blank; a row where every provider errored shows an error cell.
133133

0 commit comments

Comments
 (0)