Repository navigation
Conversation
destination defaults to None and to_dict() leaves the key out when it is,
but the CLI's control-protocol schema has it on every variant with no
.optional(). An entry without it makes the CLI drop the whole
updatedPermissions array, with nothing but a warn-level log, so the update
never lands and can_use_tool is asked for the same tool again.
PermissionUpdate(type='setMode', mode='plan').to_dict()
-> {'type': 'setMode', 'mode': 'plan'}
The TypeScript SDK declares the field non-optional on all six variants
(sdk.d.ts, PermissionUpdate), so this is also a parity gap. Default it to
'session', which holds for the run and writes nothing to a settings file
the caller did not name, and always put it on the wire.
Also add 'cliArg' to PermissionUpdateDestination. The CLI's enum and
PermissionUpdateDestination in sdk.d.ts both carry five values; this one
had four.
|
One backward-compatibility edge remains on exact head de7a1fb. Before this PR, destination is explicitly typed as optional, so existing callers can legally construct PermissionUpdate(..., destination=None). The new dataclass annotation/default no longer advertises None, but Python still accepts it at runtime. In that path to_dict() now emits: rather than applying the new session default. That recreates the same wire-level failure mode this PR is fixing: the CLI schema requires a concrete destination value and can drop the update. from_dict() already normalizes a missing/falsy destination with |
Giving the field a default fixes the omitted-argument path, but destination
was annotated Optional before, so a caller written against that signature
can still pass None. to_dict() then wrote
{"type": "setMode", "mode": "plan", "destination": null}
and a null fails the CLI's schema exactly like a missing key does, so the
updatedPermissions array gets dropped again. from_dict() already normalised
a falsy destination; the direct constructor did not. Coalesce in to_dict()
so both paths agree, with a regression over the None case.
|
Good catch, that path was open. Pushed 9bd55e1. to_dict() now coalesces, so PermissionUpdate(type="setMode", mode="plan", destination=None) sends "session" instead of null. Needed a cast to keep mypy quiet, since the annotation no longer admits None and it called the or branch unreachable, and the comment next to it says why the branch is there anyway. from_dict() was already doing this with Regression test is test_explicit_none_destination_still_sends_session. It fails on de7a1fb with |
What is wrong
PermissionUpdate.destinationdefaults toNoneandto_dict()only writes the key when it is not None, so an update built without one goes on the wire without it:The CLI's control-protocol schema has
destinationon every variant with no.optional(). From the bundled binary at 2.1.284, which is what_cli_version.pypins:An entry that fails that schema makes the CLI drop the whole
updatedPermissionsarray, and the only sign is a warn-level line,Malformed updatedPermissions from SDK host ignored. So aPermissionResultAllow(updated_permissions=[...])built in Python never lands, andcan_use_toolis asked for the same tool on the next call.The TypeScript SDK declares the field non-optional on all six variants (
sdk.d.ts,PermissionUpdate), and itsPermissionUpdateDestinationcarries five values:This SDK has four.
cliArgcannot be expressed at all.Related: #1308 is the same array being dropped, through a different field (
ruleContent: null). That one is not touched here, and the two are independent: the repro in #1308 setsdestinationexplicitly on the CLI's own suggestions, which is why it gets as far as theruleContentcheck.What this changes
destinationbecomes required with a default of"session", andto_dictalways emits it."session"is the conservative choice: it holds for the current run and writes nothing to a settings file the caller did not name. The field stays last in the dataclass, so no positional call breaks.from_dictfills in"session"when an inbound entry omits the key, so a round trip through the SDK produces something the CLI accepts.cliArgjoins the destination literal.Test Plan
Three new tests in
tests/test_types.py, all failing before the change:destinationon the wirefrom_dictwithout the key gives"session", and that survivesto_dictcliArgis in the literal and round-tripsThe existing round-trip tests all supply a
destination, so they are unaffected.