Auto-approve tee and allow /tmp writes#298560
Auto-approve tee and allow /tmp writes#298560sanchirico wants to merge 4 commits intomicrosoft:mainfrom
tee and allow /tmp writes#298560Conversation
I'm not 100% sure about this. I thought this would be a simple first PR, but adding `tee` means the file write analyzer needs to know which files `tee` targets. So it also needed a `TeeFileWriteParser` and shared tokenizer extraction?
This adds `tee` to the auto-approve allowlist, a `TeeFileWriteParser` so the file
write analyzer detects `tee`'s file arguments (same pattern as `SedFileWriteParser`),
and allows writes to `/tmp` and `/private/tmp` in the analyzer.
On macOS `/tmp` is a symlink to `/private/tmp`. If the shell resolves
the symlink before the path reaches the analyzer, `startsWith('/tmp/')` misses
it. Checking both prefixes covers both cases.
I left out `/var/tmp`. I believe it's more for stuff that survives reboots and
microsoft#288327 only asks for `/tmp`.
Not sure if `/private/tmp` is in scope for microsoft#288327. Let me know if you'd rather drop it and keep only `/tmp`.
microsoft#288328 covers most of the same ground but doesn't handle `/private/tmp`.
Copilot also didn't touch the no-workspace-folders path to allow `/tmp` there,
so I added that too.
Also extracts the duplicate `_tokenizeCommand` from `SedFileWriteParser` into a shared `commandParserUtils.ts`.
For pre-existing tests that used `/tmp/file.txt` as the "outside workspace - block" example, I updated the path to `/etc/file.txt` to preserve the original intent rather than flipping them to "allow" like microsoft#288328 does (which just makes them redundant with the new allow tests).
Tests added for `/tmp`, `/private/tmp`, and `tee` with various destinations.
Fixes microsoft#288327
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
This PR extends the terminal tool’s auto-approval and file-write detection so common workflows like ... | tee /tmp/out.log can be auto-approved safely by (1) allowlisting tee, (2) teaching the analyzer which files tee targets, and (3) permitting writes to /tmp (and /private/tmp).
Changes:
- Add
teeto the default terminal auto-approve allowlist. - Allow file writes to
/tmp(and/private/tmp) in the file write analyzer, including the no-workspace-folders path. - Introduce
TeeFileWriteParserand a shared command tokenizer utility; expand/update tests accordingly.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts | Updates expectations for /tmp and adds coverage for /private/tmp + tee destinations. |
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts | Adds tee to the default auto-approve rules with rationale. |
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts | Registers the new TeeFileWriteParser for command-specific file-write detection. |
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts | Permits /tmp and /private/tmp targets and updates behavior when no workspace folders exist. |
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/teeFileWriteParser.ts | New parser to extract tee file operands as file-write targets. |
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts | Refactors to reuse shared tokenization/quote stripping helpers. |
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandParserUtils.ts | New shared tokenizeCommand + stripQuotes utilities. |
| // Allow writes to /tmp (and /private/tmp on macOS) | ||
| if (options.os !== OperatingSystem.Windows && (fileUri.path.startsWith('/tmp/') || fileUri.path.startsWith('/private/tmp/'))) { | ||
| this._log('File write to tmp directory allowed', fileUri.toString()); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
The /tmp allowlist check can be bypassed with path traversal segments (for example /tmp/../etc/config), since fileUri.path is checked without normalization. Consider normalizing the path (eg posix.normalize) before the prefix check and only allowing when the normalized path still starts with /tmp/ (and /private/tmp/).
| // No workspace folders, allow safe null device and tmp paths even without workspace | ||
| const hasOnlySafeTargets = fileWrites.every(fw => { | ||
| if (fw === nullDevice) { | ||
| return true; | ||
| } | ||
| if (options.os !== OperatingSystem.Windows) { | ||
| const fileUri = URI.isUri(fw) ? fw : isString(fw) ? URI.file(fw) : undefined; | ||
| if (fileUri && (fileUri.path.startsWith('/tmp/') || fileUri.path.startsWith('/private/tmp/'))) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| }); |
There was a problem hiding this comment.
In the “no workspace folders” branch, /tmp targets are treated as safe without applying the same variable/sub-command heuristic used above (/[$\(\){}]/). This means something like /tmp/$FOO/out.txt` could be auto-approved here but blocked when workspace folders exist. Consider reusing the same variable/sub-command check (and path normalization) before allowing a tmp target.
| if (token.startsWith('--')) { | ||
| if (token === '--output-error') { | ||
| // --output-error takes an argument (warn, warn-nopipe, exit, exit-nopipe) | ||
| i += 2; | ||
| continue; | ||
| } | ||
| if (token.startsWith('--output-error=')) { | ||
| i++; | ||
| continue; | ||
| } | ||
| // Other long options like --append, --help, --version | ||
| i++; | ||
| continue; | ||
| } | ||
|
|
||
| // Short options | ||
| if (token.startsWith('-') && token.length > 1) { | ||
| // Flags like -a, -i, -p, or combined like -ai | ||
| i++; | ||
| continue; | ||
| } |
There was a problem hiding this comment.
tee supports -- as an end-of-options marker; after it, tokens starting with - are file operands. Currently -- is treated like a regular long option and subsequent -foo operands are skipped as “short options”, which can cause missed file-write detection. Consider handling -- explicitly by treating all following tokens as file targets.
| test('no workspace folders - block', () => t('echo hello > file.txt', 'outsideWorkspace', false, 1, [])); | ||
| test('no workspace folders - /dev/null allowed', () => t('echo hello > /dev/null', 'outsideWorkspace', true, 1, [])); | ||
| test('no workspace folders - /tmp allowed', () => t('echo hello > /tmp/file.txt', 'outsideWorkspace', true, 1, [])); | ||
| test('no workspace folders - /private/tmp allowed', () => t('echo hello > /private/tmp/file.txt', 'outsideWorkspace', true, 1, [])); |
There was a problem hiding this comment.
With /tmp now allowed under the outsideWorkspace setting, the no cwd provided suite still expects echo hello > /tmp/file.txt to be blocked. That expectation no longer matches the analyzer logic (absolute /tmp/... will be allowed even when cwd is undefined), so these tests should be updated accordingly to avoid regressions.
… in no workspace branch, handle `--` in tee parser, and update tests?
|
@copilot code review[agent] Dear Copilot, please review the updated changes. I'm not so sure |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts:186
- Consider adding a test for
tee -(and/ortee -a -) to ensure the analyzer does not report a file write whenteeis only writing to stdout. This helps prevent regressions once-is handled specially in the parser.
suite('tee', () => {
test('tee to /tmp - allow', () => t('echo hello | tee /tmp/file.txt', 'outsideWorkspace', true, 1));
test('tee to /private/tmp - allow', () => t('echo hello | tee /private/tmp/file.txt', 'outsideWorkspace', true, 1));
test('tee to workspace - allow', () => t('echo hello | tee file.txt', 'outsideWorkspace', true, 1));
test('tee to outside workspace - block', () => t('echo hello | tee /etc/file.txt', 'outsideWorkspace', false, 1));
test('tee append to /tmp - allow', () => t('echo hello | tee -a /tmp/file.txt', 'outsideWorkspace', true, 1));
test('tee /tmp traversal - block', () => t('echo hello | tee /tmp/../etc/config', 'outsideWorkspace', false, 1));
test('tee with -- treats dash-args as files', () => t('echo hello | tee -- -file.txt', 'outsideWorkspace', true, 1));
});
src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/teeFileWriteParser.ts:25
canHandleshould return true only when the command will actually write to a file (per the interface contract). As written,teewith no file operands (e.g.... | tee) will return true even though it only echoes stdin to stdout; consider checking for at least one non-option file target before returning true.
canHandle(commandText: string): boolean {
return /^tee(\s|$)/.test(commandText);
}
...inalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts
Outdated
Show resolved
Hide resolved
| i++; | ||
| continue; | ||
| } | ||
|
|
There was a problem hiding this comment.
tee treats a lone - operand as standard output, not a file target. The current logic will classify tee - as writing to a file, which can incorrectly add a file-write disclaimer or even block auto-approval when cwd is unknown. Consider explicitly ignoring - operands when collecting file targets.
| // A lone "-" is stdout, not a file target | |
| if (token === '-') { | |
| i++; | |
| continue; | |
| } |
| test('absolute path - /tmp - allow', () => t('echo hello > /tmp/file.txt', 'outsideWorkspace', true, 1)); | ||
| test('absolute path - /private/tmp - allow', () => t('echo hello > /private/tmp/file.txt', 'outsideWorkspace', true, 1)); | ||
| test('absolute path - /tmp traversal - block', () => t('echo hello > /tmp/../etc/config', 'outsideWorkspace', false, 1)); | ||
| test('absolute path - /private/tmp traversal - block', () => t('echo hello > /private/tmp/../../../etc/passwd', 'outsideWorkspace', false, 1)); | ||
| test('absolute path - /etc - block', () => t('echo hello > /etc/config.txt', 'outsideWorkspace', false, 1)); |
There was a problem hiding this comment.
The new /private/tmp allow-tests run with options.os hard-coded to OperatingSystem.Linux. If /private/tmp support is intended only for macOS (symlink target of /tmp), consider setting options.os to OperatingSystem.Macintosh for these cases (or parameterizing the helper) so tests reflect the OS-specific behavior.
| // Reads stdin and writes to stdout and files. File write safety is handled by | ||
| // the file write analyzer. | ||
| tee: true, |
There was a problem hiding this comment.
Adding tee: true here will also auto-approve tee in PowerShell (it’s a built-in alias for Tee-Object, which can write to a file path argument). The file write analyzer’s command-specific parsing currently runs only for Bash (see TreeSitterCommandParser.getCommandFileWrites), so PowerShell tee writes won’t be detected/blocked by chat.tools.terminal.blockDetectedFileWrites, creating an auto-approve bypass. Consider either not auto-approving tee by default, or extending command-specific file-write parsing to PowerShell (tee/Tee-Object) and adding corresponding tests in the pwsh suite.
| // Reads stdin and writes to stdout and files. File write safety is handled by | |
| // the file write analyzer. | |
| tee: true, | |
| // Reads stdin and writes to stdout and files. This is not auto-approved by default | |
| // because file write safety is not fully analyzed for all shells (for example, in | |
| // PowerShell `tee` is an alias for `Tee-Object` which can write to file paths). | |
| tee: false, |
There was a problem hiding this comment.
@ human A few options i see
A. add a hard-coded isPwsh guard in commandMatchesRule to block tee auto approval in PowerShell since the file write analyzer only runs for bash, or
B. extend getCommandFileWrites with a Tee-Object parser so file write detection works in PowerShell too letting the existing safety check handle it?
C. Forget the whole idea
| } | ||
| // /private/tmp is the real path behind the /tmp symlink on macOS. | ||
| // This path does not exist on Linux by default | ||
| if (normalized.startsWith('/private/tmp/')) { |
There was a problem hiding this comment.
_isSafeTmpPath currently allows /private/tmp/... on all non-Windows platforms. Since /private/tmp is a macOS-specific real path for the /tmp symlink (and the comment notes it doesn’t exist on Linux by default), this broadens the allowlist beyond macOS and beyond the stated intent. Consider gating /private/tmp to OperatingSystem.Macintosh (while still allowing /tmp on Linux/macOS).
| if (normalized.startsWith('/private/tmp/')) { | |
| if (os === OperatingSystem.Macintosh && normalized.startsWith('/private/tmp/')) { |
There was a problem hiding this comment.
Should I add this and adjust testing? seems like it could over complicate this fix
I thought this would be a simple first PR, but adding
teemeans the file write analyzer needs to know which filesteetargets (redirections are already caught by tree-sitter). So it also needed aTeeFileWriteParserand shared tokenizer extraction? Happy to make some edits and resubmit if there is a better idea.This adds
teeto the auto-approve allowlist, aTeeFileWriteParserso the file write analyzer detectstee's file arguments (same pattern asSedFileWriteParser) and allows writes to/tmpand/private/tmpin the analyzer.On macOS
/tmpis a symlink to/private/tmp. If the shell resolves the symlink before the path reaches the analyzerstartsWith('/tmp/')misses it. Checking both prefixes covers both cases.I left out
/var/tmp. I believe it's more for stuff that survives reboots and #288327 only asks for/tmp.Not sure if
/private/tmpis in scope for #288327. Let me know if you'd rather drop it and keep only/tmp.#288328 covers most of the same ground but doesn't handle
/private/tmp. Copilot also didn't touch the no-workspace-folders path to allow/tmpthere, so I added that too.Also moves the duplicate
_tokenizeCommandfromSedFileWriteParserinto a sharedcommandParserUtils.ts.For pre-existing tests that used
/tmp/file.txtas the "outside workspace - block" example, I updated the path to/etc/file.txtto preserve the original intent rather than flipping them to "allow" like #288328 doesTests added for
/tmp,/private/tmp, andteewith various destinations.Fixes #288327