From b758a7496b759b34e429fd6ad300e79efaa88d40 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sun, 19 Jul 2026 15:56:32 +0800 Subject: [PATCH] fix(subagent-acp): reject an empty config cwd at load path.resolve('') is the process cwd, so an empty configured cwd would silently reintroduce the launch-directory fallback the parent-session cwd resolution removed. Fail at plugin load with an actionable message instead. --- docs/config-catalog.md | 10 +++++----- packages/subagent/subagent-acp/README.md | 2 +- packages/subagent/subagent-acp/src/index.ts | 15 ++++++++++----- .../subagent-acp/tests/subagent-acp.spec.ts | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index d60bbeb727..87808de52e 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -896,11 +896,11 @@ export interface Config { /** Arguments passed to {@link command}. */ args: string[] /** - * Working directory override for the child process and its ACP session. A - * relative path resolves against the harness launch directory at load, and - * the result must be an existing directory. When omitted, each child - * inherits its delegating parent session's cwd — and starting one from a - * parent session that has no cwd fails. + * Working directory override for the child process and its ACP session. + * Must be non-empty; a relative path resolves against the harness launch + * directory at load, and the result must be an existing directory. When + * omitted, each child inherits its delegating parent session's cwd — and + * starting one from a parent session that has no cwd fails. */ cwd?: string /** diff --git a/packages/subagent/subagent-acp/README.md b/packages/subagent/subagent-acp/README.md index 839bfd109f..ca13967558 100644 --- a/packages/subagent/subagent-acp/README.md +++ b/packages/subagent/subagent-acp/README.md @@ -25,7 +25,7 @@ ACP advertises no start-time capabilities because this process cannot enforce th | `providerName` | `acp` | Registry name on `ctx.subagents`. | | `command` | required | Executable spawned for each run. | | `args` | `[]` | Command arguments. | -| `cwd` | parent session cwd | Working-directory override for the child process and its ACP session; a relative value resolves against the harness launch directory at load and must name an existing directory. | +| `cwd` | parent session cwd | Working-directory override for the child process and its ACP session; must be non-empty, a relative value resolves against the harness launch directory at load, and the result must name an existing directory. | | `permission` | `reject` | Auto-answer permission requests by rejecting or choosing the first allow-shaped option. | | `env` | `{}` | Explicit child environment layered over a credential-scrubbed parent environment. | | `disposeEofGraceMs` | `6000` | Grace after stdin EOF before SIGTERM. | diff --git a/packages/subagent/subagent-acp/src/index.ts b/packages/subagent/subagent-acp/src/index.ts index 3722911f28..e8f906ffcf 100644 --- a/packages/subagent/subagent-acp/src/index.ts +++ b/packages/subagent/subagent-acp/src/index.ts @@ -26,11 +26,11 @@ export interface Config { /** Arguments passed to {@link command}. */ args: string[] /** - * Working directory override for the child process and its ACP session. A - * relative path resolves against the harness launch directory at load, and - * the result must be an existing directory. When omitted, each child - * inherits its delegating parent session's cwd — and starting one from a - * parent session that has no cwd fails. + * Working directory override for the child process and its ACP session. + * Must be non-empty; a relative path resolves against the harness launch + * directory at load, and the result must be an existing directory. When + * omitted, each child inherits its delegating parent session's cwd — and + * starting one from a parent session that has no cwd fails. */ cwd?: string /** @@ -160,6 +160,11 @@ export function apply(ctx: Context, config: Config): void { const resolved = config as ResolvedConfig assertPositiveFinite('disposeEofGraceMs', resolved.disposeEofGraceMs) assertPositiveFinite('disposeGraceMs', resolved.disposeGraceMs) + // `path.resolve('')` is the process cwd — an empty string would silently + // reintroduce the launch-directory fallback this resolution removed. + if (resolved.cwd === '') { + throw new Error('subagent-acp: config cwd must not be empty — omit the key to inherit the parent session cwd') + } // Interpret a relative configured cwd against the harness launch directory // ONCE, at load, and fail a misconfigured directory here — not per start. const validated: ResolvedConfig = resolved.cwd === undefined diff --git a/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts b/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts index 9e32e3acf3..8463900681 100644 --- a/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts +++ b/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts @@ -199,6 +199,22 @@ describe('cwd resolution', () => { expect(text(result.output)).toBe(`${realpathSync(absolute)}\n${absolute}`) }) + it('rejects an empty config cwd at load', async () => { + // `path.resolve('')` is the process cwd, so an empty string would silently + // reintroduce the launch-directory fallback this resolution removed. + const ctx = new Context() + await ctx.plugin(SubagentService) + await expect(ctx.plugin(acp, { + providerName: 'acp', + command: 'true', + args: [], + cwd: '', + permission: 'reject', + env: {}, + })).rejects.toThrow('config cwd must not be empty') + await ctx.fiber.dispose() + }) + it('rejects a config cwd that is not an accessible directory at load', async () => { const ctx = new Context() await ctx.plugin(SubagentService)