Skip to content

Commit 29cfb85

Browse files
Sg312waleedlatif1
andauthored
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>
1 parent 64fb8f0 commit 29cfb85

17 files changed

Lines changed: 117 additions & 4 deletions

File tree

.devcontainer/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ services:
1919
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your_auth_secret_here}
2020
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-your_encryption_key_here}
2121
- COPILOT_API_KEY=${COPILOT_API_KEY}
22+
- MSHIP_SYSPROMPT_OVERRIDE=${MSHIP_SYSPROMPT_OVERRIDE:-}
2223
- NEXT_PUBLIC_CHAT_DISABLED=${NEXT_PUBLIC_CHAT_DISABLED:-}
2324
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL}
2425
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}

apps/sim/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
2323

2424
# Chat (Optional)
2525
# COPILOT_API_KEY= # Mint one at https://sim.ai. Without it the Sim Chat block, prompt jobs, and Inbox cannot run
26+
# MSHIP_SYSPROMPT_OVERRIDE= # Highest-priority instructions for Mothership; honored only when the validated API key owner is enterprise
2627
# 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
2728

2829
# Remote Function sandboxes (Optional)

apps/sim/app/api/copilot/api-keys/validate/route.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
mockCheckServerSideUsageLimits,
1818
mockDeriveBillingContext,
1919
mockGetHighestPrioritySubscription,
20+
mockIsEnterprisePlan,
2021
mockRequireBillingAttributionHeader,
2122
mockRequireBillingRequestIdHeader,
2223
mockResolveLegacyV0BillingAttribution,
@@ -31,6 +32,7 @@ const {
3132
mockCheckServerSideUsageLimits: vi.fn(),
3233
mockDeriveBillingContext: vi.fn(),
3334
mockGetHighestPrioritySubscription: vi.fn(),
35+
mockIsEnterprisePlan: vi.fn(),
3436
mockRequireBillingAttributionHeader: vi.fn(),
3537
mockRequireBillingRequestIdHeader: vi.fn(),
3638
mockResolveLegacyV0BillingAttribution: vi.fn(),
@@ -105,6 +107,10 @@ vi.mock('@/lib/billing/core/plan', () => ({
105107
getHighestPrioritySubscription: mockGetHighestPrioritySubscription,
106108
}))
107109

110+
vi.mock('@/lib/billing/core/subscription', () => ({
111+
isEnterprisePlan: mockIsEnterprisePlan,
112+
}))
113+
108114
vi.mock('@/lib/billing/core/usage-log', () => ({
109115
deriveBillingContext: mockDeriveBillingContext,
110116
}))
@@ -162,6 +168,7 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
162168
return ATTRIBUTION
163169
})
164170
mockGetHighestPrioritySubscription.mockResolvedValue(ACCOUNT_SUBSCRIPTION)
171+
mockIsEnterprisePlan.mockResolvedValue(false)
165172
mockDeriveBillingContext.mockReturnValue({
166173
billingEntity: ACCOUNT_BILLING_DECISION.billingEntity,
167174
billingPeriod: {
@@ -238,6 +245,23 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => {
238245
expect(mockCheckAttributedUsageLimits).toHaveBeenCalledWith(ATTRIBUTION)
239246
})
240247

248+
it('returns whether the validated key owner has an enterprise account', async () => {
249+
mockIsEnterprisePlan.mockResolvedValueOnce(true)
250+
251+
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))
252+
253+
expect(res.status).toBe(200)
254+
await expect(res.json()).resolves.toEqual({ isEnterprise: true })
255+
expect(mockIsEnterprisePlan).toHaveBeenCalledWith('user-1')
256+
})
257+
258+
it('returns false when the validated key owner is not enterprise', async () => {
259+
const res = await POST(request(OLD_GO_HOSTED_VALIDATE_BODY))
260+
261+
expect(res.status).toBe(200)
262+
await expect(res.json()).resolves.toEqual({ isEnterprise: false })
263+
})
264+
241265
it('preserves account admission for the exact workspace-less old-Go body', async () => {
242266
const res = await POST(request(OLD_GO_WORKSPACELESS_VALIDATE_BODY))
243267

apps/sim/app/api/copilot/api-keys/validate/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
serializeBillingAttributionHeader,
1818
} from '@/lib/billing/core/billing-attribution'
1919
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
20+
import { isEnterprisePlan } from '@/lib/billing/core/subscription'
2021
import { deriveBillingContext } from '@/lib/billing/core/usage-log'
2122
import {
2223
BILLING_ACCOUNT_DECISION_HEADER,
@@ -324,9 +325,11 @@ export const POST = withRouteHandler((req: NextRequest) =>
324325
)
325326
}
326327

328+
const isEnterprise = await isEnterprisePlan(userId)
329+
327330
span.setAttribute(TraceAttr.CopilotValidateOutcome, CopilotValidateOutcome.Ok)
328331
span.setAttribute(TraceAttr.HttpStatusCode, 200)
329-
return new NextResponse(null, { status: 200, headers: responseHeaders })
332+
return NextResponse.json({ isEnterprise }, { status: 200, headers: responseHeaders })
330333
} catch (error) {
331334
logger.error('Error validating usage limit', { error })
332335
span.setAttribute(TraceAttr.CopilotValidateOutcome, CopilotValidateOutcome.InternalError)

apps/sim/lib/api/contracts/copilot.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,15 @@ export const validateCopilotApiKeyBodySchema = z.object({
293293
})
294294
export type ValidateCopilotApiKeyBody = z.input<typeof validateCopilotApiKeyBodySchema>
295295

296+
export const validateCopilotApiKeyResponseSchema = z.object({
297+
/**
298+
* Server-derived entitlement for the validated key owner. Mothership treats
299+
* a missing or false value as ineligible for enterprise-only capabilities.
300+
*/
301+
isEnterprise: z.boolean(),
302+
})
303+
export type ValidateCopilotApiKeyResponse = z.output<typeof validateCopilotApiKeyResponseSchema>
304+
296305
export const listCopilotApiKeysContract = defineRouteContract({
297306
method: 'GET',
298307
path: '/api/copilot/api-keys',
@@ -486,7 +495,7 @@ export const validateCopilotApiKeyContract = defineRouteContract({
486495
path: '/api/copilot/api-keys/validate',
487496
headers: validateCopilotApiKeyHeadersSchema,
488497
body: validateCopilotApiKeyBodySchema,
489-
response: { mode: 'empty' },
498+
response: { mode: 'json', schema: validateCopilotApiKeyResponseSchema },
490499
error: validateCopilotApiKeyErrorSchema,
491500
})
492501

apps/sim/lib/copilot/request/lifecycle/run.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const {
3636
mockUpdateRunStatus: vi.fn(),
3737
mockEnv: {
3838
COPILOT_API_KEY: undefined as string | undefined,
39+
MSHIP_SYSPROMPT_OVERRIDE: undefined as string | undefined,
3940
},
4041
}))
4142

@@ -154,6 +155,7 @@ describe('runCopilotLifecycle', () => {
154155
beforeEach(() => {
155156
vi.clearAllMocks()
156157
mockEnv.COPILOT_API_KEY = undefined
158+
mockEnv.MSHIP_SYSPROMPT_OVERRIDE = undefined
157159
setEnvFlags({
158160
isHosted: false,
159161
isCopilotBillingAttributionV1Enabled: false,
@@ -204,6 +206,38 @@ describe('runCopilotLifecycle', () => {
204206
expect(executionContext).not.toHaveProperty('resolvedSecretTraceRegistry')
205207
})
206208

209+
it('forwards the configured Mothership system prompt override', async () => {
210+
mockEnv.MSHIP_SYSPROMPT_OVERRIDE = 'NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT'
211+
212+
await runCopilotLifecycle(
213+
{ message: 'hello', messageId: 'stream-system-prompt-override' },
214+
{
215+
userId: 'user-1',
216+
workspaceId: 'ws-1',
217+
}
218+
)
219+
220+
const sentBody = JSON.parse(String(mockRunStreamLoop.mock.calls[0]?.[1].body))
221+
expect(sentBody.systemPromptOverride).toBe(
222+
'NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT'
223+
)
224+
})
225+
226+
it('does not forward a blank Mothership system prompt override', async () => {
227+
mockEnv.MSHIP_SYSPROMPT_OVERRIDE = ' '
228+
229+
await runCopilotLifecycle(
230+
{ message: 'hello', messageId: 'stream-blank-system-prompt-override' },
231+
{
232+
userId: 'user-1',
233+
workspaceId: 'ws-1',
234+
}
235+
)
236+
237+
const sentBody = JSON.parse(String(mockRunStreamLoop.mock.calls[0]?.[1].body))
238+
expect(sentBody).not.toHaveProperty('systemPromptOverride')
239+
})
240+
207241
it.each([
208242
{ goRoute: undefined, expected: 'mothership' },
209243
{ goRoute: '/api/copilot', expected: 'mothership' },

apps/sim/lib/copilot/request/lifecycle/run.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,11 @@ async function runCheckpointLoop(
757757
const callerOnEvent = options.onEvent
758758
const mothershipBaseURL = await getMothershipBaseURL({ userId: options.userId })
759759
const lifecycleWorkspaceId = nonBlankString(options.workspaceId)
760+
const systemPromptOverride = env.MSHIP_SYSPROMPT_OVERRIDE
761+
762+
if (typeof systemPromptOverride === 'string' && systemPromptOverride.trim() !== '') {
763+
payload = { ...payload, systemPromptOverride }
764+
}
760765

761766
// Go's auth middleware re-validates every Sim -> Go request by reading
762767
// workspaceId from the JSON body and forwarding it to Sim's validate route,

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const env = createEnv({
6767
/** Gates risky copilot tools behind an Allow / Skip prompt. Off by default. */
6868
COPILOT_TOOL_PERMISSIONS_ENABLED: z.boolean().optional(),
6969
SIM_AGENT_API_URL: z.string().url().optional(), // URL for internal sim agent API
70+
MSHIP_SYSPROMPT_OVERRIDE: z.string().min(1).optional(), // Enterprise-only highest-priority Mothership system prompt override forwarded by Sim
7071
COPILOT_SOURCE_ENV: z.enum(['dev', 'staging', 'prod']).optional(), // Source Sim environment sent to mothership for callbacks
7172
COPILOT_DEV_URL: z.string().url().optional(), // Sim agent API URL for the dev mothership environment
7273
COPILOT_STAGING_URL: z.string().url().optional(), // Sim agent API URL for the staging mothership environment

bun.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docker-compose.local.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ services:
2323
- INTERNAL_API_SECRET=${INTERNAL_API_SECRET:-dev-internal-api-secret-min-32-chars}
2424
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
2525
- COPILOT_API_KEY=${COPILOT_API_KEY:-}
26+
- MSHIP_SYSPROMPT_OVERRIDE=${MSHIP_SYSPROMPT_OVERRIDE:-}
2627
- NEXT_PUBLIC_CHAT_DISABLED=${NEXT_PUBLIC_CHAT_DISABLED:-}
2728
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL:-}
2829
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}

0 commit comments

Comments
 (0)