Bläddra i källkod

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.
Yichen Jiang 2 månader sedan
förälder
incheckning
b758a7496b

+ 5 - 5
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
   /**

+ 1 - 1
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. |

+ 10 - 5
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

+ 16 - 0
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)