Skip to content

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

@007bsd

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions