Skip to content

📥 feat: Import Chat Uploads Into Attached Workspaces - #348

Merged
danny-avila merged 5 commits into
mainfrom
danny-avila/workspace-import-file
Oct 23, 2026
Merged

danny-avila merged 5 commits into
mainfrom
danny-avila/workspace-import-file

Conversation

@danny-avila

@danny-avila danny-avila commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Rollout order (read first)

import_file is on by default: the code bridge is experimental and lightly used, so a worker advertises it whenever it can actually perform it. That means workspace writes are enabled, LIBRECHAT_CODE_FILE_RELAY_UPSTREAM is configured, and the Code API offers it (EGRESS_GATEWAY_URL set, so grants can be minted). The Code API has no separate toggle; being able to mint grants is the only condition.

Update LibreChat first. LibreChat releases that predate LibreChat-AI/LibreChat#17376 reject a worker's whole status/capabilities when it lists an operation they don't recognize. #17376 makes LibreChat ignore unknown worker operations (forward-compatible) and is deployed to demo before workers update. Self-hosters on an older LibreChat should update LibreChat before updating workers.

Escape hatch: if a worker must serve an older LibreChat, opt out with LIBRECHAT_CODE_ALLOW_WORKSPACE_IMPORTS=false or run --no-workspace-imports. Any other value of that variable fails startup instead of silently choosing either way.

The opt-out is not stored in the saved setup profile. That profile schema rejects unknown keys, so adding a field would stop older workers from starting with a profile written by a newer setup. The environment variable still applies to workers launched from a profile.

Final contract

The contract is the one agreed for the LibreChat side, with two clarifications that only pin down behavior it left open: bytesWritten is 0 when unchanged: true, and Code API returns typed errors from /v1/workspace-tools/* before dispatch.

  • Operation. import_file, additive, BRIDGE_PROTOCOL_VERSION stays 1. A worker lists it in capabilities.workspaceTools.operations and in each eligible workspace's operations only when it can perform it. LibreChat sends it only when it is listed for the selected workspace.

  • Request (LibreChat to POST /v1/workspace-tools/requests or /execute, same envelope and auth as the other workspace operations):
    { protocolVersion: 1, operation: 'import_file', workspaceId, workspaceInstanceId?, worktree?, path, source: { storage_session_id: string, id: string }, bytes: number, sha256: string /* lowercase hex */, overwrite?: boolean /* default false */ }

    • path follows write_file's rules and may not contain a .git segment or end in / or ..
    • source uses raw Code API storage ids (21-character nanoids).
    • Callers never send egressGrant. A request that includes it is rejected with 400.
  • Result: { protocolVersion: 1, operation: 'import_file', workspaceId, path, created: boolean, unchanged?: boolean, bytesWritten: number, sha256: string }

    • New file: created: true, bytesWritten === bytes.
    • Same bytes already there: created: false, unchanged: true, bytesWritten: 0. Nothing is downloaded or written.
    • overwrite: true replacing different bytes: created: false, bytesWritten === bytes.
  • Errors ({ error, code }):

    Status Code When
    400 INVALID_WORKSPACE_TOOL_REQUEST Malformed request, raw ids not valid storage ids, or a caller-supplied egressGrant
    403 IMPORT_SOURCE_UNAUTHORIZED The caller does not own source
    503 IMPORT_UNAVAILABLE This Code API cannot mint grants (no egress gateway), or minting failed
    413 WRITE_LIMIT_EXCEEDED bytes is over the Code API or worker limit, or the target filesystem lacks free space
    409 EDIT_CONFLICT The target holds different bytes and overwrite !== true, or it changed during the import
    403 WRITE_DISABLED Writes are disabled for that workspace
    422 INVALID_PATH .git path, symlinked parent, or traversal
    422 INVALID_REQUEST Downloaded bytes did not match bytes or sha256
    503 WRITE_UNAVAILABLE The relay or storage was unavailable

    Code API returns INVALID_WORKSPACE_TOOL_REQUEST, IMPORT_SOURCE_UNAUTHORIZED, IMPORT_UNAVAILABLE and its own size check directly, before dispatch, including on the durable submit. The worker returns the other codes, plus WRITE_LIMIT_EXCEEDED for its own limit or free space. On a durable request, the worker's codes appear in the request status error.code.

Why

When a LibreChat chat runs on an attached personal-machine workspace (a BYOM bridge to a paired @librechat/code worker), files the user uploads in that chat never reach the workspace. Only programmatic Bash stages inputs, and only for the length of one run, in $LIBRECHAT_CODE_DATA_DIR. write_file takes only UTF-8 text up to 1 MiB, so zip files, images and PDFs cannot get there at all. Users expect an upload to appear in the workspace, binaries included.

How it works

  1. LibreChat sends import_file with the upload's storage source, its size, and its SHA-256.

  2. Code API checks that the caller owns the object. It applies the same rule as GET /download/:session_id/:fileId for a user-kind object:

    • the live session:<storage_session_id> key must equal the caller's own storage key;
    • an upload:<key><session><id> record must exist for that key, session and object.

    An expired session cache is refused. The caller's claim is never trusted. For durable requests, ownership is checked only when a new request ID is accepted, so a retry of an existing request returns its stored status even if the upload has since expired.

  3. Code API asks the egress gateway (POST /internal/egress-grants) for a grant that covers that one object only:

    • its input_files list holds just that object;
    • max_upload_bytes: 0 and max_output_files: 0, so nothing can be uploaded;
    • max_requests: 2, one download plus one retry;
    • it expires when the request's queue and execution budget run out, plus 30 seconds.

    The request relayed to the worker carries the grant, and source is replaced by the gateway's opaque session and object handles. A synchronous request revokes its grant when it finishes. A durable request lets its grant expire. Durable requests fingerprint the caller's request, so a retry with the same request ID returns the existing record and does not mint a second grant.

  4. The worker downloads the object through LIBRECHAT_CODE_FILE_RELAY_UPSTREAM, the same egress route programmatic inputs already use. It holds no storage credentials.

Worker behavior

  • Path rules. Same as write_file, with workspace and worktree confinement. It also refuses any .git segment, including aliases created by case folding, Unicode-ignorable characters, trailing dots or spaces, and the GIT~1 short name. It refuses a parent directory that is a symlink, a final path segment that is a symlink, and any path that resolves outside the root.
  • Existing target. The worker checks the target before downloading:
    • same SHA-256: returns unchanged: true;
    • different bytes without overwrite: true: returns EDIT_CONFLICT.
  • Writing the file.
    • Missing parent directories are created as 0755.
    • The free-space check needs room for the file plus 64 MiB.
    • The body is streamed into a temporary file in the target directory, never buffered whole. bytes is a hard cap while streaming.
    • The file gets mode 0644 and is synced; size and SHA-256 are then verified by re-reading the synced temporary file through its own descriptor (what gets installed, not just what arrived), and it is installed atomically.
    • Created parent directories are chmod'ed to 0755 explicitly, so a restrictive worker umask does not change them.
    • A create uses an exclusive link, so a file that appears meanwhile is never replaced. An overwrite rechecks the inspected inode immediately before its rename, the same check-then-rename write_file uses. Bridge mutations of one root are serialized, so only an out-of-band local writer could race that last step.
  • On failure. The worker removes the temporary file and any parent directories it created, so a failed or cancelled import leaves no trace and does not quarantine the workspace.
  • Timeouts. A transfer that stalls for 30 seconds fails. The assignment deadline bounds the whole import.

Security model

  • Ownership: checked by Code API using the existing download rule, before any grant exists.
  • Grant scope: one object, read-only, two reads, short-lived, revoked after synchronous use. The worker sees only opaque handles and the grant, never storage credentials or another object's id.
  • Paths: write_file confinement plus the .git, symlink and canonical-root checks above, enforced again at commit time.
  • Integrity: exact size and SHA-256 are verified before install, the install is atomic, the file is never executable, and there are no partial files.
  • Limits:
    • Code API: CODEAPI_BRIDGE_IMPORT_FILE_MAX_BYTES, default 100 MiB, checked before ownership or dispatch.
    • Worker: LIBRECHAT_CODE_IMPORT_FILE_MAX_BYTES / --import-file-max-bytes, default 100 MiB.
    • Worker: free-space check before writing.
  • Deny policy: import_file is a file write. It arms the same durable mutation guard as write_file. It is never advertised for a workspace without writes enabled, and such a workspace refuses it with WRITE_DISABLED. LibreChat treating the upload as consent is decided on its side; the worker and Code API policies still apply.

Compatibility

  • Additive: protocol version 1 is unchanged, and no existing request or result shape changes.

  • Code API: lists import_file in supportedWorkspaceToolOperations only when EGRESS_GATEWAY_URL is configured, because otherwise it cannot mint the grant.

  • Worker: advertises import_file only when all four hold:

    1. the operator has not opted out (LIBRECHAT_CODE_ALLOW_WORKSPACE_IMPORTS=false / --no-workspace-imports; on by default, see Rollout order);
    2. workspace writes are enabled;
    3. LIBRECHAT_CODE_FILE_RELAY_UPSTREAM is set;
    4. the Code API listed the operation (the existing two-step registration negotiation).

    An older Code API therefore never sees it, and a worker without a relay never offers it.

  • LibreChat: sends import_file only when it is advertised. Older workers and Code APIs keep their current behavior.

  • Parent directories: as with write_file, they are created only beneath a descriptor-held root (project roots). A legacy pathname-only root reports a missing parent instead.

Changes

  • packages/code/src/protocol.ts:
    • request and result types;
    • isWorkspaceImportFilePath, isBridgeWorkspaceImportFileRequest (caller form, never carries a grant) and isRelayedWorkspaceImportFileRequest (worker form, must carry one);
    • result validation and the capability allowlist;
    • a shared isWorkspaceMutationOperation, now used by both the worker and the store.
  • packages/code/src/workspace.ts: the confined, streaming, atomic import, and import_file advertisement on writable roots that have an importer.
  • packages/code/src/workspace-import.ts: HttpWorkspaceFileImporter, a bounded, stall-timed streaming download through the relay upstream that also reports free space.
  • relay.ts: now holds the grant header and object URL helpers that programmatic inputs and imports share.
  • cli.ts, workspace-instances.ts, linked-worktrees.ts: wiring, so conversation worktrees and linked-worktree lanes also support imports.
  • service/src/workspace-tools/import.ts: the ownership check, grant claims and the import service.
  • router.ts: validation, limits, authorization, grant minting and revocation, and the import execution budget (min(JOB_TIMEOUT, 5 min)). Disconnect listeners are installed before ownership and grant minting, and a caller that leaves meanwhile never reaches dispatch; its grant is revoked.
  • requests.ts: an optional prepare hook, so a durable request mints its grant once.
  • bridge/router.ts and bridge/enabled.ts: the capability offer.
  • config.ts: the size limit.
  • Docs: ADR 0014, the packages/code README, docs/remote-bridge, OpenAPI, and docker-compose.

Tests

  • packages/code/src/workspace-import.test.ts (a real LocalWorkspaceTools and HttpWorkspaceFileImporter against a local egress stub, plus a real BridgeWorker) covers:
    • protocol validation, including traversal, absolute paths, .git aliases, bad hashes, extra keys and the caller-grant split;
    • result validation;
    • advertisement rules;
    • binary import with 0755 parents and a 0644 file;
    • idempotent re-import with no second download;
    • conflict and overwrite (an executable target becomes 0644);
    • hash, oversize-stream, declared-length and short-stream mismatches, with parent directories rolled back;
    • the worker limit and the free-space limit;
    • symlinked parent, escaping symlink, symlink target, .git path and missing grant;
    • a refused grant and a stalled relay;
    • worker negotiation and guarded execution, and that a worker never advertises the operation to a Code API without it.
  • service/src/workspace-tools/import.test.ts (ioredis-mock, the real session ownership and session-key code, and real prepareSandboxEgress grant sealing) covers:
    • ownership acceptance;
    • rejection of another principal's object, a mixed session and object, a missing upload record and an expired session;
    • grant scope: one object, no uploads, two reads, TTL;
    • the synchronous route: relay, revocation, caller grant, foreign object, stolen request, oversize;
    • IMPORT_UNAVAILABLE when no grants can be minted;
    • the durable route with the real RedisWorkspaceRequests: one grant across idempotent retries, and a conflict before minting.
  • service/src/bridge/router.test.ts: registration offers import_file only when grants can be minted.
  • tests/compose-bridge-config.cjs: the API size-limit pass-through.

Add the additive `import_file` workspace operation so a chat's uploaded
files, binaries included, land in an attached BYOM workspace.

Code API verifies the caller owns the stored object with the download
route's session rule, mints a one-object, read-only, short-lived egress
grant, and relays opaque handles to the worker. The worker streams the
object through its file relay upstream into a same-directory temporary
file, enforces the declared size and SHA-256, refuses `.git` paths and
symlinked parents, and installs it atomically as 0644. Identical bytes
are an idempotent no-op; different bytes conflict unless `overwrite`.

The operation is negotiated: Code API offers it only with an egress
gateway, and workers advertise it only with writes enabled and a relay
upstream configured.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Head: 3035b45

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T05:11:50.126728Z 24c61a5 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Head: 3035b45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3035b45977

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1309 to +1310
if (target.state === 'different') {
await rename(temporary, installTarget);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make overwrite conditional on the inspected inode

When overwrite: true targets an existing file, another local process can replace that file after the preceding lstat(candidate) identity check but before this unconditional rename. The rename then silently overwrites the new inode rather than returning EDIT_CONFLICT, so a concurrent editor's save can be lost despite the operation's guarantee that it replaces only the inode it inspected. Use an atomic compare/exchange strategy or otherwise prevent a path replacement in this check-to-rename window.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narrowed rather than changed, in 528c5ea. There is no portable atomic compare-and-rename (RENAME_EXCHANGE is Linux-only and not exposed by the root-access layer), and this is the same check-then-rename window write_file/edit_file already have. Bridge mutations of one root are serialized by Code API and the worker guard, so only an out-of-band local writer can race the last step. The inode recheck now runs immediately before the rename, and the docs/comments no longer claim a stronger guarantee.

Comment thread service/src/workspace-tools/router.ts Outdated
Comment on lines +229 to +232
let preparedImport: PreparedWorkspaceImport | undefined;
if (prepareImport != null) {
try {
preparedImport = await prepareImport();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register disconnect cancellation before preparing the grant

In the synchronous /workspace-tools/execute import path, ownership lookup and the egress-gateway request are awaited before the request/response abort listeners are installed. If the client disconnects during either await, both events have already fired by the time the listeners are attached, so dispatchWorkspaceTool receives a live signal and can still mutate the workspace after the caller cancelled. Install the listeners before the first import-specific await and also check the already-aborted/closed state before dispatch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 528c5ea: abort listeners (plus an already-destroyed-socket check) are installed before ownership and grant minting, and a caller that left meanwhile returns before dispatch; its grant is still revoked in finally. Covered by a new disconnect test.

Comment on lines 730 to 733
if (entry == null) {
try {
await mkdir(current, 0o777);
await mkdir(current, mode);
created.push(current);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the promised directory mode independently of umask

On POSIX systems the mode passed to mkdir/mkdirat is filtered through the process umask. A worker launched with a restrictive umask such as 0077 therefore creates import parents as 0700, not the documented 0755, which can make the imported path inaccessible to other workspace users or sandbox identities. As is already done for the imported file, explicitly chmod newly created import directories to 0755 before committing them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 528c5ea: directories created by an import are fchmod'ed to 0755 through a held descriptor before the file is staged; the e2e test now runs under umask 0077.

Comment thread packages/code/src/workspace.ts Outdated
Comment on lines +1261 to +1262
hash.update(chunk);
await writeImportChunk(handle, chunk, written);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hash the staged file instead of only the incoming chunks

The SHA-256 is computed from the downloaded chunks before those bytes are independently written to a temporary file, and the later installation check verifies only the inode. Because that temporary file is visible inside the user-owned workspace during the transfer, another local process can modify already-written offsets or change the file after this digest check; the altered file is then installed while the result still reports the requested SHA-256. Re-read and hash the staged inode after writing, or otherwise prevent concurrent modification until installation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 528c5ea: the stream no longer drives the digest. After sync, size and SHA-256 are computed by re-reading the staged temporary file through its own descriptor, which is what gets installed. A new test rewrites staged bytes mid-transfer and expects INVALID_REQUEST with nothing installed.

Comment thread service/src/workspace-tools/router.ts Outdated
Comment on lines +189 to +193
if (request.operation === 'import_file' && options.imports != null) {
const imports = options.imports;
let sessionKey: string;
try {
sessionKey = await imports.authorize(req, request);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve durable idempotency before reauthorizing the source

Every durable import POST reauthorizes the upload before RedisWorkspaceRequests.submit can detect an existing request ID. If the initial response was lost and the upload or session record is deleted or expires before the client retries, the retry returns IMPORT_SOURCE_UNAUTHORIZED even though the matching durable request may still be queued, running, or completed and capable of mutating the workspace. Move this authorization into the new-record preparation path so a same-fingerprint retry can return the stored status without requiring the source to remain live.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 528c5ea: ownership is checked inside the durable prepare hook, which runs only for a new request ID, so a same-fingerprint retry returns the stored status even after the upload expires (covered in the durable test).

- Verify size and SHA-256 on the synced staged file, not just the stream.
- Set created parent directories to 0755 regardless of umask.
- Install disconnect listeners before ownership and grant minting, and
  never dispatch for a caller that already left.
- Authorize durable imports only for new request IDs so retries of an
  existing request stay idempotent after the upload expires.
- Gate worker advertisement behind LIBRECHAT_CODE_ALLOW_WORKSPACE_IMPORTS,
  since older LibreChat releases reject unknown operations.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Head: 528c5ea

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 528c5eac5c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/code/src/workspace-import.ts Outdated
Comment on lines +150 to +151
arm();
yield next.value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pause the stall timer while processing each chunk

When writing a yielded chunk takes longer than stallTimeoutMs—for example on a slow or temporarily blocked network filesystem—the timer armed immediately before yield aborts the response while the consumer is writing, even if the source has already delivered the next or final chunk. The subsequent reader.read() then reports a source-transfer stall and rejects an otherwise valid import; arm the timer only while awaiting response headers or reader.read(), as the option contract specifies.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 400c032: the timer is armed only around reader.read() (and the header wait) and cleared before each yield, so consumer writes never count. Covered by a slow-consumer test.

async function removeCreatedDirectories(created: readonly string[]): Promise<boolean> {
for (const directory of [...created].reverse()) {
try {
await rmdir(directory);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify created directory identity before rollback

When an out-of-band local process removes or renames an import-created parent and creates a new empty directory at the same path before the import fails, this pathname-only rmdir deletes the other process's directory. Transfers can leave these paths exposed for minutes, and created stores no inode identity, so rollback cannot distinguish the original directory from a replacement; retain and verify directory identity (or a held descriptor) before removing it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 400c032: each created directory's dev/ino is recorded through its descriptor when its mode is set; rollback removes a directory only while it is still that inode, otherwise it reports a possibly committed mutation. Covered by a test that swaps the parent mid-import.

Comment thread service/src/workspace-tools/requests.ts Outdated
const registration = await this.bridge.prepareDurableWorkspaceTool({
...args, tenantId: args.owner.tenantId,
});
const request = args.prepare == null ? args.request : await args.prepare();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revoke grants when durable enqueue is rejected

For a new durable import, this callback mints the egress grant before the atomic queue submission, but returns only the rewritten request and discards the grant ID. If Redis fails, the queue is full, or a concurrent submission wins with the same request ID, no record uses this invocation's grant and no code revokes it, so repeated rejected submissions leave unused egress-ledger entries active until their potentially ten-minute expiry. Preserve the cleanup handle and revoke the grant unless the queue record actually adopts it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 400c032: the durable prepare hook now returns a release that revokes the grant, called when queue.submit throws or returns anything but accepted. Covered by a racing-submission test (only the adopted grant survives).

Comment thread packages/code/src/workspace.ts Outdated
throw new WorkspaceToolError('Workspace storage is unavailable', 'WRITE_UNAVAILABLE');
} finally {
await handle?.close().catch(() => undefined);
if (temporaryNeedsCleanup) await unlink(temporary).catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface temporary-file cleanup failures

When an import fails before installation and unlinking its staging file also fails—especially when the destination parent already existed—this catch suppresses the cleanup failure and rethrows the original error as though no mutation remained. The workspace can therefore retain a large, possibly already chmodded 0644 file containing upload bytes while the mutation guard is cleared; report the cleanup failure as a potentially committed mutation instead of silently ignoring it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 400c032: a staging unlink failure other than ENOENT now raises WRITE_UNAVAILABLE with mutationMayHaveCommitted, so the guard stays armed.

- Roll back a created parent only while it is still the inode the import
  created; otherwise report a possibly committed mutation.
- Report an unremovable staging file as a possibly committed mutation.
- Arm the stall timer only while waiting on the source, not during writes.
- Revoke a durable import's grant when the queue does not adopt it.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Head: 400c032

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 400c03255e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.toLowerCase()
.replace(/\p{Default_Ignorable_Code_Point}/gu, '')
.replace(/[. ]+$/, '');
return folded === '.git' || folded === 'git~1';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject NTFS alternate-stream aliases for .git

On Windows with NTFS, a path such as .git::$INDEX_ALLOCATION/config passes this predicate even though ::$INDEX_ALLOCATION aliases the .git directory. installImportedFile subsequently canonicalizes that parent with realpath and installs into the resulting .git path, allowing an import to modify Git metadata despite the explicit .git prohibition. Reject NTFS alternate-data-stream syntax, particularly the $INDEX_ALLOCATION directory alias.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 393c251: .git detection now compares only the part of a segment before any :, so .git::$INDEX_ALLOCATION, .GIT:stream and the git~1 variants are refused; ordinary names containing : on POSIX stay allowed. Tests added.

Compare only the part of each segment before an NTFS stream separator, so
`.git::$INDEX_ALLOCATION` and similar aliases are refused like `.git`.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review — final review (pass 4 of 4): flag only blocking issues.

Head: 393c251

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 393c251de0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The code bridge is experimental, so import_file is now on whenever a worker
can perform it (writes enabled and a relay upstream configured). Operators
opt out with LIBRECHAT_CODE_ALLOW_WORKSPACE_IMPORTS=false or
--no-workspace-imports; any other value fails startup.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review — final review (one extra pass after the default-on change): flag only blocking issues.

Head: 24c61a5

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 24c61a525f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila merged commit 6c5491c into main Oct 23, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant