mirror of
https://github.com/simstudioai/sim.git
synced 2026-08-29 02:27: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:
committed by
GitHub
parent
64fb8f07fe
commit
29cfb8586e
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "simstudio",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user