Skip to content

Commit e9f6161

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@1a6a184bdcdccb3b31294f835a634f9d7e983d2c
1 parent cf94e96 commit e9f6161

6 files changed

Lines changed: 218 additions & 56 deletions

File tree

‎bun.lock‎

Lines changed: 47 additions & 43 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎common/src/project-file-tree.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,17 @@ async function parseGitignoreWithMode(params: {
205205
fullDirPath: string
206206
fs: CodebuffFileSystem
207207
throwOnReadError: boolean
208+
explicitRead?: boolean
208209
}): Promise<ignore.Ignore> {
209-
const { fullDirPath, fs, throwOnReadError } = params
210+
const { fullDirPath, fs, throwOnReadError, explicitRead = false } = params
210211

211212
const ig = ignore.default()
212213
const directoryEntries = throwOnReadError
213214
? new Set(await fs.readdir(fullDirPath))
214215
: undefined
215216

216217
for (const fileName of PROJECT_IGNORE_FILES) {
218+
if (explicitRead && fileName === '.gitignore') continue
217219
const ignoreFilePath = path.join(fullDirPath, fileName)
218220
if (directoryEntries) {
219221
if (!directoryEntries.has(fileName)) continue
@@ -308,12 +310,15 @@ export async function isFileIgnored(params: {
308310
fs: CodebuffFileSystem
309311
/** Allow the env template past built-in rules while strictly applying project rules. */
310312
allowEnvTemplate?: boolean
313+
/** Explicit reads skip Git/discovery exclusions, but keep agent-owned ignore rules. */
314+
explicitRead?: boolean
311315
}): Promise<boolean> {
312316
const {
313317
filePath,
314318
projectRoot,
315319
fs,
316320
allowEnvTemplate: allowEnvTemplateRequested = false,
321+
explicitRead = false,
317322
} = params
318323
const allowEnvTemplate =
319324
allowEnvTemplateRequested && isEnvTemplateFilePath(filePath)
@@ -322,7 +327,10 @@ export async function isFileIgnored(params: {
322327
const fullFilePath = path.resolve(resolvedProjectRoot, filePath)
323328
if (!isPathInside(resolvedProjectRoot, fullFilePath)) return false
324329

325-
const defaultIgnore = createDefaultIgnore(allowEnvTemplate)
330+
const defaultIgnore =
331+
explicitRead && !allowEnvTemplate
332+
? ignore.default()
333+
: createDefaultIgnore(allowEnvTemplate)
326334

327335
const relativeFilePath = toPosixPath(
328336
path.relative(resolvedProjectRoot, fullFilePath),
@@ -338,6 +346,7 @@ export async function isFileIgnored(params: {
338346
fullDirPath: currentDir,
339347
fs,
340348
throwOnReadError: allowEnvTemplate,
349+
explicitRead: explicitRead && !allowEnvTemplate,
341350
})
342351
} catch (error) {
343352
if (allowEnvTemplate) return true

‎docs/agents-and-tools.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ to forward slashes for file tools and Git Bash. Keep this context on both entry
9797
paths: the loader knows `filePath`, but raw SKILL.md content alone loses it and
9898
leaves the agent searching for resources it already loaded instructions for.
9999

100+
### Explicit file reads versus discovery
101+
102+
The SDK's `read_files` reads explicitly named text and images even when
103+
`.gitignore` or built-in discovery exclusions hide them (for example,
104+
`sandbox/` renders or dependency source). File-tree discovery and background
105+
project-understanding reads still apply those exclusions. Explicit reads honor
106+
`.codebuffignore` / `.manicodeignore` and caller-provided file filters; sensitive
107+
`.env` files remain blocked, and env templates still obey all project ignore
108+
rules. `.freebuffignore` controls workspace transfers, not this read policy.
109+
100110
### Cut-off file edits are refused, never repaired
101111

102112
`packages/agent-runtime/src/util/truncated-write-guard.ts` stops two shapes of
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { afterEach, beforeEach, expect, test } from 'bun:test'
2+
import * as fs from 'node:fs/promises'
3+
import os from 'node:os'
4+
import path from 'node:path'
5+
6+
import { FILE_READ_STATUS } from '@codebuff/common/old-constants'
7+
import {
8+
getAllFilePaths,
9+
getProjectFileTree,
10+
} from '@codebuff/common/project-file-tree'
11+
12+
import { getFiles, getImageFile } from '../tools/read-files'
13+
14+
let cwd: string
15+
const png = Buffer.from(
16+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a5VQAAAAASUVORK5CYII=',
17+
'base64',
18+
)
19+
20+
beforeEach(async () => {
21+
cwd = await fs.mkdtemp(path.join(os.tmpdir(), 'explicit-file-reads-'))
22+
const files = {
23+
'.gitignore': 'sandbox/\nartifacts/\n.env.example\n',
24+
'.codebuffignore': 'private/\n',
25+
'.manicodeignore': 'legacy-private/\n',
26+
'sandbox/note.txt': 'requested artifact',
27+
'artifacts/render.png': png,
28+
'node_modules/example/index.js': 'dependency source',
29+
'private/note.txt': 'excluded text',
30+
'private/render.png': png,
31+
'legacy-private/note.txt': 'legacy excluded text',
32+
'.env': 'sensitive fixture',
33+
'.env.example': 'ignored template fixture',
34+
'.env.sample': 'public template fixture',
35+
'.env.png': png,
36+
}
37+
for (const [name, content] of Object.entries(files)) {
38+
const file = path.join(cwd, name)
39+
await fs.mkdir(path.dirname(file), { recursive: true })
40+
await fs.writeFile(file, content)
41+
}
42+
})
43+
44+
afterEach(async () => {
45+
await fs.rm(cwd, { recursive: true, force: true })
46+
})
47+
48+
test('explicit text and image reads succeed while discovery still excludes ignored artifacts', async () => {
49+
for (const absolute of [false, true]) {
50+
const input = (name: string) => (absolute ? path.join(cwd, name) : name)
51+
expect(
52+
await getFiles({
53+
cwd,
54+
fs,
55+
filePaths: [
56+
input('sandbox/note.txt'),
57+
input('node_modules/example/index.js'),
58+
],
59+
}),
60+
).toEqual({
61+
'sandbox/note.txt': 'requested artifact',
62+
'node_modules/example/index.js': 'dependency source',
63+
})
64+
expect(
65+
await getImageFile({ cwd, fs, filePath: input('artifacts/render.png') }),
66+
).toEqual({
67+
path: 'artifacts/render.png',
68+
data: png.toString('base64'),
69+
mediaType: 'image/png',
70+
bytes: png.length,
71+
})
72+
}
73+
const discovered = getAllFilePaths(
74+
await getProjectFileTree({ projectRoot: cwd, fs }),
75+
)
76+
expect(discovered).not.toContain(path.join('sandbox', 'note.txt'))
77+
expect(discovered).not.toContain(path.join('artifacts', 'render.png'))
78+
expect(discovered).not.toContain(path.join('node_modules', 'example', 'index.js'))
79+
})
80+
81+
test('explicit reads retain agent ignore files, including nested rules', async () => {
82+
await fs.writeFile(path.join(cwd, 'sandbox/.codebuffignore'), 'note.txt\n')
83+
expect(
84+
await getFiles({
85+
cwd,
86+
fs,
87+
filePaths: ['sandbox/note.txt', 'private/note.txt', 'legacy-private/note.txt'],
88+
}),
89+
).toEqual({
90+
'sandbox/note.txt': FILE_READ_STATUS.IGNORED,
91+
'private/note.txt': FILE_READ_STATUS.IGNORED,
92+
'legacy-private/note.txt': FILE_READ_STATUS.IGNORED,
93+
})
94+
expect(await getImageFile({ cwd, fs, filePath: 'private/render.png' })).toEqual({
95+
path: 'private/render.png',
96+
error: FILE_READ_STATUS.IGNORED,
97+
})
98+
})
99+
100+
test('sensitive env files and ignored env templates stay blocked, including image-shaped env names', async () => {
101+
expect(
102+
await getFiles({
103+
cwd,
104+
fs,
105+
filePaths: ['.env', '.env.example', '.env.sample'],
106+
}),
107+
).toEqual({
108+
'.env': FILE_READ_STATUS.IGNORED,
109+
'.env.example': FILE_READ_STATUS.IGNORED,
110+
'.env.sample': `${FILE_READ_STATUS.TEMPLATE}\npublic template fixture`,
111+
})
112+
expect(await getImageFile({ cwd, fs, filePath: '.env.png' })).toEqual({
113+
path: '.env.png',
114+
error: FILE_READ_STATUS.IGNORED,
115+
})
116+
})
117+
118+
test('background project understanding still refuses gitignored text', async () => {
119+
expect(
120+
await getFiles({
121+
cwd,
122+
fs,
123+
filePaths: ['sandbox/note.txt'],
124+
refuseNonText: true,
125+
}),
126+
).toEqual({
127+
'sandbox/note.txt': FILE_READ_STATUS.IGNORED,
128+
})
129+
})

‎sdk/src/__tests__/read-files.test.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,8 @@ describe('getFiles', () => {
391391
})
392392
})
393393

394-
describe('gitignore blocking', () => {
395-
test('should return IGNORED for gitignored files', async () => {
394+
describe('agent ignore blocking', () => {
395+
test('should return IGNORED for files excluded by agent ignore rules', async () => {
396396
isFileIgnoredSpy.mockResolvedValue(true)
397397

398398
const mockFs = createMockFs({
@@ -429,6 +429,7 @@ describe('getFiles', () => {
429429
filePath: 'src/index.ts',
430430
projectRoot: '/project',
431431
fs: mockFs,
432+
explicitRead: true,
432433
})
433434
})
434435

@@ -454,8 +455,8 @@ describe('getFiles', () => {
454455
})
455456
})
456457

457-
describe('default gitignore behavior', () => {
458-
test('should block gitignored files when no fileFilter is provided', async () => {
458+
describe('default agent ignore behavior', () => {
459+
test('should honor agent ignore rules when no fileFilter is provided', async () => {
459460
isFileIgnoredSpy.mockResolvedValue(true)
460461

461462
const mockFs = createMockFs({
@@ -468,7 +469,7 @@ describe('getFiles', () => {
468469
filePaths: ['node_modules/pkg/index.js'],
469470
cwd: '/project',
470471
fs: mockFs,
471-
// No fileFilter provided - SDK applies default gitignore checking
472+
// No fileFilter provided - SDK applies agent-owned ignore rules
472473
})
473474

474475
expect(result['node_modules/pkg/index.js']).toBe(FILE_READ_STATUS.IGNORED)
@@ -1006,7 +1007,7 @@ describe('getImageFile', () => {
10061007
}
10071008
})
10081009

1009-
test('applies the gitignore and filter policy of read_files', async () => {
1010+
test('applies the agent ignore and filter policy of read_files', async () => {
10101011
const mockFs = createMockFs({
10111012
files: { '/project/shot.png': { content: 'PNGDATA' } },
10121013
})

‎sdk/src/tools/read-files.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export async function getFiles(params: {
7777
refuseNonText = false,
7878
} = params
7979
// If the caller provides a filter, they own additional filtering decisions.
80-
// Otherwise the SDK also applies default gitignore checking.
80+
// Otherwise explicit reads honor agent ignore files, not discovery exclusions.
8181
const hasCustomFilter = fileFilter !== undefined
8282

8383
const result = Object.create(null) as Record<string, string | null>
@@ -128,14 +128,15 @@ export async function getFiles(params: {
128128
const isExampleFile =
129129
isEnvTemplate || filterResult?.status === 'allow-example'
130130

131-
// Custom-filter callers own ordinary filtering decisions, except that env
132-
// templates must still obey gitignore when the read_files policy is active.
131+
// Explicit reads may name Git-ignored artifacts. Keep the full discovery
132+
// policy for env templates and the background project-understanding agent.
133133
if ((!hasCustomFilter || isEnvTemplate) && isWithinProject) {
134134
const ignored = await isFileIgnored({
135135
filePath: relativePath,
136136
projectRoot: cwd,
137137
fs,
138138
...(isEnvTemplate ? { allowEnvTemplate: true } : {}),
139+
...(!isEnvTemplate && !refuseNonText ? { explicitRead: true } : {}),
139140
})
140141
if (ignored) {
141142
result[relativePath] = FILE_READ_STATUS.IGNORED
@@ -230,13 +231,21 @@ export async function getImageFile(params: {
230231
` [${ext} images cannot be attached; models accept only PNG, JPEG, GIF and WebP. Convert it with a terminal command (for example \`magick ${relativePath} /tmp/preview.png\`) and read that instead.]`,
231232
}
232233
}
233-
if (fileFilter?.(relativePath).status === 'blocked') {
234+
if (
235+
isSensitiveEnvFilePath(relativePath) ||
236+
fileFilter?.(relativePath).status === 'blocked'
237+
) {
234238
return { path: relativePath, error: FILE_READ_STATUS.IGNORED }
235239
}
236240
if (
237241
!fileFilter &&
238242
isWithinProject &&
239-
(await isFileIgnored({ filePath: relativePath, projectRoot: cwd, fs }))
243+
(await isFileIgnored({
244+
filePath: relativePath,
245+
projectRoot: cwd,
246+
fs,
247+
explicitRead: true,
248+
}))
240249
) {
241250
return { path: relativePath, error: FILE_READ_STATUS.IGNORED }
242251
}

0 commit comments

Comments
 (0)