Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions apps/sim/app/api/mcp/serve/[serverId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
import { getMeaningfulWorkflowDescription } from '@/lib/mcp/workflow-tool-schema'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection'
import { refuseResolvedSecretProjection } from '@/executor/utils/resolved-secret-projection-refusal'
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'

const logger = createLogger('WorkflowMcpServeAPI')
Expand Down Expand Up @@ -293,7 +294,13 @@ async function projectWorkflowMcpModelContent(
throw new Error('MCP workflow execution provenance is invalid')
}
const projection = projectResolvedSecretModelContent(value, registry)
if (!projection.safe) throw new Error('MCP workflow output could not be safely projected')
if (!projection.safe) {
refuseResolvedSecretProjection({
site: 'mcpServe.workflowOutput',
message: 'MCP workflow output could not be safely projected',
registry,
})
}
return projection.value
}

Expand Down Expand Up @@ -939,7 +946,10 @@ async function handleToolsCall(
})
: rawErrorMessage
if (typeof errorMessage !== 'string') {
throw new Error('MCP workflow execution error could not be safely projected')
refuseResolvedSecretProjection({
site: 'mcpServe.executionError',
message: 'MCP workflow execution error could not be safely projected',
})
}
const status = getWorkflowErrorStatus(response.status)
const responseHeaders: Record<string, string> = {}
Expand Down
299 changes: 255 additions & 44 deletions apps/sim/executor/handlers/agent/agent-handler.ts

Large diffs are not rendered by default.

91 changes: 78 additions & 13 deletions apps/sim/executor/handlers/agent/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ import {
projectResolvedSecretModelContent,
projectResolvedSecretModelJsonStrings,
} from '@/executor/utils/resolved-secret-content-projection'
import { refuseResolvedSecretProjection } from '@/executor/utils/resolved-secret-projection-refusal'
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
import { PROVIDER_DEFINITIONS } from '@/providers/models'

const logger = createLogger('Memory')

const MEMORY_CONTENT_REFUSAL = 'Memory content could not be safely projected'

export class Memory {
async fetchMemoryMessages(ctx: ExecutionContext, inputs: AgentInputs): Promise<Message[]> {
if (!inputs.memoryType || inputs.memoryType === 'none') {
Expand Down Expand Up @@ -82,7 +85,12 @@ export class Memory {
messages
)))
) {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.storedProvenanceImport',
message: MEMORY_CONTENT_REFUSAL,
registry: ctx.resolvedSecretTraceRegistry,
inputPath: 'messages',
})
}

return Promise.all(
Expand All @@ -95,7 +103,12 @@ export class Memory {
ctx.resolvedSecretTraceRegistry?.exportProvenance().scope
)
if (!(await importDurableSecretProvenance(modelRegistry, messageProvenance, message))) {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.messageProvenanceImport',
message: MEMORY_CONTENT_REFUSAL,
registry: modelRegistry,
inputPath: 'messages',
})
}
return this.projectMessageForModel(modelRegistry, message)
})
Expand Down Expand Up @@ -213,12 +226,21 @@ export class Memory {
}

private projectMessageForModel(registry: ResolvedSecretTraceRegistry, message: Message): Message {
const functionArguments = this.readFunctionCallArguments(message.function_call)
const functionArguments = this.readFunctionCallArguments(
message.function_call,
registry,
'function_call'
)
const toolArguments = message.tool_calls?.map((toolCall) => {
if (!isPlainRecord(toolCall)) {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.toolCallShape',
message: MEMORY_CONTENT_REFUSAL,
registry,
inputPath: 'tool_calls',
})
}
return this.readFunctionCallArguments(toolCall.function)
return this.readFunctionCallArguments(toolCall.function, registry, 'tool_calls.function')
})
const contentProjection = projectResolvedSecretModelContent(message.content, registry)
const argumentProjection = projectResolvedSecretModelJsonStrings(
Expand All @@ -232,7 +254,12 @@ export class Memory {
!Array.isArray(argumentProjection.value) ||
argumentProjection.value.length !== 1 + (toolArguments?.length ?? 0)
) {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.messageContentProjection',
message: MEMORY_CONTENT_REFUSAL,
registry,
inputPath: 'content,function_call,tool_calls',
})
}

const content = contentProjection.value
Expand All @@ -241,25 +268,45 @@ export class Memory {
(functionArguments !== undefined && typeof projectedFunctionArguments !== 'string') ||
(functionArguments === undefined && projectedFunctionArguments !== undefined)
) {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.functionCallArgumentProjection',
message: MEMORY_CONTENT_REFUSAL,
registry,
inputPath: 'function_call.arguments',
})
}
if (
(toolArguments !== undefined && projectedToolArguments.length !== toolArguments.length) ||
(toolArguments === undefined && projectedToolArguments.length !== 0)
) {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.toolCallArgumentArity',
message: MEMORY_CONTENT_REFUSAL,
registry,
inputPath: 'tool_calls.function.arguments',
})
}

const projectedToolCalls = message.tool_calls?.map((toolCall, index) => {
const argument = (projectedToolArguments as unknown[])[index]
const originalFunction = isPlainRecord(toolCall) ? toolCall.function : undefined
if (originalFunction === undefined || originalFunction === null) return toolCall
if (!isPlainRecord(originalFunction)) {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.toolCallFunctionShape',
message: MEMORY_CONTENT_REFUSAL,
registry,
inputPath: 'tool_calls.function',
})
}
if (!Object.hasOwn(originalFunction, 'arguments')) return toolCall
if (typeof argument !== 'string') {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.toolCallArgumentType',
message: MEMORY_CONTENT_REFUSAL,
registry,
inputPath: 'tool_calls.function.arguments',
})
}
return {
...toolCall,
Expand All @@ -283,14 +330,32 @@ export class Memory {
}
}

private readFunctionCallArguments(functionCall: unknown): string | undefined {
/**
* Takes the registry and path from its caller so a refusal here reports the run that failed.
* Without them the refusal would deduplicate process-wide and name no cause.
*/
private readFunctionCallArguments(
functionCall: unknown,
registry: ResolvedSecretTraceRegistry,
inputPath: string
): string | undefined {
if (functionCall === undefined || functionCall === null) return undefined
if (!isPlainRecord(functionCall)) {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.functionCallShape',
message: MEMORY_CONTENT_REFUSAL,
registry,
inputPath,
})
}
if (!Object.hasOwn(functionCall, 'arguments')) return undefined
if (typeof functionCall.arguments !== 'string') {
throw new Error('Memory content could not be safely projected')
refuseResolvedSecretProjection({
site: 'memory.functionCallArgumentType',
message: MEMORY_CONTENT_REFUSAL,
registry,
inputPath,
})
}
return functionCall.arguments
}
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/executor/handlers/evaluator/evaluator-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { BlockHandler, ExecutionContext } from '@/executor/types'
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json'
import { projectResolvedSecretDiagnosticError } from '@/executor/utils/resolved-secret-content-projection'
import { refuseResolvedSecretProjection } from '@/executor/utils/resolved-secret-projection-refusal'
import type {
ResolvedSecretInputPath,
ResolvedSecretTraceRegistry,
Expand Down Expand Up @@ -81,7 +82,12 @@ export class EvaluatorBlockHandler implements BlockHandler {
modelInputPaths
)
if (!modelInputProjection.complete) {
throw new Error('Evaluator model input could not be safely projected')
refuseResolvedSecretProjection({
site: 'evaluator.contentMetricsModelInput',
message: 'Evaluator model input could not be safely projected',
registry: ctx.resolvedSecretTraceRegistry,
inputPath: 'content,metrics',
})
}
const processedContent = this.processContent(modelInputProjection.value.content)
const projectedMetrics = Array.isArray(modelInputProjection.value.metrics)
Expand Down
65 changes: 54 additions & 11 deletions apps/sim/executor/handlers/mothership/mothership-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,18 @@ import type {
StreamingExecution,
} from '@/executor/types'
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
import { refuseResolvedSecretProjection } from '@/executor/utils/resolved-secret-projection-refusal'
import type {
ResolvedSecretInputPath,
ResolvedSecretTraceRegistry,
} from '@/executor/utils/resolved-secret-trace-registry'
import type { SerializedBlock } from '@/serializer/types'

const logger = createLogger('MothershipBlockHandler')

const MOTHERSHIP_INPUT_REFUSAL = 'Mothership input could not be safely projected'
const MOTHERSHIP_SKILL_SELECTOR_REFUSAL =
'Mothership skill selector could not be safely projected for display'
const CANCELLATION_CHECK_INTERVAL_MS = 500
const MAX_MOTHERSHIP_ATTACHMENT_BYTES = 10 * 1024 * 1024
const MOTHERSHIP_EXECUTE_STREAM_HEADER = 'X-Mothership-Execute-Stream'
Expand Down Expand Up @@ -234,11 +239,15 @@ function projectPrivateMothershipSkillSelectorsForDisplay(
privateSelectorInputPaths: readonly ResolvedSecretInputPath[]
): unknown {
if (!Array.isArray(skills) || privateSelectorIndexes.size === 0) return skills
const projection = registry
.forkForInputPaths(privateSelectorInputPaths)
.projectResolvedInputSelection({ skills })
const selectorRegistry = registry.forkForInputPaths(privateSelectorInputPaths)
const projection = selectorRegistry.projectResolvedInputSelection({ skills })
if (!projection.complete || !Array.isArray(projection.value.skills)) {
throw new Error('Mothership skill selector could not be safely projected for display')
refuseResolvedSecretProjection({
site: 'mothership.skillSelectorDisplay',
message: MOTHERSHIP_SKILL_SELECTOR_REFUSAL,
registry: selectorRegistry,
inputPath: 'skills',
})
}
for (const inputIndex of privateSelectorIndexes) {
const source = skills[inputIndex]
Expand All @@ -249,7 +258,12 @@ function projectPrivateMothershipSkillSelectorsForDisplay(
typeof source.skillId !== 'string' ||
typeof projected.skillId !== 'string'
) {
throw new Error('Mothership skill selector could not be safely projected for display')
refuseResolvedSecretProjection({
site: 'mothership.skillSelectorDisplayEntry',
message: MOTHERSHIP_SKILL_SELECTOR_REFUSAL,
registry: selectorRegistry,
inputPath: 'skills.skillId',
})
}
}
return projection.value.skills
Expand Down Expand Up @@ -293,19 +307,34 @@ function assertMothershipToolSchemaProjectionsAreSafe(
if (!Array.isArray(tools)) return
const projection = registry.projectResolvedInputSelection({ tools })
if (!projection.complete || !Array.isArray(projection.value.tools)) {
throw new Error('Mothership input could not be safely projected')
refuseResolvedSecretProjection({
site: 'mothership.toolSchemaProjection',
message: MOTHERSHIP_INPUT_REFUSAL,
registry,
inputPath: 'tools',
})
}

for (const { inputIndex, selection } of selectIndexedMothershipMcpTools(tools)) {
if (!selection.schema) continue
const projectedCandidate = projection.value.tools[inputIndex]
if (!isPlainRecord(projectedCandidate)) {
throw new Error('Mothership input could not be safely projected')
refuseResolvedSecretProjection({
site: 'mothership.toolSchemaProjectedEntry',
message: MOTHERSHIP_INPUT_REFUSAL,
registry,
inputPath: 'tools.schema',
})
}
const projectedSchema = projectedCandidate.schema ?? selection.schema
const schemaProjection = projectModelSchemaAnnotations(selection.schema, projectedSchema)
if (!schemaProjection.safe) {
throw new Error('Mothership input could not be safely projected')
refuseResolvedSecretProjection({
site: 'mothership.toolSchemaAnnotations',
message: MOTHERSHIP_INPUT_REFUSAL,
registry,
inputPath: 'tools.schema',
})
}
}
}
Expand All @@ -316,7 +345,11 @@ function assertMothershipStructuralInputsDoNotResolveSecrets(
): void {
const provenance = registry.exportCommittedProvenanceForInputPaths(inputPaths)
if (!provenance.complete) {
throw new Error('Mothership input could not be safely projected')
refuseResolvedSecretProjection({
site: 'mothership.structuralInputProvenance',
message: MOTHERSHIP_INPUT_REFUSAL,
registry,
})
}
if (provenance.entries.length > 0) {
throw new Error('Mothership structural model inputs cannot contain secret references')
Expand Down Expand Up @@ -640,7 +673,12 @@ async function buildMothershipFileAttachments(
}
const projectedFiles = normalizeFileInput(projectedFilesInput)
if (!projectedFiles || projectedFiles.length !== files.length) {
throw new Error('Mothership input could not be safely projected')
refuseResolvedSecretProjection({
site: 'mothership.fileAttachmentArity',
message: MOTHERSHIP_INPUT_REFUSAL,
registry: ctx.resolvedSecretTraceRegistry,
inputPath: 'files',
})
}

const userFiles = files.map((file) =>
Expand Down Expand Up @@ -767,7 +805,12 @@ export class MothershipBlockHandler implements BlockHandler {
modelInputPaths
)
if (!modelInputProjection.complete || typeof modelInputProjection.value.prompt !== 'string') {
throw new Error('Mothership input could not be safely projected')
refuseResolvedSecretProjection({
site: 'mothership.modelInput',
message: MOTHERSHIP_INPUT_REFUSAL,
registry: sourceRegistry,
inputPath: 'prompt,files,tools,skills',
})
}
const messages = [
{
Expand Down
15 changes: 13 additions & 2 deletions apps/sim/executor/handlers/pi/pi-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import type {
NormalizedBlockOutput,
StreamingExecution,
} from '@/executor/types'
import { refuseResolvedSecretProjection } from '@/executor/utils/resolved-secret-projection-refusal'
import { isPiSupportedProvider, resolvePiModelId } from '@/providers/pi-providers'
import { getProviderFromModel } from '@/providers/utils'
import type { SerializedBlock } from '@/serializer/types'
Expand Down Expand Up @@ -172,7 +173,12 @@ export class PiBlockHandler implements BlockHandler {
[['task']]
)
if (!taskProjection.complete || typeof taskProjection.value.task !== 'string') {
throw new Error('Pi input could not be safely projected')
refuseResolvedSecretProjection({
site: 'pi.taskModelInput',
message: 'Pi input could not be safely projected',
registry: ctx.resolvedSecretTraceRegistry,
inputPath: 'task',
})
}
const task = taskProjection.value.task
const model = asOptString(inputs.model) ?? DEFAULT_MODEL
Expand Down Expand Up @@ -431,7 +437,12 @@ export class PiBlockHandler implements BlockHandler {
[['searchApiKey']]
)
if (!searchInputProjection.complete) {
throw new Error('Pi search input could not be safely projected')
refuseResolvedSecretProjection({
site: 'pi.searchApiKeyInput',
message: 'Pi search input could not be safely projected',
registry: ctx.resolvedSecretTraceRegistry,
inputPath: 'searchApiKey',
})
}
const projectedApiKey = Object.is(searchInputProjection.value.searchApiKey, rawSearchApiKey)
? apiKey
Expand Down
Loading
Loading