Skip to content

🗺️ feat: List Attached Workspace Files - #92

Merged
danny-avila merged 4 commits into
mainfrom
danny-avila/code-workspace-list
Sep 3, 2026
Merged

🗺️ feat: List Attached Workspace Files#92
danny-avila merged 4 commits into
mainfrom
danny-avila/code-workspace-list

Conversation

@danny-avila

Copy link
Copy Markdown
Collaborator

Summary

I added bounded, deterministic file discovery for worker-local attached workspaces so agents can inspect an existing project, Git repository, or empty directory without knowing filenames in advance. This PR is stacked on #90.

  • Add the provider-neutral list_files request and result contracts to @librechat/code.
  • Validate result paths against the requested subtree, reject duplicates and unexpected metadata, and cap listings at 500 paths.
  • Execute rg --files without a shell, preserve normal ignore behavior, avoid symlink following, enforce a ten-second deadline, and support cancellation.
  • Advertise the operation only through workers using the updated workspace executor.
  • Document the relative-path data boundary and repository-optional workspace model.

Change Type

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Testing

  • Ran npm test in packages/code: 126 tests passed.
  • Ran focused bridge store and workspace route tests with Bun: 4 tests passed.
  • Ran a live Code API/Redis/outbound-worker test against a plain local directory. A subtree listing returned src/app.ts and src/worker.ts; an empty directory returned an empty bounded result.

Test Configuration:

  • macOS arm64
  • Code API: 127.0.0.1:23117
  • Redis: 127.0.0.1:26385
  • Static local bridge authentication
  • @librechat/code outbound worker with --worker-dir pointing to a non-Git temporary directory

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • I have made pertinent documentation changes
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes
  • Any changes dependent on mine have been merged and published in downstream modules.

Base automatically changed from danny-avila/code-workspace-api to main September 3, 2026 00:06
@danny-avila
danny-avila force-pushed the danny-avila/code-workspace-list branch from 25e4920 to a2a25e1 Compare September 3, 2026 00:08
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the current PR head a2a25e1. Confirm that this exact commit is the reviewed commit and ignore findings that apply only to earlier heads.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 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-09-03T00:54:01.804832Z 5c716c7 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
danny-avila force-pushed the danny-avila/code-workspace-list branch from a2a25e1 to 190bb9b Compare September 3, 2026 00:11
@danny-avila

danny-avila commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review exact commit 190bb9b. The prior requested review became stale only because prerequisite PR #90 was squash-merged and this branch was restacked directly onto the resulting main commit; feature behavior is unchanged.

@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: 190bb9b943

ℹ️ 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.ts Outdated
'path',
'--null',
'--',
canonicalListPath,

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 Keep the listed directory bound to the checked inode

If workspace contents can change concurrently, the path checked by realpath() can be replaced with an escaping symlink before this rg process opens canonicalListPath, causing filenames outside the registered root to be returned. The rg --help description limits --follow/--no-follow to links encountered “while traversing directories”; an explicit command-line directory that is a symlink is still followed (for example, rg --files --no-follow -- link lists files below link). This violates the documented symlink-escape boundary, so traversal must remain tied to the verified directory inode or each emitted path must be confined before it is returned.

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 d524fb3. Every representable candidate is now resolved and checked against the canonical workspace root after enumeration and before it is returned, so a replaced explicit directory cannot expose an escaping result.

Comment thread packages/code/src/workspace.ts Outdated
Comment on lines +587 to +591
const portablePath = sep === '\\' ? path.split(sep).join('/') : path;
const normalizedPath = portablePath.startsWith('./')
? portablePath.slice(2)
: portablePath;
paths.push(

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 Filter filenames that the portable protocol cannot represent

On POSIX workspaces, a valid filename containing a backslash (for example, foo\bar) is emitted unchanged here because sep is /. The resulting list then fails isWorkspaceToolResult(), whose portable-path check rejects every backslash, so dispatchWorkspaceTool() turns the otherwise fulfilled settlement into RESULT_INVALID and the entire listing request fails. Skip or otherwise encode filesystem paths that cannot satisfy the result contract before adding them to paths.

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 d524fb3. Unrepresentable filesystem names are filtered before result construction, with a POSIX backslash-filename regression test.

Comment on lines 665 to 668
this.capabilities = {
protocolVersion: BRIDGE_PROTOCOL_VERSION,
operations: ['read_file', 'search_text'],
operations: ['read_file', 'search_text', 'list_files'],
workspaces,

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 Negotiate list_files before advertising it to v1 servers

When this independently installed worker CLI is upgraded before its Code API deployment, it still identifies the capability payload as protocol version 1 but now always advertises three operations. The preceding v1 server validates workspace capabilities with operations.length <= 2 and accepts only read_file or search_text, so it returns HTTP 400 for every registration; the worker then remains in its reconnect loop and cannot lease even ordinary sandbox work. Preserve a registration-compatible capability set until server support is known, or introduce explicit protocol negotiation for this operation.

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 d524fb3 with explicit registration negotiation. A new worker first advertises the v1-compatible read/search subset; a supporting Code API returns its workspace operation set and the worker then re-registers with list_files. Older servers keep ordinary sandbox and existing workspace operations available. Both paths have regression tests.

@danny-avila
danny-avila force-pushed the danny-avila/code-workspace-list branch from 190bb9b to d524fb3 Compare September 3, 2026 00:22
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review exact commit d524fb3. All three findings on 190bb9b are addressed with focused coverage; confirm this exact head and ignore earlier revisions.

@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: d524fb3909

ℹ️ 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 +291 to +293
this.registrationCapabilities = this.options.capabilities;
try {
registration = await register(this.registrationCapabilities);

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 Reset the timeout before negotiated re-registration

When the server supports list_files and the compatible registration consumes a substantial part of registrationTransportTimeoutMs, this second request shares the AbortController and timer started before the first request. For example, with the default 10-second timeout, two otherwise acceptable 6-second responses always abort the second request after about 4 seconds; the catch resets to compatible capabilities, and the worker repeats the same failing sequence without ever leasing work. Give each registration request its own transport timeout or handle exhaustion by retaining the successful compatible registration.

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 1dce623. If optional list_files promotion times out or otherwise fails, the worker now retains the already-successful compatible registration and proceeds with the baseline capability set; caller cancellation still aborts registration. Added a stalled-promotion regression test.

Comment thread packages/code/src/workspace.ts Outdated
Comment on lines +653 to +657
for (const candidate of candidates) {
let canonicalPath: string;
try {
canonicalPath = await realpath(
resolveWorkspacePath(root, candidate.filesystemPath),

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 Enforce listing cancellation during candidate verification

After rg exits, its 10-second timer is cleared, but this loop can perform up to 1,000 serial realpath calls without consulting the abort signal or any deadline. On a slow or network-mounted workspace, or when the caller aborts during verification, list_files can therefore continue well past its timeout or assignment deadline and keep the worker occupied; direct LocalWorkspaceTools callers may even receive a normal result after aborting. Carry the overall listing deadline into this loop and check cancellation and timeout between candidates.

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 1dce623. The listing deadline now begins before initial path canonicalization and is enforced, together with cancellation, around every candidate realpath/stat verification rather than ending when ripgrep exits.

Comment thread packages/code/src/workspace.ts Outdated
Comment on lines +656 to +658
canonicalPath = await realpath(
resolveWorkspacePath(root, candidate.filesystemPath),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Exclude non-regular explicit listing targets

When request.path directly names an in-workspace FIFO, socket, or device, ripgrep emits that positional target even though normal directory traversal skips it; this was confirmed against rg --help (--files “Print[s] each file that would be searched”) and by running the exact flags against a FIFO. Because this validation only resolves and confines the candidate without checking its type, list_files reports a path that read_file and search_text reject as non-regular. Stat each resolved candidate and retain only regular files.

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 1dce623. Each confined candidate is statted within the same deadline and only regular files are returned. Added an explicit FIFO regression test.

@danny-avila
danny-avila force-pushed the danny-avila/code-workspace-list branch from d524fb3 to 1dce623 Compare September 3, 2026 00:31
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the current PR head 1dce623. Confirm that this exact commit is the reviewed commit and ignore findings that apply only to earlier heads. The second-round findings are addressed, and I completed the cross-seam review of registration fallback, mixed-version rollout, listing confinement, deadline/cancellation propagation, result bounds, and file-type consistency.

@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: 1dce623726

ℹ️ 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.ts Outdated
candidates.push({ filesystemPath: normalizedPath, resultPath });
};

child.stdout.setEncoding('utf8');

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 Parse listing paths without lossy UTF-8 replacement

On POSIX, setEncoding('utf8') replaces invalid filename bytes with U+FFFD before isSafePortableRelativePath() checks representability. If a workspace contains both an unrepresentable name such as raw byte 0xff and a valid file named , an rg result for the former is converted into the latter; verification then stats the valid file and may return it even when that file was excluded by ignore rules. Fresh evidence in this head is that the raw rg --null stream is decoded at this line before validation; parse it as buffers and reject byte sequences that do not round-trip as UTF-8.

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 d0383e8069a7b9fbf880c78ae1bc02b179afebf4. Listing output now remains byte-oriented through NUL framing and uses fatal UTF-8 decoding per filename, so invalid byte sequences are rejected before any filesystem verification can alias them to U+FFFD. Added a Linux-capable regression covering the invalid-byte/valid-U+FFFD collision.

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.

Commit reference correction: the published fix is d0383e8.

Comment thread packages/code/src/protocol.ts Outdated
hasOnlyKeys(result, WORKSPACE_LIST_RESULT_KEYS) &&
Array.isArray(result.paths) &&
result.paths.length <= maxResults &&
new Set(result.paths).size === result.paths.length &&

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 Reject normalized duplicate listing paths

When a custom or buggy worker returns aliases such as src/app.ts and src/./app.ts, both entries pass the portable-path and requested-subtree checks, while this raw-string Set treats them as distinct. The Code API therefore accepts duplicate references to the same logical file, allowing duplicates to consume the result bound and reach callers despite the result contract's duplicate validation; either require canonical result paths or deduplicate their normalized forms.

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 d0383e8069a7b9fbf880c78ae1bc02b179afebf4. Result validation now deduplicates normalized portable paths, so aliases such as src/app.ts and src/./app.ts cannot represent the same logical file twice. Added the reported pair as a protocol regression test.

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.

Commit reference correction: the published fix is d0383e8.

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the current PR head d0383e8069a7b9fbf880c78ae1bc02b179afebf4. Confirm that this exact commit is the reviewed commit and ignore findings that apply only to earlier heads. The third-round findings are addressed by enforcing one canonical portability invariant at both seams: byte-exact fatal UTF-8 decoding at enumeration and normalized duplicate rejection at protocol validation.

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Correction: please review the current PR head d0383e8. Confirm that this exact commit is the reviewed commit and ignore findings that apply only to earlier heads. The third-round findings are addressed by enforcing one canonical portability invariant at both seams: byte-exact fatal UTF-8 decoding at enumeration and normalized duplicate rejection at protocol validation.

@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: d0383e8ce0

ℹ️ 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.ts Outdated
if (rawPath.length === 0 || stoppedForLimit) return;
let path: string;
try {
path = new TextDecoder('utf-8', { fatal: true }).decode(rawPath);

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 leading UTF-8 BOMs in filenames

At the requested head d0383e8ce0165368eed0259866406f4d6bf05269, an explicitly listed top-level path beginning with U+FEFF is emitted by rg with EF BB BF at byte zero, but TextDecoder strips that sequence by default. The new decoder therefore changes foo into foo, causing verification to resolve the wrong sibling (potentially returning an ignored file to direct callers) or causing server-side subtree validation to reject the result. Set ignoreBOM: true or enforce an exact decode/re-encode round trip so valid UTF-8 filenames remain byte-exact.

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 5c716c773317c8cd9a4f0a9138e3c9562fb2f199. Filename decoding now preserves a leading BOM and requires the decoded string to re-encode to the exact original bytes, while still rejecting malformed UTF-8. Added the reported BOM/ignored-sibling regression.

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.

Commit reference correction: the published fix is 5c716c7.

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the current PR head 5c716c7. Confirm that this exact commit is the reviewed commit and ignore findings that apply only to earlier heads. Filename enumeration now enforces byte-exact valid UTF-8, including preservation of leading BOM code points; normalized result aliases are rejected at the protocol boundary.

@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: 5c716c714e

ℹ️ 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 +687 to +689
regularFile = (
await withinListDeadline(stat(canonicalPath), signal, deadline)
).isFile();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Exclude explicit symlink file aliases

When request.path is a symlink to a regular file inside the workspace, the initial realpath() causes ripgrep to enumerate the target, but the result is remapped back to the symlink alias. Because this check stats only the canonical target, list_files returns that alias even though read_file opens the reported path with O_NOFOLLOW and rejects it as INVALID_PATH. Check the final component in the reported namespace or omit such aliases so listings contain usable file paths.

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 b602bb2. Listing now validates the final component in the reported namespace with lstat and requires that its canonical target exactly matches the enumerated file, so an explicit file symlink alias is omitted while safe directory-alias descendants remain usable.

Comment thread packages/code/src/workspace.ts Outdated
'path',
'--null',
'--',
canonicalListPath,

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 ignore rules for requested subtrees

When request.path names a directory or file excluded by .ignore or .gitignore, passing canonicalListPath as ripgrep's positional path makes the exclusion ineffective. I checked ripgrep 15.1.0's rg --help, whose <PATH> documentation states: “File paths specified on the command line override glob and ignore rules.” Consequently, a root listing can hide vendor/, while listing with path: "vendor" reveals the entire ignored subtree and may traverse a very large dependency tree. Enumerate from an unignored ancestor and filter to the requested subtree, or independently enforce ignore matching for the explicit target.

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 b602bb2. Ripgrep now always enumerates from the workspace root and uses a glob to narrow to the canonical requested subtree, so positional paths no longer override .ignore/.gitignore policy. Added an explicitly ignored subtree regression test.

@danny-avila
danny-avila merged commit adf2adf into main Sep 3, 2026
5 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.

2 participants