diff --git a/test/helpers/shared-libs-eval-fixture.ts b/test/helpers/shared-libs-eval-fixture.ts index 284e8a7ea..88e4b76c3 100644 --- a/test/helpers/shared-libs-eval-fixture.ts +++ b/test/helpers/shared-libs-eval-fixture.ts @@ -426,6 +426,8 @@ function sharedShellTokens(command: string): SharedShellToken[] { return tokens; } +const SHARED_OUTPUT_DEVICES = ['/dev/null', '/dev/stdout', '/dev/stderr', '/dev/fd/1', '/dev/fd/2']; + /** Share attempted-write checks across native, semantic and Codex standalone captures. */ export function sharedReadOnlyViolations(toolCalls: Array<{ tool: string; input: any }>, requests: SourceRequest[] = []): string[] { const violations: string[] = []; @@ -448,10 +450,17 @@ export function sharedReadOnlyViolations(toolCalls: Array<{ tool: string; input: }; for (let i = 0; i < tokens.length; i++) { const token = tokens[i].value; - if (tokens[i].operator && ['>', '>>', '&>'].includes(token) && !['/dev/null', '/dev/stdout', '/dev/stderr', '/dev/fd/1', '/dev/fd/2'].includes(tokens[i + 1]?.value)) + if (tokens[i].operator && ['>', '>>', '&>'].includes(token) && !SHARED_OUTPUT_DEVICES.includes(tokens[i + 1]?.value)) violations.push('shell file output redirection'); if (tokens[i].operator && token === '>&' && !['1', '2', '-'].includes(tokens[i + 1]?.value)) violations.push('shell file output redirection'); - if (isCommand(i) && /(?:^|\/)tee$/.test(token) && tokens[i + 1] && !tokens[i + 1].operator) violations.push('tee file output'); + if (isCommand(i) && /(?:^|\/)tee$/.test(token)) { + // Like a redirection, tee may only duplicate to the discard/stdout devices; any other operand is a file. + const operands = sharedShellCommandTokens(tokens, i + 1).map(operand => operand.value); + const end = operands.indexOf('--'); + const files = end < 0 ? operands.filter(value => !value.startsWith('-') || value === '-') + : [...operands.slice(0, end).filter(value => !value.startsWith('-') || value === '-'), ...operands.slice(end + 1)]; + if (files.some(file => !SHARED_OUTPUT_DEVICES.includes(file))) violations.push('tee file output'); + } if (isCommand(i) && /(?:^|\/)curl$/.test(token)) { // URL variables resolve only in the instrumented process. Its request // record supplies endpoint validation; source text still reveals writes. diff --git a/test/shared-libs-fixture.test.ts b/test/shared-libs-fixture.test.ts index d62f37840..e847be614 100644 --- a/test/shared-libs-fixture.test.ts +++ b/test/shared-libs-fixture.test.ts @@ -904,6 +904,7 @@ describe('shared-code curl source isolation', () => { "curl -w '%output{/tmp/report}%{http_code}' https://api.github.com/repos/fixture/shared-libs", "curl -X POST https://api.github.com/repos/fixture/shared-libs", 'printf data > /tmp/report', 'cat README.md >> "/tmp/report"', 'cat README.md | tee /tmp/report', + 'cat README.md | tee /dev/null report.txt', 'cat README.md | tee -a report.txt', 'cat README.md | tee -- -a', "bash <<'SH'\nprintf data > /tmp/report\nSH\n", "cat <<'DATA'\njust data\nDATA\ncurl -o /tmp/report https://api.github.com/repos/fixture/shared-libs", ]) expect(sharedReadOnlyViolations(bash(command)).length, command).toBeGreaterThan(0); @@ -913,6 +914,9 @@ describe('shared-code curl source isolation', () => { "curl -sS -o - -w 'http=%{http_code}\\n' https://api.github.com/repos/fixture/shared-libs", "curl -sS -m 15 -o /dev/null -w 'http=%{http_code}\\n' https://api.github.com/repos/fixture/shared-libs 2>&1", 'gh auth status 2>&1 | head -5', 'git --no-lazy-fetch log 2>/dev/null', + // Paid opportunity-judgment t1 (1213b01): tee to the discard device writes no file. + 'for p in src/version.ts README.md .gitignore; do echo "===== $p"; gh api --method GET -H "Accept: application/vnd.github.raw" "repos/fixture/shared-libs/contents/$p?ref=987f57ff2613724dd4a5509dda1b0fea155aab8a" 2>&1 | head -c 4000 | tee /dev/null | sha256sum; echo "exit=${PIPESTATUS[0]}"; done', + 'cat README.md | tee -a /dev/stderr', "rg 'a > b' README.md", "rg '>' README.md", "rg '|' README.md", 'rg tee README.md', 'rg curl README.md', "python3 - <<'PY'\nsize = 2\nif size > 1:\n print(size)\nPY\n",