Skip to content

Commit 69baa1a

Browse files
committed
feat: block sensitive files from being read with strikethrough UI
1 parent 9a4fac1 commit 69baa1a

File tree

10 files changed

+1249
-10
lines changed

10 files changed

+1249
-10
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, test, expect } from 'bun:test'
2+
3+
import { isSensitiveFile, isEnvTemplateFile } from '../../utils/create-run-config'
4+
5+
describe('isSensitiveFile', () => {
6+
test.each([
7+
// Env files (blocked)
8+
['.env', true],
9+
['.env.local', true],
10+
['config/.env.production', true],
11+
12+
// Env templates (allowed)
13+
['.env.example', false],
14+
['.env.sample', false],
15+
['.env.template', false],
16+
17+
// Sensitive extensions
18+
['private.pem', true],
19+
['server.key', true],
20+
['cert.p12', true],
21+
['app.keystore', true],
22+
['server.crt', true],
23+
24+
// Sensitive basenames
25+
['.htpasswd', true],
26+
['.netrc', true],
27+
['credentials', true],
28+
['.npmrc', true],
29+
['.yarnrc.yml', true],
30+
['auth.json', true],
31+
['terraform.tfvars', true],
32+
33+
// SSH keys (prefix pattern)
34+
['id_rsa', true],
35+
['id_ed25519', true],
36+
['id_rsa_github', true],
37+
['id_rsa.pub', false], // public keys allowed
38+
39+
// Credentials suffix pattern
40+
['aws_credentials', true],
41+
['db_credentials', true],
42+
43+
// Substring patterns
44+
['kubeconfig', true],
45+
['my-kubeconfig.yaml', true],
46+
['terraform.tfstate', true],
47+
['prod.tfstate.backup', true],
48+
49+
// Non-sensitive (should NOT be blocked)
50+
['package.json', false],
51+
['README.md', false],
52+
['src/index.ts', false],
53+
['.envrc', false],
54+
['credentials.ts', false],
55+
['terraform.tf', false],
56+
['kube-config.ts', false],
57+
])('%s → %s', (file, expected) => {
58+
expect(isSensitiveFile(file)).toBe(expected)
59+
})
60+
})
61+
62+
describe('isEnvTemplateFile', () => {
63+
test.each([
64+
['.env.example', true],
65+
['.env.sample', true],
66+
['.env.template', true],
67+
['config/.env.example', true],
68+
['.env', false],
69+
['.env.local', false],
70+
['package.json', false],
71+
])('%s → %s', (file, expected) => {
72+
expect(isEnvTemplateFile(file)).toBe(expected)
73+
})
74+
})

cli/src/components/tools/read-files.tsx

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,60 @@
1+
import { TextAttributes } from '@opentui/core'
2+
3+
import { useTheme } from '../../hooks/use-theme'
4+
import {
5+
isEnvTemplateFile,
6+
isSensitiveFile,
7+
} from '../../utils/create-run-config'
18
import { SimpleToolCallItem } from './tool-call-item'
29
import { defineToolComponent } from './types'
310

411
import type { ToolRenderConfig } from './types'
512

13+
function FilePathsDescription({ filePaths }: { filePaths: string[] }) {
14+
const theme = useTheme()
15+
16+
return (
17+
<>
18+
{filePaths.map((fp, idx) => {
19+
const isLast = idx === filePaths.length - 1
20+
const separator = isLast ? '' : ', '
21+
22+
if (isSensitiveFile(fp)) {
23+
return (
24+
<span key={fp}>
25+
<span fg={theme.muted} attributes={TextAttributes.STRIKETHROUGH}>
26+
{fp}
27+
</span>
28+
<span fg={theme.muted}> (blocked)</span>
29+
<span fg={theme.foreground}>{separator}</span>
30+
</span>
31+
)
32+
}
33+
34+
if (isEnvTemplateFile(fp)) {
35+
return (
36+
<span key={fp}>
37+
<span fg={theme.foreground}>{fp}</span>
38+
<span fg={theme.muted}> (allowed - example only)</span>
39+
<span fg={theme.foreground}>{separator}</span>
40+
</span>
41+
)
42+
}
43+
44+
return (
45+
<span key={fp} fg={theme.foreground}>
46+
{fp}
47+
{separator}
48+
</span>
49+
)
50+
})}
51+
</>
52+
)
53+
}
54+
655
/**
756
* UI component for read_files tool.
8-
* Displays all file paths as comma-separated list.
9-
* Does not support expand/collapse - always shows as a simple list.
57+
* Displays file paths with labels for blocked/template files.
1058
*/
1159
export const ReadFilesComponent = defineToolComponent({
1260
toolName: 'read_files',
@@ -26,9 +74,23 @@ export const ReadFilesComponent = defineToolComponent({
2674
return { content: null }
2775
}
2876

77+
// Check if any files need special labels
78+
const hasSpecialFiles = filePaths.some(
79+
(fp) => isSensitiveFile(fp) || isEnvTemplateFile(fp),
80+
)
81+
2982
return {
3083
content: (
31-
<SimpleToolCallItem name="Read" description={filePaths.join(', ')} />
84+
<SimpleToolCallItem
85+
name="Read"
86+
description={
87+
hasSpecialFiles ? (
88+
<FilePathsDescription filePaths={filePaths} />
89+
) : (
90+
filePaths.join(', ')
91+
)
92+
}
93+
/>
3294
),
3395
}
3496
},

cli/src/components/tools/tool-call-item.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ const renderExpandedContent = (
123123

124124
interface SimpleToolCallItemProps {
125125
name: string
126-
description: string
126+
/** Description - can be a string or ReactNode for rich formatting */
127+
description: string | ReactNode
127128
descriptionColor?: string
128129
}
129130

@@ -142,7 +143,12 @@ export const SimpleToolCallItem = ({
142143
<span fg={theme.foreground} attributes={TextAttributes.BOLD}>
143144
{name}
144145
</span>
145-
<span fg={descriptionColor ?? theme.foreground}> {description}</span>
146+
<span fg={theme.foreground}> </span>
147+
{typeof description === 'string' ? (
148+
<span fg={descriptionColor ?? theme.foreground}>{description}</span>
149+
) : (
150+
description
151+
)}
146152
</text>
147153
</box>
148154
)

cli/src/utils/create-run-config.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1+
import path from 'path'
2+
13
import {
24
createEventHandler,
35
createStreamChunkHandler,
46
} from './sdk-event-handlers'
57

68
import type { EventHandlerState } from './sdk-event-handlers'
7-
import type { AgentDefinition, MessageContent, RunState } from '@codebuff/sdk'
89
import type { Logger } from '@codebuff/common/types/contracts/logger'
10+
import type {
11+
AgentDefinition,
12+
FileFilter,
13+
MessageContent,
14+
RunState,
15+
} from '@codebuff/sdk'
916

1017
export type CreateRunConfigParams = {
1118
logger: Logger
@@ -18,6 +25,70 @@ export type CreateRunConfigParams = {
1825
signal: AbortSignal
1926
}
2027

28+
const SENSITIVE_EXTENSIONS = new Set([
29+
'.pem',
30+
'.key',
31+
'.p12',
32+
'.pfx',
33+
'.jks',
34+
'.keystore',
35+
'.crt',
36+
'.cer',
37+
])
38+
const SENSITIVE_BASENAMES = new Set([
39+
'.htpasswd',
40+
'.netrc',
41+
'credentials',
42+
'.npmrc',
43+
'.yarnrc',
44+
'.yarnrc.yml',
45+
'auth.json',
46+
'.pypirc',
47+
'terraform.tfvars',
48+
'.terraformrc',
49+
])
50+
51+
// Pattern matches (grouped by match type)
52+
const SENSITIVE_PATTERNS = {
53+
prefix: ['id_rsa', 'id_ed25519', 'id_dsa', 'id_ecdsa'], // SSH private keys
54+
suffix: ['_credentials'],
55+
substring: ['kubeconfig', '.tfstate'],
56+
}
57+
58+
const isEnvFile = (basename: string) =>
59+
(basename === '.env' || basename.startsWith('.env.')) &&
60+
!isEnvTemplateFile(basename)
61+
62+
const matchesPattern = (str: string) =>
63+
SENSITIVE_PATTERNS.prefix.some(
64+
(p) => str.startsWith(p) && !str.endsWith('.pub'),
65+
) ||
66+
SENSITIVE_PATTERNS.suffix.some((s) => str.endsWith(s)) ||
67+
SENSITIVE_PATTERNS.substring.some((sub) => str.includes(sub))
68+
69+
const ENV_TEMPLATE_SUFFIXES = ['.env.example', '.env.sample', '.env.template']
70+
71+
export const isEnvTemplateFile = (filePath: string) =>
72+
ENV_TEMPLATE_SUFFIXES.some((suffix) =>
73+
path.basename(filePath).endsWith(suffix),
74+
)
75+
76+
/**
77+
* Check if a file is a sensitive file that should be blocked from reading.
78+
*/
79+
export function isSensitiveFile(filePath: string): boolean {
80+
const basename = path.basename(filePath)
81+
const basenameLower = basename.toLowerCase()
82+
const ext = path.extname(filePath).toLowerCase()
83+
84+
return (
85+
isEnvFile(basename) ||
86+
SENSITIVE_EXTENSIONS.has(ext) ||
87+
SENSITIVE_BASENAMES.has(basename) ||
88+
matchesPattern(basenameLower)
89+
)
90+
}
91+
2192
export const createRunConfig = (params: CreateRunConfigParams) => {
2293
const {
2394
logger,
@@ -40,5 +111,10 @@ export const createRunConfig = (params: CreateRunConfigParams) => {
40111
handleStreamChunk: createStreamChunkHandler(eventHandlerState),
41112
handleEvent: createEventHandler(eventHandlerState),
42113
signal: params.signal,
114+
fileFilter: ((filePath: string) => {
115+
if (isSensitiveFile(filePath)) return { status: 'blocked' }
116+
if (isEnvTemplateFile(filePath)) return { status: 'allow-example' }
117+
return { status: 'allow' }
118+
}) satisfies FileFilter,
43119
}
44120
}

common/src/old-constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export const ONE_TIME_LABELS = [
5757

5858
export const FILE_READ_STATUS = {
5959
DOES_NOT_EXIST: '[FILE_DOES_NOT_EXIST]',
60-
IGNORED: '[FILE_IGNORED_BY_GITIGNORE_OR_CODEBUFF_IGNORE]',
60+
IGNORED: '[BLOCKED]',
61+
TEMPLATE: '[TEMPLATE]',
6162
OUTSIDE_PROJECT: '[FILE_OUTSIDE_PROJECT]',
6263
TOO_LARGE: '[FILE_TOO_LARGE]',
6364
ERROR: '[FILE_READ_ERROR]',

0 commit comments

Comments
 (0)