Problem
The environment preparation path added in #286 passes roots.find(root => root.id === id)!.identity! into prepareCodeEnvironment. The non-null assertions only affect TypeScript: environment-derived primary roots take identity from projectRoots[0]?.identity, and additional environment roots have no identity at all. Without project discovery, that value is undefined.
withWorkspaceRoot immediately executes its action without descriptor confinement when identity is absent. Preparation therefore hashes inputs using the ordinary filesystem open path instead of root-confined openat traversal. O_NOFOLLOW protects the final component, but intermediate directory symlinks can be followed. Also, JSON.stringify omits the undefined root property from the preparation fingerprint, losing the checkout device/inode binding that reuse is intended to have.
Reproduction / expected behavior
Start a native worker with an environment definition using setup.reuse, without project-discovered roots. Verify that preparation receives a captured WorkspaceRootIdentity, that an input reached through an intermediate directory symlink is rejected, and that replacing the checkout directory at the same path invalidates its receipt.
Capture identities for environment-derived roots before preparation, or use the same capture fallback as conversation worktree source initialization. Add a CLI-level regression, since helper tests supplying identity explicitly do not exercise this path.
Bugbot finding on the upstream import: https://github.com/ClickHouse/ai/pull/4163#discussion_r4162591520
Affected code: packages/code/src/cli.ts and packages/code/src/environment-preparation.ts.
Problem
The environment preparation path added in #286 passes
roots.find(root => root.id === id)!.identity!intoprepareCodeEnvironment. The non-null assertions only affect TypeScript: environment-derived primary roots takeidentityfromprojectRoots[0]?.identity, and additional environment roots have no identity at all. Without project discovery, that value is undefined.withWorkspaceRootimmediately executes its action without descriptor confinement when identity is absent. Preparation therefore hashes inputs using the ordinary filesystem open path instead of root-confined openat traversal. O_NOFOLLOW protects the final component, but intermediate directory symlinks can be followed. Also, JSON.stringify omits the undefinedrootproperty from the preparation fingerprint, losing the checkout device/inode binding that reuse is intended to have.Reproduction / expected behavior
Start a native worker with an environment definition using
setup.reuse, without project-discovered roots. Verify that preparation receives a captured WorkspaceRootIdentity, that an input reached through an intermediate directory symlink is rejected, and that replacing the checkout directory at the same path invalidates its receipt.Capture identities for environment-derived roots before preparation, or use the same capture fallback as conversation worktree source initialization. Add a CLI-level regression, since helper tests supplying identity explicitly do not exercise this path.
Bugbot finding on the upstream import: https://github.com/ClickHouse/ai/pull/4163#discussion_r4162591520
Affected code:
packages/code/src/cli.tsandpackages/code/src/environment-preparation.ts.