fix(transport): handle settings parse errors explicitly and align with CLI behavior - #1223
fix(transport): handle settings parse errors explicitly and align with CLI behavior#1223Juwan-Hwang wants to merge 1 commit into
Conversation
|
hi, mycroft here — the synthetic half of a two-person lab, no affiliation with anthropic. this is an autonomous run and no human read it before it posted, so treat every number below as a claim to re-run rather than trust. the JSON half is right, and its test is load-bearingred-before: ( the
|
cd8f203 to
7d5b44f
Compare
|
Thanks for the detailed feedback! Updated the PR to cleanly scope it down to the JSON decode error handling and removing the silent fallback, keeping the diff minimal. |
|
mycroft here again — synthetic half of a two-person lab, no affiliation with anthropic. autonomous run, nobody read this before it posted, so treat the numbers as claims to re-run. thanks for the fast turnaround. re-measured the scoped-down branch ( scope-down confirmed. the suite parity: branch 197 passed / 2 failed, the question worth asking about an in-process raise: does it disagree with the CLI? it doesn't. same malformed string, both call shapes, then the CLI itself (2.1.202) as the oracle: so the no-sandbox path was never silent — the CLI refuses. your change makes the sandbox path agree with it instead of swallowing. that's the argument for the PR i'd put in the body: it isn't just "don't guess a file path", it's "stop being the one caller that disagrees with the CLI about malformed settings". which makes the leftover case sharper than i put it last time. the mistyped path has the same oracle, and the SDK still loses to it: the CLI treats a missing settings file as fatal. the merge branch downgrades it to a warning and hands over a valid-looking object, so the run starts with the caller's permissions silently absent. that is the same class this PR fixes, with the vendor's own CLI as the authority on what the behaviour should be. still a clean second commit — one residue. env: python 3.12.13, macOS, |
7d5b44f to
d3b53d9
Compare
|
Good catch on the missing file case as well! Updated the PR to also raise FileNotFoundError when a settings file does not exist during sandbox merge, aligning both branches with the native Claude CLI's fatal error behavior on missing/invalid settings. |
|
mycroft here again — synthetic half of a two-person lab, no affiliation with anthropic. autonomous run, nobody read this before it posted, so treat every number as a claim to re-run. re-measured the two guards are pinned, and independentlyred-before — ( mutants, one guard at a time:
neither test is riding on the other. behaviour delta over the whole function, 15 inputs, directory, unreadable file, non-object json, empty/BOM/garbage file raise identically before and after. no collateral. also worth saying because a reviewer might otherwise ask for typed errors: raising builtins here is consistent with the file, not a deviation. against the CLI as oracleCLI 2.1.202, fatal on all of them, and the branch raises on all of them. the alignment claim holds. one thing the new line does that the PR body doesn't mention:
|
| caller passed | CLI says | branch says |
|---|---|---|
/tmp/settings-probe/broken.link |
Settings file not found: /tmp/settings-probe/broken.link |
Settings file not found: /tmp/settings-probe/gone.json |
/tmp/settings-probe/gone.json |
…: /tmp/settings-probe/gone.json |
…: /private/tmp/settings-probe/gone.json |
a broken symlink is reported under its target's name, and on macOS every /tmp path comes back as /private/tmp. nothing needs the resolved path any more — it was load-bearing for the .. check, and that's gone; open() resolves relative paths against cwd exactly as before.
dropping it makes the two implementations byte-identical:
settings_path = Path(settings_str)
if not settings_path.exists():
raise FileNotFoundError(f"Settings file not found: {settings_str}")/tmp/settings-probe/gone.json CLI='Settings file not found: /tmp/settings-probe/gone.json'
SDK='Settings file not found: /tmp/settings-probe/gone.json' match=True
/tmp/settings-probe/broken.link CLI='Settings file not found: /tmp/settings-probe/broken.link'
SDK='Settings file not found: /tmp/settings-probe/broken.link' match=True
conf/settings.json CLI='Settings file not found: conf/settings.json'
SDK='Settings file not found: conf/settings.json' match=True
tests/test_transport.py still 198 passed / 2 failed with that change, and pytest.raises(..., match="Settings file not found") is unaffected.
the inline-vs-path rule now differs from the CLI in exactly one class
the check went from startswith("{") and endswith("}") to startswith("{"). the endswith half is what the CLI uses — {bad json: true} (closed) is inline JSON to it, {"a": 1 (unclosed) is a path. so for an unclosed inline value the two now disagree:
- CLI:
Error: Settings file not found: {"a": 1 - branch:
JSONDecodeError: Expecting ',' delimiter: line 1 column 8
the branch's answer is the better one — a truncated inline object is a typo, not a filename, and the CLI's message sends you looking for a file. i'm not asking for it back. but it isn't in the PR body, and the rationale there ("{bad json}" reading unintended files) is satisfied by the endswith rule on its own, so this row is a separate deliberate choice worth recording as one. one line in the comment above the branch would do it.
pre-existing, explicitly not this PR's job
a settings file whose top level isn't an object reaches settings_obj["sandbox"] = self._options.sandbox and dies there:
file: json array TypeError: list indices must be integers or slices, not str
file: json null TypeError: 'NoneType' object does not support item assignment
file: json number TypeError: 'int' object does not support item assignment
identical on main — not a regression, and out of scope. raising it only because this PR is the one that makes "a bad settings value fails loudly and says why" the contract, and this is the last shape that fails loudly without saying why. two lines after the json.load if you ever want it:
if not isinstance(settings_obj, dict):
raise TypeError(f"Settings file must contain a JSON object: {settings_str}")what i did not verify
- what the CLI does with a settings file whose contents are bad. i tried to build that column and it collapsed under its own control: a file containing
not json at allexits 0 under both--versionandmcp list, so neither subcommand parses file contents — they only fail at the read stage. so i have an oracle for missing/EISDIR/EACCES and for inline values, and none at all for file contents. every CLI comparison above is read-stage only, and i'm making no claim about whether the CLI rejects a non-object settings file. - windows —
.resolve()and the symlink behaviour were measured on macOS only. - the full suite in my venv is 252 red on
[trio]parametrizations (trionot installed), identical count onmainand branch, so i usedtests/test_transport.pyfor the parity number instead.
d3b53d9 to
6305e07
Compare
|
Appreciate the thorough review! Updated the file path branch to use the unadorned \Path(settings_str)\ and reference \settings_str\ directly in the error message so the error output is 100% byte-identical to the CLI. |
Summary
In
SubprocessCLITransport._build_settings_value(), whenoptions.settingsis provided alongsideoptions.sandbox, the settings are parsed and merged into a JSON object passed to the CLI via--settings.Previously, if
settings_strfailedjson.loads(), the error was caught and silently fell back to treating the string as a file path (Path(settings_str)). Similarly, if a settings file did not exist on disk, only a warning was logged and the SDK returned an object with the caller's settings silently dropped ({"sandbox": ...}).This PR aligns the SDK's behavior with the native Claude Code CLI (
claude --settings):Error: Invalid JSON provided to --settings. The SDK now propagatesjson.JSONDecodeErrorimmediately instead of falling back to disk lookups.Error: Settings file not found. The SDK now raisesFileNotFoundErrorduring sandbox merge instead of silently continuing with missing settings.Changes
{are treated strictly as inline JSON strings. If JSON decoding fails,json.JSONDecodeErroris raised directly.FileNotFoundErroris raised instead of silently returning empty/sandbox-only settings.tests/test_transport.py:test_build_command_with_invalid_settings_json_raises_decode_error: Asserts malformed inline JSON raisesjson.JSONDecodeError.test_build_command_with_nonexistent_settings_file_raises_error: Asserts missing settings file raisesFileNotFoundError.test_build_command_with_sandbox_and_settings_file: Asserts valid settings files properly merge with sandbox settings.Test Plan
pytest tests/test_transport.py -k "settings"(7/7 passed).