Summary
PermissionRequest.from_dict() reads a missing action field as the literal string "store", which then gets parsed as the enum value PermissionRequestMemoryAction.STORE instead of None. Round-tripping from_dict(to_dict(x)) for any PermissionRequest with action=None silently turns it into a STORE action.
This is the same shape as already-fixed bug #1139 (SessionTaskCompleteData.summary), in a different class.
Affected versions
main at commit dd2dcbc439256acfb9feb2cff07c0b9c820091b8. Generated file; same logic ships in every Python release built from this codegen.
Affected source
python/copilot/generated/session_events.py:1845 — the misdefault on action:
action = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryAction, x)], obj.get("action", "store"))
Compare the immediately-adjacent sibling field on line 1846, which is correct (no default):
The dataclass declaration (line 1812) has the right in-memory default:
action: PermissionRequestMemoryAction | None = None
to_dict() correctly omits action when it is None. The asymmetry is in from_dict only.
Reproduction
from copilot.generated.session_events import PermissionRequest, PermissionRequestKind
original = PermissionRequest(kind=PermissionRequestKind.MEMORY)
print("original.action: ", original.action)
print("to_dict() has 'action' key?", "action" in original.to_dict())
roundtrip = PermissionRequest.from_dict(original.to_dict())
print("roundtrip.action: ", roundtrip.action)
print("equal? ", roundtrip == original)
Expected vs actual output
Expected:
original.action: None
to_dict() has 'action' key? False
roundtrip.action: None
equal? True
Actual:
original.action: None
to_dict() has 'action' key? False
roundtrip.action: PermissionRequestMemoryAction.STORE
equal? False
Root cause
The obj.get("action", "store") default coerces a missing key to the string "store". from_union([from_none, parse_enum], "store") then succeeds via parse_enum, producing PermissionRequestMemoryAction.STORE — the enum value whose wire representation is "store".
The sibling field on line 1846 (args) does it correctly with obj.get("args") and no default, so missing keys flow through correctly and produce None.
This is the same misdefault pattern as already-fixed bug #1139 (SessionTaskCompleteData.summary), where obj.get("summary", "") was changed to obj.get("summary") in line 2816.
Impact
PermissionRequest is part of the permission-handling flow in python/copilot/session.py. Any permission request without an action field becomes a STORE action after parsing.
- Code that pattern-matches on
action == None to mean "no memory action requested" sees STORE instead and may take an unintended branch.
- Round-tripping (caching, replay, audit logs) silently mutates the field.
Summary
PermissionRequest.from_dict()reads a missingactionfield as the literal string"store", which then gets parsed as the enum valuePermissionRequestMemoryAction.STOREinstead ofNone. Round-trippingfrom_dict(to_dict(x))for anyPermissionRequestwithaction=Nonesilently turns it into aSTOREaction.This is the same shape as already-fixed bug #1139 (
SessionTaskCompleteData.summary), in a different class.Affected versions
mainat commitdd2dcbc439256acfb9feb2cff07c0b9c820091b8. Generated file; same logic ships in every Python release built from this codegen.Affected source
python/copilot/generated/session_events.py:1845— the misdefault onaction:Compare the immediately-adjacent sibling field on line 1846, which is correct (no default):
The dataclass declaration (line 1812) has the right in-memory default:
to_dict()correctly omitsactionwhen it isNone. The asymmetry is infrom_dictonly.Reproduction
Expected vs actual output
Expected:
Actual:
Root cause
The
obj.get("action", "store")default coerces a missing key to the string"store".from_union([from_none, parse_enum], "store")then succeeds viaparse_enum, producingPermissionRequestMemoryAction.STORE— the enum value whose wire representation is"store".The sibling field on line 1846 (
args) does it correctly withobj.get("args")and no default, so missing keys flow through correctly and produceNone.This is the same misdefault pattern as already-fixed bug #1139 (
SessionTaskCompleteData.summary), whereobj.get("summary", "")was changed toobj.get("summary")in line 2816.Impact
PermissionRequestis part of the permission-handling flow inpython/copilot/session.py. Any permission request without anactionfield becomes aSTOREaction after parsing.action == Noneto mean "no memory action requested" seesSTOREinstead and may take an unintended branch.