Skip to content

LOU-X6: workspace tools (file system and shell) over pluggable providers - #58

Merged
LinuxDevil merged 4 commits into
mainfrom
lou-x6-workspace-tools
Oct 1, 2026
Merged

LinuxDevil merged 4 commits into
mainfrom
lou-x6-workspace-tools

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What

Workspace tools for Claude-Code-style coding agents, in src/tools/workspace/ (exported from src/tools/index.ts and the package root):

const workspace = new NodeWorkspace({ root: './project' });
const tools = [...createFsTools(workspace), createShellTool(workspace)]; // shell needs approval by default
  • FsProvider (readFile, writeFile, stat, readdir, mkdir, rm, optional glob) and ShellProvider (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?) creates read_file (numbered lines, offset/limit, capped, says how to continue), write_file, edit_file (exact replacement; clear errors for a missing or non-unique old_string, with a CRLF hint), list_dir, glob and grep (an invalid regex is a clean tool error). Options: readOnly, per-tool needsApproval, and size caps.
  • createShellTool(shell, options?): needsApproval defaults to true (or takes a predicate over the command). allow/deny patterns are checked before approval, and string allow patterns refuse shell operators, so git status; rm -rf ~ does not pass as git status. timeout_ms defaults to 120s and is clamped to maxTimeoutMs. Output keeps head and tail and marks the cut. The run's AbortSignal kills the process tree.
  • Providers:
    • NodeWorkspace: real fs and child_process, confined to root.
    • MemoryWorkspace: in-memory tree with the same path rules and a programmable, recorded exec stub.
    • SandboxShell: a ShellProvider over any SandboxAdapter, for example the Docker SubprocessSandbox. It needed about 60 lines, so it is in this PR.
  • Docs: new docs/workspace-tools.md that spells out the security model, linked from a README feature bullet, the README Documentation list and docs/api-overview.md. llms.txt/llms-full.txt are regenerated.

Why

Both competitors ship file and shell tools: open-harness has createFsTools/createBashTool over FsProvider/ShellProvider, and eve has built-in bash, read_file, write_file, glob and grep. This SDK had no workspace tools.

Security model (core of the ticket)

  • Lexical check (all providers, every platform). Rejected: .. 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.
  • Symlinks. realpath runs 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/grep walks never follow links. rm removes the link itself, never its target.
  • Errors. Rejections throw WorkspaceError. The model sees { error: 'WorkspaceError', toolName, message } and the run continues. Messages never include host paths.
  • Shell environment. Commands run with cwd = root and an allow-listed environment: PATH, HOME/USERPROFILE, TEMP/TMP/TMPDIR, LANG, plus on Windows SystemRoot, SystemDrive, ComSpec, PATHEXT and WINDIR, which cmd.exe needs to start. env and inheritEnv are opt-in. Host API keys do not reach commands.
  • Process kill. Timeout and abort kill the whole tree: the process group on POSIX, taskkill /T /F on Windows.
  • Limits are documented. NodeWorkspace's shell is NOT a sandbox. Deny lists are bypassable. TOCTOU races and hard links are not covered. grep regexes are not time-limited.

How verified

Merged origin/main at 60d3d57 (ci: run type tests; docs: ticket status after wave 3 (#56)) and resolved one conflict in docs/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 0
  • npm 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 0
  • npm run test:coverage: Test Files 110 passed (110), Tests 1522 passed | 4 skipped (1526), All files | 86.43 | 91.15 | 89.48 | 86.43
  • npm 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 cleanly
  • npm run docs:llms:check: exit 0

New tests: 108 in total, 1 skipped on this machine.

  • paths.test.ts: every escape vector on linux and win32, plus glob semantics.
  • fsTools.test.ts: full behavior on MemoryWorkspace, including caps, edit uniqueness, and a rejected path reaching the model as a tool error through createAgent + mockModel.
  • shellTool.test.ts: approval default pauses an AgentExecutor run; predicate; adversarial allow/deny cases; timeout clamping; head/tail cap.
  • NodeWorkspace.test.ts: runs on a temp dir.
    • Every escape vector for read, write, stat, readdir, mkdir and rm.
    • Real symlink escapes: a junction/dir link pointing outside, including new files under it; a dangling link; a link that stays inside; a file symlink.
    • A secret env var does not leak.
    • Output cap.
    • Timeout and abort kill a 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 a createAgent run throw when the model calls shell. The docs say so and show AgentExecutor + approvalStore, or needsApproval: false + allow for createAgent. Follow-up idea: let createAgent accept an approvalStore (or onApproval) so the safe default works there too.
  • SandboxShell cannot cancel a running container, because SandboxAdapter has no cancel hook. On abort it returns aborted: true at once, and the container stops at its timeout. Follow-up idea: add an optional signal to SandboxRunOptions.
  • Files touched outside src/tools/workspace/: src/tools/index.ts (one export line), README.md, docs/api-overview.md, docs/workspace-tools.md, and the generated llms.txt/llms-full.txt.

🤖 Generated with Claude Code

LinuxDevil and others added 4 commits October 1, 2026 09:58
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>
@LinuxDevil
LinuxDevil merged commit b5cd5f5 into main Oct 1, 2026
1 check 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