Summary
CopilotSession.ExecutePermissionAndRespondAsync instantiates the abstract polymorphic base type Rpc.PermissionDecision directly instead of one of its registered derived types. STJ polymorphic serialization strips the discriminator from the base instance, the wire payload becomes effectively {}, and the CLI's permission receiver fails with Unhandled permission result kind: [object Object].
This affects every consumer that handles permission requests for built-in tools (view, bash, edit, create_file, etc.) — they all route through the v2 event-driven permission flow that hits this code path. Verified present in 0.3.0 and 1.0.0-beta.1.
Repro
Any session that:
- Provides a custom
OnPermissionRequest (or uses PermissionHandler.ApproveAll)
- Lets the agent invoke a built-in SDK tool such as
view
- Returns any
PermissionRequestResult other than NoResult from the handler
Result: tool returns the error message Unhandled permission result kind: [object Object] and the session never reaches idle (so any SendAndWaitAsync-style call eventually times out).
Custom-defined tools registered with ToolOptions.ForReadOnlyTool() (auto-approved on the SDK side) are unaffected because they don't go through this path.
Root cause
CopilotSession.ExecutePermissionAndRespondAsync (decompiled from GitHub.Copilot.SDK 0.3.0/lib/net8.0/GitHub.Copilot.SDK.dll):
PermissionRequestResult result = await handler(permissionRequest, invocation);
if (!(result.Kind == new PermissionRequestResultKind("no-result")))
{
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, new PermissionDecision
{
Kind = result.Kind.Value // ← sets the discriminator on the abstract base
});
}
PermissionDecision is configured for polymorphic serialization with derived-type discriminators:
[JsonPolymorphic(TypeDiscriminatorPropertyName = "kind",
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
[JsonDerivedType(typeof(PermissionDecisionApproveOnce), "approve-once")]
[JsonDerivedType(typeof(PermissionDecisionApproveForSession), "approve-for-session")]
[JsonDerivedType(typeof(PermissionDecisionApproveForLocation), "approve-for-location")]
[JsonDerivedType(typeof(PermissionDecisionReject), "reject")]
[JsonDerivedType(typeof(PermissionDecisionUserNotAvailable), "user-not-available")]
public class PermissionDecision
{
[JsonPropertyName("kind")]
public virtual string Kind { get; set; } = string.Empty;
}
When STJ serializes a PermissionDecision whose runtime type is the base class:
- No registered
[JsonDerivedType] matches the runtime type.
UnknownDerivedTypeHandling.FallBackToBaseType writes the base type's properties.
- The
Kind property is the polymorphic discriminator — STJ owns its emission and ignores user-set values for unrecognized derived types.
Net wire output: {}. CLI's JS layer then formats kind as [object Object].
Confirmed via unit test
Round-trip test against vanilla JsonSerializerOptions(JsonSerializerDefaults.Web):
var decision = new PermissionDecision { Kind = "approve-once" };
var json = JsonSerializer.Serialize(decision);
// Expected: {"kind":"approve-once"}
// Actual: {}
(The legacy PermissionRequestResult { Kind = PermissionRequestResultKind.Approved } serializes correctly to {"kind":"approve-once"} — that path is fine. The bug is specifically in the SDK's conversion to the polymorphic PermissionDecision.)
Suggested fix
Map the legacy PermissionRequestResult.Kind to the appropriate registered derived type:
PermissionDecision decision = result.Kind.Value switch
{
"approve-once" => new PermissionDecisionApproveOnce(),
"reject" => new PermissionDecisionReject(),
"user-not-available" => new PermissionDecisionUserNotAvailable(),
_ => throw new InvalidOperationException($"Unmapped kind: {result.Kind.Value}")
};
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, decision);
(The full set may also need PermissionDecisionApproveForSession / PermissionDecisionApproveForLocation mappings if the legacy API is ever extended to express those.)
Same fix applies to the catch branch a few lines below that also constructs new PermissionDecision { Kind = ... }.
Workaround for consumers
Until fixed upstream, consumers can 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 v0.3.0 (also reproduces in 1.0.0-beta.1)
- Runtime: .NET 8 / net10.0
- OS: Windows 10
Summary
CopilotSession.ExecutePermissionAndRespondAsyncinstantiates the abstract polymorphic base typeRpc.PermissionDecisiondirectly instead of one of its registered derived types. STJ polymorphic serialization strips the discriminator from the base instance, the wire payload becomes effectively{}, and the CLI's permission receiver fails withUnhandled permission result kind: [object Object].This affects every consumer that handles permission requests for built-in tools (
view,bash,edit,create_file, etc.) — they all route through the v2 event-driven permission flow that hits this code path. Verified present in 0.3.0 and 1.0.0-beta.1.Repro
Any session that:
OnPermissionRequest(or usesPermissionHandler.ApproveAll)viewPermissionRequestResultother thanNoResultfrom the handlerResult: tool returns the error message
Unhandled permission result kind: [object Object]and the session never reaches idle (so anySendAndWaitAsync-style call eventually times out).Custom-defined tools registered with
ToolOptions.ForReadOnlyTool()(auto-approved on the SDK side) are unaffected because they don't go through this path.Root cause
CopilotSession.ExecutePermissionAndRespondAsync(decompiled fromGitHub.Copilot.SDK 0.3.0/lib/net8.0/GitHub.Copilot.SDK.dll):PermissionDecisionis configured for polymorphic serialization with derived-type discriminators:When STJ serializes a
PermissionDecisionwhose runtime type is the base class:[JsonDerivedType]matches the runtime type.UnknownDerivedTypeHandling.FallBackToBaseTypewrites the base type's properties.Kindproperty is the polymorphic discriminator — STJ owns its emission and ignores user-set values for unrecognized derived types.Net wire output:
{}. CLI's JS layer then formatskindas[object Object].Confirmed via unit test
Round-trip test against vanilla
JsonSerializerOptions(JsonSerializerDefaults.Web):(The legacy
PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }serializes correctly to{"kind":"approve-once"}— that path is fine. The bug is specifically in the SDK's conversion to the polymorphicPermissionDecision.)Suggested fix
Map the legacy
PermissionRequestResult.Kindto the appropriate registered derived type:(The full set may also need
PermissionDecisionApproveForSession/PermissionDecisionApproveForLocationmappings if the legacy API is ever extended to express those.)Same fix applies to the
catchbranch a few lines below that also constructsnew PermissionDecision { Kind = ... }.Workaround for consumers
Until fixed upstream, consumers can route approval through
Hooks.OnPreToolUsereturningPreToolUseHookOutput.PermissionDecision = "allow"— that path uses a plain string field (not the polymorphic class) and preempts the brokenOnPermissionRequestflow entirely.Environment
GitHub.Copilot.SDKv0.3.0 (also reproduces in 1.0.0-beta.1)