Preserve instruction symlink guard through fs seam
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
||||
FsEditOutcome,
|
||||
FsEditRequest,
|
||||
FsInfo,
|
||||
FsPathInfo,
|
||||
FsTarget,
|
||||
FsWriteIntent,
|
||||
FsWriteOutcome,
|
||||
@@ -41,6 +42,7 @@ async function write(path: string, content: string): Promise<void> {
|
||||
|
||||
class RecordingFileSystem extends FileSystem {
|
||||
entries = new Map<string, { type: FsInfo['type']; content?: string }>()
|
||||
lstatTypes = new Map<string, FsPathInfo['type']>()
|
||||
throwOnStat = new Set<string>()
|
||||
readTargets: string[] = []
|
||||
|
||||
@@ -61,6 +63,19 @@ class RecordingFileSystem extends FileSystem {
|
||||
return info
|
||||
}
|
||||
|
||||
override async lstat(path: string, opts?: { cwd?: string }): Promise<FsPathInfo | undefined> {
|
||||
const target = await this.resolve(path, opts)
|
||||
const lstatType = this.lstatTypes.get(target.targetKey)
|
||||
if (lstatType !== undefined) return { version: FsVersion(`lstat:${target.targetKey}`), type: lstatType }
|
||||
const info = await this.stat(target)
|
||||
if (info === undefined) return undefined
|
||||
return {
|
||||
version: info.version,
|
||||
type: info.type,
|
||||
...(info.size !== undefined ? { size: info.size } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
override async readText(target: FsTarget): Promise<string> {
|
||||
this.readTargets.push(target.targetKey)
|
||||
return this.entries.get(target.targetKey)?.content ?? ''
|
||||
@@ -250,6 +265,31 @@ describe('project instruction discovery', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects symlinked instruction files through ctx.fs instead of following repository-controlled links', async () => {
|
||||
const root = await tempRepo()
|
||||
const home = await tempRepo()
|
||||
const outside = await tempRepo()
|
||||
try {
|
||||
await mkdir(join(root, '.git'), { recursive: true })
|
||||
await write(join(outside, 'secret.txt'), 'outside secret')
|
||||
await symlink(join(outside, 'secret.txt'), join(root, 'AGENTS.md'))
|
||||
const ctx = new Context()
|
||||
await mountProjectInstructions(ctx, { dshHome: home })
|
||||
|
||||
const request: GenerateOptions = {
|
||||
model: 'mock',
|
||||
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
|
||||
}
|
||||
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
|
||||
|
||||
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
await rm(home, { recursive: true, force: true })
|
||||
await rm(outside, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('disables baseline loading when the byte budget is zero', async () => {
|
||||
const root = await tempRepo()
|
||||
const home = await tempRepo()
|
||||
@@ -772,6 +812,33 @@ describe('project instruction request injection', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('skips provider-visible instruction candidates when ctx.fs stat disagrees after no-follow preflight', async () => {
|
||||
const root = await tempRepo()
|
||||
const home = await tempRepo()
|
||||
try {
|
||||
await mkdir(join(root, '.git'), { recursive: true })
|
||||
await write(join(root, 'AGENTS.md'), 'node fs rule')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(RecordingFileSystem)
|
||||
const fs = ctx.fs as RecordingFileSystem
|
||||
fs.entries.set(join(root, '.git'), { type: 'directory' })
|
||||
fs.entries.set(join(root, 'AGENTS.md'), { type: 'directory' })
|
||||
fs.lstatTypes.set(join(root, 'AGENTS.md'), 'file')
|
||||
await ctx.plugin(projectInstructions, { dshHome: home })
|
||||
|
||||
const request: GenerateOptions = {
|
||||
model: 'mock',
|
||||
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
|
||||
}
|
||||
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
|
||||
|
||||
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
await rm(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('loads instruction files when ctx.fs omits the metadata size', async () => {
|
||||
const root = await tempRepo()
|
||||
const home = await tempRepo()
|
||||
|
||||
Reference in New Issue
Block a user