From 754e37cedb77d8389b92db5c55a0c9f1c9dd948b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 6 Aug 2026 16:53:36 -0700 Subject: [PATCH] fix(execution): count the runtime payload as secret material in scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An execution with no mounted files and no env secret still carries `params` and `contextVariables` into the sandbox — the runtime payload is serialized into a private-input file, so resolved block outputs and workflow variables land as plaintext regardless of `_sandboxFiles`. The scope predicate only looked at mounts and env secrets, so a binary derived from them was classified exact-empty. The route has no catalog for those values and cannot tell a secret-bearing one from an ordinary one, so they count as in scope. Only an execution with nothing at all in scope earns an exact-empty binary. This narrows where the relaxation applies rather than regressing anything: every binary export was unknown before this branch, so a workflow Function block carrying block references keeps exactly the behavior it has today. The mothership path is unaffected — its tool sets no contextVariables, blockData, or workflowVariables, which is the case this branch exists to fix. Values, not keys, for the params check: `executionParams._context` is set to undefined before the context is built, so a key count reads every execution as carrying params. Co-Authored-By: Claude Opus 5 (1M context) --- .../app/api/function/execute/route.test.ts | 34 +++++++++++++++++++ apps/sim/app/api/function/execute/route.ts | 23 +++++++++++++ 2 files changed, 57 insertions(+) diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index ac983adfcb..c7595053c0 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -890,6 +890,40 @@ describe('Function Execute API Route', () => { ) }) + it('keeps a binary export unknown when resolved context variables reach the sandbox', async () => { + envFlagsMock.isRemoteSandboxEnabled = true + mockExecuteInSandbox.mockResolvedValueOnce({ + result: 'done', + stdout: '', + sandboxId: 'sandbox-123', + exportedFiles: { '/home/user/small.jpg': '/9j/4AAQ' }, + }) + + const response = await POST( + createMockRequest('POST', { + code: 'print("done")', + language: 'python', + workspaceId: 'workspace-1', + // Resolved upstream block output — the route has no catalog to classify it. + contextVariables: { upstreamValue: 'could-be-anything' }, + outputs: { + files: [ + { + path: 'files/small.jpg', + sandboxPath: '/home/user/small.jpg', + mimeType: 'image/jpeg', + }, + ], + }, + }) + ) + + expect(response.status).toBe(200) + expect(mockWriteWorkspaceFileByPath).toHaveBeenCalledWith( + expect.objectContaining({ secretProvenance: { status: 'unknown' } }) + ) + }) + it('keeps a binary export unknown when files were mounted without a provenance envelope', async () => { envFlagsMock.isRemoteSandboxEnabled = true mockExecuteInSandbox.mockResolvedValueOnce({ diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 5d3d95df6d..3623d043f3 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -990,6 +990,12 @@ interface FunctionRouteExecutionContext { outputSecretPlaintextsByName: Map mountedFileSecretProvenanceScanner?: MountedFileSecretProvenanceScanner hasMountedSandboxFiles: boolean + /** + * Whether the serialized runtime payload carried `params` or `contextVariables` into the sandbox. + * Those are resolved block outputs and workflow variables — the route has no catalog for them, so + * it cannot tell a secret-bearing one from an ordinary value. + */ + hasUnclassifiedRuntimeInputs: boolean } type ResolvedSecretNamesMetadataType = @@ -1219,9 +1225,15 @@ function activateOutputSecretProvenance( * are unclassifiable rather than clean: absence of an envelope is absence of evidence, not evidence * the mount carried nothing. Those fail closed here so the classification can never be stronger * than what the caller actually attested to. + * + * The serialized runtime payload counts too. It carries `params` and `contextVariables` — resolved + * block outputs and workflow variables — into the sandbox as a private-input file, and the route + * has no catalog to tell a secret-bearing one from an ordinary value. Only an execution with + * nothing at all in scope earns an exact-empty binary. */ function hasSecretMaterialInScope(context: FunctionRouteExecutionContext): boolean { if (context.outputSecretPlaintextsByName.size > 0) return true + if (context.hasUnclassifiedRuntimeInputs) return true const scanner = context.mountedFileSecretProvenanceScanner return scanner ? scanner.hasSecrets : context.hasMountedSandboxFiles } @@ -2031,6 +2043,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => { outputSecretPlaintextsByName: new Map(), mountedFileSecretProvenanceScanner, hasMountedSandboxFiles: (_sandboxFiles?.length ?? 0) > 0, + // Values, not keys: `executionParams._context` is explicitly set to undefined just above, + // so a key count would read every execution as carrying params. Raised below once the + // resolved context variables are known. + hasUnclassifiedRuntimeInputs: Object.values(executionParams).some( + (value) => value !== undefined + ), } for (const [name, plaintext] of Object.entries(envVars)) { if (!plaintext) continue @@ -2074,6 +2092,11 @@ export const POST = withRouteHandler(async (req: NextRequest) => { ...codeResolution.contextVariables, ...preResolvedContextVariables, } + // Resolved block outputs and workflow variables ride into the sandbox inside the runtime + // payload, so an output file can carry them even with nothing mounted and no env secret. + if (Object.keys(contextVariables).length > 0) { + routeContext.hasUnclassifiedRuntimeInputs = true + } const compilation = await compileCodePlaceholders({ code: codeResolution.resolvedCode, language: lang,