Summary
PATCH /api/projects/:slug accepts a repo value with no validation and lets it relocate the project's workingDir and contentDir to a directory that does not exist. The project is left bricked — every turn hangs for 60s and fails — and any pre-existing chats, which are keyed on the old cwd, are stranded.
path and managed are already re-asserted from current precisely to prevent this. repo moves the cwd through the identical workingDirFor() code path and gets none of that protection.
Reproduction (v0.65.0, plain notebook project)
curl -sX POST /api/projects -H 'content-type: application/json' -d '{"name":"Demo"}'
# workingDir: <dataDir>/projects/demo
curl -sX PATCH /api/projects/demo -H 'content-type: application/json' \
-d '{"repo":"not a url at all ;rm -rf /"}'
# -> 200 OK
After:
workingDir: <dataDir>/projects/demo/not-a-url-at-all--rm--rf # does NOT exist
contentDir: <dataDir>/projects/demo/not-a-url-at-all--rm--rf # curated notes orphaned
No clone happens (that is promote()'s job). Every subsequent turn then fails:
CLI execution failed: Timeout waiting for new session file in
…-not-a-url-at-all--rm--rf (waited 60000ms)
sessionId: null
Confirmed 3×. Reachable from the ordinary Settings-pane PATCH route, not just curl.
Root cause
-
create() validates with isValidRepoUrl() — projects.ts:532. promote() validates too — projects.ts:769. update() does neither.
-
update() re-asserts path and managed from current (projects.ts:875-877) with a comment that states the reasoning exactly:
one must not be given a path by a stray key, which would silently hand its keeper a cwd somewhere else on the box that never went through validatePath
repo is the third field that determines workingDirFor() and is left mutable and unvalidated.
Suggested fix
Either re-assert repo from current the way path/managed are (changing a backing store is a migration, not a PATCH — cf. #708), or run isValidRepoUrl() in update() and refuse a change that would move workingDir for a project that already has chats.
Test gap
packages/server/test/integration/projects-crud.test.ts covers PATCH but never asserts that the backing fields are immutable. A test asserting "PATCH cannot move workingDir" would cover path, managed and repo together.
Found during a v0.65.0 exploratory QA session; verified independently against a live instance.
Summary
PATCH /api/projects/:slugaccepts arepovalue with no validation and lets it relocate the project'sworkingDirandcontentDirto a directory that does not exist. The project is left bricked — every turn hangs for 60s and fails — and any pre-existing chats, which are keyed on the old cwd, are stranded.pathandmanagedare already re-asserted fromcurrentprecisely to prevent this.repomoves the cwd through the identicalworkingDirFor()code path and gets none of that protection.Reproduction (v0.65.0, plain notebook project)
After:
No clone happens (that is
promote()'s job). Every subsequent turn then fails:Confirmed 3×. Reachable from the ordinary Settings-pane PATCH route, not just curl.
Root cause
create()validates withisValidRepoUrl()—projects.ts:532.promote()validates too —projects.ts:769.update()does neither.update()re-assertspathandmanagedfromcurrent(projects.ts:875-877) with a comment that states the reasoning exactly:repois the third field that determinesworkingDirFor()and is left mutable and unvalidated.Suggested fix
Either re-assert
repofromcurrentthe waypath/managedare (changing a backing store is a migration, not a PATCH — cf. #708), or runisValidRepoUrl()inupdate()and refuse a change that would moveworkingDirfor a project that already has chats.Test gap
packages/server/test/integration/projects-crud.test.tscovers PATCH but never asserts that the backing fields are immutable. A test asserting "PATCH cannot moveworkingDir" would coverpath,managedandrepotogether.Found during a v0.65.0 exploratory QA session; verified independently against a live instance.