Skip to content

v0.7.45: access control groups enhancements, sso domain verification, agent thinking and streaming options, opus 5, daytona failover#5938

Open
waleedlatif1 wants to merge 31 commits into
mainfrom
staging
Open

v0.7.45: access control groups enhancements, sso domain verification, agent thinking and streaming options, opus 5, daytona failover#5938
waleedlatif1 wants to merge 31 commits into
mainfrom
staging

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

waleedlatif1 and others added 24 commits July 23, 2026 15:27
…private MCP) (#5901)

* fix(mcp): stream pinned transport via undici.request + redirect interceptor

Extends the Bun undici-streaming fix to createPinnedFetchWithDispatcher (providers,
A2A, self-hosted-private MCP over SSE). It now routes through undiciRequestAsResponse
like the guarded builder, so streaming bodies deliver under Bun. Unlike the guarded
path it has no followRedirectsGuarded wrapper (it's handed straight to provider SDKs),
so redirects are followed via undici's redirect interceptor composed onto the pinned
Agent — every hop still dispatches through the pinned connect.lookup (resolvedIP), so a
redirect can't escape to another address, matching the old fetch guarantee. secureFetchWithPinnedIP (raw Node http, tools path) is untouched.

* fix(mcp): honor redirect mode + drop cross-origin credentials on pinned fetch

Replaces the always-on redirect interceptor with redirect-mode-aware handling:
- redirect:'manual' returns the 3xx without following (detectMcpAuthType inspects it)
- redirect:'error' throws on a 3xx
- default 'follow' uses followRedirectsGuarded, which drops ALL headers on a
  cross-origin hop (so a redirect can't disclose a provider api-key to another
  origin — Greptile P1) and stamps the final response.url + redirected flag.
Extracts the shared Request-lift helper used by both guarded and pinned builders.

* fix(mcp): don't block private IP-literal URLs on the pinned fetch path

Routing the pinned fetch through followRedirectsGuarded added an initial
assertGuardedRedirectTarget check the old undici.fetch path never ran, which
would block a self-hosted MCP configured with a private IP-literal URL
(e.g. http://10.0.0.5:3000/mcp) — its own transport. The pinned path's callers
already validate the target and the private carve-out intentionally pins to a
private IP, so skip the initial-target check (validateInitialTarget: false) while
still validating every redirect hop. Adds a regression test.

* fix(mcp): carry redirect mode from a Request input in liftFetchArgs

liftFetchArgs copied method/headers/body/signal from a Request but omitted
redirect, so a Request({ redirect: 'manual' }) on the pinned path defaulted to
'follow' and was transparently followed. Copy input.redirect (explicit init still
wins). Adds a Request-input redirect-mode test.

* fix(mcp): permit the pinned IP as a redirect target (initial + hops), block other private IPs

Consolidates the pinned-path redirect policy into one mechanism. followRedirectsGuarded
took validateInitialTarget to skip the initial private-IP check, but per-hop checks still
blocked a self-hosted MCP redirecting to its own pinned private IP (e.g. a trailing-slash
301 to http://10.0.0.5/mcp/). Replace it with allowRedirectToIp: the pinned fetch permits
exactly its own validated IP as a target — initial URL and any hop that stays on it — while
every OTHER private target (e.g. the 169.254.169.254 metadata IP) stays blocked. Tests cover
the same-IP hop (followed) and the metadata-IP escape (still refused).
…d colocate chat deploy auth (#5902)

* improvement(access-control): editable group details, status filters, block tooltips, chat auth colocation

- General tab gains editable Name and Description fields wired into the
  existing dirty buffer, so the header Save/Discard chips and the
  unsaved-changes guard cover them. Save only sends changed fields and
  surfaces the route's duplicate-name 409.
- Blocks, Model Providers, and Platform tabs gain an All/Enabled/Disabled
  filter beside their search field, evaluated against the editing buffer.
- Every block row carries an Info badge with the block's description. The
  badge sits outside the label/expand button so it never toggles the row.
- The chat deploy toggle moves out of Deploy Tabs into the Chat section
  alongside its allowed-auth-modes dropdown, mirroring the Files section.

* improvement(access-control): polish group detail filters, tooltips, and details fields

Follows the cleanup review:
- reconcile the post-save baseline from the server response instead of
  local values, matching the scope/default writes
- pin the status-filter dropdown width so the search field stops resizing
- restore flex-1 on the block-name button and move the row hover surface
  to the wrapper so Info badges align in a column
- add empty states for filter-empty lists and neutralize Select All there
- surface a 'Name is required' message next to the disabled Save
- align hint text on the field-hint tokens and drop a redundant TSDoc

* refactor(access-control): keep chat and files toggles in the platform registry

The first pass pulled hideDeployChatbot out of the declarative platformFeatures
array and hand-rolled a Chat section beside the existing bespoke Files one. That
forfeited search, status filtering, Select All, category grouping and the Info
hint, and the replacement platformSectionVisible re-implemented two of those
with different semantics — searching 'deploy' or 'deployment' hid the very
control named Deployment, and Select All silently skipped both toggles.

Both toggles are now ordinary registry entries under their own Chat and Files
categories, with an id-keyed featureExtras map supplying the nested auth-mode
dropdown. Search, filtering, Select All, hints and the empty state are correct
by construction, and the parallel filter pipeline is gone.

Also from the review:
- index the allow-lists into Sets so per-row membership checks are O(1)
- split the search and status passes so the common 'all' filter returns the
  searched list by reference and a checkbox toggle no longer re-sorts ~180 rows
- extract StatusFilterChip and AuthModeField instead of stamping out the
  dropdowns three and two times
- derive nameChanged/descriptionChanged once instead of repeating the
  comparisons in the save payload
- lock the config key-order invariant the dirty check depends on with a test

* polish(access-control): apply the second cleanup round

- indent the nested auth-mode field so it lines up under its toggle's label
  instead of reading as a sibling row, and label the dropdown for screen readers
- order Chat right after Deploy Tabs so the three deploy targets stay adjacent
- flush the trailing Select All chips on the providers and platform rows
- drop the doubled margin on the name error (SettingRow already gaps it)
- hoist PLATFORM_FEATURES and PLATFORM_CATEGORY_ORDER to module scope
- drop the useCallback on the two save/discard handlers; nothing observes their
  identity and their deps changed on every keystroke anyway
- fix a comment that still pointed at a 'Hide Chat' toggle that no longer exists

* fix(access-control): keep the block disclosure chevron inside its toggle button

Splitting the chevron out so the Info badge could sit beside the name left the
chevron with no click handler — the visible expansion affordance did nothing.
It goes back inside the button; Info stays outside it, since an Info trigger is
itself a button and cannot nest.

* chore(access-control): use structuredClone in the config key-order test

check:utils bans JSON.parse(JSON.stringify(...)). The clone only needs to hand
the schema a distinct object; structuredClone preserves key order the same way.

* fix(access-control): trim descriptions so a padded value can't wedge the form

descriptionChanged compared a trimmed draft against the raw saved description,
so a group whose stored description carried padding opened dirty and could
never be cleared — Discard restored the same padded string, and the
unsaved-changes guard then blocked navigation until a save rewrote it.

The contract now trims description on create and update, matching what name
already did, and the dirty check trims both sides so existing padded rows
behave too.

* improvement(access-control): seed the description buffer trimmed

Keeps the editing buffer and the dirty baseline normalized the same way, so a
legacy row with padding no longer shows stray whitespace in the input and the
buffer never round-trips padding a save would strip anyway.

* fix(emcn): forward aria-label/aria-labelledby from ChipDropdown to its trigger

ChipDropdown destructures only its known props, so an aria-labelledby passed by
a consumer never reached the trigger button — AuthModeField's wiring to its
visible label was silently dropped and the control had no accessible name.

Both attributes are now explicit, typed props forwarded to the trigger. Kept as
two named props rather than a rest spread so the component still owns its
chrome and consumers can't smuggle arbitrary attributes onto the button.

* fix(access-control): correct the nested field indent, platform row hover, and aria name

- aria-labelledby REPLACES the content-derived name, so naming the auth-mode
  dropdown with its caption alone dropped the selected value from the accessible
  name — worse than no attribute. It now references caption + trigger, which
  needed ChipDropdown to forward id as well.
- The nested field's pl-[30px] assumed a 14px checkbox; the default is 16px, so
  the caption sat 2px left of the label it hangs under while the dropdown (not
  flush, so mx-0.5) sat at 32. Both are pl-8 + flush now.
- Platform feature rows kept their hover surface on the label, so the highlight
  stopped 20px short of the row edge while the Blocks tab ran flush. Moved to
  the wrapper, matching the core-blocks cell.
- Split the platform search and status passes like the provider and block lists,
  so a toggle no longer recomputes three chained memos.
- Dropped an unreachable allLabel and a comment that would go stale on merge.

* fix(access-control): trim the name comparison the same way as description

nameChanged compared a trimmed buffer against an untrimmed baseline — the exact
asymmetry already fixed for description. A group stored before the name schema
gained .trim() opens permanently dirty: Save/Discard visible and the
unsaved-changes modal firing on back, until a save rewrites it.

Both buffers now seed trimmed and compare trimmed on both sides. Also dims the
Info badge along with the row it belongs to when the block is disallowed.
* feat(sandbox): add Daytona as a manual-flip failover for E2B

E2B was a hard single point of failure: lib/execution/e2b.ts had no retry
and no fallback, so a failed Sandbox.create() killed Python function blocks,
JS-with-imports, shell, doc generation and the Pi cloud agent outright.

Extract a SandboxRunner boundary (lib/execution/remote-sandbox) with an E2B
runner and a Daytona runner, selected once per execution by the
sandbox-provider-daytona AppConfig flag. Everything above the provider
boundary — marker parsing, mount materialization, file export, corruption
handling — is unchanged.

Selection resolves before create() and never mid-execution, since user code
has side effects. Each sandbox kind fails closed when its snapshot id is unset.

Notes on the Daytona adapter:
- language binds at create(), not per call: Daytona applies it as a sandbox
  label and silently runs JS through Python if passed to codeRun
- Python routes via CodeInterpreter for its {name,value,traceback} error shape,
  which matches E2B's and keeps formatE2BError's line offsets correct
- timeouts convert ms to seconds
- the streaming path delivers env via the filesystem API, as
  SessionExecuteRequest has no env field and secrets must not reach a command line

Drops the dead E2BExecutionResult.images field (populated, never consumed).

* improvement(sandbox): select the provider by SANDBOX_PROVIDER env var

Replaces the boolean sandbox-provider-daytona feature flag with a
SANDBOX_PROVIDER env var naming the provider ('e2b' default, or 'daytona').
A boolean doesn't scale to a third adapter; a keyed registry does.

- PROVIDERS is a Record<SandboxProviderId, SandboxProvider>, so adding an
  adapter is one entry plus one id-union member — an unhandled provider is a
  compile error, not a runtime surprise
- resolveProvider() reads env synchronously and throws on an unknown value
  (fail fast) instead of an async feature-flag lookup
- drops the sandbox-provider-daytona flag and SANDBOX_PROVIDER_DAYTONA fallback

Verified end-to-end: a Python function block through the running app routes to
Daytona (Creating Daytona sandbox, kind: code) with SANDBOX_PROVIDER=daytona.

* fix(sandbox): gate remote execution by provider availability, not E2B

Addresses the review round on #5860.

- Availability was gated on isE2bEnabled / isE2BDocEnabled, so a Daytona-only
  deployment (E2B_ENABLED unset) had its Python/shell/JS-with-imports and doc
  paths rejected before the provider-neutral sandbox call could run. Replace both
  with provider-aware flags (isRemoteSandboxEnabled / isDocSandboxEnabled) derived
  from the selected SANDBOX_PROVIDER's own credentials + image. E2B behavior is
  unchanged (the E2B branch mirrors the old definitions exactly).
- Make the function-block gate error messages provider-neutral.
- Daytona's streaming runCommand (Pi) returned empty stdout/stderr and delivered
  output only via callbacks, so the Pi cloud flow — which parses markers from
  stdout and formats errors from stderr — saw nothing. Accumulate the streamed
  chunks and return them while still forwarding to the callbacks.

Renames the env-flag exports (and the @sim/testing mock) to match. Adds a
conformance test that the streamed Pi output lands in stdout/stderr.

* fix(sandbox): resolve SANDBOX_PROVIDER case-insensitively

Addresses the round-2 review on #5860. env-flags lowercased SANDBOX_PROVIDER
for the availability gate, but resolveProvider looked up the raw value in a
lowercase-keyed map — so 'Daytona' passed the gate then threw Unknown
SANDBOX_PROVIDER at create. resolveProvider now normalizes casing identically.

* fix(sandbox): use getErrorMessage in build/verify scripts

check:utils flagged the inline `error instanceof Error ? error.message : ...`
pattern in the two new scripts. Use getErrorMessage from @sim/utils/errors,
matching the repo convention the check enforces.

* fix(sandbox): fall back to stdout for Daytona failure text

Daytona merges both streams into stdout and returns an empty stderr, but the
shell-error, base64-export, and URL-mount error builders read only
result.stderr — so Daytona failures surfaced a generic 'Process exited with
code N' / 'base64 failed' / 'curl exited N' instead of the real command output
that the API and agents rely on. Fall back to stdout before the generic message
(provider-agnostic: E2B still populates stderr). Strengthens the shell-error
conformance test to assert the real output surfaces.

* fix(sandbox): enforce timeout on Daytona streaming; drop leftover E2B copy

- Daytona's streaming path (Pi) started the command with runAsync:true and then
  awaited getSessionCommandLogs with no bound, so a hung command never timed out
  the way E2B's commands.run({ timeoutMs }) does. Race the log stream against the
  timeout; on expiry return exit 124 with the accumulated output, and the finally's
  deleteSession terminates the still-running command.
- Two user-facing strings still named E2B after the provider-neutral rename (the
  isolated-vm sandboxPath remediation and the disabled-xlsx message). Made both
  provider-neutral.

Adds a streaming-timeout conformance test.

* fix(sandbox): handle orphaned stream promise + preserve error detail

Two regressions from the previous timeout fix:
- When the timeout won the race, the abandoned getSessionCommandLogs promise
  would reject on deleteSession with no handler (unhandledRejection). Attach a
  .catch that records the error and yields an 'error' outcome, so a late
  rejection is always handled.
- The streaming catch dropped the thrown error, so failures before any chunks
  (env write, executeSessionCommand, missing cmdId) surfaced as empty output.
  Fall back to getErrorMessage(error) when nothing streamed.

Adds conformance tests for the stream-reject and start-throw paths.
… citation rules (#5904)

* improvement(content): surface last-verified and updated dates, codify citation rules

Comparison pages already computed a latest-verified date from every fact
source's asOf and emitted it as JSON-LD dateModified, but never showed it.
Library and blog posts had the same gap: dateModified existed only as an
invisible meta tag. A freshness signal that no reader can see does nothing
for the reader deciding whether to trust the page.

- /comparisons/[provider] renders "Last verified <date>" from the existing
  getLatestVerifiedDate(), so the visible date and the JSON-LD read the
  same value and cannot drift
- /library and /blog posts render "Updated <date>" next to the publish
  date, only when the modified day actually differs; otherwise the meta
  fallback stays. dateModified is emitted exactly once either way
- collapse three duplicated toLocaleDateString blocks in
  content-post-page into one module-scope formatDate (same UTC pinning,
  identical output)
- landing-seo-geo rules: add citation/internal-linking and freshness
  sections, and extend the rule's paths to content MDX so it attaches when
  authoring posts, not just TSX

* fix(content): wrap post metadata row so the Updated date cannot overflow on narrow screens

The published/updated/authors/share row was a non-wrapping flex. With the
Updated label active and two authors it overflowed at 390px; the row also
already overflowed at 320px on staging before this PR, with no Updated
label at all. flex-wrap fixes both and leaves desktop identical.

* improvement(comparisons): fold last-verified date into the intro sentence

The paragraph already ended "Every fact below is sourced and dated" and a
separate line then stated the date, which read as redundant and added a
standalone metadata row to the header. Folding it into that sentence drops
the extra line and keeps the date as real server-rendered text in a <time>
element, so crawlers and AI answer engines still see it. A tooltip would
not: Tooltip.Content renders null during SSR and while closed.
…se (#5906)

* fix(landing): restore single-paragraph hero copy on home and enterprise

* fix(landing): tighten enterprise hero description

* revert(landing): restore original home and enterprise hero headings

* improvement(landing): simplify enterprise hero description

* improvement(landing): restore building-and-managing homepage headline
… pricing (#5913)

* improvement(library): add citations and internal links, correct stale pricing

The library had zero third-party citations across 16 posts (~51k words) and
averaged 1.2 internal links per post, with six posts at zero. Auditing every
dollar figure against the vendor's own pricing page while adding the citations
turned up several claims that had gone stale, plus one product that is being
shut down.

Corrections, each verified against the vendor's page on 2026-07-23:
- Relay.app is winding down (signups closed 2026-07-16, free accounts end
  2026-08-15, paid 2026-09-14). It was recommended as the human-in-the-loop
  pick in best-zapier-alternatives; that recommendation now points to a
  platform you can still sign up for
- Make Core is $12/mo billed monthly, not $10.59; annual saves ~15%. One post
  also described $12 as the annual rate
- Pabbly Connect lifetime starts at $349, not $249, and the Standard/Pro/
  Ultimate tier structure quoted no longer exists
- Workato publishes no pricing at all, so the "~$1,000/month" figure is
  replaced with the fact that every deal is quoted through sales
- Dify is at ~149k GitHub stars, not 131k
- n8n cloud Starter is EUR 20/mo billed annually for 2,500 executions

Verified-correct figures were left alone and given a source link: Zapier
$29.99/mo monthly for 750 tasks, Activepieces 10 free flows then $5/flow/mo,
Power Automate $15/user/mo, Lindy $49.99/mo, and the Grand View Research RPA
market figures.

Also: 59 outbound citations and 3-6 internal links per post (zero posts left
without internal links), and MDX external links now carry target=_blank plus
rel=noopener noreferrer, which the landing SEO/GEO rule requires but the MDX
anchor was not applying.

* fix(content): treat only non-Sim hosts as external links in MDX

The first pass classified any http(s) href as external, so the 33 absolute
Sim URLs already in content (sim.ai, sim.ai/slack, www.sim.ai/blog/*, and 14
docs.sim.ai pages) would have opened in a new tab with rel=noopener, which is
wrong for first-party links and contradicted the comment above the check.

Classification now compares the hostname against the apex derived from
SITE_URL, so the apex and any Sim subdomain stay same-tab. SITE_URL is used
rather than getBaseDomain() so a post renders identically in dev, preview,
and production instead of varying with NEXT_PUBLIC_APP_URL. The leading dot
in the suffix check keeps lookalikes such as evil-sim.ai external.
…oard (#5907)

* fix(helm): correct chart docs, examples, and dead config across the board

Audit-driven accuracy pass over the chart's entire documentation surface,
verified by rendering every example against the templates:

- migrations run as an init container on the app pod, not a Job — fix the
  README component list, troubleshooting commands, and sim-helm skill refs;
  drop the dead migrations-job NetworkPolicy ingress rule
- referenced-but-never-created resources: document the GKE ManagedCertificate
  creation (values-gcp), comment out the key-file Secret mount that stuck all
  pods in ContainerCreating (values-gcp), enable certManager for the postgres
  TLS issuerRef (values-production), add the cert-manager cluster-issuer
  annotation nginx needs (values-azure)
- values-external-db: networkPolicy.egress is a list, not a map (the map
  rendered an invalid manifest); fill schema-failing placeholder host/username
- realtime >1 replica requires REDIS_URL (Socket.IO Redis adapter) — default
  examples to 1 replica with the scaling note, and warn where autoscaling
  HPAs override replicaCount
- pod anti-affinity selectors matched nothing (simstudio vs sim name label)
- kubernetes.mdx: install commands were missing required CRON_SECRET and
  postgresql password (failed at template time), wrong deployment name in
  port-forward, stale version requirements, unsupported key-remapping claim
- remove unimplemented app.secrets.existingSecret.keys from values + schema;
  fix README PDB default, cronjob list, /metrics caveat, NOTES secret count,
  Azure-only StorageClass in generic examples, dead SOCKET_SERVER_URL and
  GOOGLE_CLOUD_* env, ESO apiVersion mismatch, and skill-reference drift
- bump chart to 1.0.1

* fix(helm): review round 1 — scoped example egress, in-tab secret note, copilot Job wording

- external-db example egress scopes to a placeholder database CIDR instead of
  to: [] (which allowed every destination on 5432, defeating the isolation
  the example teaches)
- kubernetes.mdx cloud tabs state explicitly that they reuse the variables
  generated in the Installation block
- Copilot migrations really do run as a Helm-hook Job — restore Job wording
  there (only the app migrations are an init container)

* fix(helm): template-sweep fixes — telemetry validity, ESO rollout checksums, dead passwordKey knob

- telemetry: memory_limiter gets the required check_interval (collector
  failed startup validation whenever telemetry.enabled=true); the jaeger
  exporter was removed from collector-contrib in v0.86 — export to Jaeger
  via its native OTLP endpoint instead (otlp/jaeger, default port 4317)
- app/realtime rollout checksums now hash the ExternalSecret manifest too,
  mirroring the copilot pattern — with ESO enabled the inline Secret renders
  empty, so remoteRefs changes never rolled the pods
- remove the unimplemented existingSecret.passwordKey knob (values, schema,
  README, dead helpers): nothing consumed it, and a non-default value
  silently produced a DATABASE_URL with an unexpandable placeholder; secrets
  must use the standard POSTGRES_PASSWORD / EXTERNAL_DB_PASSWORD keys
- drop the orphaned sim.migrations.labels helper (its only consumer was the
  dead NetworkPolicy rule removed earlier)
- helm test pod image resolves through sim.image so global.imageRegistry
  mirroring applies; NetworkPolicy realtime-ingress comment reflects actual
  traffic direction; smoke unittest suite loads the newly referenced
  external-secret template

* chore(helm): bump chart to 1.1.0 with upgrade notes

Removing (inert) documented values keys and changing the rollout-checksum
inputs is a values-surface change — per SemVer chart conventions that is
more than a patch. Adds an Upgrading section documenting the one-time pod
roll, the removed no-op keys, and the Jaeger-over-OTLP change.

* fix(helm): review round — external-db NP opt-in with real-CIDR-first flow, prod Jaeger OTLP endpoint

- external-db example ships networkPolicy disabled so a verbatim install
  always reaches the database; the scoped egress rule stays as the documented
  opt-in (set your CIDR first, then enable)
- values-production still pointed telemetry.jaeger at the legacy 14250
  collector port — now Jaeger's OTLP gRPC endpoint to match the otlp/jaeger
  exporter

* feat(helm): autoscaling.realtime.enabled toggle so examples can scale the app without unsafe realtime replicas

Cursor correctly flagged that comment-level warnings didn't stop a verbatim
production/external-db install from running the realtime HPA at minReplicas 2
without REDIS_URL (silent cross-pod event loss). Adds an opt-out toggle
(default true — existing deployments unchanged): the realtime HPA renders only
when autoscaling.realtime.enabled, and the realtime Deployment keeps
spec.replicas under its control when the HPA is excluded. The three
autoscaling examples set it false with the Redis rationale; README and
upgrade notes document the toggle.

* fix(helm): review round — whitelabeled realtime HPA opt-out, external-db isolation on with required CIDR in install flow

- values-whitelabeled now actually sets autoscaling.realtime.enabled: false
  (the earlier batch aborted before reaching this file — Cursor caught it)
- external-db keeps networkPolicy enabled (no isolation regression); the DB
  egress CIDR is marked REQUIRED and wired into both documented install
  commands via --set, so the copy-paste flow sets the real subnet in the same
  breath as the DB host

* fix(helm): use a syntactically valid example CIDR in the external-db install commands

An unreplaced <YOUR_DB_CIDR> literal fails Kubernetes CIDR validation and
aborts the install; every sibling placeholder in the same command (host,
username) installs fine and simply doesn't connect until replaced. The CIDR
now behaves the same way: valid example value (10.20.0.0/24), explicitly
marked as the operator's database subnet in both commands and the values
comment.

* fix(helm): remove text after continuation backslashes in external-db install commands

A trailing comment after the line-continuation backslash (and equally a
comment line spliced mid-command) breaks the shell command when copied —
the remaining --set overrides run as separate commands and required-value
validation fails. Both documented commands now reconstruct to bash-clean
multi-line invocations (verified with bash -n); the CIDR guidance lives in
the networkPolicy section comment.

* fix(docs): cloud-tab installs are alternatives via helm upgrade --install

Following Installation and then a cloud tab ran helm install twice for the
same release and failed on the second. The tabs now state they replace the
generic install and use helm upgrade --install, which is idempotent and also
converts an existing generic install to the cloud values.

* fix(docs): honest conversion caveat for cloud-tab upgrades over an existing install

The cloud values rename the bundled Postgres database to simstudio, which
Postgres only applies at first initialization — an in-place conversion of a
generic install would point DATABASE_URL at a nonexistent database. Document
the two safe paths: keep the original name via --set, or uninstall + delete
PVCs and install fresh.

* fix(helm): external-db header install command declares its secrets and includes CRON_SECRET

The primary documented command failed the chart's required-value validation
(missing app.env.CRON_SECRET with cronjobs default-on) and referenced an
undeclared DB_PASSWORD. It now shows the export lines for every variable it
uses and sets CRON_SECRET; both commands verified with bash -n.

* chore(helm): restore values.schema.json formatting — surgical deletions only

The earlier programmatic edit reformatted the whole file (~390 lines of
whitespace churn hiding the 12 real deleted lines). Re-applied the removal
of the dead keys/passwordKey properties as text-level deletions preserving
the original style.

* fix(helm+docs): explicit secret exports in external-db header; conversion must reuse original secrets

- all five export lines are written out (three were only named in a trailing
  comment, so a verbatim copy passed empty required values)
- the cloud-conversion caveat now leads with reusing the original secret
  values (helm get values) — a regenerated ENCRYPTION_KEY makes previously
  encrypted credentials undecryptable
* feat(agent-stream): add agent-events thinking/tool streaming for chat and canvas

Ship the agent-events-v1 protocol with provider tool loops, dual-gated chat thinking, DeepSeek/Groq/OpenAI reasoning wiring, and ChatGPT-like thinking chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): clear stuck streaming UI and format db snapshot

Biome was failing CI on migrations/meta/0261_snapshot.json. Also settle
assistant streaming/tool flags when SSE ends without a terminal frame,
without clobbering Stop's finalized content.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): satisfy biome format and import order

Auto-format the sim package for CI lint:check, and repair the Anthropic
streaming tool-loop payload after an unsafe delete-to-undefined rewrite.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): keep drained answer on abort and update migration journal test

Treat AbortError from reader.cancel as a cancelled pump result so soft-complete
retains answerText. Point the workspace storage migration journal assertion at
0261_chat_include_thinking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): keep Stop notice when server emits cancel error

Ignore terminal SSE error frames after the user aborts so
"Client cancelled request" cannot overwrite "Response stopped by user".

Co-authored-by: Cursor <cursoragent@cursor.com>

* improvement(chat): ChatGPT-style thinking shimmer and stick-to-bottom scroll

Add left-to-right shimmer on live thinking label/body, keep scroll working by
shimmering an inner node, and follow the answer only while near the bottom.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): stop pump on client disconnect; soft-complete agents only

Abort the agent stream pump when the projected HTTP body is cancelled so
provider work does not continue after disconnect. Limit AbortError soft-success
to Agent blocks so Function/HTTP cancels still fail in logs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): persist includeThinking across pause snapshots

Paused chat runs with Include thinking enabled were dropping the flag when
serializing the pause snapshot, so resume always rebuilt streams without
thinking/tool SSE frames.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): keep drained answer text when stream times out

Persist pump answerText onto the streaming execution before throwing on
timeout, and carry that partial content into the failed block output so
logs match what the client already saw.

Co-authored-by: Cursor <cursoragent@cursor.com>

* improvement(chat): auto-collapse tools chrome when tool streaming ends

Match thinking UX: open while tools run, collapse when finished, and keep
the panel open only if the user manually reopens it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): settle canvas stream chrome on failure paths

Clear agentStreamActive and settle running tool chips when blocks error,
timeouts cancel runs, or execution ends without stream:done so the output
panel does not stay on live Thinking/Using tools chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(lint): organize imports in terminal console store

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): mark open tools cancelled on HITL pause

Pause can interrupt a tool loop without tool end events; settling those
chips as success incorrectly showed unfinished tools as complete.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(db): drop branch-local 0261 migration ahead of staging merge

* chore(db): regenerate include_thinking migration as 0266 post staging merge

* fix(providers): resolve type errors in streaming tool loop call sites

* fix(agent-stream): gate agent events opt-in and correct provider loop behavior

- streamToolCalls and provider thinking requests now require run-level
  agentEvents opt-in (canvas on, chat dual-gated, API off) so existing
  runs keep pre-agent-events behavior exactly
- OpenAI reasoning summaries opt-in + strip-and-retry on unverified-org 400
- streaming loops run tool postProcess again (firecrawl/exa async results)
- bedrock live loop falls back to silent path for responseFormat
- deepseek: reasoning_content pass-back unconditional, 'none' sends disabled
- groq: x_groq.usage fallback, reasoning params gated, qwen none disables
- gemini: functionCall parts echoed verbatim, local ids only for events
- truncated turns (max_tokens/length) no longer execute partial tool calls
- MAX_TOOL_ITERATIONS exit flushes last turn text as final answer
- iterations reports actual model calls; shared loop plumbing extracted

* refactor(agent-stream): consolidate protocol, dedupe client/server plumbing, hygiene

- canonical ChatStreamFrame union + type guards consumed by server emitters
  and the chat client; stream_error restored to legacy log-only handling
- strip thinking/tool args from providerTiming on public final envelopes
- shared tool-chip lifecycle module for chat, canvas, and console store
- shared sink-to-execution-events forwarder replaces the copy-pasted
  adapter in the execute route and HITL manager; LIVE_ONLY event set shared
- stream:thinking payload field renamed data->text; canvas thinking batched
- abort reasons carried as AbortError DOMExceptions so raw fetch consumers
  classify correctly; thinking cap renamed to chars and scope-documented
- kimi wired for agent events like the other compat providers
- deleted dead exports/step-N comments; fixtures match real wire shapes;
  loop tests use explicit mocks instead of importOriginal

* test(agent-stream): cover the dual-gated execution path and typed abort reasons

- chat route tests assert agentEvents reaches executeWorkflow only when
  policy and protocol header agree
- execution-limits tests assert AbortError-typed reasons
- executor metadata type carries agentEvents

* fix(deploy-modal): align include-thinking spacing with the modal's 6.5px rhythm

* docs(agent-stream): autogenerate per-model thinking/tool stream support on the Agent block page

- capabilities.thinking.streamed ('full' | 'summary' | 'none') on models.ts,
  explicit for the Anthropic family where visibility varies per generation;
  getThinkingStreamVisibility exposes the derivation for docs and UI alike
- scripts/sync-agent-stream-docs.ts regenerates the support tables between
  markers in workflows/blocks/agent.mdx from the model registry and
  STREAMING_TOOL_CALL_PROVIDERS; --check fails on drift or missing metadata
- wired agent-stream-docs:check into CI next to the other sync gates

* feat(anthropic): request summarized thinking display for omitted-default Claude models

The newest Claude generations (Fable 5, Sonnet 5, Opus 4.8/4.7) default
thinking.display to omitted — empty thinking blocks, no deltas. On
agent-events runs Sim now opts back in with display: 'summarized', driven
by the registry's streamed metadata; legacy runs keep the exact
pre-agent-events request shape. Registry, generated docs, and the family
capability table updated accordingly.

* docs(skills): cover thinking.streamed and agent-stream docs sync in model skills

* chore(deps): upgrade @anthropic-ai/sdk to 0.114.0 and adopt official types

- adaptive thinking, display, and output_config are now SDK-typed; the only
  remaining custom payload field is output_format (beta-header structured
  outputs, which the SDK models as output_config.format instead)
- anthropic stream events narrow on the SDK's discriminated unions instead
  of anonymous casts; compat deltas type content/tool_calls from the OpenAI
  SDK with vendor reasoning fields as an explicit optional extension
- @sim/auth exposes an explicit VerifyAuth contract so its declarations no
  longer reference better-auth's nested zod instance (TS2883 under fresh
  install layouts); realtime consumer aligned
- docs app zod pinned to the repo's exact 4.3.6 so ai SDK types bind the
  same zod instance (docs type-check was latently broken)
- knowledge embedding tests made hermetic against local .env keys and
  hosted rotation fallback

* refactor(providers): replace legacy as-any stream casts with annotated typed casts

* refactor(providers): finish provider audit — remove dead byte-stream helper, annotate remaining legacy casts

Audit of all 26 providers for the agent-events feature confirmed every
streaming execution declares agent-events-v1 and every adapter emits
AgentStreamEvent objects. Cleanup from the audit: the unconsumed legacy
createOpenAICompatibleStream byte helper is deleted, and the remaining
streamResponse-as-any casts (xai, nvidia, kimi, meta, zai, sakana) are
annotated typed casts matching the groq/deepseek fix.

* feat(streaming): stream answer text live during tool loops via turn_end protocol

The live tool loops buffered all answer text per model turn (classification
of intermediate vs final is only known at turn end), so gated surfaces saw
thinking stream, then dead air with the thinking chrome stuck open, then the
whole answer at once.

Loops now emit text deltas live as `turn: 'pending'` plus a `turn_end`
event per turn. The pump buffers pending text and projects it to the byte
path (answerText/logs/memory/legacy clients) only on a final turn_end, so
all settled semantics are unchanged. Gated surfaces render the pending text
as it streams and reconcile with a reset when a turn resolves to tools:

- public chat: live `chunk` frames from the sink + dual-gated `chunk_reset`;
  byte-path frame emission is suppressed to avoid duplicates (kept for
  response-format transformed streams via clientStreamTransformed)
- canvas: forwarder emits live `stream:chunk` + `stream:chunk_reset`; the
  execute route and HITL resume readers stop re-emitting byte chunks; panel
  chat tracks per-block segments and replaces content on flush
- chat client: per-block text segments, chunk_reset handling, and thinking
  chrome now settles on tool start as well as first answer chunk

* fix(streaming): address validated review findings across provider gating and reset reconciliation

Three-reviewer pass over the branch, findings validated against staging:

- agent-handler forwards agentEvents to executeProviderRequest — the flag was
  computed but dropped in the field-by-field copy, so provider-side thinking
  requests (OpenAI summaries, Gemini includeThoughts, Anthropic summarized
  display) never activated on opted-in runs
- openai: restore summary:'auto' alongside explicit reasoning effort — staging
  always paired them; gating summary purely on agentEvents changed legacy
  payloads
- gemini: Gemini 2 + tools + responseFormat falls back to the silent path;
  the live loop never applied the deferred responseSchema for AUTO tools
- openai-compat loop: malformed tool-argument JSON fails the call instead of
  executing with defaulted {} args (staging parsed inside the execution try)
- openai-compat parser: a vendor id arriving after a synthesized start no
  longer renames the call (start/end ids stayed consistent)
- stream-pump: abort closes the byte projection so a drain blocked on
  backpressure cannot deadlock teardown
- chunk_reset removes the block from the client text order (deployed chat +
  panel chat) so a reset block re-registers at arrival position — fixes
  separator/order corruption when parallel blocks stream around a reset
- resume route echoes the negotiated X-Sim-Stream-Protocol response header
  (parity with the chat route); docs: [DONE] wire shape + final-vs-error
  terminal semantics corrected

* chore(deps): exempt pinned @anthropic-ai/sdk 0.114.0 from the release-age gate

CI's bun install --frozen-lockfile blocks 0.114.0 (published 2026-07-23,
younger than the 7-day supply-chain gate). The pin is exact and was vetted
for the agent-events streaming work; following the existing bunfig pattern,
the exclusion ages out on 2026-07-30 and should be dropped then.

* chore(providers): fix double-cast-allowed annotation placement for the strict boundary audit

The audit only recognizes the annotation on the line directly above the cast;
two annotations had drifted behind intervening code lines (groq stream params,
deepseek loop messages) and the OpenAI reasoning-summary widening cast was
never annotated. No behavior change.

* fix(chat): settle straggler tool chips as error when final reports failure

A failed run can still terminate with a `final` frame carrying success: false;
running chips previously settled green regardless of the outcome.

* fix(canvas): wire agent stream chrome into run-from-block

Run-from-block executions emit the same live stream:thinking/stream:tool
events as full runs but registered none of the handlers, so the terminal
never showed thinking or tool chips on that path. The per-run chrome
(batched thinking writes + tool chip lifecycle + settlement on stream done,
block error, and every terminal execution state) is extracted into a shared
createAgentStreamChrome factory consumed by both paths.

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
* feat(skills): permissions layer

* chore(db): drop skill_member migration 0261 for regeneration on latest staging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0262 on latest staging

Same DDL as the dropped 0261 (skill_member table, enums, indexes, skill.workspace_shared)
plus the hand-written write-user backfill, renumbered after staging's 0261.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0263 after staging merge

Staging claimed 0262 (strong_storm); same DDL plus the hand-written
write-user backfill, renumbered on the merged snapshot chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(db): drop skill_member migration 0263 for regeneration on latest staging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0264 after staging merge

Staging claimed 0263 (workflow_fork_sync_excluded); same DDL plus the
hand-written write-user backfill, renumbered on the merged snapshot chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* make editing skills full page

* fix disclaimer

* edit access msg

* fix lint

* chore(db): drop skill_member migration 0264 for regeneration on latest staging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0265 after staging merge

Staging claimed 0264 (fat_ikaris); same DDL plus the hand-written
write-user backfill, renumbered on the merged snapshot chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix tests

* simplify system

* fix

* fix lint

* add mship skills docs

* chore(db): drop skill_member migration 0265 for regeneration on latest staging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0266 after staging merge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(deps): override zod to 4.3.6 to dedupe nested copies breaking type-check

better-auth 1.6.23 and fumadocs-mdx resolve ^4.3.6 to a nested zod 4.4.3,
which makes @sim/auth's inferred betterAuth types non-portable (TS2883) and
split docs onto a second zod instance. Both ranges accept the repo-wide
pinned 4.3.6, so a single hoisted copy satisfies everything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix lint

* feat(skills,tools): fullscreen skill create + shared custom tool editor

Moves the rich-markdown and custom-tool editing surfaces out of modals and
onto full-page surfaces, and collapses the duplicated chrome behind shared
components.

Skills
- Add /skills/new, a full-page create surface mirroring the skill detail page
  (CredentialDetailLayout + DetailSection + unsaved-changes guard). "Add to
  Sim" navigates there instead of opening a modal.
- Import moves to a header action (SkillImportButton) backed by a shared
  readSkillFile helper; the GitHub-URL import and its /api/skills/import route
  are removed.
- Skill name validation is now one shared validateSkillName, replacing three
  copies of the kebab-case rule and its messages.
- The skill editor roster renders through the shared MemberRow instead of
  re-deriving its identity block, with a locked role control and a lock-reason
  tooltip explaining inherited workspace-admin access.

Custom tools
- Extract the canvas modal's schema/code editors into a shared
  custom-tool-editor module (fields, wand generation, schema helpers), cutting
  custom-tool-modal.tsx by ~900 lines.
- Settings > Custom tools gains a full-page detail sub-view (SettingsPanel +
  SettingsSection + saveDiscardActions), deep-linkable via ?custom-tool-id.
  Rows are clickable; delete now lives only in the detail view.
- Replace legacy Button/Input/Badge/Label with the chip family, move chip-field
  chrome into CodeEditor behind an error prop, and delete its dead wand button.

Rich markdown field
- maxHeight is now opt-in: omit it on a page and the editor grows with its
  content so the page owns the only scrollbar. Modals pass explicit caps.
- The field variant drops to font-weight 400 to match adjacent chip fields.

* fix(skills): address review round on create navigation, 409 copy, and editor audit

- Skill create navigated using the first element of the upsert response, but
  that endpoint returns the caller's whole skill list (built-ins prepended) —
  match the new skill by its workspace-unique name instead.
- The suggested-skill 409 toast claimed the skill existed but was not shared
  and told the user to ask a skill admin. Every workspace member can already
  see and use every skill, so a 409 only means the name is taken.
- Adding an editor emitted the skill_shared event and SKILL_MEMBER_ADDED audit
  even when onConflictDoNothing skipped the insert on a concurrent add. Gate
  both on the insert actually returning a row.

* chore: format skills-resolver test import

* fix(skills,tools): audit fixes — autocomplete boundary, resize clipping, error routing

Two real regressions introduced while simplifying the extracted editor:

- The schema-param autocomplete's trigger was rewritten to match a trailing
  identifier, but the completion still split on separators. The two disagreed,
  so typing `data.ci` opened the menu and selecting replaced `data.ci` whole —
  eating the member-access prefix. Both now share one SCHEMA_PARAM_WORD regex.
- The uncapped markdown field measured its height only on value change while
  always setting overflow-hidden, so any width change that re-wrapped lines
  clipped the tail with no scrollbar to reach it. Now re-measures via
  ResizeObserver.

Also from the audit:
- Generation writes bypass the code field's change handler, so an open
  autocomplete stayed over a disabled streaming editor; close it on busy.
- Delete failures rendered in the Schema section's error slot on both custom
  tool surfaces; route them to a toast instead.
- Skill create navigated away while still dirty, stranding the unsaved-changes
  guard's history sentinel so Back landed on an empty create form.
- The Description field on skill create never received its error border.
- Drop a double-applied opacity-50 (the editor already dims when disabled), a
  dead try/catch around a non-throwing call that also shadowed the error prop,
  and a stale reference to a /tools page that does not exist.
- Docs still described the removed GitHub-URL import and the old Add Skill
  dialog; rewrite for the create page and file/paste import.

* feat(tools): read-only tool detail, create lands on the new tool, drop dead wand prompt API

- Viewers without edit rights could not open a custom tool at all, while the
  equivalent skill and custom-block surfaces both offer a read-only view. The
  detail page now takes `readOnly`: editors inert, no Save/Discard/Delete, no
  Generate. Creating still requires edit rights.
- Creating a tool bounced back to the list while creating a skill lands on the
  new skill. Tools now do the same. The upsert returns the workspace's whole
  tool list (newest first) rather than just the new row, so the id is matched
  by title instead of by index — the same trap that produced the skill-create
  navigation bug.
- Remove `openPrompt`/`closePrompt` from useWand. `closePrompt`'s last callers
  went away with the custom-tool-modal extraction and `openPrompt` had none
  before it; nothing reads `isPromptVisible` any more either.

* fix(tools): read-only editors, design-system wrench, skills-matching tool identity

- readOnly never reached the editors: the prop gated actions and Generate but
  the schema and code fields were still typable for viewers without edit rights.
  Wire disabled through both fields into CodeEditor.
- The row icon used lucide's Wrench (strokeWidth 2) where @sim/emcn/icons ships
  one drawn for this system (1.55, tuned viewBox), and it inherited body text
  colour instead of --text-icon. Swap it.
- Give the tool detail page the same identity heading as skill detail: tile,
  name, and description at the top left, instead of only a header title.
- Extract ResourceTile so the skills and tools tiles share one definition
  (SkillTile now composes it), and add an opt-in `iconFilled` to
  SettingsResourceRow so the tools list tile matches the skills gallery. Both
  default to today's behaviour for every existing consumer.

* fix(mentions): use the product's own glyph for every @ mention kind

The `@` menu and the inserted chip mapped kinds to arbitrary lucide icons —
`Sparkles` for a skill, a generic `File` for every file — while the rest of the
product has a settled glyph per resource. Mirror CHAT_CONTEXT_KIND_REGISTRY,
which Chat's `@` menu already renders from:

- skill now uses AgentSkillsIcon, the same glyph SkillTile shows everywhere
- workflow / folder / table / knowledge use the @sim/emcn/icons set the sidebar
  and the chat registry use
- file derives its icon from the filename extension, so a .pdf and a .csv are
  distinguishable, matching the file list and Chat's context chips
- integration keeps the block's brand icon from the registry

Also drop the generic placeholder. `kind` is untrusted — the node schema
defaults it to `''` and a hand-written `sim:` link can carry anything — but an
unrecognized kind now yields no icon instead of a meaningless box, which is what
the chat registry does. The menu already guarded a missing icon; the chip now
does too, so this cannot crash on a malformed link.

* chore(db): drop skill_member migration 0266 for regeneration on latest staging

* feat(db): regenerate skill_member migration as 0267 after staging merge

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Waleed Latif <walif6@gmail.com>
…face (#5916)

* refactor(skills): one definition of the skill fields across every surface

The Name / Description / Content trio was implemented three times — once in the
canvas modal and once each in the create and detail pages — and had already
drifted: the name hint appeared on create, was missing entirely on detail, and
sat in a different slot in the modal; the content editor was 260px on the pages
and 200px in the modal; only the modal marked the fields required.

Extract SkillFields, the DetailSection form both full-page surfaces render, and
move the shared copy into skill-copy.ts. The modal keeps ChipModalField — that
is required inside a ChipModalBody, so it cannot share the pages' JSX — but now
reads the same placeholder, hint, and max-length constants, so the wording can
no longer diverge.

The detail page's local FieldLockTooltip moves into SkillFields and is now
available to both pages, and the dynamic rich-editor import drops from three
copies to two.

No behavioral change beyond the drift being resolved: the name hint now shows on
the detail page as it already did on create.

* refactor(skills): derive modal saving state, collapse the field message line

From the cleanup passes over this branch:

- SkillModal mirrored `mutation.isPending` into a local `saving` useState, which
  .claude/rules/sim-hooks.md forbids. It also carried a latent bug: the
  `finally { setSaving(false) }` ran after `onSave()` had closed the modal, so it
  set state on an unmounting component, and a throw from `onSave()` would leave
  the modal stuck in a saving state. Derived from the two mutations instead.
- The hint/error line under a field was written three times in SkillFields, in
  three slightly different shapes. One FieldMessage helper now renders all three.
- The name hint is an authoring instruction, so it no longer shows under a field
  that cannot be edited (a built-in skill, or a viewer who is not an editor).
…y chat answers) (#5915)

* fix(providers): stop the final regenerated stream from re-calling tools and clobbering the answer

After the silent tool loop settles, OpenAI Responses and Gemini re-issue a
streaming request purely to stream the answer as prose — but with tools still
attached and auto tool choice, a reasoning model can re-decide to call a tool
there. Streamed calls are never executed on this path, so the run ends with a
dead function call, an empty streamed answer, and the stream callback
clobbering the tool loop's settled text with '' (deployed chat rendered
{"content": ""}).

Force tool_choice 'none' / functionCallingConfig NONE on the regeneration and
keep the tool loop's settled answer whenever the stream ends without text.

* feat(openai): settled tool chips on the regenerated answer stream

The silent Responses tool loop has no live stream while tools run, so opted-in
consumers saw no tool chips at all for OpenAI. The loop now records each
executed call and prepends settled tool_call_start/end pairs (name + status
only) to the agent-events stream ahead of the regenerated answer. Runs without
a sink never see these events, so legacy output is unchanged.

* fix(providers): apply the regeneration guard fleet-wide

Audit of every silent tool loop for the same race fixed for OpenAI/Gemini
(final regenerated stream re-calls a tool that is never executed, ending with
an empty answer that clobbers the settled text):

- anthropic (both implementations): tool_choice {type:'none'} on the
  regeneration (tools must stay — history carries tool_use blocks) + keep the
  settled answer when the stream ends without text
- groq: was re-applying the ORIGINAL tool_choice, so forced-tool runs
  re-forced the tool on the regeneration — guaranteed dead call; now 'none'
  + fallback
- deepseek, mistral, cerebras, azure-openai (legacy chat path), openrouter,
  xai: 'auto' -> 'none' + fallback
- bedrock: fallback only — Bedrock's ToolChoice has no 'none' and toolConfig
  is required when history carries toolUse blocks

Already guarded (no change): meta, sakana, nvidia, vllm, litellm, baseten,
together, fireworks, kimi, zai, ollama.
Co-authored-by: Sim Pi Agent <pi@sim.ai>
…wedged saves (#5905)

* fix(data-retention): clamp sub-day retention values so saves aren't wedged

A stored value under 12 hours rounded to '0' days on load and was re-sent as 0, which the contract rejects (min 24) — blocking every save on the page, including unrelated fields.

Clamp hours->days into the contract's range on read, and throw instead of emitting 0/NaN on write.

* improvement(docs): rewrite data retention for workspace overrides + PII redaction

- Document PII redaction (Logs / Workflow input / Block outputs stages, entity types, languages, custom regex patterns)
- Replace the stale 'no per-workspace overrides' section with the retention-policies list and override inheritance
- Correct log retention (also covers background job logs) and soft deletion (adds Chat conversations, KB documents)
- Add PII + override screenshots, refresh the main one
* feat(sso): DNS domain verification gating org SSO registration

Add org-scoped domain ownership verification (DNS TXT challenge) as the
security precondition for configuring SSO. Closes the first-come domain-claim
vuln where any org could wire another company's domain to its own IdP.

- New sso_domain table + migration 0266; existing org SSO domains are
  grandfathered as verified so live tenants are unaffected
- Verified-domains settings UI (enterprise-gated) with add/verify/remove
- Register route now requires a verified domain for org-scoped registration;
  personal SSO and already-grandfathered domains are unaffected
- Self-host register script writes the verified sso_domain row directly, so
  script-driven registration stays backwards compatible

* fix(sso): harden domain verification against concurrency + fix CI lint

Addresses review findings on state invariants under concurrent/failed writes:
- Add unique index on (organization_id, domain) so concurrent claims can't
  create duplicate pending rows; POST re-reads and stays idempotent on conflict
- Verify flips the row only if it's still the exact pending challenge checked
  (guards deletion/token-rotation mid-DNS-lookup) and maps the partial unique
  index violation to 409 instead of an unhandled 500
- Wrap the self-host script's provider write + verified-domain upsert in a
  transaction so a failed ownership write can't leave a provider committed
- Format 0266 snapshot/journal with biome (fixes @sim/db lint:check)

* fix(sso): re-check domain verification before provider write (TOCTOU)

The register gate checked the verified sso_domain row only at handler entry,
then ran OIDC discovery before writing the provider. A verified row removed
during that window could still complete registration. Extract the check into a
closure and call it both as an entry fast-fail and authoritatively right before
registerSSOProvider, alongside the existing domain-conflict re-check.

* fix(sso): stop rotating verification token on idempotent re-add

Re-adding a pending domain rotated its verification token, which invalidated a
TXT record the admin may have already published and — under two concurrent
re-adds — could return a token the racing write had already superseded, so the
admin's DNS record would never verify. Return the existing row unchanged
instead; the pending token is always shown in the UI, so it is never lost.

* fix(sso): close register TOCTOU with compensating delete + harden edges

Audit-driven hardening:
- Close the residual register TOCTOU: registerSSOProvider is create-only (throws
  if the providerId exists), so a compensating delete after the write is provably
  safe — it can only remove the just-created row. If verification was revoked
  during the write, roll the provider back and 403.
- Verify is now idempotent under concurrency: a same-org row already flipped to
  verified by a racing request returns 200, not a confusing 409.
- Grandfather backfill + self-host script now match normalizeSSODomain's dominant
  transforms (lower + trim + strip leading wildcard) so a non-canonical legacy
  domain can't miss the runtime gate's lookup. Prod backfill result is unchanged.
- Cleanup: drop dead default export, align card radius to sibling convention.

* fix(sso): redact domain tokens from non-admins + fix script stale-update

Round-5 review findings:
- GET /domains redacted the pending TXT verification token (a management
  secret) to any org member. Now only owner/admins read it; members see the
  list and status without it. Non-Enterprise orgs get an empty list (entitlement
  flag only), never the domains/tokens.
- Self-host script decided update-vs-insert from a read taken OUTSIDE the
  transaction; a provider deleted mid-flight made the UPDATE match zero rows
  silently while the verified-domain upsert still committed (orphaned domain).
  The decision now happens inside the transaction from the UPDATE's row count.

* docs(sso): drop unshipped enforce-SSO / auto-join copy from verified domains

Verified domains currently only gate SSO configuration. Remove the
forward-looking references to enforcing SSO and auto-joining members (deferred
to a later release) from the docs, settings copy, nav description, and schema
comment so we don't promise unshipped features.

* fix(sso): guard rollback to new providers only + Enterprise-gate domain removal

Round-6 review findings:
- The compensating provider rollback now only fires when the provider did not
  exist before this request (providerExistedBefore). registerSSOProvider is
  create-only today so reaching the rollback already implies a fresh create, but
  this makes the safety local and future-proof: if Better Auth ever allowed
  updating an existing provider, a revoked-verification rollback must not delete
  that pre-existing row.
- DELETE /domains now requires an Enterprise plan like add/list/verify, so all
  domain mutations share one entitlement (the UI already hides removal from
  non-Enterprise orgs). Adds a delete-route test.

* fix(sso): roll back the SSO provider by row id, not logical keys

The compensating rollback deleted by (providerId, orgId). providerId is unique,
so if this request's row were deleted and recreated by a concurrent registration
in the narrow window before the rollback, the logical-key delete would remove
that other request's provider. Delete by the primary-key id registerSSOProvider
returns instead, so only the exact row this request created is ever removed.

* chore(sso): final-review polish — trim script read, unify copy, doc migration edge

Cosmetic cleanup from a final 4-track adversarial review (no bugs found in the
new logic):
- Self-host script: narrow the pre-transaction existence read to select({ id })
  instead of SELECT * (it only feeds a log line now).
- Unify invalid-domain copy ("for example acme.com") and the verified-elsewhere
  409 wording ("is already verified by another organization") across routes.
- p-3 shorthand on the domain row card.
- Document the migration's rare two-orgs-share-a-domain grandfather behavior
  (login unaffected; validated no such duplicates in prod).

* fix(sso): apply attribute mapping + make SSO edit work; drop dead guard

Two pre-existing SSO bugs the final review surfaced (prod has one SSO org, RVW,
script-registered with the default mapping, so neither change affects it):

- Attribute mapping was passed at the top level of the register payload, which
  Better Auth ignores — it reads oidcConfig.mapping / samlConfig.mapping. Nest it
  so custom mappings actually apply. (Default mapping is unchanged, so existing
  logins are unaffected.)
- Editing an SSO provider was broken: registerSSOProvider is create-only and
  threw on the existing providerId → generic 500. Route now detects a provider
  the caller already owns and updates it via Better Auth's updateSSOProvider, and
  surfaces Better Auth's own error status/message instead of a blanket 500.

Also drops the now-unnecessary providerExistedBefore guard (the rollback deletes
by the created row's primary-key id and register is create-only) and the earlier
final-review polish (script read, unified copy, migration edge note).

Smoke-test SSO login + edit on staging before merge (auth-path change).

* fix(sso): require null org on personal-mode provider lookups (gate bypass)

The personal branch of both provider-ownership lookups keyed on
(providerId, userId) without requiring organizationId IS NULL. Because org
providers store userId = their creator and providerId is globally unique, an org
admin could send a personal-mode request (no orgId) — which skips the membership
check and the domain-verification gate — yet still match, and then via the new
update path move, their org's provider to an unverified domain. Add
isNull(organizationId) to the personal branch of both clauses so it can only
match a genuinely personal provider, matching the route's own isOwnedByCaller.

Found by an adversarial review of the update path added in 394bda9.

* fix(sso): script updates the observed provider by id, not providerId

Inside the registration transaction the script updated WHERE providerId — the
logical key. If the observed provider was deregistered and a replacement created
with the same providerId before the transaction ran, that update would clobber
the replacement's config and ownership. Update the specific observed row by its
primary-key id instead; if it's gone we insert, which fails cleanly on the
providerId unique constraint rather than overwriting the replacement.

* fix(sso): script upserts provider via delete-then-insert (no unique constraint)

sso_provider.provider_id is a plain (non-unique) index and prod holds legitimate
duplicates, so the previous "update by id, else insert" could create a duplicate
provider when the observed row was deregistered and replaced before the
transaction — the fallback insert would succeed. Delete every row for the
providerId then insert exactly one, inside the transaction, so the providerId
ends up as exactly this config atomically. Linked accounts key on the providerId
string (not the row id), so existing logins are unaffected.

* fix(sso): guard compensating-delete row id so rollback can't silently no-op

* chore(sso): regenerate migration as 0268 after merging staging

Staging landed migrations 0266/0267, colliding with our 0266. Removed our
migration, merged staging, and regenerated cleanly with drizzle-kit as
0268_sso_domain_verification (identical sso_domain table + indexes), then
re-appended the grandfather backfill. api-validation baseline reconciled to 973
(staging 970 + our 3 domain routes). Also make the register-route test's
registerSSOProvider mock return an id so the guarded compensating delete runs.

* refactor(sso): share normalizeSSODomain via @sim/utils so script matches gate

The self-host script canonicalized SSO domains with a minimal inline transform
(lower+trim+wildcard) that diverged from the app's full normalizeSSODomain
(protocol, port, path, trailing dot, email local part) — equivalent spellings
could store a different ownership key than the runtime gate looks up. Move
normalizeSSODomain into @sim/utils/sso-domain (a pure function) so the register
route, the domain-claim route, and the script all use the identical canonicalizer.
The script now skips the verified-domain record when SSO_DOMAIN isn't a valid
registrable domain instead of storing a malformed key.
* feat(providers): add Claude Opus 5 model

* fix(providers): route Opus 5 through adaptive thinking

Opus 5 supports only adaptive thinking (no extended thinking / budget_tokens),
same as Opus 4.8/4.7. Add opus-5 to supportsAdaptiveThinking so thinking
requests use thinking.type: adaptive instead of falling through to budget_tokens
extended thinking, which Opus 4.7+ rejects with a 400.

* docs(agent): regenerate agent-stream docs for Opus 5
…ides (#5928)

* content(library): add observability, procurement, and MCP security guides

Three AEO guides, each dated into a recent empty day in the posting
calendar (2026-07-19, 07-21, 07-22) so publication is spread across days
rather than dumped on one.

- ai-agent-observability: what observability is, why APM falls short for
  non-deterministic agents, what to instrument per lifecycle stage, and the
  signals to track
- ai-agents-in-procurement: what procurement agents do, buy-vs-build, where
  they add value, and how to start narrow
- mcp-security: tool poisoning, confused-deputy/OAuth flaws, and supply-chain
  risk, plus how to build and govern MCP servers securely

Every third-party factual claim carries an outbound citation to a verified
primary source (OpenTelemetry semconv, Fiddler, LangChain and PwC surveys,
Icertis/ProcureCon, Ironclad, GEP, the MCPoison/CurXecute CVEs on NVD,
Anthropic's MCP announcement, RFC 8707/9700, OAuth 2.1, the MCP auth spec),
and each post carries 3 internal links to related library posts. One claim
from the source copy — a "November 2025" date on the WhatsApp MCP exfil case
— could not be verified and was dropped; the case itself is cited to Docker's
writeup. Covers come from the autogeneration pipeline via the standard
/library/<slug>/cover.jpg path.

* fix(library): correct two unverifiable claims in the new guides

Accuracy pass on the three new posts turned up two claims that could not be
substantiated:

- HIPAA compliance: the procurement post claimed Sim has "SOC2 and HIPAA
  compliance," but the canonical compliance data (lib/compare/data/sim.ts)
  states SOC2 only, and explicitly that Sim offers self-hosting "beyond SOC2,
  rather than additional certifications." Removed HIPAA; kept SOC2 plus
  self-hosting for data residency. (The same claim exists in ~5 pre-existing
  library posts and should be corrected separately.)
- "more than 18,000 servers were listed on MCP Market": no source
  substantiates this figure. Replaced with "thousands of community-built
  servers," which the ecosystem supports, keeping the Anthropic and MCP Market
  links.

Every other third-party claim was verified against a primary source (both
CVEs on NVD, RFC 8707 title, RFC 9700 as January 2025, and all six survey
statistics).
…uite (#5927)

* improvement(ship): pre-flight regenerate artifacts + parallel audit suite

Add a two-phase pre-ship step: (A) regenerate every committed artifact
(agent-stream-docs, skills, contract syncs) in parallel so generated files
never drift into a CI failure, then (B) run lint plus the full audit suite
CI's Lint and Test job enforces, in parallel, aborting ship on any failure.
Regenerate the ship command projections.

* fix(ship): scope Phase A to in-repo generators + propagate failures

Cursor review: (1) mship:generate is an umbrella over the 9 contract
generators and biome-formats the shared generated dir, so running it
alongside its constituents races/corrupts; it also reads an external
copilot-contract source that ENOENTs in a bare worktree. (2) bare wait
swallowed generator exit codes. Narrow Phase A to the always-in-repo
generators (agent-stream-docs, skills) and collect each job's exit
status in both phases; document domain generators as on-demand only.

* fix(ship): make pre-flight status checks exit non-zero on failure

Cursor/Greptile: grep && echo ❌ || echo ✅ always exits 0, so a failed
generator/audit didn't actually gate ship (regressed the sequential
bun-run checks whose non-zero exit agents rely on). Replace both Phase A
and Phase B aggregations with an if-grep that exits 1 on any failure.

* fix(ship): gate lint exit in Phase B pre-flight

Cursor: bare bun run lint before the audit grep meant a non-zero lint
was ignored — if audits then passed, the block exited 0 and ship
continued. Gate lint with || { echo …; exit 1; } so an unfixable lint
error aborts before the audits run.
…end gotcha (#5931)

* fix(sso): surface DNS verification failures and the provider auto-append gotcha

Post-merge audit follow-ups for verified domains (#5909):

- The host field handed admins the FQDN `_sim-challenge.acme.com`. GoDaddy,
  Namecheap, Hover and most cPanel panels append the zone to whatever is typed,
  yielding `_sim-challenge.acme.com.acme.com` — the record looks right in their
  panel but never verifies, and our 422 tells them to wait 48 hours. Add a hint
  under the field (and a docs callout) telling those admins to enter just the
  label.
- DNS failures were logged at debug, but production log level is ERROR, so a
  blocked-egress or SERVFAIL condition was invisible to us and misreported to the
  admin as "record not found yet". Log infrastructure-class failures at warn with
  the DNS error code; keep the genuinely-absent codes at debug.
- Trim the joined TXT value before comparing: several DNS panels pad the stored
  string, which otherwise fails an exact match forever.
- Lower the resolver to 2s/1 try. c-ares multiplies timeout across servers and
  retries by ~7x, so the previous 5s/2-try config could block a verify request
  for ~35s when resolvers are unreachable.
- Cover `checkDomainTxtRecord` — the function that decides whether the gate opens
  had no tests. Adds exact match, chunk-joined value, match among unrelated
  records, padded value, near-miss, another org's token, absent record,
  infrastructure failure, and empty-response cases.

* fix(sso): log DNS infrastructure failures at error so prod actually surfaces them

Production's default minimum log level is ERROR, so the warn introduced in the
previous commit was still filtered out — the fault stayed invisible exactly as
before. A resolver failure that is not 'record absent' is a genuine
infrastructure error, so ERROR is both the visible and the honest severity.

* fix(sso): state the zone-removal rule instead of a wrong subdomain hint

The hint computed the bare label as the first segment of the challenge host, so
for a subdomain like eng.acme.com it advised entering `_sim-challenge` when the
host relative to the acme.com zone is `_sim-challenge.eng` — following it would
publish the record on the wrong name and verification would never succeed, the
exact failure the hint exists to prevent. Deriving the real zone needs the Public
Suffix List (acme.co.uk defeats naive label-stripping), so state the rule instead:
enter the host with the trailing zone removed. Docs show both the apex and the
subdomain form.
…5935)

.npmrc set ignore-scripts=true globally, which overrode Bun's
trustedDependencies allowlist and blocked every lifecycle script —
including the repo's own root scripts. isolated-vm therefore shipped
unbuilt and each contributor had to npm rebuild it by hand.

The .npmrc line landed in the May 2025 Bun migration, seven months before
isolated-vm was introduced. Two later commits added isolated-vm to
trustedDependencies (root, then apps/sim) and both were silently inert.

- delete .npmrc; lifecycle policy now lives solely in root trustedDependencies
- add isolated-vm to root trustedDependencies
- drop the workspace-level trustedDependencies from apps/sim (Bun only reads
  the root list, so it never had any effect)
- drop the .npmrc entry from CODEOWNERS

Naming any package in trustedDependencies replaces Bun's curated
default-trust list, so only isolated-vm and sharp may run install scripts.
Docker is unaffected — all three Dockerfiles pass --ignore-scripts
explicitly and rebuild isolated-vm against the Node ABI by hand.
)

Reconcile compliance and traction wording across three library guides
against the canonical data in lib/compare/data/sim.ts, and tighten a couple
of related phrasings for consistency.
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner July 24, 2026 19:57
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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

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

@gitguardian

gitguardian Bot commented Jul 24, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
35144032 Triggered Generic Database Assignment d48722a helm/sim/examples/values-external-db.yaml View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@vercel

vercel Bot commented Jul 24, 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 Jul 24, 2026 11:20pm

Request Review

@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Org SSO registration now depends on verified domains with TOCTOU rollback—auth misconfiguration could block legitimate tenants. Chat streaming changes affect public SSE clients and dual-gate behavior; regressions could leak thinking into legacy clients or break stop/abort flows.

Overview
This release bundles several user-facing capabilities and hardening changes across chat, enterprise auth, skills, and developer workflow.

Agent thinking and tool streaming adds an opt-in agent-events-v1 protocol: deployed chat only exposes thinking and tool lifecycle when Include thinking is on and the client sends X-Sim-Stream-Protocol (the hosted chat always opts in). The chat UI and useChatStreaming route thinking, chunk_reset for intermediate tool turns, and tool chips separately from answer text, with stick-to-bottom scrolling and shared abort handling. Docs gain generated per-model stream capability tables (agent-stream-docs:generate / CI agent-stream-docs:check) and a new Agent stream events deployment page.

Enterprise SSO now requires DNS-verified domains before org-scoped SSO registration, with re-checks around the write and rollback if verification is revoked mid-flight. SSO saves can update an owned provider via updateSSOProvider, and attribute mapping is nested under oidcConfig / samlConfig where Better Auth reads it. Verified Domains enterprise docs were added.

Skills documentation and product copy cover editors (who can change a skill vs who can use it) and Chat / skill tagging. Ship and add-model skills were expanded to regenerate agent-stream-docs and skills:sync before a parallel CI audit suite.

Smaller doc and marketing updates include data retention (PII redaction, workspace overrides), library/blog “Updated” dates and comparison “last verified” copy, Kubernetes/Helm install fixes, and removal of root .npmrc ignore-scripts.

Reviewed by Cursor Bugbot for commit f0f9870. Configure here.

@github-actions github-actions Bot added the requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep label Jul 24, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Cross-repo companion check

One or more companion PRs aren't merged into main yet (aggregated across the feature PRs in this release). Merging this without them will leave copilot and sim out of sync — merge them in lockstep.

  • ⚠️ simstudioai/mothership#369 — merged into staging (this PR targets main) — feat(sandbox): build the mothership sandbox images on Daytona too

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f0f9870. Configure here.

}
const nearBottom = distanceFromBottom <= NEAR_BOTTOM_THRESHOLD_PX
stickToBottomRef.current = nearBottom
setShowScrollButton(!nearBottom)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stream scroll traps stick-to-bottom

Medium Severity

Programmatic auto-scroll during streaming sets ignoreScrollRef for 50ms on every UI flush, and flushes can fire about every frame. That keeps user scroll events ignored, so stickToBottomRef often cannot flip to false and viewers cannot scroll away while tokens arrive. The stream path always uses behavior: 'auto', where the hold after a synchronous scrollIntoView is unnecessary and causes the lockout.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f0f9870. Configure here.

Comment thread apps/sim/app/api/auth/sso/register/route.ts
@blacksmith-sh

This comment has been minimized.

…O v1 default (#5939)

* improvement(helm): hygiene pass — CI gating, strict values schema, ESO v1 default, ci values

Closes the gaps from a best-practices audit of the chart (template-level
conformance was already clean: full label set, 82 unit tests, kubeconform-
valid renders):

- new Helm Chart workflow gates every chart change: helm lint, the 82
  helm-unittest cases, kubeconform validation of default + all-components
  renders (k8s 1.29 strict, CRD catalog), and a render of all 10 example
  values files
- helm/sim/ci/ values files (chart-testing convention) so the chart lints
  and templates cleanly out of the box with dummy secrets
- values.schema.json declares all 30 top-level keys (16 were invisible) and
  sets root additionalProperties: false, so top-level typos fail fast
- externalSecrets.apiVersion defaults to v1: current ESO releases removed
  the v1beta1 compatibility path in 2026, so the old default produced
  rejected manifests on new installs; NOTES/values comments updated
- wait-for-postgres init container gets requests/limits (the only container
  in the chart without them; broke ResourceQuota'd namespaces)
- drop the telemetry Prometheus scrape config for app/realtime — neither
  exposes /metrics, so it was dead config that also rendered a realtime
  target with realtime disabled
- chart 1.2.0 with README upgrade notes

* fix(helm): version-agnostic schema-error assertions in the secret-length suite

Helm v4 phrases schema rejections as 'minLength: got N, want 32' while
v3 says 'String length must be greater than or equal to 32'. The suite
grepped for 'minLength' only, so it passed on local helm v4 and failed on
CI's v3.16.4 — the enforcement itself works on both. Patterns now assert
the key name plus either wording. Caught by the new Helm Chart workflow
on its very first run.

* improvement(helm): kind install test + chart version-bump gate in CI

Benchmarked against the flagship OSS charts (ingress-nginx, argo-cd,
kube-prometheus-stack, grafana, bitnami, cert-manager): sim already exceeds
most of them on validation rigor (strict schema — 4 of 6 ship none; 82 unit
tests vs argo-cd's zero; kubeconform manifest validation none of them run),
but every top community chart repo actually installs the chart on a kind
cluster in chart CI — the one majority practice we lacked. Adds:

- install job: kind cluster, helm install with ci/default + a new
  small-footprint ci/kind-values.yaml overlay (default app requests of 4Gi
  can't schedule on a CI node), --wait, then the chart's helm test hook,
  with pod/event/log diagnostics on failure
- version-bump job (PR-only): fails when helm/sim/** changes without a
  Chart.yaml version increment — argo/kps/grafana all enforce this

* fix(helm): declare naming overrides in the strict schema; SHA-pin CI actions

- nameOverride/fullnameOverride are consumed by sim.name/sim.fullname but
  were never in values.yaml, so root additionalProperties: false rejected
  Helm's standard naming overrides — both now declared (a helper-wide sweep
  confirmed they were the only template-read keys missing), with a smoke
  regression test that installs under the strict schema and asserts the
  override lands in resource names
- CI supply-chain hardening: checkout/setup-helm/kind-action pinned to full
  commit SHAs (tag comments retained) and the kubeconform archive verified
  against its published sha256

* fix(helm): immutable unittest runner and fail-closed version gate in CI

- helm-unittest runs via the project's official docker image pinned by
  immutable sha256 digest instead of a plugin install from a mutable git tag
- the version-bump gate fetches the full base ref (a --depth=1 fetch could
  leave no merge base), computes the merge-base and diff outside the if so
  any git failure fails the job instead of falling into the skip branch

* fix(ci): run the helm-unittest container as the runner UID

The digest-pinned image runs as a non-root user that cannot write into the
runner-owned bind mount (it creates tests/__snapshot__, absent from the
checkout since empty dirs aren't tracked). Standard bind-mount pattern:
--user "$(id -u):$(id -g)" with HOME=/tmp for helm's cache.

* fix(helm): appVersion points at a real GHCR tag (v0.7.44)

The kind install job caught this on its first full run: the default image
tag (Chart.AppVersion 0.6.73) returns 404 on GHCR for all three images —
the registry's tags are v-prefixed — so an unpinned default install could
never pull. Updated appVersion to v0.7.44 (verified 200 for simstudio,
realtime, and migrations manifests), stale values comment refreshed, and
an upgrade note added. The CI kind values deliberately stay tag-free so
the job keeps exercising the true default path.
#5922)

* improvement(providers): validation pass, and stream tool loop improvements

* remove deploy options correctly

* fix

* feat(providers): prompt caching capability and usage-based cache pricing

Replace the arbitrary cached-rate heuristic with a single cache-aware pricing
function, and add prompt caching as an opt-in capability for Anthropic.

Pricing: priceModelUsage in cost-policy.ts is now the only place cache
arithmetic happens. Provider adapters normalize their wire shape into
ModelUsage (input always excludes cache buckets); the pricing function never
branches on provider. This removes five divergent behaviors, including the
!!request.context heuristic that gave Router and Evaluator an unearned 10x
input discount, and the overwrite that silently billed Anthropic cache reads
and writes at zero. Also parses OpenAI cache_write_tokens, previously ignored.

Caching: Anthropic gets a capability-gated advanced switch that places
cache_control on the last tool and last system block; system is now always a
TextBlockParam array. OpenAI gets a stable per-block prompt_cache_key with no
UI, since its caching is automatic.

* fix(providers): route OpenAI and Gemini block cost through cache-aware pricing

Cache-aware pricing only reached trace segments. The billable block cost still
called calculateCost on the cache-inclusive prompt total, so OpenAI cache hits
and Gemini implicit-cache hits were charged at the full input rate and GPT-5.6+
cache writes went unbilled.

Both providers now accumulate cache buckets and price through priceModelUsage,
matching the Anthropic token convention where input excludes cache reads and
writes. Cached counts are clamped to the prompt total so an over-reporting
payload cannot bill more input than the request contained.

* fix(streaming): redact tool payloads on selected outputs in public chat

Redaction only ran on the empty-selection branch, but a deployment almost
always selects outputs, so it was dead in the case it exists for. Selecting
toolCalls streamed the raw arguments and results to a public chat client in a
chunk frame, and providerTiming carried thinking content the same way.

Both paths now extract from the sanitized block output rather than the raw log:
the streamed selected output, which is the reachable vector, and the final
envelope. Sanitizing the source rather than per selected path means a newly
selectable field cannot reopen the hole.

* refactor(providers): drop unreachable billing fallbacks

Every provider pricing helper took a policy parameter no caller passed. Worse
than dead: passing one would have double-applied the margin the central layer
already applies. Removed, so providers can only price at list.

Also removed guards that cannot fire. The central fallback normalized cache
buckets no provider can reach it with (all three that report cache usage price
themselves) and did so at a 1x write multiplier no vendor charges.
priceModelUsage re-validated token counts the adapter had already clamped, and
applyModelCostPolicy defaulted a required total field.

Validation now happens once, in the adapter that parses the vendor payload and
is the only layer that knows cache buckets are a subset of the prompt total.
waleedlatif1 and others added 2 commits July 24, 2026 16:12
…nner sizing (#5945)

* fix(ci): key node_modules sticky disk on the lockfile hash

* improvement(ci): per-image Blacksmith runner sizing + cold-build memory preflight

* fix(deps): exclude @next/swc binaries from the release-age gate

* fix(deps): pin @next/swc binaries so frozen installs get a compiler

* docs(ci): explain ARM runner sizing rationale
…inputs/outputs (#5942)

* improvement(whatsapp): validate + improve integration skill for file inputs/outputs

* fix lint

* add whatsapp subblock migration
#5671 rewrote the shimmer-sweep keyframes to run 100% -> -100%, which is
already left-to-right, but left the `reverse` from #5650 on the animation
shorthand. The two flips cancel into a right-to-left sweep on every
ShimmerText consumer: subagent labels, tool-call rows, and the new
agent-stream thinking chrome.

Drop `reverse` so direction lives in exactly one place — the keyframes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants