Skip to content

Commit 5cf1f9b

Browse files
improvement(provenance): cleanup secrets boundary (#6374)
* fix(secrets): preserve raw outputs with durable provenance * improvement(provenance): cleanup boundary * fix copy resources * fix fork copies to work with provenance * address comments * fix
1 parent 40c0a57 commit 5cf1f9b

287 files changed

Lines changed: 15249 additions & 5935 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/function/execute/route.test.ts

Lines changed: 175 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ describe('Function Execute API Route', () => {
590590
createMockRequest(
591591
'POST',
592592
{
593-
code: 'return environmentVariables.API_KEY',
593+
code: 'return {{API_KEY}}',
594594
envVars: { API_KEY: 'secret-at-the-end' },
595595
workflowId: 'workflow-1',
596596
workspaceId: 'workspace-1',
@@ -686,7 +686,7 @@ describe('Function Execute API Route', () => {
686686
createMockRequest(
687687
'POST',
688688
{
689-
code: 'print("done")',
689+
code: 'print("{{API_KEY}}")',
690690
language: 'python',
691691
workspaceId: 'workspace-1',
692692
envVars: { API_KEY: 'secret-value' },
@@ -821,6 +821,54 @@ describe('Function Execute API Route', () => {
821821
expect(mockExecuteInSandbox).not.toHaveBeenCalled()
822822
})
823823

824+
it('runs with authenticated incomplete mount provenance and marks exported bytes unknown', async () => {
825+
envFlagsMock.isRemoteSandboxEnabled = true
826+
mockExecuteInSandbox.mockResolvedValueOnce({
827+
result: 'raw result',
828+
stdout: '',
829+
sandboxId: 'sandbox-123',
830+
exportedFiles: { '/home/user/output.txt': 'raw output' },
831+
})
832+
833+
const response = await POST(
834+
createMockRequest(
835+
'POST',
836+
{
837+
code: 'print("done")',
838+
language: 'python',
839+
workspaceId: 'workspace-1',
840+
outputs: {
841+
files: [
842+
{
843+
path: 'files/output.txt',
844+
sandboxPath: '/home/user/output.txt',
845+
mimeType: 'text/plain',
846+
},
847+
],
848+
},
849+
[PRIVATE_SECRET_PROVENANCE_FIELD]: {
850+
version: 1,
851+
complete: false,
852+
selections: [],
853+
},
854+
},
855+
{ [PRIVATE_SECRET_PROVENANCE_HEADER]: PRIVATE_SECRET_PROVENANCE_BUNDLE_V1 }
856+
)
857+
)
858+
859+
expect(response.status).toBe(200)
860+
expect((await response.json()).output.result).toEqual(
861+
expect.objectContaining({ fileId: 'wf_output_txt', vfsPath: 'files/output.txt' })
862+
)
863+
expect(mockExecuteInSandbox).toHaveBeenCalledOnce()
864+
expect(mockWriteWorkspaceFileByPath).toHaveBeenCalledWith(
865+
expect.objectContaining({
866+
buffer: Buffer.from('raw output'),
867+
secretProvenance: { status: 'unknown' },
868+
})
869+
)
870+
})
871+
824872
it('does not rewrite a static export path that happens to equal a resolved secret', async () => {
825873
envFlagsMock.isRemoteSandboxEnabled = true
826874
mockExecuteInSandbox.mockResolvedValueOnce({
@@ -853,6 +901,7 @@ describe('Function Execute API Route', () => {
853901
expect(mockWriteWorkspaceFileByPath).toHaveBeenCalledWith(
854902
expect.objectContaining({
855903
target: expect.objectContaining({ path: 'files/report-secret-value.txt' }),
904+
secretProvenance: { status: 'exact', entries: [] },
856905
})
857906
)
858907
expect(JSON.stringify(data)).toContain('files/report-secret-value.txt')
@@ -890,7 +939,7 @@ describe('Function Execute API Route', () => {
890939
)
891940
})
892941

893-
it('keeps a binary export unknown when files were mounted without a provenance envelope', async () => {
942+
it('classifies a binary export exact-empty when ordinary files were mounted without secret provenance', async () => {
894943
envFlagsMock.isRemoteSandboxEnabled = true
895944
mockExecuteInSandbox.mockResolvedValueOnce({
896945
result: 'done',
@@ -919,7 +968,7 @@ describe('Function Execute API Route', () => {
919968

920969
expect(response.status).toBe(200)
921970
expect(mockWriteWorkspaceFileByPath).toHaveBeenCalledWith(
922-
expect.objectContaining({ secretProvenance: { status: 'unknown' } })
971+
expect.objectContaining({ secretProvenance: { status: 'exact', entries: [] } })
923972
)
924973
})
925974

@@ -987,7 +1036,7 @@ describe('Function Execute API Route', () => {
9871036

9881037
const response = await POST(
9891038
createMockRequest('POST', {
990-
code: 'print("done")',
1039+
code: 'print("{{API_KEY}}")',
9911040
language: 'python',
9921041
workspaceId: 'workspace-1',
9931042
envVars: { API_KEY: 'secret-value' },
@@ -2002,6 +2051,117 @@ describe('Function Execute API Route', () => {
20022051
expect(Object.values(request.contextVariables)).not.toContain('must-not-bind')
20032052
})
20042053

2054+
it('does not infer provenance from an unused low-entropy environment value', async () => {
2055+
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result: 'Box eSign', stdout: '' })
2056+
2057+
const response = await POST(
2058+
createMockRequest(
2059+
'POST',
2060+
{
2061+
code: 'return "Box eSign"',
2062+
envVars: { SERVICENOW_PASSWORD: 'x' },
2063+
},
2064+
{ 'x-sim-request-private-tool-metadata': 'resolved-secret-names-v1' }
2065+
)
2066+
)
2067+
const data = await response.json()
2068+
2069+
expect(response.status).toBe(200)
2070+
expect(data.output.result).toBe('Box eSign')
2071+
expect(data.__resolvedSecretNames).toEqual([])
2072+
})
2073+
2074+
it('does not build provenance matchers for unused oversized environment values', async () => {
2075+
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result: 'safe', stdout: '' })
2076+
2077+
const response = await POST(
2078+
createMockRequest(
2079+
'POST',
2080+
{
2081+
code: 'return "safe"',
2082+
envVars: { UNUSED: 'x'.repeat(65 * 1024) },
2083+
},
2084+
{ 'x-sim-request-private-tool-metadata': 'resolved-secret-names-v1' }
2085+
)
2086+
)
2087+
2088+
expect(response.status).toBe(200)
2089+
expect((await response.json()).__resolvedSecretNames).toEqual([])
2090+
})
2091+
2092+
it('conservatively reports only compiled secrets when bounded output classification is exceeded', async () => {
2093+
const result = Array.from({ length: 100_001 }, () => 'ordinary')
2094+
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result, stdout: '' })
2095+
2096+
const response = await POST(
2097+
createMockRequest(
2098+
'POST',
2099+
{
2100+
code: 'const key = {{API_KEY}}; return params.items',
2101+
params: { items: result },
2102+
envVars: { API_KEY: 'secret-value', UNUSED: 'x' },
2103+
},
2104+
{ 'x-sim-request-private-tool-metadata': 'resolved-secret-names-v1' }
2105+
)
2106+
)
2107+
const data = await response.json()
2108+
2109+
expect(response.status).toBe(200)
2110+
expect(response.headers.get('x-sim-private-tool-metadata')).toBe('resolved-secret-names-v1')
2111+
expect(data.output.result).toHaveLength(100_001)
2112+
expect(data.output.result[0]).toBe('ordinary')
2113+
expect(data.__resolvedSecretNames).toEqual(['API_KEY'])
2114+
})
2115+
2116+
it('conservatively reports a compiled secret whose value exceeds matcher capacity', async () => {
2117+
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result: 'ordinary', stdout: '' })
2118+
2119+
const response = await POST(
2120+
createMockRequest(
2121+
'POST',
2122+
{
2123+
code: 'const key = {{OVERSIZED_SECRET}}; return "ordinary"',
2124+
envVars: { OVERSIZED_SECRET: 's'.repeat(64 * 1024 + 1), UNUSED: 'x' },
2125+
},
2126+
{ 'x-sim-request-private-tool-metadata': 'resolved-secret-names-v1' }
2127+
)
2128+
)
2129+
const data = await response.json()
2130+
2131+
expect(response.status).toBe(200)
2132+
expect(data.output.result).toBe('ordinary')
2133+
expect(data.__resolvedSecretNames).toEqual(['OVERSIZED_SECRET'])
2134+
})
2135+
2136+
it('tracks only compiled names when configured secrets share the same value', async () => {
2137+
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result: 'true', stdout: '' })
2138+
const oneResponse = await POST(
2139+
createMockRequest(
2140+
'POST',
2141+
{
2142+
code: 'return {{SECOND}}',
2143+
envVars: { FIRST: 'true', SECOND: 'true' },
2144+
},
2145+
{ 'x-sim-request-private-tool-metadata': 'resolved-secret-names-v1' }
2146+
)
2147+
)
2148+
2149+
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result: 'true', stdout: '' })
2150+
const bothResponse = await POST(
2151+
createMockRequest(
2152+
'POST',
2153+
{
2154+
code: 'const first = {{FIRST}}; return {{SECOND}}',
2155+
envVars: { FIRST: 'true', SECOND: 'true' },
2156+
},
2157+
{ 'x-sim-request-private-tool-metadata': 'resolved-secret-names-v1' }
2158+
)
2159+
)
2160+
2161+
expect((await oneResponse.json()).__resolvedSecretNames).toEqual(['SECOND'])
2162+
expect((await bothResponse.json()).__resolvedSecretNames).toEqual(['FIRST', 'SECOND'])
2163+
})
2164+
20052165
it('lowers missing shell placeholders while preserving comments and heredoc delimiters', async () => {
20062166
envFlagsMock.isRemoteSandboxEnabled = true
20072167
const response = await POST(
@@ -2134,7 +2294,7 @@ describe('Function Execute API Route', () => {
21342294
expect(mockExecuteInSandbox).not.toHaveBeenCalled()
21352295
})
21362296

2137-
it('reports exact secret values returned through placeholders and the environment map', async () => {
2297+
it('reports exact secret values returned through placeholders without inferring direct environment reads', async () => {
21382298
mockExecuteInIsolatedVM.mockResolvedValueOnce({
21392299
result: 'secret-valueother-secret',
21402300
stdout: '',
@@ -2171,14 +2331,15 @@ describe('Function Execute API Route', () => {
21712331
const directData = await directResponse.json()
21722332

21732333
expect(envData.__resolvedSecretNames).toEqual(['ENV_ONLY', 'SHARED'])
2174-
expect(directData.__resolvedSecretNames).toEqual(['API_KEY'])
2334+
expect(directData.output.result).toBe('secret-value')
2335+
expect(directData.__resolvedSecretNames).toEqual([])
21752336
})
21762337

21772338
it.each([
21782339
{ name: 'numeric', secret: '123', result: 123 },
21792340
{ name: 'boolean', secret: 'true', result: true },
21802341
])(
2181-
'records provenance for a typed $name secret returned through direct environment access',
2342+
'preserves a typed $name value returned through legacy direct environment access without inferred provenance',
21822343
async ({ secret, result }) => {
21832344
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result, stdout: '' })
21842345

@@ -2197,11 +2358,11 @@ describe('Function Execute API Route', () => {
21972358
const data = await response.json()
21982359

21992360
expect(data.output.result).toBe(result)
2200-
expect(data.__resolvedSecretNames).toEqual(['API_KEY'])
2361+
expect(data.__resolvedSecretNames).toEqual([])
22012362
}
22022363
)
22032364

2204-
it('reports shell substitutions and exact secret output from direct environment access', async () => {
2365+
it('reports placeholder output without inferring provenance from legacy shell environment access', async () => {
22052366
envFlagsMock.isRemoteSandboxEnabled = true
22062367
mockExecuteShellInSandbox.mockResolvedValueOnce({
22072368
result: null,
@@ -2245,7 +2406,8 @@ describe('Function Execute API Route', () => {
22452406
const directData = await directResponse.json()
22462407

22472408
expect(referencedData.__resolvedSecretNames).toEqual(['API_KEY'])
2248-
expect(directData.__resolvedSecretNames).toEqual(['API_KEY'])
2409+
expect(directData.output.stdout).toBe('secret-value')
2410+
expect(directData.__resolvedSecretNames).toEqual([])
22492411
})
22502412

22512413
it('returns nonzero shell stderr as a visible 422 error and diagnostic output', async () => {
@@ -2289,8 +2451,8 @@ describe('Function Execute API Route', () => {
22892451
)
22902452

22912453
expect(response.status).toBe(200)
2292-
expect((await response.json()).__resolvedSecretNames).toBeUndefined()
2293-
expect(response.headers.get('x-sim-private-tool-metadata')).toBeNull()
2454+
expect((await response.json()).__resolvedSecretNames).toEqual([])
2455+
expect(response.headers.get('x-sim-private-tool-metadata')).toBe('resolved-secret-names-v1')
22942456
expect(mockExecuteInIsolatedVM).toHaveBeenCalled()
22952457
})
22962458

0 commit comments

Comments
 (0)