feat(mship): mship sysprompt override (#6469)

* Override

* Validation improvements

* remove from helm

* update helm

* Update Helm chart version from 1.6.0 to 1.5.2

sid wuz here

---------

Co-authored-by: Waleed <walif6@gmail.com>
This commit is contained in:
Siddharth Ganesan
2026-08-08 19:18:46 -07:00
committed by GitHub
parent 64fb8f07fe
commit 29cfb8586e
17 changed files with 117 additions and 4 deletions
+1
View File
@@ -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}
+1
View File
@@ -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)
@@ -17,6 +17,7 @@ const {
mockCheckServerSideUsageLimits,
mockDeriveBillingContext,
mockGetHighestPrioritySubscription,
mockIsEnterprisePlan,
mockRequireBillingAttributionHeader,
mockRequireBillingRequestIdHeader,
mockResolveLegacyV0BillingAttribution,
@@ -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(),
@@ -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,
}))
@@ -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: {
@@ -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))
@@ -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,
@@ -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)
+10 -1
View File
@@ -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',
@@ -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,
})
@@ -36,6 +36,7 @@ const {
mockUpdateRunStatus: vi.fn(),
mockEnv: {
COPILOT_API_KEY: undefined as string | undefined,
MSHIP_SYSPROMPT_OVERRIDE: undefined as string | undefined,
},
}))
@@ -154,6 +155,7 @@ describe('runCopilotLifecycle', () => {
beforeEach(() => {
vi.clearAllMocks()
mockEnv.COPILOT_API_KEY = undefined
mockEnv.MSHIP_SYSPROMPT_OVERRIDE = undefined
setEnvFlags({
isHosted: false,
isCopilotBillingAttributionV1Enabled: false,
@@ -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' },
@@ -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,
+1
View File
@@ -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
-1
View File
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
+1
View File
@@ -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}
+1
View File
@@ -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
+1
View File
@@ -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}
+1 -1
View File
@@ -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
@@ -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
+23
View File
@@ -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:
@@ -37,6 +53,7 @@ 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:
@@ -44,6 +61,12 @@ tests:
- 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
+4
View File
@@ -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"
+5
View File
@@ -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
@@ -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: ""