Skip to content

Commit 1dddf80

Browse files
committed
fix(sandbox): scan session output without recursion
1 parent b440da7 commit 1dddf80

2 files changed

Lines changed: 49 additions & 9 deletions

File tree

‎apps/sim/lib/mothership/tools/handlers/workbench-confidentiality.live.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import {
7777
readSandboxResourceScope,
7878
withSandboxResourceScope,
7979
} from '@/lib/mothership/tools/sandbox-resources'
80+
import { buildMothershipSandboxSession } from '@/lib/mothership/tools/sandbox-session'
8081
import { chatSandboxSessionKey } from '@/lib/mothership/tools/sandbox-session-key'
8182
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
8283
import { buildFunctionExecuteBody, functionExecuteTool } from '@/tools/function/execute'
@@ -369,6 +370,31 @@ describe('persistent workbench output confidentiality', () => {
369370
expect(result.raw.output).toHaveProperty('result.length', 100_001)
370371
expect(result.projected.safe).toBe(true)
371372
})
373+
it.each([
374+
['array', '[', ']'],
375+
['object', '{"nested":', '}'],
376+
])(
377+
'classifies deeply nested %s output without exhausting the call stack',
378+
async (_kind, open, close) => {
379+
await inResourceScope(async () => {
380+
const session = await buildMothershipSandboxSession({
381+
...scope,
382+
sessionKey: chatSandboxSessionKey(chatId),
383+
})
384+
const credential = session.envs!.SIM_API_KEY
385+
const nested = (leaf: string) =>
386+
JSON.parse(open.repeat(12_000) + JSON.stringify(leaf) + close.repeat(12_000))
387+
expect(session.outputProvenance!(nested('ordinary output'))).toEqual({
388+
status: 'exact',
389+
entries: [],
390+
})
391+
expect(session.outputProvenance!(nested(credential))).toMatchObject({
392+
status: 'exact',
393+
entries: [{ name: 'SIM_API_KEY' }],
394+
})
395+
})
396+
}
397+
)
372398
it('revokes callback authentication before a result reaches the model', async () => {
373399
const result = await run(
374400
'printf "%s" "$SIM_API_KEY" > session-key.txt; printf "%s" "$SIM_ENDPOINT" > session-endpoint.txt; printf done'

‎apps/sim/lib/mothership/tools/sandbox-session.ts‎

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,30 @@ import {
2727

2828
const logger = createLogger('MothershipSandboxSession')
2929

30-
/** Scans parsed sandbox JSON in place, including keys, without allocating a second payload. */
30+
/** Scans parsed sandbox JSON without copying the payload or consuming the call stack. */
3131
function containsSessionCredential(value: unknown, matcher: ResolvedSecretMatcher): boolean {
32-
if (typeof value === 'string') return containsResolvedSecret(value, matcher)
33-
if (value === null || typeof value !== 'object') return false
34-
if (Array.isArray(value)) return value.some((item) => containsSessionCredential(item, matcher))
35-
const record = toRecord(value)
36-
for (const key in record) {
37-
if (!Object.hasOwn(record, key)) continue
38-
if (containsResolvedSecret(key, matcher) || containsSessionCredential(record[key], matcher)) {
39-
return true
32+
function* fields(record: Record<string, unknown>): Generator<unknown> {
33+
for (const key in record) {
34+
if (!Object.hasOwn(record, key)) continue
35+
yield key
36+
yield record[key]
37+
}
38+
}
39+
40+
const pending: Iterator<unknown>[] = [[value].values()]
41+
while (pending.length > 0) {
42+
const next = pending[pending.length - 1].next()
43+
if (next.done) {
44+
pending.pop()
45+
continue
46+
}
47+
const current = next.value
48+
if (typeof current === 'string') {
49+
if (containsResolvedSecret(current, matcher)) return true
50+
} else if (Array.isArray(current)) {
51+
pending.push(current.values())
52+
} else if (current !== null && typeof current === 'object') {
53+
pending.push(fields(toRecord(current)))
4054
}
4155
}
4256
return false

0 commit comments

Comments
 (0)