Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ services:
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your_auth_secret_here}
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-your_encryption_key_here}
- COPILOT_API_KEY=${COPILOT_API_KEY}
- MSHIP_SYSPROMPT_OVERRIDE=${MSHIP_SYSPROMPT_OVERRIDE:-}
- NEXT_PUBLIC_CHAT_DISABLED=${NEXT_PUBLIC_CHAT_DISABLED:-}
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL}
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
Expand Down
1 change: 1 addition & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000

# Chat (Optional)
# COPILOT_API_KEY= # Mint one at https://sim.ai. Without it the Sim Chat block, prompt jobs, and Inbox cannot run
# MSHIP_SYSPROMPT_OVERRIDE= # Highest-priority instructions for Mothership; honored only when the validated API key owner is enterprise
# NEXT_PUBLIC_CHAT_DISABLED=true # Hides the Chat module: the workspace lands on your first workflow, and the chats list, scheduled tasks, and editor Chat panel are absent. Chat is shown when unset; `bun run setup` sets this for you if you skip the chat key

# Remote Function sandboxes (Optional)
Expand Down
24 changes: 24 additions & 0 deletions apps/sim/app/api/copilot/api-keys/validate/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const {
mockCheckServerSideUsageLimits,
mockDeriveBillingContext,
mockGetHighestPrioritySubscription,
mockIsEnterprisePlan,
mockRequireBillingAttributionHeader,
mockRequireBillingRequestIdHeader,
mockResolveLegacyV0BillingAttribution,
Expand All @@ -31,6 +32,7 @@ const {
mockCheckServerSideUsageLimits: vi.fn(),
mockDeriveBillingContext: vi.fn(),
mockGetHighestPrioritySubscription: vi.fn(),
mockIsEnterprisePlan: vi.fn(),
mockRequireBillingAttributionHeader: vi.fn(),
mockRequireBillingRequestIdHeader: vi.fn(),
mockResolveLegacyV0BillingAttribution: vi.fn(),
Expand Down Expand Up @@ -105,6 +107,10 @@ vi.mock('@/lib/billing/core/plan', () => ({
getHighestPrioritySubscription: mockGetHighestPrioritySubscription,
}))

vi.mock('@/lib/billing/core/subscription', () => ({
isEnterprisePlan: mockIsEnterprisePlan,
}))

vi.mock('@/lib/billing/core/usage-log', () => ({
deriveBillingContext: mockDeriveBillingContext,
}))
Expand Down Expand Up @@ -162,6 +168,7 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
return ATTRIBUTION
})
mockGetHighestPrioritySubscription.mockResolvedValue(ACCOUNT_SUBSCRIPTION)
mockIsEnterprisePlan.mockResolvedValue(false)
mockDeriveBillingContext.mockReturnValue({
billingEntity: ACCOUNT_BILLING_DECISION.billingEntity,
billingPeriod: {
Expand Down Expand Up @@ -238,6 +245,23 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
expect(mockCheckAttributedUsageLimits).toHaveBeenCalledWith(ATTRIBUTION)
})

it('returns whether the validated key owner has an enterprise account', async () => {
mockIsEnterprisePlan.mockResolvedValueOnce(true)

const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))

expect(res.status).toBe(200)
await expect(res.json()).resolves.toEqual({ isEnterprise: true })
expect(mockIsEnterprisePlan).toHaveBeenCalledWith('user-1')
})

it('returns false when the validated key owner is not enterprise', async () => {
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))

expect(res.status).toBe(200)
await expect(res.json()).resolves.toEqual({ isEnterprise: false })
})

it('preserves account admission for the exact workspace-less old-Go body', async () => {
const res = await POST(request(OLD_GO_WORKSPACELESS_VALIDATE_BODY))

Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/copilot/api-keys/validate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
serializeBillingAttributionHeader,
} from '@/lib/billing/core/billing-attribution'
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
import { isEnterprisePlan } from '@/lib/billing/core/subscription'
import { deriveBillingContext } from '@/lib/billing/core/usage-log'
import {
BILLING_ACCOUNT_DECISION_HEADER,
Expand Down Expand Up @@ -324,9 +325,11 @@ export const POST = withRouteHandler((req: NextRequest) =>
)
}

const isEnterprise = await isEnterprisePlan(userId)

span.setAttribute(TraceAttr.CopilotValidateOutcome, CopilotValidateOutcome.Ok)
span.setAttribute(TraceAttr.HttpStatusCode, 200)
return new NextResponse(null, { status: 200, headers: responseHeaders })
return NextResponse.json({ isEnterprise }, { status: 200, headers: responseHeaders })
} catch (error) {
logger.error('Error validating usage limit', { error })
span.setAttribute(TraceAttr.CopilotValidateOutcome, CopilotValidateOutcome.InternalError)
Expand Down
11 changes: 10 additions & 1 deletion apps/sim/lib/api/contracts/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,15 @@ export const validateCopilotApiKeyBodySchema = z.object({
})
export type ValidateCopilotApiKeyBody = z.input<typeof validateCopilotApiKeyBodySchema>

export const validateCopilotApiKeyResponseSchema = z.object({
/**
* Server-derived entitlement for the validated key owner. Mothership treats
* a missing or false value as ineligible for enterprise-only capabilities.
*/
isEnterprise: z.boolean(),
})
export type ValidateCopilotApiKeyResponse = z.output<typeof validateCopilotApiKeyResponseSchema>

export const listCopilotApiKeysContract = defineRouteContract({
method: 'GET',
path: '/api/copilot/api-keys',
Expand Down Expand Up @@ -486,7 +495,7 @@ export const validateCopilotApiKeyContract = defineRouteContract({
path: '/api/copilot/api-keys/validate',
headers: validateCopilotApiKeyHeadersSchema,
body: validateCopilotApiKeyBodySchema,
response: { mode: 'empty' },
response: { mode: 'json', schema: validateCopilotApiKeyResponseSchema },
error: validateCopilotApiKeyErrorSchema,
})

Expand Down
34 changes: 34 additions & 0 deletions apps/sim/lib/copilot/request/lifecycle/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const {
mockUpdateRunStatus: vi.fn(),
mockEnv: {
COPILOT_API_KEY: undefined as string | undefined,
MSHIP_SYSPROMPT_OVERRIDE: undefined as string | undefined,
},
}))

Expand Down Expand Up @@ -154,6 +155,7 @@ describe('runCopilotLifecycle', () => {
beforeEach(() => {
vi.clearAllMocks()
mockEnv.COPILOT_API_KEY = undefined
mockEnv.MSHIP_SYSPROMPT_OVERRIDE = undefined
setEnvFlags({
isHosted: false,
isCopilotBillingAttributionV1Enabled: false,
Expand Down Expand Up @@ -204,6 +206,38 @@ describe('runCopilotLifecycle', () => {
expect(executionContext).not.toHaveProperty('resolvedSecretTraceRegistry')
})

it('forwards the configured Mothership system prompt override', async () => {
mockEnv.MSHIP_SYSPROMPT_OVERRIDE = 'NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT'

await runCopilotLifecycle(
{ message: 'hello', messageId: 'stream-system-prompt-override' },
{
userId: 'user-1',
workspaceId: 'ws-1',
}
)

const sentBody = JSON.parse(String(mockRunStreamLoop.mock.calls[0]?.[1].body))
expect(sentBody.systemPromptOverride).toBe(
'NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT'
)
})

it('does not forward a blank Mothership system prompt override', async () => {
mockEnv.MSHIP_SYSPROMPT_OVERRIDE = ' '

await runCopilotLifecycle(
{ message: 'hello', messageId: 'stream-blank-system-prompt-override' },
{
userId: 'user-1',
workspaceId: 'ws-1',
}
)

const sentBody = JSON.parse(String(mockRunStreamLoop.mock.calls[0]?.[1].body))
expect(sentBody).not.toHaveProperty('systemPromptOverride')
})

it.each([
{ goRoute: undefined, expected: 'mothership' },
{ goRoute: '/api/copilot', expected: 'mothership' },
Expand Down
5 changes: 5 additions & 0 deletions apps/sim/lib/copilot/request/lifecycle/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,11 @@ async function runCheckpointLoop(
const callerOnEvent = options.onEvent
const mothershipBaseURL = await getMothershipBaseURL({ userId: options.userId })
const lifecycleWorkspaceId = nonBlankString(options.workspaceId)
const systemPromptOverride = env.MSHIP_SYSPROMPT_OVERRIDE

if (typeof systemPromptOverride === 'string' && systemPromptOverride.trim() !== '') {
payload = { ...payload, systemPromptOverride }
}

// Go's auth middleware re-validates every Sim -> Go request by reading
// workspaceId from the JSON body and forwarding it to Sim's validate route,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const env = createEnv({
/** Gates risky copilot tools behind an Allow / Skip prompt. Off by default. */
COPILOT_TOOL_PERMISSIONS_ENABLED: z.boolean().optional(),
SIM_AGENT_API_URL: z.string().url().optional(), // URL for internal sim agent API
MSHIP_SYSPROMPT_OVERRIDE: z.string().min(1).optional(), // Enterprise-only highest-priority Mothership system prompt override forwarded by Sim
COPILOT_SOURCE_ENV: z.enum(['dev', 'staging', 'prod']).optional(), // Source Sim environment sent to mothership for callbacks
COPILOT_DEV_URL: z.string().url().optional(), // Sim agent API URL for the dev mothership environment
COPILOT_STAGING_URL: z.string().url().optional(), // Sim agent API URL for the staging mothership environment
Expand Down
1 change: 0 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ services:
- INTERNAL_API_SECRET=${INTERNAL_API_SECRET:-dev-internal-api-secret-min-32-chars}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- COPILOT_API_KEY=${COPILOT_API_KEY:-}
- MSHIP_SYSPROMPT_OVERRIDE=${MSHIP_SYSPROMPT_OVERRIDE:-}
- NEXT_PUBLIC_CHAT_DISABLED=${NEXT_PUBLIC_CHAT_DISABLED:-}
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL:-}
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
Expand Down
1 change: 1 addition & 0 deletions docker-compose.ollama.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ services:
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-sim_auth_secret_$(openssl rand -hex 16)}
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-$(openssl rand -hex 32)}
- COPILOT_API_KEY=${COPILOT_API_KEY}
- MSHIP_SYSPROMPT_OVERRIDE=${MSHIP_SYSPROMPT_OVERRIDE:-}
- NEXT_PUBLIC_CHAT_DISABLED=${NEXT_PUBLIC_CHAT_DISABLED:-}
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL}
- OLLAMA_URL=http://ollama:11434
Expand Down
1 change: 1 addition & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ services:
- CRON_SECRET=${CRON_SECRET:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- COPILOT_API_KEY=${COPILOT_API_KEY:-}
- MSHIP_SYSPROMPT_OVERRIDE=${MSHIP_SYSPROMPT_OVERRIDE:-}
- NEXT_PUBLIC_CHAT_DISABLED=${NEXT_PUBLIC_CHAT_DISABLED:-}
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL:-}
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
Expand Down
2 changes: 1 addition & 1 deletion helm/sim/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: sim
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
type: application
version: 1.5.1
version: 1.5.2
appVersion: "v0.7.44"
kubeVersion: ">=1.25.0-0"
home: https://sim.ai
Expand Down
1 change: 1 addition & 0 deletions helm/sim/examples/values-external-secrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ externalSecrets:
INTERNAL_API_SECRET: "sim/app/internal-api-secret"
CRON_SECRET: "sim/app/cron-secret"
API_ENCRYPTION_KEY: "sim/app/api-encryption-key"
# MSHIP_SYSPROMPT_OVERRIDE: "sim/app/mship-system-prompt-override"
postgresql:
password: "sim/postgresql/password"
# Only needed when copilot.enabled=true and copilot.server.secret.create=true
Expand Down
23 changes: 23 additions & 0 deletions helm/sim/tests/secret-modes_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,26 @@ tests:
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
app.env.MSHIP_SYSPROMPT_OVERRIDE: "NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT"
postgresql.auth.password: xxxxxxxx
asserts:
- isKind: { of: Secret }
- equal: { path: metadata.name, value: t-sim-app-secrets }
- equal:
path: stringData.MSHIP_SYSPROMPT_OVERRIDE
value: "NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT"

- it: inline mode omits an unset Mothership system prompt override
template: secrets-app.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
asserts:
- notExists:
path: stringData.MSHIP_SYSPROMPT_OVERRIDE

- it: existingSecret mode skips the chart-managed Secret
templates:
Expand All @@ -37,13 +53,20 @@ tests:
externalSecrets.remoteRefs.app.ENCRYPTION_KEY: path/to/enc
externalSecrets.remoteRefs.app.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.app.CRON_SECRET: path/to/cron
externalSecrets.remoteRefs.app.MSHIP_SYSPROMPT_OVERRIDE: path/to/mship-system-prompt-override
externalSecrets.remoteRefs.postgresql.password: path/to/pgpw
postgresql.auth.password: xxxxxxxx
asserts:
- isKind: { of: ExternalSecret }
- equal: { path: metadata.name, value: t-sim-app-secrets }
- equal: { path: spec.secretStoreRef.name, value: sim-store }
- equal: { path: spec.secretStoreRef.kind, value: ClusterSecretStore }
- contains:
path: spec.data
content:
secretKey: MSHIP_SYSPROMPT_OVERRIDE
remoteRef:
key: path/to/mship-system-prompt-override

- it: ESO mode skips the chart-managed Secret
template: secrets-app.yaml
Expand Down
4 changes: 4 additions & 0 deletions helm/sim/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@
"type": "string",
"description": "Set to 'true' to hide GitHub OAuth login even when credentials are configured"
},
"MSHIP_SYSPROMPT_OVERRIDE": {
"type": "string",
"description": "Optional enterprise-only highest-priority system prompt override forwarded to Mothership"
},
"OPENAI_API_KEY": {
"type": "string",
"description": "Primary OpenAI API key"
Expand Down
5 changes: 5 additions & 0 deletions helm/sim/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ app:
OCR_AZURE_MODEL_NAME: "" # Azure Mistral OCR model name
OCR_AZURE_API_KEY: "" # Azure Mistral OCR API key

# Mothership Copilot Configuration
MSHIP_SYSPROMPT_OVERRIDE: "" # Optional enterprise-only highest-priority system prompt override forwarded to Mothership

# AI Provider API Keys (leave empty if not using)
OPENAI_API_KEY: "" # Primary OpenAI API key
OPENAI_API_KEY_1: "" # Additional OpenAI API key for load balancing
Expand Down Expand Up @@ -1847,6 +1850,8 @@ externalSecrets:
CRON_SECRET: ""
# Path to API_ENCRYPTION_KEY in external store (optional)
API_ENCRYPTION_KEY: ""
# Path to MSHIP_SYSPROMPT_OVERRIDE in external store (optional)
MSHIP_SYSPROMPT_OVERRIDE: ""
# Path to REDIS_URL in external store (optional)
REDIS_URL: ""

Expand Down
Loading