Follow-up to commit 91530b07 which only covered agents.adapter_config
.cwd. An audit found three additional user-facing endpoints that
accept filesystem paths without normalization. Same zero-terminal bug
in each: user supplies "~/foo", server stores it raw, downstream
consumers can't resolve the tilde.
Extract the two helpers (expandUserPath, normalizeWorkspaceDir) from
the agents.ts closure into a shared utility so all endpoints use the
same primitive.
new: server/src/utils/path-normalization.ts
- expandUserPath(candidate): resolves ~ / ~/foo to homedir() and
path.resolve() to absolute. Null-safe on non-string input.
- normalizeWorkspaceDir(rawPath, { field }): expand + assert
absolute + mkdir -p + stat isDirectory + log the change.
Throws unprocessable (422) on any filesystem failure with a
field-aware error message.
changed: server/src/routes/agents.ts
- Replaced the inline expandUserPath + normalizeAdapterConfigPaths
helpers with a narrow wrapper that delegates to the shared utility.
Three call sites (create, hire, patch) unchanged in behavior.
- Removed now-unused imports: mkdir, stat, homedir.
changed: server/src/routes/projects.ts
- POST /projects/:id/workspaces: normalize req.body.cwd before the
service call.
- PATCH /projects/:id/workspaces/:workspaceId: same.
- Added import.
changed: server/src/routes/execution-workspaces.ts
- PATCH /execution-workspaces/🆔 normalize req.body.cwd before the
patch object is built.
- Added import.
changed: server/src/services/nexus-settings.ts
- In set(): expand ~ in piperBinaryPath and whisperBinaryPath before
merging and validating. These are executable paths so we expand
but don't mkdir — caller still has to install the binary itself,
but the stored path is now resolvable by the server.
- Added import.
Not extended:
- Storage provider baseDir: computed at startup from environment,
not user request body. Sandboxed. No change needed.
- Instructions bundle paths: indirectly covered — the legacy path
resolver depends on cwd being absolute, which 91530b07 ensures.
- Chat file upload object keys: system-generated, not user-supplied.
Verification: npx tsc --noEmit on server — zero errors introduced in
any touched file. Dev server on :6100 still returns 200.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>