Skip to content

refactor(sandbox): tidy up service, API, and test code; truncate shell output - #173

Draft
Simpleyyt wants to merge 2 commits into
mainfrom
cursor/tidy-sandbox-service-a780
Draft

refactor(sandbox): tidy up service, API, and test code; truncate shell output#173
Simpleyyt wants to merge 2 commits into
mainfrom
cursor/tidy-sandbox-service-a780

Conversation

@Simpleyyt

@Simpleyyt Simpleyyt commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Cleans up the sandbox service without changing its API contract (verified against the routes the backend's docker_sandbox.py calls), and adds output truncation to the shell API.

Shell output truncation (new)

  • shell/view and console records now truncate output to max_length (default 10000 chars), keeping the most recent tail with a (truncated) prefix; shell/exec results get the same default
  • New optional max_length field on the shell/view request, mirroring file/read
  • Stored per-session output is capped at 1MB (with 64KB slack to avoid copying on every 128-byte append), discarding the oldest output — previously a chatty long-running process grew session memory without bound

app/services/shell.py

  • Replace the untyped Dict[str, Dict[str, Any]] session store with a typed ShellSession dataclass
  • Extract _get_session (deduplicates 5 copies of the "session not found" check) and _terminate_process (deduplicates graceful-terminate/force-kill logic used by both exec_command and kill_process)
  • Remove dead code: unused ShellTask model, shell_tasks store, unused subprocess/Tuple imports, and an unused get_console_records call in exec_command
  • Fix the console parameter being shadowed by a local variable in view_shell
  • Straighten out exec_command control flow (single create-process path, no nested try/except swallowing) and guard the output reader so a replaced process can't write into the new command's output
  • Compile the ANSI-escape regex once at module level

app/services/supervisor.py

  • Connect to supervisord lazily (ServerProxy connects on first RPC call), so importing the app no longer requires a running supervisord
  • Deduplicate activate_timeout / extend_timeout into a shared _schedule_shutdown
  • Fix the import-time shutdown timer: it previously created an asyncio task on a never-running loop (silently doing nothing); now it uses get_running_loop() and falls back to threading.Timer when no loop is running
  • Centralize timer cancellation in _cancel_timer, remove bare except: blocks, unify auto_expand/auto_extend naming

app/services/file.py

  • Remove unused imports (subprocess, mimetypes, BinaryIO), rename misleading *_async inner functions (they are sync functions run via asyncio.to_thread), simplify ensure_file and exception re-raising

API layer

  • Move TimeoutRequest out of the route file into app/schemas/supervisor.py
  • Serialize all supervisor responses with model_dump() for consistency; add response_model=Response to the upload route
  • Drop redundant empty-session-ID checks now handled by the service layer; use os.path.basename for the download filename

Misc

  • main.py: remove duplicated log-level setup
  • config.py: migrate deprecated class Config to SettingsConfigDict
  • Tests: upload to /tmp instead of the mounted repo directory — this removes sandbox/resource/test_upload_unique.txt, which a previous test run had accidentally committed into the repo; clean up unused imports and reuse the client fixture

Testing

  • cd sandbox && uv run pytest — 3 passed against the running dev sandbox container
  • ✅ Manual curl verification of every endpoint against the dev container: shell/exec (fast + long-running + session reuse), shell/view (with console records), shell/wait (timeout 400), shell/write (interactive cat), shell/kill, file/read|write|replace|search|find|upload|download (incl. sudo read, invalid regex 400, missing file 404), supervisor/status, supervisor/timeout/activate|extend|cancel|status
  • ✅ Truncation verified against the dev container: small output untouched; 200KB seq output returns 10011 chars starting with (truncated) and keeping the last line; max_length=50 honored on shell/view; console records truncated too; 5MB yes output stored at ~1MB in memory with the tail (DONE_MARKER) preserved
  • ✅ Full-stack end-to-end GUI test (frontend + backend + mockserver + refactored sandbox): sent a task in the web UI at localhost:5173; the mock-LLM-driven agent flow completed — plan, notify message, web search, three file/writes, shell/exec (ls -a), browser navigation, completion with attachments. Sandbox access logs show all file/read, file/write, shell/exec, shell/view calls returning 200, and the files exist in the sandbox with the expected content.

e2e_agent_flow_shell_file_tools.mp4

Shell tool view showing ls -a output with the created files
Completed task with attachment cards

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

cursoragent and others added 2 commits July 6, 2026 15:52
- shell service: replace untyped session dicts with a ShellSession dataclass,
  add _get_session/_terminate_process helpers, remove dead ShellTask model and
  shell_tasks store, fix console-variable shadowing in view_shell, drop unused
  imports, and guard the output reader against writing into a replaced process
- file service: remove unused imports, rename misleading *_async inner
  functions, deduplicate exception re-raising, simplify ensure_file
- supervisor service: connect to supervisord lazily (ServerProxy connects on
  first RPC call) so importing the app no longer requires a running
  supervisord; deduplicate activate/extend into _schedule_shutdown; fix the
  import-time timer to fall back to threading.Timer only when no event loop is
  running; unify auto_expand/auto_extend naming; centralize timer cancellation
- API layer: move TimeoutRequest into app/schemas/supervisor.py, serialize all
  supervisor responses with model_dump for consistency, add response_model to
  the upload route, drop redundant empty-session-ID checks handled by the
  service layer, use os.path.basename for download filename
- main.py: remove duplicated log-level setup; config.py: migrate deprecated
  class Config to SettingsConfigDict
- tests: upload to /tmp instead of the mounted repo directory (removes the
  accidentally committed sandbox/resource/test_upload_unique.txt), clean up
  unused imports, reuse the client fixture in conftest

Co-authored-by: Simpleyyt <simpleyyt@gmail.com>
- view_shell / get_console_records now truncate output to max_length
  (default 10000 chars), keeping the most recent tail with a
  '(truncated)' prefix; exec_command results get the same default
- add optional max_length to the shell/view request, mirroring file/read
- cap stored session output at 1MB per session (with 64KB slack to
  avoid copying on every append), discarding the oldest output

Co-authored-by: Simpleyyt <simpleyyt@gmail.com>
@cursor cursor Bot changed the title refactor(sandbox): tidy up service, API, and test code refactor(sandbox): tidy up service, API, and test code; truncate shell output Jul 7, 2026
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