mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
co-authored by
Waleed
parent
64fb8f07fe
commit
29cfb8586e
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user