Skip to content

PermissionDecision polymorphic base instantiation still produces empty JSON for Approved and UserNotAvailable paths — follow-up to #1194 #1403

Description

@AntonBaluev

Summary

Follow-up to #1194, which was closed COMPLETED on 2026-05-17 with the maintainer comment "This has been fixed." The fix in #1317 only addressed the Rejected path; the Approved and UserNotAvailable paths still construct new PermissionDecision { Kind = result.Kind.Value } on the abstract polymorphic base, which serializes as {} and triggers the same Unhandled permission result kind: [object Object] failure mode the original issue described.

The full handler-signature change that would resolve every path (handler returns Task<PermissionDecision> directly, eliminating the legacy-result → polymorphic-base conversion) landed on main in 24d5ff64 (2026-05-22 17:45 +0100), but after v1.0.0-beta.6 was tagged (2026-05-22 03:13 UTC, ~14 hours earlier). git tag --contains 24d5ff64 returns empty — no released tag carries the full fix.

What's in v1.0.0-beta.6

dotnet/src/Session.cs:768-791 at that tag:

private async Task ExecutePermissionAndRespondAsync(
    string requestId,
    PermissionRequest permissionRequest,
    Func<PermissionRequest, PermissionInvocation, Task<PermissionRequestResult>> handler)
{
    ...
    var result = await handler(permissionRequest, invocation);
    ...
    if (result.Kind == new PermissionRequestResultKind("no-result")) return;

    PermissionDecision decision = result.Kind == PermissionRequestResultKind.Rejected
        ? new PermissionDecisionReject { Feedback = result.Feedback }       // ← Rejected: properly typed
        : new PermissionDecision { Kind = result.Kind.Value };              // ← every other Kind: STILL THE BUG
    await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, decision);
}

And the catch fallback a few lines below:

await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, new PermissionDecision
{
    Kind = PermissionRequestResultKind.UserNotAvailable.Value                // ← also still the bug
});

PermissionDecision is configured for polymorphic serialization with UnknownDerivedTypeHandling.FallBackToBaseType — when the runtime type is the abstract base, STJ writes the base's properties only and ignores the Kind discriminator (it's the polymorphic discriminator; STJ owns its emission). Net wire output: {}. The CLI then formats kind as [object Object] for any non-Rejected path.

Repro

Same as #1194, but specifically targeting an Approved (or UserNotAvailable) decision:

var session = await client.CreateSessionAsync(new SessionConfig {
    OnPermissionRequest = PermissionHandler.ApproveAll,   // ← built-in: returns Approved
    Model = "gpt-4",
});
// Let the agent invoke a built-in tool that goes through the v2 event-driven flow
// (view, bash, edit, create_file, etc.). Watch the CLI side log:
//   Unhandled permission result kind: [object Object]

The PermissionHandler.ApproveAll helper in dotnet/src/PermissionHandlers.cs returns new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved } — that's the canonical path everyone using ApproveAll for tests / quickstarts hits, and it goes through the still-buggy branch.

Confirmed via the same kind of round-trip as #1194

Against vanilla JsonSerializerOptions(JsonSerializerDefaults.Web):

var decision = new PermissionDecision { Kind = "approve-once" };
var json = JsonSerializer.Serialize(decision);
// Expected: {"kind":"approve-once"}
// Actual:   {}

(Confirmed in #1194 and unchanged at beta.6.)

Suggested fix

The fix on main already does the right thing — handler returns Task<PermissionDecision> directly, no legacy-result mapping needed. Just cut a release that includes 24d5ff64 (or its containing PR if the API change was bundled with other follow-ups). For consumers stuck on v1.0.0-beta.6 until then, the in-place fix mirrors #1194's suggested mapping but applied to every result.Kind instead of only Rejected:

PermissionDecision decision = result.Kind.Value switch
{
    "approve-once"        => new PermissionDecisionApproveOnce(),
    "approve-for-session" => new PermissionDecisionApproveForSession(),
    "approve-for-location"=> new PermissionDecisionApproveForLocation(),
    "approve-permanently" => new PermissionDecisionApprovePermanently(),
    "reject"              => new PermissionDecisionReject { Feedback = result.Feedback },
    "user-not-available"  => new PermissionDecisionUserNotAvailable(),
    _                     => throw new InvalidOperationException($"Unmapped PermissionRequestResultKind: {result.Kind.Value}"),
};

Same fix applies to the catch fallback — hardcode new PermissionDecisionUserNotAvailable() directly.

Workaround for consumers

Unchanged from #1194: route approval through Hooks.OnPreToolUse returning PreToolUseHookOutput.PermissionDecision = "allow" — that path uses a plain string field, not the polymorphic class, and preempts the broken OnPermissionRequest flow entirely.

Environment

  • Package: GitHub.Copilot.SDK v1.0.0-beta.6 (latest published)
  • Verified against tag v1.0.0-beta.6 in this repo + decompiled NuGet DLL
  • Runtime: .NET 8 / net10.0
  • OS: Windows 10

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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