Skip to content

fix: output limits, BaseException safety, include confinement, usings/statics/inner exceptions (0.3.0) - #5

Merged
IvanMurzak merged 3 commits into
mainfrom
fix/output-limits-base-exceptions
Oct 26, 2026
Merged

IvanMurzak merged 3 commits into
mainfrom
fix/output-limits-base-exceptions

Conversation

@IvanMurzak

Copy link
Copy Markdown
Owner

Contract v0.3 (spec/contract.md), both runtimes, golden fixtures for every shared rule. Version 0.3.0.

Findings fixed

From the Unreal battery:

  • KeyboardInterrupt / GeneratorExit / custom BaseException escaped the Python runner (it caught (Exception, SystemExit) only), so Unreal reported "Python bootstrap failed" with the runner's traceback. Now every BaseException is a Runtime error with the snippet frames. C# has no equivalent: everything catchable derives from Exception (a stack overflow still ends the process, now stated in §3.4).
  • No output limits. New optional request fields maxValueChars / maxLogChars / maxLogLineChars / maxErrorChars (defaults 20000 / 20000 / 2000 / 20000, the numbers Unreal-MCP and Godot-MCP cap at; 0 = none), response field truncated, §6. Logs keep the first lines up to 3/4 of the budget and the last lines that fit, with one "N log line(s) left out" entry, applied as lines arrive. Tracebacks keep the first and last 10 frames; at most 20 diagnostics (+ CE0002).
  • Probing the Python runner also found: an exception whose str() raises escaped; os.chdir leaked into the host; BOM / PEP 263 includes were rejected; malformed JSON fields (includeRoot: 5, imports: [5]) raised; an odd converter value broke run_json; del sys.path, a lowered recursion limit, rot13 coding declarations and deep JSON escaped; tracebacks could show a stray snippet.py from the cwd.

Forwarded from the Unity battery:

  • Includes are confined to the include root (host opt-in: AllowIncludeOutsideRoot / includeOutsideRoot), explicit files must be .cs / .py, paths are reported relative, diagnostics capped.
  • A bad usings entry is reported as file = "usings", using 'X': ... instead of wrapper coordinates.
  • Snippet statics no longer persist across identical requests on Mono: a snippet that declares static state loads a fresh copy per run (re-running type initializers is not reliable: .NET Core does not re-run them). Stateless snippets stay cached.
  • Host alias pattern for colliding defaults (Object = UnityEngine.Object) documented in §3.4.

Forwarded from the Godot battery:

  • Inner exceptions are reported ( ---> Type: message + frames, at most 5); Python reports Caused by: / Raised while handling:.

Quality stages

  • simplify: commit: one clip/truncation idiom, one inside-root test, cached static-state reflection, linear log stream.
  • code review: commit: findings of a two-reviewer high-depth review fixed (see the commit message).

Tests

  • Python 3.14 and 3.11: 37 tests (golden through run_json).
  • .NET: Roslyn 3.8 / 4.8 / 5.9 on net8 + net10 (116), net48 (105); SourceConsumer C# 9 with Roslyn 3.8 and 4.8; scripts/pack.sh prints 0.3.0.

🤖 Generated with Claude Code

IvanMurzak and others added 3 commits October 26, 2026 12:50
…xceptions (0.3.0)

Contract v0.3, both runtimes, golden cases for each:
- Output limits (spec 6): maxValueChars / maxLogChars / maxLogLineChars / maxErrorChars
  (defaults 20000 / 20000 / 2000 / 20000, 0 = none), applied as logs arrive (first and
  last lines kept), `truncated` in the response; tracebacks keep first and last 10
  frames; at most 20 diagnostics (CE0002 counts the rest).
- Python: every BaseException (KeyboardInterrupt, GeneratorExit, custom) is a Runtime
  error instead of escaping into the host; unprintable exception messages, chained
  causes/contexts, working directory restored, includes honour BOM / coding
  declarations, malformed JSON request fields are InvalidRequest, an odd converter
  value no longer breaks the JSON response.
- Includes must stay inside the include root (host opt-in otherwise) and be .cs / .py.
- C#: a bad using is reported as itself (file `usings`), inner exceptions are reported,
  a snippet with static state of its own loads fresh on Mono instead of sharing statics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tic-state check

- Python: _clip is pure (as C#); truncated is derived in one place; one compile-error
  clause; _cost() shared by the log budget; source lines are looked up only for kept
  frames; the log stream splits only incoming text (linear for many small writes).
- C#: one CodeDiagnostic initializer for usings and code; one IsUnder() test for
  confinement and relative paths; the static-state reflection runs once per cache
  entry; InnerException needs no guard.
- Tests use the runtime constants instead of repeating them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ep, path edge cases

Python:
- Every restore step (recursion limit first, sys.path even after `del sys.path`, cwd,
  bytecode flag, modules, linecache) runs on its own; run() has a last-resort guard.
- run_json / run_b64 never raise (deep JSON, non-string input, unserializable
  converter values); falsy wrong-type JSON fields are InvalidRequest.
- Non-text coding declarations (rot13) are InvalidRequest instead of escaping.
- Only modules the run imported are dropped: host modules under an include folder stay.
- Tracebacks show the agent's own source lines (linecache seeded), ignore a
  snippet's sys.tracebacklimit, and keep exactly 10 + 10 frames (no folding).
- Duplicate includes are detected case-insensitively where paths are.
C#:
- No includes -> the include root is not touched (an empty root works again).
- A host log entry with a null message no longer crashes the run.
- Early errors are clipped too; CE0001 hints come before compiler errors in the cap.
- Confinement: case-sensitive outside Windows, drive roots keep their separator,
  NotSupportedException / SecurityException from a bad path are InvalidRequest.
- StaticState.Has never throws (unknown -> load fresh).
Spec: one unfinished log line is held until it ends; the recursion limit is restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IvanMurzak
IvanMurzak merged commit 6d9c0aa into main Oct 26, 2026
13 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