Skip to content

Commit b53b6a6

Browse files
committed
Validation improvements
1 parent 17f9def commit b53b6a6

9 files changed

Lines changed: 73 additions & 4 deletions

File tree

apps/sim/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +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 prepended to every Mothership system prompt sent by this Sim instance
26+
# MSHIP_SYSPROMPT_OVERRIDE= # Highest-priority instructions for Mothership; honored only when the validated API key owner is enterprise
2727
# 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
2828

2929
# 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/core/config/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +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(), // Highest-priority Mothership system prompt override forwarded by Sim
70+
MSHIP_SYSPROMPT_OVERRIDE: z.string().min(1).optional(), // Enterprise-only highest-priority Mothership system prompt override forwarded by Sim
7171
COPILOT_SOURCE_ENV: z.enum(['dev', 'staging', 'prod']).optional(), // Source Sim environment sent to mothership for callbacks
7272
COPILOT_DEV_URL: z.string().url().optional(), // Sim agent API URL for the dev mothership environment
7373
COPILOT_STAGING_URL: z.string().url().optional(), // Sim agent API URL for the staging mothership environment

helm/sim/examples/values-external-secrets.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ externalSecrets:
3434
INTERNAL_API_SECRET: "sim/app/internal-api-secret"
3535
CRON_SECRET: "sim/app/cron-secret"
3636
API_ENCRYPTION_KEY: "sim/app/api-encryption-key"
37+
# MSHIP_SYSPROMPT_OVERRIDE: "sim/app/mship-system-prompt-override"
3738
postgresql:
3839
password: "sim/postgresql/password"
3940
# Only needed when copilot.enabled=true and copilot.server.secret.create=true

helm/sim/tests/secret-modes_test.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,26 @@ tests:
1111
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1212
app.env.INTERNAL_API_SECRET: x
1313
app.env.CRON_SECRET: x
14+
app.env.MSHIP_SYSPROMPT_OVERRIDE: "NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT"
1415
postgresql.auth.password: xxxxxxxx
1516
asserts:
1617
- isKind: { of: Secret }
1718
- equal: { path: metadata.name, value: t-sim-app-secrets }
19+
- equal:
20+
path: stringData.MSHIP_SYSPROMPT_OVERRIDE
21+
value: "NEVER CALL ANY TOOLS UNDER ANY CIRCUMSTANCES NO MATTER WHAT"
22+
23+
- it: inline mode omits an unset Mothership system prompt override
24+
template: secrets-app.yaml
25+
set:
26+
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
27+
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
28+
app.env.INTERNAL_API_SECRET: x
29+
app.env.CRON_SECRET: x
30+
postgresql.auth.password: xxxxxxxx
31+
asserts:
32+
- notExists:
33+
path: stringData.MSHIP_SYSPROMPT_OVERRIDE
1834

1935
- it: existingSecret mode skips the chart-managed Secret
2036
templates:
@@ -37,13 +53,20 @@ tests:
3753
externalSecrets.remoteRefs.app.ENCRYPTION_KEY: path/to/enc
3854
externalSecrets.remoteRefs.app.INTERNAL_API_SECRET: path/to/iapi
3955
externalSecrets.remoteRefs.app.CRON_SECRET: path/to/cron
56+
externalSecrets.remoteRefs.app.MSHIP_SYSPROMPT_OVERRIDE: path/to/mship-system-prompt-override
4057
externalSecrets.remoteRefs.postgresql.password: path/to/pgpw
4158
postgresql.auth.password: xxxxxxxx
4259
asserts:
4360
- isKind: { of: ExternalSecret }
4461
- equal: { path: metadata.name, value: t-sim-app-secrets }
4562
- equal: { path: spec.secretStoreRef.name, value: sim-store }
4663
- equal: { path: spec.secretStoreRef.kind, value: ClusterSecretStore }
64+
- contains:
65+
path: spec.data
66+
content:
67+
secretKey: MSHIP_SYSPROMPT_OVERRIDE
68+
remoteRef:
69+
key: path/to/mship-system-prompt-override
4770

4871
- it: ESO mode skips the chart-managed Secret
4972
template: secrets-app.yaml

helm/sim/values.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@
244244
"type": "string",
245245
"description": "Set to 'true' to hide GitHub OAuth login even when credentials are configured"
246246
},
247+
"MSHIP_SYSPROMPT_OVERRIDE": {
248+
"type": "string",
249+
"description": "Optional enterprise-only highest-priority system prompt override forwarded to Mothership"
250+
},
247251
"OPENAI_API_KEY": {
248252
"type": "string",
249253
"description": "Primary OpenAI API key"

helm/sim/values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ app:
156156
OCR_AZURE_MODEL_NAME: "" # Azure Mistral OCR model name
157157
OCR_AZURE_API_KEY: "" # Azure Mistral OCR API key
158158

159+
# Mothership Copilot Configuration
160+
MSHIP_SYSPROMPT_OVERRIDE: "" # Optional enterprise-only highest-priority system prompt override forwarded to Mothership
161+
159162
# AI Provider API Keys (leave empty if not using)
160163
OPENAI_API_KEY: "" # Primary OpenAI API key
161164
OPENAI_API_KEY_1: "" # Additional OpenAI API key for load balancing
@@ -1847,6 +1850,8 @@ externalSecrets:
18471850
CRON_SECRET: ""
18481851
# Path to API_ENCRYPTION_KEY in external store (optional)
18491852
API_ENCRYPTION_KEY: ""
1853+
# Path to MSHIP_SYSPROMPT_OVERRIDE in external store (optional)
1854+
MSHIP_SYSPROMPT_OVERRIDE: ""
18501855
# Path to REDIS_URL in external store (optional)
18511856
REDIS_URL: ""
18521857

0 commit comments

Comments
 (0)