Skip to content

PermissionPromptRequest.action round-trip turns None into PermissionPromptRequestMemoryAction.STORE #1140

Description

@007bsd

Summary

PermissionPromptRequest.from_dict() reads a missing action field as the literal string "store", which then gets parsed as the enum value PermissionPromptRequestMemoryAction.STORE instead of None. Round-tripping from_dict(to_dict(x)) for any PermissionPromptRequest 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:1695 — the misdefault on action:
    action = from_union([from_none, lambda x: parse_enum(PermissionPromptRequestMemoryAction, x)], obj.get("action", "store"))

Compare the immediately-adjacent sibling field on line 1694, which is correct (no default):

access_kind = from_union([from_none, lambda x: parse_enum(PermissionPromptRequestPathAccessKind, x)], obj.get("accessKind"))

The dataclass declaration (line 1664) has the right in-memory default:

action: PermissionPromptRequestMemoryAction | None = None

And to_dict() correctly omits action when it is None (line 1755):

if self.action is not None:
    result["action"] = ...

The asymmetry is in from_dict only.

Reproduction

from copilot.generated.session_events import PermissionPromptRequest, PermissionPromptRequestKind

original = PermissionPromptRequest(kind=PermissionPromptRequestKind.MEMORY)
print("original.action:  ", original.action)
print("to_dict() has 'action' key?", "action" in original.to_dict())
roundtrip = PermissionPromptRequest.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:  PermissionPromptRequestMemoryAction.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 PermissionPromptRequestMemoryAction.STORE — the enum value whose wire representation is "store".

The sibling field on line 1694 (access_kind) does it correctly with obj.get("accessKind") and no default, so missing keys flow through from_none 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

  • PermissionPromptRequest is dispatched by 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions