Skip to content

Commit 1eaa52b

Browse files
authored
🪜 fix: Create Missing Parents on Pathname-Only Roots So Imports Land (#360)
A root registered by path alone (--worker-dir, --workspace, --environment) holds no descriptor, so write_file and import_file never created a missing parent there and reported NOT_FOUND. Yet the worker advertised import_file on every writable root, so LibreChat's first write of .librechat/attachments/.gitignore failed on such roots and no chat upload was ever copied. A write or import whose parent is missing now borrows a root descriptor for that one request and runs exactly as it does on a selected project root: mkdirat anchored to the held fd, root ancestry checked per level, rollback of directories an import created. A root the worker cannot hold by descriptor is no longer advertised import_file.
1 parent 82222d7 commit 1eaa52b

4 files changed

Lines changed: 286 additions & 87 deletions

File tree

‎packages/code/README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,13 @@ with `--allow-workspace-writes` or
630630
`overwrite: false` to require an atomic create that returns `EDIT_CONFLICT` if
631631
the target already exists. Code API dispatches that mode only after the worker
632632
and server negotiate `create` in `writeFileModes`.
633+
`write_file` and `import_file` create missing parent directories one level at
634+
a time through `mkdirat` on a held root descriptor, so a swapped symlink cannot
635+
redirect them outside the root. Selected projects, linked-worktree lanes and
636+
conversation worktrees hold that descriptor for every request; a root
637+
registered by path alone (`--worker-dir`, `--workspace`, `--environment`) holds
638+
one for just the write or import whose parent is missing. A root the worker
639+
cannot hold by descriptor is never advertised `import_file`.
633640

634641
`import_file` copies one stored chat upload into a writable workspace as a
635642
binary-safe file. It is on by default whenever the worker can perform it:
@@ -644,7 +651,7 @@ and relays a short-lived grant scoped to that object alone; the worker holds no
644651
storage credential. The worker applies `write_file`'s path rules, refuses any
645652
`.git` segment (including case, Unicode-ignorable, trailing-dot and `GIT~1`
646653
aliases) and symlinked parents, creates missing parents as 0755 under a held
647-
root, streams into a same-directory temporary file with `bytes` as a hard cap,
654+
root descriptor, streams into a same-directory temporary file with `bytes` as a hard cap,
648655
verifies size and SHA-256, syncs, and installs the file atomically with mode
649656
0644. Identical existing bytes return `unchanged: true` without a transfer;
650657
different bytes return `EDIT_CONFLICT` unless `overwrite: true`. A failure

‎packages/code/src/workspace-import.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,47 @@ test('imports binary bytes atomically into new 0755 parents as a 0644 file, what
258258
assert.equal(stub.hits, 1);
259259
});
260260

261+
test('a pathname-only root takes chat attachments into a folder it creates, as LibreChat sends them', async (t) => {
262+
const bytes = randomBytes(8192);
263+
const stub = await egressStub(t, bytes);
264+
const { root, tools } = await workspace(t, stub, { held: false });
265+
assert.ok(tools.capabilities.workspaces[0]?.operations?.includes('import_file'));
266+
const previousUmask = process.umask(0o077);
267+
t.after(() => process.umask(previousUmask));
268+
269+
const ignore = await tools.execute({
270+
protocolVersion: 1,
271+
operation: 'write_file',
272+
workspaceId: 'primary',
273+
path: '.librechat/attachments/.gitignore',
274+
content: '*\n',
275+
overwrite: false,
276+
});
277+
assert.equal((ignore as { created: boolean }).created, true);
278+
const request = importRequest(bytes, { path: '.librechat/attachments/report.pdf', overwrite: false });
279+
assert.equal((await tools.execute(request) as { created: boolean }).created, true);
280+
assert.deepEqual(await readFile(join(root, '.librechat', 'attachments', 'report.pdf')), bytes);
281+
282+
const direct = importRequest(bytes, { path: 'uploads/2026/archive.zip' });
283+
assert.equal((await tools.execute(direct) as { created: boolean }).created, true);
284+
assert.equal((await stat(join(root, 'uploads'))).mode & 0o777, 0o755);
285+
assert.equal((await stat(join(root, 'uploads', '2026'))).mode & 0o777, 0o755);
286+
assert.equal((await stat(join(root, 'uploads', '2026', 'archive.zip'))).mode & 0o777, 0o644);
287+
assert.equal(stub.hits, 2);
288+
});
289+
290+
test('a failed import on a pathname-only root removes the parents it created', async (t) => {
291+
const stub = await egressStub(t, Buffer.from('tampered bytes'));
292+
const { root, tools } = await workspace(t, stub, { held: false });
293+
294+
const mismatch = await rejection(
295+
tools.execute(importRequest(Buffer.from('expected bytes'), { path: '.librechat/attachments/file.bin' })),
296+
);
297+
assert.equal(mismatch.code, 'INVALID_REQUEST');
298+
assert.equal(mismatch.mutationMayHaveCommitted, false);
299+
assert.deepEqual(await readdir(root), []);
300+
});
301+
261302
test('re-importing identical bytes is idempotent and never downloads again', async (t) => {
262303
const bytes = randomBytes(4096);
263304
const stub = await egressStub(t, bytes);

‎packages/code/src/workspace.test.ts‎

Lines changed: 120 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,55 +1812,67 @@ test('writes create missing parent directories inside a held workspace root', as
18121812
assert.equal((await stat(join(root, 'src', 'feature', 'deep'))).isDirectory(), true);
18131813
});
18141814

1815-
test('parent creation never follows symlinks, crosses files, or leaves the workspace', async (t) => {
1816-
const parent = await realpath(
1817-
await mkdtemp(join(tmpdir(), 'librechat-code-workspace-parent-')),
1818-
);
1819-
t.after(() => rm(parent, { recursive: true, force: true }));
1820-
const root = join(parent, 'root');
1821-
const outside = join(parent, 'outside');
1822-
await mkdir(join(root, 'src'), { recursive: true });
1823-
await mkdir(outside);
1824-
await writeFile(join(root, 'file.txt'), 'file');
1825-
await symlink(outside, join(root, 'linked-outside'));
1826-
await symlink(join(root, 'src'), join(root, 'alias'));
1827-
await symlink(join(parent, 'missing-target'), join(root, 'dangling'));
1828-
const tools = await heldRootTools(root);
1815+
/** A root registered by path alone, as `--worker-dir`, `--workspace` and `--environment` do. */
1816+
async function pathnameRootTools(root: string): Promise<LocalWorkspaceTools> {
1817+
return LocalWorkspaceTools.create({
1818+
workspaces: [{ id: 'primary', root, writable: true }],
1819+
});
1820+
}
18291821

1830-
for (const [path, code] of [
1831-
['linked-outside/new/notes.txt', 'INVALID_PATH'],
1832-
['alias/new/notes.txt', 'INVALID_PATH'],
1833-
['dangling/new/notes.txt', 'INVALID_PATH'],
1834-
['file.txt/new/notes.txt', 'INVALID_PATH'],
1835-
['../outside/new/notes.txt', 'INVALID_REQUEST'],
1836-
['new/../../outside/notes.txt', 'INVALID_REQUEST'],
1837-
] as const) {
1838-
await assert.rejects(
1839-
tools.execute({
1840-
protocolVersion: 1,
1841-
operation: 'write_file',
1842-
workspaceId: 'primary',
1843-
path,
1844-
content: 'blocked',
1845-
}),
1846-
(error: unknown) =>
1847-
error instanceof WorkspaceToolError &&
1848-
error.code === code &&
1849-
!error.mutationMayHaveCommitted,
1850-
path,
1822+
for (const [kind, createTools] of [
1823+
['held', heldRootTools],
1824+
['pathname-only', pathnameRootTools],
1825+
] as const) {
1826+
test(`parent creation never follows symlinks, crosses files, or leaves a ${kind} workspace`, async (t) => {
1827+
const parent = await realpath(
1828+
await mkdtemp(join(tmpdir(), 'librechat-code-workspace-parent-')),
18511829
);
1852-
}
1853-
assert.deepEqual(await readdir(outside), []);
1854-
assert.deepEqual(await readdir(join(root, 'src')), []);
1855-
assert.deepEqual((await readdir(root)).sort(), [
1856-
'alias',
1857-
'dangling',
1858-
'file.txt',
1859-
'linked-outside',
1860-
'src',
1861-
]);
1862-
await assert.rejects(stat(join(parent, 'missing-target')));
1863-
});
1830+
t.after(() => rm(parent, { recursive: true, force: true }));
1831+
const root = join(parent, 'root');
1832+
const outside = join(parent, 'outside');
1833+
await mkdir(join(root, 'src'), { recursive: true });
1834+
await mkdir(outside);
1835+
await writeFile(join(root, 'file.txt'), 'file');
1836+
await symlink(outside, join(root, 'linked-outside'));
1837+
await symlink(join(root, 'src'), join(root, 'alias'));
1838+
await symlink(join(parent, 'missing-target'), join(root, 'dangling'));
1839+
const tools = await createTools(root);
1840+
1841+
for (const [path, code] of [
1842+
['linked-outside/new/notes.txt', 'INVALID_PATH'],
1843+
['alias/new/notes.txt', 'INVALID_PATH'],
1844+
['dangling/new/notes.txt', 'INVALID_PATH'],
1845+
['file.txt/new/notes.txt', 'INVALID_PATH'],
1846+
['../outside/new/notes.txt', 'INVALID_REQUEST'],
1847+
['new/../../outside/notes.txt', 'INVALID_REQUEST'],
1848+
] as const) {
1849+
await assert.rejects(
1850+
tools.execute({
1851+
protocolVersion: 1,
1852+
operation: 'write_file',
1853+
workspaceId: 'primary',
1854+
path,
1855+
content: 'blocked',
1856+
}),
1857+
(error: unknown) =>
1858+
error instanceof WorkspaceToolError &&
1859+
error.code === code &&
1860+
!error.mutationMayHaveCommitted,
1861+
path,
1862+
);
1863+
}
1864+
assert.deepEqual(await readdir(outside), []);
1865+
assert.deepEqual(await readdir(join(root, 'src')), []);
1866+
assert.deepEqual((await readdir(root)).sort(), [
1867+
'alias',
1868+
'dangling',
1869+
'file.txt',
1870+
'linked-outside',
1871+
'src',
1872+
]);
1873+
await assert.rejects(stat(join(parent, 'missing-target')));
1874+
});
1875+
}
18641876

18651877
test('a write finishes once it has created parents, even if cancelled', async (t) => {
18661878
const root = await realpath(await mkdtemp(join(tmpdir(), 'librechat-code-workspace-')));
@@ -1914,12 +1926,65 @@ test('a write cancelled before creating parents leaves the workspace unchanged',
19141926
assert.deepEqual(await readdir(root), []);
19151927
});
19161928

1917-
test('a pathname-only workspace root reports a missing parent instead of creating it', async (t) => {
1918-
const root = await mkdtemp(join(tmpdir(), 'librechat-code-workspace-'));
1929+
test('a pathname-only workspace root creates missing parents like a held root', async (t) => {
1930+
const root = await realpath(await mkdtemp(join(tmpdir(), 'librechat-code-workspace-')));
19191931
t.after(() => rm(root, { recursive: true, force: true }));
1920-
const tools = await LocalWorkspaceTools.create({
1921-
workspaces: [{ id: 'primary', root, writable: true }],
1922-
});
1932+
const tools = await pathnameRootTools(root);
1933+
1934+
for (const [path, overwrite] of [
1935+
['.librechat/attachments/.gitignore', false],
1936+
['missing/deep/notes.txt', undefined],
1937+
] as const) {
1938+
const result = await tools.execute({
1939+
protocolVersion: 1,
1940+
operation: 'write_file',
1941+
workspaceId: 'primary',
1942+
path,
1943+
content: '*\n',
1944+
...(overwrite === undefined ? {} : { overwrite }),
1945+
});
1946+
assert.deepEqual(result, {
1947+
protocolVersion: 1,
1948+
operation: 'write_file',
1949+
workspaceId: 'primary',
1950+
path,
1951+
created: true,
1952+
bytesWritten: 2,
1953+
});
1954+
assert.equal(await readFile(join(root, ...path.split('/')), 'utf8'), '*\n');
1955+
}
1956+
assert.equal((await stat(join(root, '.librechat', 'attachments'))).isDirectory(), true);
1957+
1958+
await assert.rejects(
1959+
tools.execute({
1960+
protocolVersion: 1,
1961+
operation: 'write_file',
1962+
workspaceId: 'primary',
1963+
path: '.librechat/attachments/.gitignore',
1964+
content: '!keep\n',
1965+
overwrite: false,
1966+
}),
1967+
(error: unknown) =>
1968+
error instanceof WorkspaceToolError &&
1969+
error.code === 'EDIT_CONFLICT' &&
1970+
!error.mutationMayHaveCommitted,
1971+
'a create never replaces a file in a folder it just created',
1972+
);
1973+
assert.equal(await readFile(join(root, '.librechat', 'attachments', '.gitignore'), 'utf8'), '*\n');
1974+
});
1975+
1976+
test('a pathname-only root that has been replaced by a symlink creates nothing', async (t) => {
1977+
const parent = await realpath(
1978+
await mkdtemp(join(tmpdir(), 'librechat-code-workspace-parent-')),
1979+
);
1980+
t.after(() => rm(parent, { recursive: true, force: true }));
1981+
const root = join(parent, 'root');
1982+
const elsewhere = join(parent, 'elsewhere');
1983+
await mkdir(root);
1984+
await mkdir(elsewhere);
1985+
const tools = await pathnameRootTools(root);
1986+
await rm(root, { recursive: true });
1987+
await symlink(elsewhere, root);
19231988

19241989
await assert.rejects(
19251990
tools.execute({
@@ -1931,10 +1996,10 @@ test('a pathname-only workspace root reports a missing parent instead of creatin
19311996
}),
19321997
(error: unknown) =>
19331998
error instanceof WorkspaceToolError &&
1934-
error.code === 'NOT_FOUND' &&
1935-
error.message === 'Workspace path does not exist',
1999+
error.code === 'INVALID_PATH' &&
2000+
!error.mutationMayHaveCommitted,
19362001
);
1937-
assert.deepEqual(await readdir(root), []);
2002+
assert.deepEqual(await readdir(elsewhere), []);
19382003
});
19392004

19402005
test('a missing workspace path is NOT_FOUND only when reached through the workspace', async (t) => {

0 commit comments

Comments
 (0)