Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ See [Observability](/platform/self-hosting/observability).
| `NEXT_PUBLIC_CHAT_DISABLED` | Set to `true` to hide the Chat module: the workspace lands on your first workflow, with no chats list, scheduled tasks, or editor Chat panel. Chat is shown when unset; `bun run setup` sets it for you if you skip the chat key |
| `PII_REDACTION` | Redact PII from workflow logs via Data Retention rules; requires the PII service and a cluster-reachable `INTERNAL_API_BASE_URL` |
| `PII_GRANULAR_REDACTION` | Additionally expose the execution-altering redaction stages |
| `DURABLE_SECRET_PROVENANCE_ENFORCED_SURFACES` | Durable stores where a value whose secret provenance was never recorded fails the run instead of logging a warning. `all`, or a comma-separated subset of `memory`, `table-row`, `knowledge`. Unset (nothing enforced) by default |
| `ADMIN_API_KEY` | Admin API key for GitOps operations and organization provisioning |

## Enterprise Features
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-menu-chip/types'

/**
* The Platform menu - Sim's modules. Six items in a three-column grid. Each
* description names the outcome the module unlocks for your agents.
* The Platform menu - Sim's modules. Five items in a three-column grid, so the
* bottom-right cell is empty. Each description names the outcome the module
* unlocks for your agents.
*/
export const PLATFORM_MENU: NavMenu = {
label: 'Platform',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-m
* `--surface-4` ring (`p-[3px]`, overlay shadow) wrapping an inner `--bg`
* surface, with the item grid padded inside.
*
* The grid renders three visual columns on six tracks (each tile spans two),
* which keeps six-item menus pixel-identical to a plain three-column grid while
* letting a five-item menu center its two-tile last row - the second-to-last
* tile starts on track two, so the short row sits symmetrically instead of
* leaving a hole in the corner.
* The grid is a plain three-column grid filled in reading order, so a menu with
* a non-multiple-of-three item count leaves its gap in the bottom-right corner
* rather than centering the short row.
*/

interface NavMenuChipProps {
Expand Down Expand Up @@ -81,14 +79,7 @@ export function NavMenuChip({ menu }: NavMenuChipProps) {
<div className={cn(PANEL_BASE, !closed && PANEL_REVEAL)}>
<div className='w-[840px] rounded-xl border border-[var(--border-muted)] bg-[var(--surface-4)] p-[3px] shadow-[var(--shadow-overlay)]'>
<div className='rounded-lg border border-[var(--border-1)] bg-[var(--bg)] p-2'>
<div
className={cn(
'grid grid-cols-6 gap-1 [&>*]:col-span-2',
items.length % 3 === 2 && '[&>*:nth-last-child(2)]:col-start-2'
)}
role='group'
aria-label={label}
>
<div className='grid grid-cols-3 gap-1' role='group' aria-label={label}>
{items.map((item) => (
<NavMenuItem key={item.title} item={item} onSelect={handleSelect} />
))}
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/_shell/providers/posthog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import type { PostHog } from 'posthog-js'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { getEnv, isTruthy, publicEnvMissingAtModuleInit } from '@/lib/core/config/env'

const logger = createLogger('PostHogProvider')

Expand Down Expand Up @@ -49,6 +49,9 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
persistence: 'localStorage+cookie',
})
}
if (publicEnvMissingAtModuleInit) {
posthog.capture('runtime_env_missing_at_module_init')
}
clientRef.current = posthog
setProvider(() => PHProvider)
})
Expand Down
27 changes: 13 additions & 14 deletions apps/sim/app/_shell/public-env-script.test.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
/**
* @vitest-environment node
*/
import { EnvScript } from 'next-runtime-env'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { PublicEnvScript } from '@/app/_shell/public-env-script'

/**
* Guards the loading strategy, not the markup. A plain `<script>` rendered from
* the root layout lands after the `<script async>` chunk tags Next emits at the
* top of the document, so a chunk can execute - and hydration can begin - before
* `window.__ENV` is populated. Delegating to `<EnvScript>` keeps the
* `beforeInteractive` guarantee that `next-runtime-env` applies by default.
* Guards the one property that matters: the emitted tag assigns `window.__ENV`
* itself. Next's `beforeInteractive` strategy instead pushes the assignment onto
* `self.__next_s`, a queue `appBootstrap` reads exactly once and abandons when it
* is empty - so whenever the bootstrap chunk runs before the parser reaches this
* tag, the assignment is discarded and `window.__ENV` is never defined for that
* document. See the component's TSDoc for the full ordering argument.
*/
describe('PublicEnvScript', () => {
it('delegates to next-runtime-env EnvScript rather than emitting a raw script tag', () => {
const element = PublicEnvScript()
it('emits a script that assigns window.__ENV directly', () => {
const markup = renderToStaticMarkup(<PublicEnvScript />)

expect(element.type).toBe(EnvScript)
expect(element.type).not.toBe('script')
expect(markup).toContain("window['__ENV'] =")
})

it('does not opt out of the beforeInteractive strategy', () => {
const { disableNextScript, nextScriptProps } = PublicEnvScript().props
it('does not defer the assignment into the __next_s queue', () => {
const markup = renderToStaticMarkup(<PublicEnvScript />)

expect(disableNextScript).toBeUndefined()
expect(nextScriptProps?.strategy ?? 'beforeInteractive').toBe('beforeInteractive')
expect(markup).not.toContain('__next_s')
})

it('passes only NEXT_PUBLIC_ variables through to the browser', () => {
Expand Down
38 changes: 24 additions & 14 deletions apps/sim/app/_shell/public-env-script.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,31 @@ const HOSTED_PUBLIC_ENV = Object.fromEntries(
/**
* Static equivalent of `next-runtime-env`'s `<PublicEnvScript>` for the hosted
* deployment. It renders the library's own `<EnvScript>`, so the emitted markup
* and its `beforeInteractive` loading strategy are identical to the self-hosted
* path - only the env read differs. `<PublicEnvScript>` additionally calls
* `unstable_noStore()`, which opts the entire app into dynamic rendering; that
* only pays off for self-hosted Docker images that re-inject env per deploy
* without a rebuild, so hosted reads the env once here and stays static.
* is identical to the self-hosted path - only the env read differs.
* `<PublicEnvScript>` additionally calls `unstable_noStore()`, which opts the
* entire app into dynamic rendering; that only pays off for self-hosted Docker
* images that re-inject env per deploy without a rebuild, so hosted reads the
* env once here and stays static.
*
* `beforeInteractive` is load-bearing, not an optimization. A plain `<script>`
* rendered from the root layout lands at the end of `<head>`, after the ~40
* `<script async>` chunk tags Next emits at the top of the document; an `async`
* script runs as soon as its fetch resolves, so on a warm cache a Next chunk
* can execute - and hydration can begin - before the parser reaches the env
* tag, leaving `window.__ENV` undefined for the first render.
* `beforeInteractive` instead queues the script into `self.__next_s`, which
* Next's `appBootstrap` drains to completion before calling `hydrate()`.
* `disableNextScript` is load-bearing. Without it, `<EnvScript>` defaults to
* Next's `<Script strategy='beforeInteractive'>`, which does not assign
* `window.__ENV` at all - it emits a tag that pushes the assignment onto
* `self.__next_s`. That queue has exactly one consumer, `appBootstrap`, which
* reads it once and short-circuits to `hydrate()` when it is empty. The
* bootstrap chunk's `<script async>` tag sits ~13KB earlier in the document
* than this tag, so whenever that chunk executes before the parser arrives
* here, the queue is drained empty, nothing ever drains it again, and
* `window.__ENV` stays undefined for the entire lifetime of the document -
* every `getEnv` read empty, until a reload happens to win the race.
*
* A plain `<script>` assigns unconditionally when the parser reaches it. When
* it is reached before the bootstrap chunk runs it lands strictly earlier than
* the queue drain would have; when it is not, the value still arrives a few
* milliseconds late instead of never. There is no supported way to place an
* inline script ahead of the framework's own bootstrap tags - React emits those
* in the preamble, before any content from the component tree - so the goal is
* to make losing that race harmless rather than to try to win it.
*/
export function PublicEnvScript() {
return <EnvScript env={HOSTED_PUBLIC_ENV} />
return <EnvScript env={HOSTED_PUBLIC_ENV} disableNextScript />
}
1 change: 1 addition & 0 deletions apps/sim/app/api/guardrails/validate/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ describe('POST /api/guardrails/validate', () => {
expect(res.status).toBe(200)
expect(mockImportProvenance).toHaveBeenCalledWith(provenance, 'secret value', ['input'], {
trusted: true,
origin: 'guardrailsRoute.inputProvenance',
})
})

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/guardrails/validate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
provenanceInspection.value,
inputStr,
['input'],
{ trusted: true }
{ trusted: true, origin: 'guardrailsRoute.inputProvenance' }
)
).success
: true
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/knowledge/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
!(await importDurableSecretProvenance(
resultSecretRegistry,
metadata.provenance,
renderedMetadata
renderedMetadata,
'knowledge'
))
) {
resultSecretRegistry.markIncomplete()
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/mcp/serve/[serverId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ async function projectWorkflowMcpModelContent(
): Promise<unknown> {
const registry = new ResolvedSecretTraceRegistry([], scope)
const imported = await registry.importCrossingProvenance(privateProvenance, value, {
origin: 'mcpServe.workflowCrossing',
trusted: true,
})
if (!imported || !registry.isComplete()) {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/memory/secret-provenance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export async function createMemoryResponse(options: {
status: sidecar?.status ?? null,
entries: sidecar?.entries,
})
await importDurableSecretProvenance(registry, provenance, record.data)
await importDurableSecretProvenance(registry, provenance, record.data, 'memory')
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/providers/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,10 @@ describe('POST /api/providers', () => {
)

expect(res.status).toBe(200)
expect(mockImportProvenance).toHaveBeenCalledWith(provenance, { trusted: true })
expect(mockImportProvenance).toHaveBeenCalledWith(provenance, {
trusted: true,
origin: 'providersRoute.requestProvenance',
})
})

it('projects legacy private prompt provenance on the provider-facing copy', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/providers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const provenanceReady =
await providerRuntimeContext.resolvedSecretTraceRegistry.importProvenance(
provenanceInspection.value,
{ trusted: true }
{ trusted: true, origin: 'providersRoute.requestProvenance' }
)
if (!provenanceReady || !providerRuntimeContext.resolvedSecretTraceRegistry.isComplete()) {
return NextResponse.json(
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/workflows/[id]/log/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ export const POST = withRouteHandler(
if (trustedProvenance === undefined) {
resolvedSecretTraceRegistry.markIncomplete()
} else {
await resolvedSecretTraceRegistry.importProvenance(trustedProvenance, { trusted: true })
await resolvedSecretTraceRegistry.importProvenance(trustedProvenance, {
trusted: true,
origin: 'workflowLogRoute.trustedProvenance',
})
}
loggingSession.setResolvedSecretTraceRegistry(resolvedSecretTraceRegistry)

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
</>
)}

{isHosted ? <PublicEnvScript /> : <RuntimePublicEnvScript />}
{isHosted ? <PublicEnvScript /> : <RuntimePublicEnvScript disableNextScript />}
</head>
<body className={`${season.variable} font-season`} suppressHydrationWarning>
{/* Google Tag Manager (noscript) — hosted only */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,12 @@ const MONACO_LANGUAGE_BY_EXTENSION: Partial<Record<string, string>> = {
graphql: 'graphql',
gql: 'graphql',
json: 'json',
jsonl: 'json',
/**
* Not `json`: JSON Lines holds one value per line, which Monaco's
* single-document parser flags as invalid. Validation is global
* (`jsonDefaults`), so opting JSONL out is the only per-file lever.
*/
jsonl: 'plaintext',
yaml: 'yaml',
yml: 'yaml',
toml: 'toml',
Expand Down
Loading
Loading