Repository navigation
LOU-X6: workspace tools (file system and shell) over pluggable providers - #58
Merged
Merged
Conversation
Adds file system and shell tools for Claude-Code-style coding agents (src/tools/workspace, exported from the tools barrel and the root): - FsProvider / ShellProvider: small documented interfaces so the tools run against any backend (local dir, memory, Docker, E2B/Daytona/Cloudflare). - createFsTools(fs, options?): read_file (numbered lines, offset/limit, capped with a continue hint), write_file, edit_file (exact replacement, helpful errors for missing / non-unique old_string unless replace_all), list_dir, glob, grep (invalid regex is a clean tool error). readOnly and per-tool needsApproval options. - createShellTool(shell, options?): needsApproval defaults to true (or a predicate over the command); allow/deny patterns checked before approval (string allow patterns refuse shell operators so `git status; rm -rf ~` does not pass); timeout default 120s, clamped to maxTimeoutMs; output capped keeping head and tail; the run's AbortSignal kills the process. - NodeWorkspace: every path is normalized (no .., absolute, drive-letter, UNC, NUL; mixed separators; Windows ADS/device names) and resolved with realpath on the deepest existing ancestor, so symlinks pointing outside the root are rejected, including for not-yet-existing write targets and dangling links. Commands run with cwd = root and an allow-listed env (PATH, HOME/USERPROFILE, TEMP/TMP, LANG + Windows essentials), so host API keys do not leak. Timeout/abort kill the whole process tree (process group on POSIX, taskkill /T on Windows). - MemoryWorkspace: in-memory tree with the same path rules and a programmable, recorded exec stub for tests. - SandboxShell: ShellProvider over any SandboxAdapter (e.g. the Docker SubprocessSandbox). Rejections are WorkspaceErrors, which reach the model as structured tool errors instead of aborting the run. docs/workspace-tools.md spells out the security model, including that NodeWorkspace's shell is not a sandbox. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/api-overview.md
llms.txt / llms-full.txt are generated from docs (LOU-D12, merged from main) and must include the new docs/workspace-tools.md page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # README.md # llms-full.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Workspace tools for Claude-Code-style coding agents, in
src/tools/workspace/(exported fromsrc/tools/index.tsand the package root):FsProvider(readFile,writeFile,stat,readdir,mkdir,rm, optionalglob) andShellProvider(exec(command, { cwd, timeoutMs, signal, env }) -> { stdout, stderr, exitCode, timedOut, aborted? }). Both are small and documented so users can back them with E2B, Daytona, Cloudflare and similar.createFsTools(fs, options?)createsread_file(numbered lines, offset/limit, capped, says how to continue),write_file,edit_file(exact replacement; clear errors for a missing or non-uniqueold_string, with a CRLF hint),list_dir,globandgrep(an invalid regex is a clean tool error). Options:readOnly, per-toolneedsApproval, and size caps.createShellTool(shell, options?):needsApprovaldefaults totrue(or takes a predicate over the command).allow/denypatterns are checked before approval, and string allow patterns refuse shell operators, sogit status; rm -rf ~does not pass asgit status.timeout_msdefaults to 120s and is clamped tomaxTimeoutMs. Output keeps head and tail and marks the cut. The run'sAbortSignalkills the process tree.NodeWorkspace: real fs and child_process, confined toroot.MemoryWorkspace: in-memory tree with the same path rules and a programmable, recordedexecstub.SandboxShell: aShellProviderover anySandboxAdapter, for example the DockerSubprocessSandbox. It needed about 60 lines, so it is in this PR.docs/workspace-tools.mdthat spells out the security model, linked from a README feature bullet, the README Documentation list anddocs/api-overview.md.llms.txt/llms-full.txtare regenerated.Why
Both competitors ship file and shell tools: open-harness has
createFsTools/createBashTooloverFsProvider/ShellProvider, and eve has built-inbash,read_file,write_file,globandgrep. This SDK had no workspace tools.Security model (core of the ticket)
..above the root, absolute paths, drive letters (C:\,C:x), UNC and\\?\paths, and NUL bytes. Backslashes count as separators everywhere, so mixed separators cannot hide a... On Windows,:(ADS), device names (CON,NUL, ...) and dot/space-only segments (..acts as..) are also rejected.realpathruns on the deepest existing ancestor, and the result must stay inside the root's real path. This also covers write targets that do not exist yet. Dangling links are never followed.glob/grepwalks never follow links.rmremoves the link itself, never its target.WorkspaceError. The model sees{ error: 'WorkspaceError', toolName, message }and the run continues. Messages never include host paths.cwd= root and an allow-listed environment:PATH,HOME/USERPROFILE,TEMP/TMP/TMPDIR,LANG, plus on WindowsSystemRoot,SystemDrive,ComSpec,PATHEXTandWINDIR, which cmd.exe needs to start.envandinheritEnvare opt-in. Host API keys do not reach commands.taskkill /T /Fon Windows.NodeWorkspace's shell is NOT a sandbox. Deny lists are bypassable. TOCTOU races and hard links are not covered.grepregexes are not time-limited.How verified
Merged
origin/mainat60d3d57(ci: run type tests; docs: ticket status after wave 3 (#56)) and resolved one conflict indocs/api-overview.md, keeping both the workspace rows and the new todo-tools row. Then ran the full definition of done on the merged result:npx tsc --noEmit: exit 0npm run lint:✖ 433 problems (0 errors, 433 warnings). These are all existing warnings; the new files add none.npm run build && npm run build --workspace=packages/create-loushy-agent: both exit 0npm run test:coverage:Test Files 110 passed (110),Tests 1522 passed | 4 skipped (1526),All files | 86.43 | 91.15 | 89.48 | 86.43npm run fallow:✓ No issues found,✓ No code duplication found,✓ 0 above threshold · 2610 analyzed · maintainability 91.9 (good)npm run docs:verify-snippets:all 49 snippet(s) type-check against src/; 6 also run cleanlynpm run docs:llms:check: exit 0New tests: 108 in total, 1 skipped on this machine.
paths.test.ts: every escape vector onlinuxandwin32, plus glob semantics.fsTools.test.ts: full behavior onMemoryWorkspace, including caps, edit uniqueness, and a rejected path reaching the model as a tool error throughcreateAgent+mockModel.shellTool.test.ts: approval default pauses anAgentExecutorrun; predicate; adversarial allow/deny cases; timeout clamping; head/tail cap.NodeWorkspace.test.ts: runs on a temp dir.node -e "setTimeout(()=>{},60000)"child and a grandchild it spawned; the test checks the grandchild's PID is gone.SandboxShell.test.ts: uses a fake adapter.Tested on Windows 11. The file-symlink case was skipped there with a clear message (EPERM: Windows needs Developer Mode or admin rights for file symlinks). The directory cases use junctions and ran. The POSIX process-group kill path is exercised by CI on Linux.
Notes for reviewer
createAgent()has no approval store, so the shell tool's safe default (needsApproval: true) makes acreateAgentrun throw when the model callsshell. The docs say so and showAgentExecutor+approvalStore, orneedsApproval: false+allowforcreateAgent. Follow-up idea: letcreateAgentaccept anapprovalStore(oronApproval) so the safe default works there too.SandboxShellcannot cancel a running container, becauseSandboxAdapterhas no cancel hook. On abort it returnsaborted: trueat once, and the container stops at its timeout. Follow-up idea: add an optionalsignaltoSandboxRunOptions.src/tools/workspace/:src/tools/index.ts(one export line),README.md,docs/api-overview.md,docs/workspace-tools.md, and the generatedllms.txt/llms-full.txt.🤖 Generated with Claude Code