fix(subagent-acp): require search permission on the child cwd

statSync().isDirectory() is true for a mode-600 directory, but a
subprocess cwd needs X_OK — spawn would fail EACCES, deferring a
self-contained configuration error past the load/start boundary the
resolution owns. Probe accessSync(X_OK) alongside the type check so
"not an accessible directory" is literally what the guard verifies.
This commit is contained in:
Yichen Jiang
2026-07-21 16:51:32 +08:00
parent d919e1b813
commit fcbbd22af7
2 changed files with 36 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { chmodSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -215,6 +215,29 @@ describe('cwd resolution', () => {
await ctx.fiber.dispose()
})
it('rejects a config cwd directory without search permission at load', async () => {
// statSync().isDirectory() is true for a mode-600 directory, but a
// subprocess cwd needs SEARCH permission — spawn would fail EACCES.
const tmp = mkdtempSync(join(tmpdir(), 'acp-noexec-'))
chmodSync(tmp, 0o600)
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await expect(ctx.plugin(acp, {
providerName: 'acp',
command: 'true',
args: [],
cwd: tmp,
permission: 'reject',
env: {},
})).rejects.toThrow('not an accessible directory')
await ctx.fiber.dispose()
} finally {
chmodSync(tmp, 0o700)
rmSync(tmp, { recursive: true, force: true })
}
})
it('rejects a config cwd that is not an accessible directory at load', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)