Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ gitignore. This built-in policy is scoped to
SDK-mediated agent file reads (including `read_files`); it does not add
terminal-command or internal-edit restrictions.

Ignore-rule blocks on `read_files` return `[BLOCKED]` followed by a trailing
reason. `.codebuffignore` — a project-level ignore file with `.gitignore`
syntax, checked alongside `.gitignore` — is the escape hatch: adjust or
negate the matching rule there to allow tool reads of a specific file (a
file-level negation cannot re-include a path whose parent directory is itself
excluded).

Custom `overrideTools.read_files` implementations must preserve project
gitignore behavior for env templates. The built-in CLI, Desktop, Web, and Cloud
bridges already do this.
Expand Down
88 changes: 81 additions & 7 deletions sdk/src/__tests__/read-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,74 @@ describe('getFiles', () => {
fs: mockFs,
})

expect(result['node_modules/package/index.js']).toBe(
FILE_READ_STATUS.IGNORED,
expect(
result['node_modules/package/index.js']!.startsWith(
FILE_READ_STATUS.IGNORED,
),
).toBe(true)
expect(result['node_modules/package/index.js']).toContain(
'excluded by ignore rules',
)
})

test('ignore-rule block explains reason and unblock path; env-policy block stays opaque', async () => {
isFileIgnoredSpy.mockResolvedValue(true)

const mockFs = createMockFs({
files: {
'/project/AGENTS.local.md': { content: 'private notes' },
},
})

const result = await getFiles({
filePaths: ['AGENTS.local.md'],
cwd: '/project',
fs: mockFs,
})

expect(
result['AGENTS.local.md']!.startsWith(FILE_READ_STATUS.IGNORED),
).toBe(true)
expect(result['AGENTS.local.md']).toContain('excluded by ignore rules')
expect(result['AGENTS.local.md']).toContain('cannot re-include')

// Ignored-and-deleted: the block must not assert the file exists.
const goneFs = createMockFs({ files: {} })
const goneResult = await getFiles({
filePaths: ['AGENTS.local.md'],
cwd: '/project',
fs: goneFs,
})
expect(
goneResult['AGENTS.local.md']!.startsWith(FILE_READ_STATUS.IGNORED),
).toBe(true)
expect(goneResult['AGENTS.local.md']).not.toContain('exists on disk')

// Internal-edit path (no env policy): a secret blocked by built-in
// ignore defaults must stay opaque too.
const editFs = createMockFs({
files: { '/project/.env': { content: 'SECRET=value' } },
})
const editResult = await getFiles({
filePaths: ['.env'],
cwd: '/project',
fs: editFs,
enforceEnvPolicy: false,
})
expect(editResult['.env']).toBe(FILE_READ_STATUS.IGNORED)

// The env-policy block must NOT leak a reason or an unblock hint.
const envFs = createMockFs({
files: { '/project/.env': { content: 'SECRET=value' } },
})
const envResult = await getFiles({
filePaths: ['.env'],
cwd: '/project',
fs: envFs,
})
expect(envResult['.env']).toBe(FILE_READ_STATUS.IGNORED)
})

test('should call isFileIgnored with correct parameters', async () => {
const mockFs = createMockFs({
files: {
Expand Down Expand Up @@ -436,7 +499,11 @@ describe('getFiles', () => {
})

expect(result['src/index.ts']).toBe('main code')
expect(result['node_modules/pkg/index.js']).toBe(FILE_READ_STATUS.IGNORED)
expect(
result['node_modules/pkg/index.js']!.startsWith(
FILE_READ_STATUS.IGNORED,
),
).toBe(true)
})
})

Expand All @@ -454,10 +521,13 @@ describe('getFiles', () => {
filePaths: ['node_modules/pkg/index.js'],
cwd: '/project',
fs: mockFs,
// No fileFilter provided - SDK applies default gitignore checking
})

expect(result['node_modules/pkg/index.js']).toBe(FILE_READ_STATUS.IGNORED)
expect(
result['node_modules/pkg/index.js']!.startsWith(
FILE_READ_STATUS.IGNORED,
),
).toBe(true)
expect(isFileIgnoredSpy).toHaveBeenCalled()
})

Expand Down Expand Up @@ -615,8 +685,12 @@ describe('getFiles', () => {
fileFilter: () => ({ status: 'allow' }),
})

expect(result['.env.example']).toBe(FILE_READ_STATUS.IGNORED)
expect(result['.ENV.SAMPLE']).toBe(FILE_READ_STATUS.IGNORED)
expect(result['.env.example']!.startsWith(FILE_READ_STATUS.IGNORED)).toBe(
true,
)
expect(result['.ENV.SAMPLE']!.startsWith(FILE_READ_STATUS.IGNORED)).toBe(
true,
)
expect(isFileIgnoredSpy).toHaveBeenCalledTimes(2)
})

Expand Down
4 changes: 3 additions & 1 deletion sdk/src/__tests__/run-file-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ describe('CodebuffClientOptions fileFilter', () => {
})

expect(result.output.type).toBe('lastMessage')
expect(requestedFiles['.env.example']).toBe(FILE_READ_STATUS.IGNORED)
expect(
requestedFiles['.env.example']!.startsWith(FILE_READ_STATUS.IGNORED),
).toBe(true)
})

it('should pass fileFilter to requestOptionalFile as well', async () => {
Expand Down
22 changes: 21 additions & 1 deletion sdk/src/tools/read-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,27 @@ export async function getFiles(params: {
...(isEnvTemplate ? { allowEnvTemplate: true } : {}),
})
if (ignored) {
result[relativePath] = FILE_READ_STATUS.IGNORED
// The internal-edit path (enforceEnvPolicy: false) skips the env
// gate above, so secrets can reach this branch via built-in ignore
// defaults. Never explain or hint unblocking for them.
if (isSensitiveEnvFilePath(relativePath)) {
result[relativePath] = FILE_READ_STATUS.IGNORED
continue
}
// Keep the sentinel as the prefix (consumers match with startsWith)
// and append the reason, following the FILE_TOO_LARGE precedent.
// The ignore check never touches the file itself, so only claim
// existence when a stat confirms it.
let exists = false
try {
await fs.stat(fullPath)
exists = true
} catch {
// missing or unreadable: omit the existence claim
}
result[relativePath] =
FILE_READ_STATUS.IGNORED +
`: ${isEnvTemplate ? 'blocked by ignore-rule checking' : 'excluded by ignore rules'} (.gitignore, .codebuffignore, or built-in defaults), not an OS permission issue.${exists ? ' The file exists on disk;' : ''} glob and code_search omit it for the same reason. To allow tool reads, adjust or negate the matching rule in .codebuffignore (a file-level negation cannot re-include a path under an excluded directory).`
continue
}
}
Expand Down
Loading